/******************************************************************************* * 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; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; import java.util.Set; import java.util.UUID; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutorService; import javax.swing.ImageIcon; import javax.swing.SwingUtilities; import org.apache.felix.service.command.CommandProcessor; import org.dcm4che3.data.Tag; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.weasis.core.api.command.Option; import org.weasis.core.api.command.Options; import org.weasis.core.api.explorer.ObservableEvent; import org.weasis.core.api.explorer.model.DataExplorerModel; import org.weasis.core.api.explorer.model.Tree; import org.weasis.core.api.explorer.model.TreeModel; import org.weasis.core.api.explorer.model.TreeModelNode; import org.weasis.core.api.gui.util.AppProperties; import org.weasis.core.api.gui.util.GuiExecutor; import org.weasis.core.api.media.data.Codec; import org.weasis.core.api.media.data.MediaElement; import org.weasis.core.api.media.data.MediaSeries; import org.weasis.core.api.media.data.MediaSeriesGroup; import org.weasis.core.api.media.data.MediaSeriesGroupNode; import org.weasis.core.api.media.data.Series; import org.weasis.core.api.media.data.TagView; import org.weasis.core.api.media.data.TagW; import org.weasis.core.api.media.data.Thumbnail; import org.weasis.core.api.service.BundleTools; import org.weasis.core.api.util.GzipManager; import org.weasis.core.api.util.ThreadUtil; import org.weasis.core.ui.docking.UIManager; import org.weasis.core.ui.editor.SeriesViewerFactory; import org.weasis.core.ui.editor.ViewerPluginBuilder; import org.weasis.dicom.codec.DicomEncapDocElement; import org.weasis.dicom.codec.DicomEncapDocSeries; import org.weasis.dicom.codec.DicomImageElement; import org.weasis.dicom.codec.DicomMediaIO; import org.weasis.dicom.codec.DicomSeries; import org.weasis.dicom.codec.DicomSpecialElement; import org.weasis.dicom.codec.DicomVideoElement; import org.weasis.dicom.codec.DicomVideoSeries; import org.weasis.dicom.codec.KOSpecialElement; import org.weasis.dicom.codec.PRSpecialElement; import org.weasis.dicom.codec.RejectedKOSpecialElement; import org.weasis.dicom.codec.SortSeriesStack; import org.weasis.dicom.codec.TagD; import org.weasis.dicom.codec.display.Modality; import org.weasis.dicom.codec.utils.SplittingModalityRules; import org.weasis.dicom.codec.utils.SplittingModalityRules.Rule; import org.weasis.dicom.codec.utils.SplittingRules; import org.weasis.dicom.explorer.wado.DicomManager; import org.weasis.dicom.explorer.wado.DownloadManager; import org.weasis.dicom.explorer.wado.LoadRemoteDicomManifest; import org.weasis.dicom.explorer.wado.LoadRemoteDicomURL; import org.weasis.dicom.explorer.wado.LoadSeries; @org.osgi.service.component.annotations.Component(immediate = false, property = { CommandProcessor.COMMAND_SCOPE + "=dicom", CommandProcessor.COMMAND_FUNCTION + "=get", CommandProcessor.COMMAND_FUNCTION + "=close" }, service = DicomModel.class) public class DicomModel implements TreeModel, DataExplorerModel { private static final Logger LOGGER = LoggerFactory.getLogger(DicomModel.class); public static final String NAME = "DICOM"; //$NON-NLS-1$ public static final String PREFERENCE_NODE = "dicom.model"; //$NON-NLS-1$ public static final TreeModelNode patient = new TreeModelNode(1, 0, TagW.PatientPseudoUID, new TagView(TagD.getTagFromIDs(Tag.PatientName, Tag.PatientID))); public static final TreeModelNode study = new TreeModelNode(2, 0, TagD.get(Tag.StudyInstanceUID), new TagView(TagD.getTagFromIDs(Tag.StudyDate, Tag.AccessionNumber, Tag.StudyID, Tag.StudyDescription))); public static final TreeModelNode series = new TreeModelNode(3, 0, TagW.SubseriesInstanceUID, new TagView(TagD.getTagFromIDs(Tag.SeriesDescription, Tag.SeriesNumber, Tag.SeriesTime))); public static final ExecutorService LOADING_EXECUTOR = ThreadUtil.buildNewSingleThreadExecutor("Dicom Model"); //$NON-NLS-1$ private static final List<TreeModelNode> modelStructure = Arrays.asList(TreeModelNode.ROOT, patient, study, series); private final Tree<MediaSeriesGroup> model; private PropertyChangeSupport propertyChange = null; private final SplittingRules splittingRules; public DicomModel() { model = new Tree<>(MediaSeriesGroupNode.rootNode); splittingRules = new SplittingRules(); } @Override public synchronized List<Codec> getCodecPlugins() { ArrayList<Codec> codecPlugins = new ArrayList<>(1); synchronized (BundleTools.CODEC_PLUGINS) { for (Codec codec : BundleTools.CODEC_PLUGINS) { if (codec != null && "Sun java imageio".equals(codec.getCodecName()) == false //$NON-NLS-1$ && codec.isMimeTypeSupported("application/dicom") && !codecPlugins.contains(codec)) { //$NON-NLS-1$ codecPlugins.add(codec); } } } return codecPlugins; } @Override public Collection<MediaSeriesGroup> getChildren(MediaSeriesGroup node) { return model.getSuccessors(node); } @Override public MediaSeriesGroup getHierarchyNode(MediaSeriesGroup parent, Object valueID) { if (parent != null || valueID != null) { synchronized (model) { for (MediaSeriesGroup node : getChildren(parent)) { if (node.matchIdValue(valueID)) { return node; } } } } return null; } public void replacePatientUID(String oldPatientUID, String newPatientUID) { MediaSeriesGroup pt = getHierarchyNode(MediaSeriesGroupNode.rootNode, oldPatientUID); Collection<MediaSeriesGroup> studies = getChildren(pt); Map<MediaSeriesGroup, Collection<MediaSeriesGroup>> studyMap = new HashMap<>(); for (MediaSeriesGroup st : studies) { studyMap.put(st, getChildren(st)); } removeHierarchyNode(MediaSeriesGroupNode.rootNode, pt); pt.setTagNoNull(TagW.PatientPseudoUID, newPatientUID); addHierarchyNode(MediaSeriesGroupNode.rootNode, pt); for (Entry<MediaSeriesGroup, Collection<MediaSeriesGroup>> stEntry : studyMap.entrySet()) { MediaSeriesGroup st = stEntry.getKey(); addHierarchyNode(pt, st); for (MediaSeriesGroup s : stEntry.getValue()) { addHierarchyNode(st, s); } } firePropertyChange(new ObservableEvent(ObservableEvent.BasicAction.UPDATE, DicomModel.this, oldPatientUID, pt)); } public MediaSeriesGroup getStudyNode(String studyUID) { Objects.requireNonNull(studyUID); synchronized (model) { for (MediaSeriesGroup pt : getChildren(MediaSeriesGroupNode.rootNode)) { for (MediaSeriesGroup st : getChildren(pt)) { if (st.matchIdValue(studyUID)) { return st; } } } } return null; } public MediaSeriesGroup getSeriesNode(String seriesUID) { Objects.requireNonNull(seriesUID); synchronized (model) { for (MediaSeriesGroup pt : getChildren(MediaSeriesGroupNode.rootNode)) { for (MediaSeriesGroup st : getChildren(pt)) { for (MediaSeriesGroup item : getChildren(st)) { if (item.matchIdValue(seriesUID)) { return item; } } } } } return null; } @Override public void addHierarchyNode(MediaSeriesGroup root, MediaSeriesGroup leaf) { synchronized (model) { model.addLeaf(root, leaf); } } @Override public void removeHierarchyNode(MediaSeriesGroup root, MediaSeriesGroup leaf) { synchronized (model) { Tree<MediaSeriesGroup> tree = model.getTree(root); if (tree != null) { tree.removeLeaf(leaf); } } } @Override public MediaSeriesGroup getParent(MediaSeriesGroup node, TreeModelNode modelNode) { if (node != null && modelNode != null) { TagW matchTagID = modelNode.getTagElement(); if (node.getTagID().equals(matchTagID)) { return node; } synchronized (model) { Tree<MediaSeriesGroup> tree = model.getTree(node); if (tree != null) { Tree<MediaSeriesGroup> parent; while ((parent = tree.getParent()) != null) { if (parent.getHead().getTagID().equals(matchTagID)) { return parent.getHead(); } tree = parent; } } } } return null; } public void dispose() { removeAllPropertyChangeListener(); synchronized (model) { for (MediaSeriesGroup pt : getChildren(MediaSeriesGroupNode.rootNode)) { for (MediaSeriesGroup st : getChildren(pt)) { for (MediaSeriesGroup item : getChildren(st)) { item.dispose(); } } } } model.clear(); } @Override public String toString() { return NAME; } @Override public List<TreeModelNode> getModelStructure() { return modelStructure; } @Override public void addPropertyChangeListener(PropertyChangeListener propertychangelistener) { if (propertyChange == null) { propertyChange = new PropertyChangeSupport(this); } propertyChange.addPropertyChangeListener(propertychangelistener); } @Override public void removePropertyChangeListener(PropertyChangeListener propertychangelistener) { if (propertyChange != null) { propertyChange.removePropertyChangeListener(propertychangelistener); } } public void removeAllPropertyChangeListener() { if (propertyChange != null) { for (PropertyChangeListener listener : propertyChange.getPropertyChangeListeners()) { propertyChange.removePropertyChangeListener(listener); } } } @Override public void firePropertyChange(final ObservableEvent event) { if (propertyChange != null) { if (event == null) { throw new NullPointerException(); } if (SwingUtilities.isEventDispatchThread()) { propertyChange.firePropertyChange(event); } else { SwingUtilities.invokeLater(() -> propertyChange.firePropertyChange(event)); } } } public void mergeSeries(List<MediaSeries<? extends MediaElement>> seriesList) { if (seriesList != null && seriesList.size() > 1) { String uid = TagD.getTagValue(seriesList.get(0), Tag.SeriesInstanceUID, String.class); boolean sameOrigin = true; if (uid != null) { for (int i = 1; i < seriesList.size(); i++) { if (!uid.equals(TagD.getTagValue(seriesList.get(i), Tag.SeriesInstanceUID))) { sameOrigin = false; break; } } } if (sameOrigin) { int min = Integer.MAX_VALUE; MediaSeries<? extends MediaElement> base = seriesList.get(0); for (MediaSeries<? extends MediaElement> s : seriesList) { Integer splitNb = (Integer) s.getTagValue(TagW.SplitSeriesNumber); if (splitNb != null && min > splitNb) { min = splitNb; base = s; } } for (MediaSeries<? extends MediaElement> s : seriesList) { if (s != base) { base.addAll((Collection) s.getMedias(null, null)); removeSeriesWithoutDisposingMedias(s); } } // Force to sort the new merged media list List sortedMedias = base.getSortedMedias(null); Collections.sort(sortedMedias, SortSeriesStack.instanceNumber); // update observer this.firePropertyChange( new ObservableEvent(ObservableEvent.BasicAction.REPLACE, DicomModel.this, base, base)); } } } public void removeSpecialElement(DicomSpecialElement dicomSpecialElement) { if (dicomSpecialElement == null) { return; } String patientPseudoUID = (String) dicomSpecialElement.getTagValue(TagW.PatientPseudoUID); MediaSeriesGroup patientGroup = getHierarchyNode(MediaSeriesGroupNode.rootNode, patientPseudoUID); if (patientGroup == null) { return; } String studyUID = TagD.getTagValue(dicomSpecialElement, Tag.StudyInstanceUID, String.class); MediaSeriesGroup studyGroup = getHierarchyNode(patientGroup, studyUID); if (studyGroup == null) { return; } String seriesUID = TagD.getTagValue(dicomSpecialElement, Tag.SeriesInstanceUID, String.class); Series<?> dicomSeries = (Series<?>) getHierarchyNode(studyGroup, seriesUID); if (dicomSeries == null) { return; } if (isSpecialModality(dicomSeries)) { List<DicomSpecialElement> specialElementList = (List<DicomSpecialElement>) dicomSeries.getTagValue(TagW.DicomSpecialElementList); List<DicomSpecialElement> patientSpecialElementList = (List<DicomSpecialElement>) patientGroup.getTagValue(TagW.DicomSpecialElementList); if (specialElementList == null || patientSpecialElementList == null) { return; } specialElementList.remove(dicomSpecialElement); if (patientSpecialElementList.remove(dicomSpecialElement)) { firePropertyChange( new ObservableEvent(ObservableEvent.BasicAction.UPDATE, this, null, dicomSpecialElement)); } if (specialElementList.isEmpty()) { removeSeries(dicomSeries); } } } public void removeSeriesWithoutDisposingMedias(MediaSeriesGroup dicomSeries) { if (dicomSeries != null) { // remove first series in UI (Dicom Explorer, Viewer using this series) firePropertyChange( new ObservableEvent(ObservableEvent.BasicAction.REMOVE, DicomModel.this, null, dicomSeries)); // remove in the data model MediaSeriesGroup studyGroup = getParent(dicomSeries, DicomModel.study); removeHierarchyNode(studyGroup, dicomSeries); LOGGER.info("Remove Series (no dispose): {}", dicomSeries); //$NON-NLS-1$ } } public void removeSeries(MediaSeriesGroup dicomSeries) { if (dicomSeries != null) { if (!DownloadManager.TASKS.isEmpty() && dicomSeries instanceof DicomSeries) { DownloadManager.stopDownloading((DicomSeries) dicomSeries, this); } // remove first series in UI (Dicom Explorer, Viewer using this series) firePropertyChange( new ObservableEvent(ObservableEvent.BasicAction.REMOVE, DicomModel.this, null, dicomSeries)); // remove in the data model MediaSeriesGroup studyGroup = getParent(dicomSeries, DicomModel.study); removeHierarchyNode(studyGroup, dicomSeries); dicomSeries.dispose(); LOGGER.info("Remove Series: {}", dicomSeries); //$NON-NLS-1$ } } public void removeStudy(MediaSeriesGroup studyGroup) { if (studyGroup != null) { if (!DownloadManager.TASKS.isEmpty()) { for (MediaSeriesGroup group : getChildren(studyGroup)) { if (group instanceof DicomSeries) { DownloadManager.stopDownloading((DicomSeries) group, this); } } } firePropertyChange( new ObservableEvent(ObservableEvent.BasicAction.REMOVE, DicomModel.this, null, studyGroup)); for (MediaSeriesGroup group : getChildren(studyGroup)) { group.dispose(); } MediaSeriesGroup patientGroup = getParent(studyGroup, DicomModel.patient); removeHierarchyNode(patientGroup, studyGroup); LOGGER.info("Remove Study: {}", studyGroup); //$NON-NLS-1$ } } public void removePatient(MediaSeriesGroup patientGroup) { if (patientGroup != null) { if (!DownloadManager.TASKS.isEmpty()) { for (MediaSeriesGroup studyGroup : getChildren(patientGroup)) { for (MediaSeriesGroup group : getChildren(studyGroup)) { if (group instanceof DicomSeries) { DownloadManager.stopDownloading((DicomSeries) group, this); } } } } firePropertyChange( new ObservableEvent(ObservableEvent.BasicAction.REMOVE, DicomModel.this, null, patientGroup)); for (MediaSeriesGroup studyGroup : getChildren(patientGroup)) { for (MediaSeriesGroup group : getChildren(studyGroup)) { group.dispose(); } } List<DicomSpecialElement> sps = (List<DicomSpecialElement>) patientGroup.getTagValue(TagW.DicomSpecialElementList); if (sps != null) { for (DicomSpecialElement d : sps) { d.dispose(); } } removeHierarchyNode(MediaSeriesGroupNode.rootNode, patientGroup); LOGGER.info("Remove Patient: {}", patientGroup); //$NON-NLS-1$ } } /** * DicomSpecialElement are added at patientGroupLevel since StudyInstanceUID and SeriesInstanceUID are not relevant * with the CurrentRequestedProcedureEvidenceSequence which can reference any SOPInstance of any Study and Series of * the Patient * * @param series */ public void addSpecialModality(Series series) { List<DicomSpecialElement> seriesSpecialElementList = (List<DicomSpecialElement>) series.getTagValue(TagW.DicomSpecialElementList); if (seriesSpecialElementList == null || seriesSpecialElementList.isEmpty()) { return; } MediaSeriesGroup patientGroup = getParent(series, DicomModel.patient); if (patientGroup == null) { return; } List<DicomSpecialElement> patientSpecialElementList = (List<DicomSpecialElement>) patientGroup.getTagValue(TagW.DicomSpecialElementList); if (patientSpecialElementList == null) { patientSpecialElementList = new CopyOnWriteArrayList<>(); patientGroup.setTag(TagW.DicomSpecialElementList, patientSpecialElementList); } for (DicomSpecialElement seriesSpecialElement : seriesSpecialElementList) { if (!patientSpecialElementList.contains(seriesSpecialElement)) { patientSpecialElementList.add(seriesSpecialElement); } } } public static boolean isSpecialModality(MediaSeries<?> series) { String modality = (series == null) ? null : TagD.getTagValue(series, Tag.Modality, String.class); return modality != null && ("PR".equals(modality) || "KO".equals(modality)); //$NON-NLS-1$ //$NON-NLS-2$ } public static Collection<KOSpecialElement> getKoSpecialElements(MediaSeries<DicomImageElement> dicomSeries) { // Get all DicomSpecialElement at patient level List<DicomSpecialElement> specialElementList = getSpecialElements(dicomSeries); if (specialElementList != null) { String referencedSeriesInstanceUID = TagD.getTagValue(dicomSeries, Tag.SeriesInstanceUID, String.class); return DicomSpecialElement.getKoSpecialElements(specialElementList, referencedSeriesInstanceUID); } return Collections.emptyList(); } public static Collection<RejectedKOSpecialElement> getRejectionKoSpecialElements( MediaSeries<DicomImageElement> dicomSeries) { // Get all DicomSpecialElement at patient level List<DicomSpecialElement> specialElementList = getSpecialElements(dicomSeries); if (specialElementList != null) { String referencedSeriesInstanceUID = TagD.getTagValue(dicomSeries, Tag.SeriesInstanceUID, String.class); return DicomSpecialElement.getRejectionKoSpecialElements(specialElementList, referencedSeriesInstanceUID); } return Collections.emptyList(); } public static RejectedKOSpecialElement getRejectionKoSpecialElement(MediaSeries<DicomImageElement> dicomSeries, String sopUID, Integer dicomFrameNumber) { // Get all DicomSpecialElement at patient level List<DicomSpecialElement> specialElementList = getSpecialElements(dicomSeries); if (specialElementList != null) { String referencedSeriesInstanceUID = TagD.getTagValue(dicomSeries, Tag.SeriesInstanceUID, String.class); return DicomSpecialElement.getRejectionKoSpecialElement(specialElementList, referencedSeriesInstanceUID, sopUID, dicomFrameNumber); } return null; } public static List<PRSpecialElement> getPrSpecialElements(MediaSeries<DicomImageElement> dicomSeries, String sopUID, Integer dicomFrameNumber) { // Get all DicomSpecialElement at patient level List<DicomSpecialElement> specialElementList = getSpecialElements(dicomSeries); if (!specialElementList.isEmpty()) { String referencedSeriesInstanceUID = TagD.getTagValue(dicomSeries, Tag.SeriesInstanceUID, String.class); return DicomSpecialElement.getPRSpecialElements(specialElementList, referencedSeriesInstanceUID, sopUID, dicomFrameNumber); } return Collections.emptyList(); } public static List<DicomSpecialElement> getSpecialElements(MediaSeries<DicomImageElement> dicomSeries) { if (dicomSeries == null) { return Collections.emptyList(); } List<DicomSpecialElement> list = null; DataExplorerModel model = (DataExplorerModel) dicomSeries.getTagValue(TagW.ExplorerModel); if (model instanceof DicomModel) { MediaSeriesGroup patientGroup = ((DicomModel) model).getParent(dicomSeries, DicomModel.patient); if (patientGroup != null) { list = (List<DicomSpecialElement>) patientGroup.getTagValue(TagW.DicomSpecialElementList); } } return list == null ? Collections.emptyList() : list; } public static <E> List<E> getSpecialElements(MediaSeriesGroup group, Class<E> clazz) { if (group != null && clazz != null && clazz.isAssignableFrom(clazz)) { List<DicomSpecialElement> kos = (List<DicomSpecialElement>) group.getTagValue(TagW.DicomSpecialElementList); if (kos != null) { List<E> list = new ArrayList<>(); for (DicomSpecialElement el : kos) { if (clazz.isInstance(el)) { list.add((E) el); } } return list; } } return Collections.emptyList(); } public static <E> E getFirstSpecialElement(MediaSeriesGroup group, Class<E> clazz) { if (group != null && clazz != null && clazz.isAssignableFrom(clazz)) { List<DicomSpecialElement> sps = (List<DicomSpecialElement>) group.getTagValue(TagW.DicomSpecialElementList); if (sps != null) { for (DicomSpecialElement el : sps) { if (clazz.isInstance(el)) { return (E) el; } } } } return null; } public static boolean hasSpecialElements(MediaSeriesGroup group, Class<? extends DicomSpecialElement> clazz) { if (group != null && clazz != null) { List<DicomSpecialElement> kos = (List<DicomSpecialElement>) group.getTagValue(TagW.DicomSpecialElementList); if (kos != null) { for (DicomSpecialElement el : kos) { if (clazz.isInstance(el)) { return true; } } } } return false; } public void openrelatedSeries(KOSpecialElement koSpecialElement, MediaSeriesGroup patient) { if (koSpecialElement != null && patient != null) { SeriesViewerFactory plugin = UIManager.getViewerFactory(DicomMediaIO.SERIES_MIMETYPE); if (plugin != null && !(plugin instanceof MimeSystemAppFactory)) { Set<String> koSet = koSpecialElement.getReferencedSeriesInstanceUIDSet(); List<MediaSeries<MediaElement>> seriesList = new ArrayList<>(); for (MediaSeriesGroup st : this.getChildren(patient)) { for (MediaSeriesGroup s : this.getChildren(st)) { if (koSet.contains(TagD.getTagValue(s, Tag.SeriesInstanceUID))) { seriesList.add((MediaSeries<MediaElement>) s); } } } if (!seriesList.isEmpty()) { String uid = UUID.randomUUID().toString(); Map<String, Object> props = Collections.synchronizedMap(new HashMap<String, Object>()); props.put(ViewerPluginBuilder.CMP_ENTRY_BUILD_NEW_VIEWER, false); props.put(ViewerPluginBuilder.BEST_DEF_LAYOUT, false); props.put(ViewerPluginBuilder.ICON, new ImageIcon(getClass().getResource("/icon/16x16/key-images.png"))); //$NON-NLS-1$ props.put(ViewerPluginBuilder.UID, uid); ViewerPluginBuilder builder = new ViewerPluginBuilder(plugin, seriesList, this, props); ViewerPluginBuilder.openSequenceInPlugin(builder); this.firePropertyChange( new ObservableEvent(ObservableEvent.BasicAction.SELECT, uid, null, koSpecialElement)); } } } } private void splitSeries(DicomMediaIO dicomReader, Series original, MediaElement media) { Series s = splitSeries(dicomReader, original); s.addMedia(media); LOGGER.info("Series splitting: {}", s); //$NON-NLS-1$ } private Series splitSeries(DicomMediaIO dicomReader, Series original) { MediaSeriesGroup st = getParent(original, DicomModel.study); String seriesUID = TagD.getTagValue(original, Tag.SeriesInstanceUID, String.class); int k = 1; while (true) { String uid = "#" + k + "." + seriesUID; //$NON-NLS-1$ //$NON-NLS-2$ MediaSeriesGroup group = getHierarchyNode(st, uid); if (group == null) { break; } k++; } String uid = "#" + k + "." + seriesUID; //$NON-NLS-1$ //$NON-NLS-2$ Series s = dicomReader.buildSeries(uid); dicomReader.writeMetaData(s); Object val = original.getTagValue(TagW.SplitSeriesNumber); if (val == null) { original.setTag(TagW.SplitSeriesNumber, 1); } s.setTag(TagW.SplitSeriesNumber, k + 1); s.setTag(TagW.ExplorerModel, this); addHierarchyNode(st, s); LOGGER.info("Series splitting: {}", s); //$NON-NLS-1$ return s; } private void replaceSeries(DicomMediaIO dicomReader, Series original, MediaElement media) { MediaSeriesGroup st = getParent(original, DicomModel.study); String seriesUID = TagD.getTagValue(original, Tag.SeriesInstanceUID, String.class); int k = 1; while (true) { String uid = "#" + k + "." + seriesUID; //$NON-NLS-1$ //$NON-NLS-2$ MediaSeriesGroup group = getHierarchyNode(st, uid); if (group == null) { break; } k++; } String uid = "#" + k + "." + seriesUID; //$NON-NLS-1$ //$NON-NLS-2$ Series s = dicomReader.buildSeries(uid); dicomReader.writeMetaData(s); Object val = original.getTagValue(TagW.SplitSeriesNumber); if (val == null) { // -1 convention to exclude this Series original.setTag(TagW.SplitSeriesNumber, -1); } s.setTag(TagW.SplitSeriesNumber, k); s.setTag(TagW.ExplorerModel, this); addHierarchyNode(st, s); s.addMedia(media); LOGGER.info("Replace Series: {}", s); //$NON-NLS-1$ } private void rebuildSeries(DicomMediaIO dicomReader, MediaElement media) { String studyUID = TagD.getTagValue(dicomReader, Tag.StudyInstanceUID, String.class); String patientPseudoUID = (String) dicomReader.getTagValue(TagW.PatientPseudoUID); MediaSeriesGroup pt = getHierarchyNode(MediaSeriesGroupNode.rootNode, patientPseudoUID); if (pt == null) { MediaSeriesGroup st = getStudyNode(studyUID); if (st == null) { pt = new MediaSeriesGroupNode(TagW.PatientPseudoUID, patientPseudoUID, DicomModel.patient.getTagView()); dicomReader.writeMetaData(pt); addHierarchyNode(MediaSeriesGroupNode.rootNode, pt); LOGGER.info("Adding patient: {}", pt); //$NON-NLS-1$ } else { pt = getParent(st, DicomModel.patient); LOGGER.warn("DICOM patient attributes are inconsitent! Name or ID is different within an exam."); //$NON-NLS-1$ } } MediaSeriesGroup st = getHierarchyNode(pt, studyUID); if (st == null) { st = new MediaSeriesGroupNode(TagD.get(Tag.StudyInstanceUID), studyUID, DicomModel.study.getTagView()); dicomReader.writeMetaData(st); addHierarchyNode(pt, st); } String seriesUID = TagD.getTagValue(dicomReader, Tag.SeriesInstanceUID, String.class); Series dicomSeries = (Series) getHierarchyNode(st, seriesUID); if (dicomSeries == null) { dicomSeries = dicomReader.buildSeries(seriesUID); dicomReader.writeMetaData(dicomSeries); dicomSeries.setTag(TagW.ExplorerModel, this); addHierarchyNode(st, dicomSeries); LOGGER.info("Series rebuilding: {}", dicomSeries); //$NON-NLS-1$ } dicomSeries.addMedia(media); // Load image and create thumbnail in this Thread Thumbnail t = (Thumbnail) dicomSeries.getTagValue(TagW.Thumbnail); if (t == null) { t = DicomExplorer.createThumbnail(dicomSeries, this, Thumbnail.DEFAULT_SIZE); dicomSeries.setTag(TagW.Thumbnail, t); t.repaint(); } firePropertyChange(new ObservableEvent(ObservableEvent.BasicAction.ADD, this, null, dicomSeries)); } @Override public boolean applySplittingRules(Series original, MediaElement media) { if (media != null && media.getMediaReader() instanceof DicomMediaIO) { DicomMediaIO dicomReader = (DicomMediaIO) media.getMediaReader(); String seriesUID = TagD.getTagValue(original, Tag.SeriesInstanceUID, String.class); if (!seriesUID.equals(TagD.getTagValue(dicomReader, Tag.SeriesInstanceUID))) { rebuildSeries(dicomReader, media); return true; } if (original instanceof DicomSeries) { DicomSeries initialSeries = (DicomSeries) original; // Handle cases when the Series is created before getting the image (downloading) if (media instanceof DicomVideoElement || media instanceof DicomEncapDocElement) { if (original.size(null) > 0) { // When the series already contains elements (images), always split video and document splitSeries(dicomReader, original, media); } else { replaceSeries(dicomReader, original, media); } return true; } if (media instanceof DicomSpecialElement) { List<DicomSpecialElement> specialElementList = (List<DicomSpecialElement>) initialSeries.getTagValue(TagW.DicomSpecialElementList); if (specialElementList == null) { specialElementList = new CopyOnWriteArrayList<>(); initialSeries.setTag(TagW.DicomSpecialElementList, specialElementList); } else if ("SR".equals(TagD.getTagValue(dicomReader, Tag.Modality))) { //$NON-NLS-1$ // Split SR series to have only one object by series Series s = splitSeries(dicomReader, initialSeries); specialElementList = new CopyOnWriteArrayList<>(); specialElementList.add((DicomSpecialElement) media); s.setTag(TagW.DicomSpecialElementList, specialElementList); return false; } specialElementList.add((DicomSpecialElement) media); return false; } int frames = dicomReader.getMediaElementNumber(); if (frames < 1) { initialSeries.addMedia((DicomImageElement) media); } else { Modality modality = Modality.getModality(TagD.getTagValue(initialSeries, Tag.Modality, String.class)); SplittingModalityRules splitRules = splittingRules.getSplittingModalityRules(modality, Modality.DEFAULT); List<Rule> rules; if (splitRules == null) { rules = Collections.emptyList(); } else { rules = frames > 1 ? splitRules.getMultiFrameRules() : splitRules.getSingleFrameRules(); } // If similar add to the original series if (isSimilar(rules, initialSeries, media)) { initialSeries.addMedia((DicomImageElement) media); return false; } // else try to find a similar previous split series MediaSeriesGroup study = getParent(initialSeries, DicomModel.study); int k = 1; while (true) { String uid = "#" + k + "." + seriesUID; //$NON-NLS-1$ //$NON-NLS-2$ MediaSeriesGroup group = getHierarchyNode(study, uid); if (group instanceof DicomSeries) { if (isSimilar(rules, (DicomSeries) group, media)) { ((DicomSeries) group).addMedia((DicomImageElement) media); return false; } } else { break; } k++; } // no matching series exists, so split series splitSeries(dicomReader, initialSeries, media); return true; } } else if (original instanceof DicomVideoSeries || original instanceof DicomEncapDocSeries) { if (original.size(null) > 0) { // Always split when it is a video or a encapsulated document if (media instanceof DicomVideoElement || media instanceof DicomEncapDocElement) { splitSeries(dicomReader, original, media); return true; } else { findMatchingSeriesOrsplit(original, media); } } else { original.addMedia(media); } } } return false; } private boolean findMatchingSeriesOrsplit(Series original, MediaElement media) { DicomMediaIO dicomReader = (DicomMediaIO) media.getMediaReader(); int frames = dicomReader.getMediaElementNumber(); if (frames < 1) { original.addMedia(media); } else { String seriesUID = TagD.getTagValue(original, Tag.SeriesInstanceUID, String.class); Modality modality = Modality.getModality(TagD.getTagValue(original, Tag.Modality, String.class)); SplittingModalityRules splitRules = splittingRules.getSplittingModalityRules(modality, Modality.DEFAULT); List<Rule> rules; if (splitRules == null) { rules = Collections.emptyList(); } else { rules = frames > 1 ? splitRules.getMultiFrameRules() : splitRules.getSingleFrameRules(); } // If similar add to the original series if (isSimilar(rules, original, media)) { original.addMedia(media); return false; } // else try to find a similar previous split series MediaSeriesGroup study = getParent(original, DicomModel.study); int k = 1; while (true) { String uid = "#" + k + "." + seriesUID; //$NON-NLS-1$ //$NON-NLS-2$ MediaSeriesGroup group = getHierarchyNode(study, uid); if (group instanceof Series) { if (isSimilar(rules, (Series) group, media)) { ((Series) group).addMedia(media); return false; } } else { break; } k++; } // no matching series exists, so split series splitSeries(dicomReader, original, media); return true; } return false; } private static boolean isSimilar(List<Rule> list, Series<?> s, final MediaElement media) { final MediaElement firstMedia = s.getMedia(0, null, null); if (firstMedia == null) { // no image return true; } // Not similar when the instances have different classes (even when inheriting class) if (firstMedia.getClass() != media.getClass()) { return false; } for (Rule rule : list) { if (!rule.isTagValueMatching(firstMedia, media)) { return false; } } return true; } public void get(String[] argv) throws IOException { final String[] usage = { "Load DICOM files remotely or locally", //$NON-NLS-1$ "Usage: dicom:get ([-l PATH]... [-r URI]... [-p] [-i DATA]... [-w URI]...)", //$NON-NLS-1$ "PATH is either a directory(recursive) or a file", " -l --local=PATH open DICOMs from local disk", //$NON-NLS-1$ //$NON-NLS-2$ " -r --remote=URI open DICOMs from an URI", //$NON-NLS-1$ " -p --portable open DICOMs from configured directories at the same level of the executable", //$NON-NLS-1$ " -i --iwado=DATA open DICOMs from an XML manifest (GZIP-Base64)", //$NON-NLS-1$ " -w --wado=URI open DICOMs from an XML manifest", " -? --help show help" }; //$NON-NLS-1$//$NON-NLS-2$ final Option opt = Options.compile(usage).parse(argv); final List<String> largs = opt.getList("local"); //$NON-NLS-1$ final List<String> rargs = opt.getList("remote"); //$NON-NLS-1$ final List<String> iargs = opt.getList("iwado"); //$NON-NLS-1$ final List<String> wargs = opt.getList("wado"); //$NON-NLS-1$ if (opt.isSet("help") //$NON-NLS-1$ || (largs.isEmpty() && rargs.isEmpty() && iargs.isEmpty() && wargs.isEmpty() && !opt.isSet("portable"))) { //$NON-NLS-1$ opt.usage(); return; } GuiExecutor.instance().execute(() -> { firePropertyChange( new ObservableEvent(ObservableEvent.BasicAction.SELECT, DicomModel.this, null, DicomModel.this)); getCommand(opt, largs, rargs, iargs, wargs); }); } private void getCommand(Option opt, List<String> largs, List<String> rargs, List<String> iargs, List<String> wargs) { // start importing local dicom series list if (opt.isSet("local")) { //$NON-NLS-1$ File[] files = new File[largs.size()]; for (int i = 0; i < files.length; i++) { files[i] = new File(largs.get(i)); } LOADING_EXECUTOR.execute(new LoadLocalDicom(files, true, DicomModel.this)); } if (opt.isSet("remote")) { //$NON-NLS-1$ LOADING_EXECUTOR.execute(new LoadRemoteDicomURL(rargs.toArray(new String[rargs.size()]), DicomModel.this)); } // build WADO series list to download if (opt.isSet("wado")) { //$NON-NLS-1$ LOADING_EXECUTOR.execute(new LoadRemoteDicomManifest(wargs, DicomModel.this)); } if (opt.isSet("iwado")) { //$NON-NLS-1$ List<String> xmlFiles = new ArrayList<>(iargs.size()); for (int i = 0; i < iargs.size(); i++) { try { File tempFile = File.createTempFile("wado_", ".xml", AppProperties.APP_TEMP_DIR); //$NON-NLS-1$ //$NON-NLS-2$ if (GzipManager.gzipUncompressToFile(Base64.getDecoder().decode(iargs.get(i)), tempFile)) { xmlFiles.add(tempFile.getPath()); } } catch (Exception e) { LOGGER.info("ungzip manifest", e); //$NON-NLS-1$ } } LOADING_EXECUTOR.execute(new LoadRemoteDicomManifest(xmlFiles, DicomModel.this)); } // Get DICOM folder (by default DICOM, dicom, IHE_PDI, ihe_pdi) at the same level at the Weasis // executable file if (opt.isSet("portable")) { //$NON-NLS-1$ String prop = System.getProperty("weasis.portable.dicom.directory"); //$NON-NLS-1$ String baseDir = System.getProperty("weasis.portable.dir"); //$NON-NLS-1$ if (prop != null && baseDir != null) { String[] dirs = prop.split(","); //$NON-NLS-1$ for (int i = 0; i < dirs.length; i++) { dirs[i] = dirs[i].trim().replaceAll("/", File.separator); //$NON-NLS-1$ } File[] files = new File[dirs.length]; boolean notCaseSensitive = AppProperties.OPERATING_SYSTEM.startsWith("win");//$NON-NLS-1$ if (notCaseSensitive) { Arrays.sort(dirs, String.CASE_INSENSITIVE_ORDER); } String last = null; for (int i = 0; i < files.length; i++) { if (notCaseSensitive && last != null && dirs[i].equalsIgnoreCase(last)) { last = null; } else { last = dirs[i]; files[i] = new File(baseDir, dirs[i]); } } List<LoadSeries> loadSeries = null; File dcmDirFile = new File(baseDir, "DICOMDIR"); //$NON-NLS-1$ if (dcmDirFile.canRead()) { // Copy images in cache if property weasis.portable.dicom.cache = true (default is true) DicomDirLoader dirImport = new DicomDirLoader(dcmDirFile, DicomModel.this, DicomManager.getInstance().isPortableDirCache()); loadSeries = dirImport.readDicomDir(); } if (loadSeries != null && !loadSeries.isEmpty()) { LOADING_EXECUTOR.execute(new LoadDicomDir(loadSeries, DicomModel.this)); } else { LOADING_EXECUTOR.execute(new LoadLocalDicom(files, true, DicomModel.this)); } } } } public void close(String[] argv) throws IOException { final String[] usage = { "Close DICOM files", //$NON-NLS-1$ "Usage: dicom:close (-a | ([-y UID]... [-s UID]...))", //$NON-NLS-1$ " -a --all close all the patients", //$NON-NLS-1$ " -y --study=UID close a study, UID is Study Instance UID", //$NON-NLS-1$ " -s --series=UID close a series, UID is Series Instance UID", " -? --help show help" }; //$NON-NLS-1$ //$NON-NLS-2$ final Option opt = Options.compile(usage).parse(argv); final List<String> yargs = opt.getList("study"); //$NON-NLS-1$ final List<String> sargs = opt.getList("series"); //$NON-NLS-1$ if (opt.isSet("help") || (yargs.isEmpty() && sargs.isEmpty() && !opt.isSet("all"))) { //$NON-NLS-1$ //$NON-NLS-2$ opt.usage(); return; } GuiExecutor.instance().execute(() -> { firePropertyChange( new ObservableEvent(ObservableEvent.BasicAction.SELECT, DicomModel.this, null, DicomModel.this)); closeCommand(opt, yargs, sargs); }); } private void closeCommand(Option opt, List<String> yargs, List<String> sargs) { if (opt.isSet("all")) { //$NON-NLS-1$ for (MediaSeriesGroup patientGroup : model.getSuccessors(MediaSeriesGroupNode.rootNode)) { removePatient(patientGroup); } } else { if (opt.isSet("study")) { //$NON-NLS-1$ for (String studyUID : yargs) { removeStudy(getStudyNode(studyUID)); } } if (opt.isSet("series")) { //$NON-NLS-1$ for (String seriesUID : sargs) { findSeriesToRemove(seriesUID); } } } } private void findSeriesToRemove(String seriesUID) { for (MediaSeriesGroup ptGroup : model.getSuccessors(MediaSeriesGroupNode.rootNode)) { for (MediaSeriesGroup stGroup : model.getSuccessors(ptGroup)) { // Remove all the split series int k = 1; while (true) { String uid = "#" + k + "." + seriesUID; //$NON-NLS-1$ //$NON-NLS-2$ MediaSeriesGroup group = getHierarchyNode(stGroup, uid); if (group != null) { removeSeries(group); } else { break; } k++; } MediaSeriesGroup seGroup = getHierarchyNode(stGroup, seriesUID); if (seGroup != null) { removeSeries(seGroup); return; } } } } @Override public TreeModelNode getTreeModelNodeForNewPlugin() { return patient; } }