/*******************************************************************************
* FreeQDA, a software for professional qualitative research data
* analysis, such as interviews, manuscripts, journal articles, memos
* and field notes.
*
* Copyright (C) 2011 Dirk Kitscha, Jörg große Schlarmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*******************************************************************************/
package net.sf.freeqda.common.registry;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import net.sf.freeqda.common.projectmanager.ProjectManager;
import net.sf.freeqda.common.projectmanager.TextNode;
import net.sf.freeqda.common.tagregistry.TagNode;
import net.sf.freeqda.common.tagregistry.events.DocumentSelectionAddTagEvent;
import net.sf.freeqda.common.tagregistry.events.DocumentSelectionManipulateTagsListener;
import net.sf.freeqda.common.widget.TagableStyleRange;
public class DocumentRegistry {
/**
* Stores the (singleton) instance to this class
*/
private static DocumentRegistry SINGLETON_INSTANCE;
/**
* Returns the (singleton) instance of the DocumentRegistry class
* @return the (singleton) instance of the DocumentRegistry class
*/
public static final DocumentRegistry getInstance() {
if (SINGLETON_INSTANCE==null) {
synchronized (DocumentRegistry.class) {
if (SINGLETON_INSTANCE==null) {
SINGLETON_INSTANCE = new DocumentRegistry();
}
}
}
return SINGLETON_INSTANCE;
}
public static HashMap<TagNode, SimpleRangeList> createTagRangeList(TextNode textNode) {
HashMap<TagNode, SimpleRangeList> res = new HashMap<TagNode, SimpleRangeList>();
for (TagableStyleRange styleRange: textNode.getNEWStyleRanges()) {
SimpleRange range = new SimpleRange(styleRange.start, styleRange.length);
if (styleRange.getTags() != null) {
for (TagNode appliedTag: styleRange.getTags()) {
SimpleRangeList tagList = null;
if (res.containsKey(appliedTag)) {
tagList = res.get(appliedTag);
}
else {
tagList = new SimpleRangeList();
res.put(appliedTag, tagList);
}
tagList.add(range);
}
}
}
return res;
}
public static TagableStyleRange[] getTransposedStyleRanges(TextNode textNode, int sectionStart, int sectionStop /*, int charOffset */) {
TagableStyleRange[] stylesInRange = getStylesInRange(textNode, sectionStart, sectionStop);
for (TagableStyleRange range: stylesInRange) {
range.start -= sectionStart;
}
return stylesInRange;
}
private static TagableStyleRange[] getStylesInRange(TextNode textNode, int sectionStart, int sectionStop) {
LinkedList<TagableStyleRange> res = new LinkedList<TagableStyleRange>();
if (textNode.getNEWStyleRanges() != null) {
for (TagableStyleRange currentRange: textNode.getNEWStyleRanges()) {
int currentRangeStop = currentRange.start + currentRange.length;
if ((currentRange.start >= sectionStart) && (currentRangeStop <= sectionStop) && (currentRange.length > 0)) {
res.add((TagableStyleRange)currentRange.clone());
}
else if (currentRange.start < sectionStart) {
/*
* range starts before the section
*/
if (currentRangeStop >= sectionStart) {
/*
* range ende after section start => overlap
*/
TagableStyleRange resultRange = (TagableStyleRange) currentRange.clone();
if (currentRangeStop <= sectionStop) {
/*
* range ends inside the section
*/
resultRange.start = sectionStart;
resultRange.length = resultRange.length - sectionStart + currentRange.start; // stop at range.stop
}
else {
/*
* range exceeds the section
*/
resultRange.start = sectionStart;
resultRange.length = sectionStop - sectionStart; // stop at section stop
}
if (resultRange.length > 0) res.add(resultRange);
}
}
else if (currentRangeStop > sectionStop) {
/*
* range ends after the section
*/
if (currentRange.start <= sectionStop) {
/*
* range starts before section end => overlap
*/
TagableStyleRange resultRange = (TagableStyleRange) currentRange.clone();
if (currentRange.start >= sectionStart) {
/*
* range starts inside the section
*/
resultRange.start = currentRange.start;
resultRange.length = currentRange.length - currentRangeStop + sectionStop; // stop at sectionStop
}
else {
/*
* range exceeds the section
*/
resultRange.start = sectionStart;
resultRange.length = sectionStop - sectionStart; // stop at section stop
}
if (resultRange.length > 0) res.add(resultRange);
}
}
}
}
return res.toArray(new TagableStyleRange[0]);
}
/**
* This map holds the modifications applied to each text node
*/
private HashMap<TextNode, DocumentData> documentDataMap;
private final HashMap<TextNode, LinkedList<DocumentSelectionManipulateTagsListener>> tagAddedListener = new HashMap<TextNode, LinkedList<DocumentSelectionManipulateTagsListener>>();
private DocumentRegistry() {
documentDataMap = new HashMap<TextNode, DocumentData>();
}
public void init(List<TextNode> textNodeList) {
cleanup();
for (TextNode textNode: textNodeList) {
documentDataMap.put(textNode, new DocumentData(textNode));
}
updateCodeStats();
ProjectManager.getInstance().fireProjectDataModifiedEvent();
}
public void cleanup() {
documentDataMap.clear();
tagAddedListener.clear();
ProjectManager.getInstance().fireProjectDataModifiedEvent();
}
public void registerDocumentSelectionManipulateTagsListener(TextNode textNode, DocumentSelectionManipulateTagsListener listener) {
LinkedList<DocumentSelectionManipulateTagsListener> listenerList = tagAddedListener.get(textNode);
if (listenerList == null) {
listenerList = new LinkedList<DocumentSelectionManipulateTagsListener>();
tagAddedListener.put(textNode, listenerList);
}
listenerList.add(listener);
}
public void removeDocumentSelectionManipulateTagsListener(TextNode textNode, DocumentSelectionManipulateTagsListener listener) {
LinkedList<DocumentSelectionManipulateTagsListener> listenerList = tagAddedListener.get(textNode);
if (listenerList == null) {
// throw new NullPointerException("Should remove a listener for text node ("+textNode+") but the listener list is null!");
}
else {
listenerList.remove(listener);
if (listenerList.size() == 0) {
tagAddedListener.keySet().remove(textNode);
}
}
}
public void addTagToRange(TextNode textNode, TagNode tag, int selectionStart, int selectionLength) {
DocumentData documentData = documentDataMap.get(textNode);
if (documentData == null) {
documentData = new DocumentData(textNode);
documentDataMap.put(textNode, documentData);
}
/*
* add the desired range to the list
*/
HashMap<TagNode,SimpleRangeList> codedRanges = documentData.getCodedRanges();
SimpleRangeList tagRangeList = codedRanges.get(tag);
if (tagRangeList == null) {
tagRangeList = new SimpleRangeList();
codedRanges.put(tag, tagRangeList);
}
tagRangeList.add(new SimpleRange(selectionStart, selectionLength));
LinkedList<TagableStyleRange> res = new LinkedList<TagableStyleRange>();
int processingStart = selectionStart;
int selectionEnd = selectionStart + selectionLength;
/*
*/
ArrayList<TagableStyleRange> workingRanges = documentData.getWorkingStyleRanges();
for (TagableStyleRange range: workingRanges) {
int rangeEnd = range.start + range.length - 1;
/*
* check if the range is completed before the selection
*/
if (rangeEnd < processingStart) {
res.add(range);
}
/*
* check if the range is after the selection
*/
else if (range.start > selectionEnd) {
if (processingStart < selectionEnd) {
/*
* there is a gap between the last range inside the selection and
* the selection end that needs to be filled here
*/
TagableStyleRange fillRange = new TagableStyleRange(processingStart, selectionEnd-processingStart, new TagNode[] { tag });
res.add(fillRange);
processingStart = selectionEnd + 1;
}
res.add(range);
//TODO add all remaining ranges and exit the loop
}
/*
* the range overlaps the selection
*/
else {
TagableStyleRange processedRange = (TagableStyleRange) range.clone();
int processedRangeEnd = processedRange.start + processedRange.length;
if (processedRange.start < selectionStart) {
/*
* Split the range apart, add the first part unmodified and continue with the second one
*/
TagableStyleRange unmodifiedRange = (TagableStyleRange) range.clone();
unmodifiedRange.length = selectionStart - processedRange.start;
/*
* commit changes to the processed range
*/
processedRange.length -= unmodifiedRange.length;
processedRange.start = selectionStart;
res.add(unmodifiedRange);
}
/*
* check for a gap between processing start and range.start
*/
if (processingStart < processedRange.start) {
/*
* fill the gap
*/
TagableStyleRange fillRange = new TagableStyleRange(processingStart, processedRange.start-processingStart, new TagNode[] { tag });
res.add(fillRange);
processingStart = processedRange.start + 1;
}
/*
* check if the (rest of the) range is completely inside the selection
*/
if (processedRangeEnd <= selectionEnd) {
/*
* modify range and add it to result
*/
processedRange.addTag(tag);
res.add(processedRange);
processingStart = processedRangeEnd;
}
else {
/*
* update the remaining part of the range
*/
TagableStyleRange remainingRange = (TagableStyleRange) processedRange.clone();
remainingRange.length = processedRangeEnd - selectionEnd;
remainingRange.start = selectionEnd;
/*
* modify range inside the selection and add it to result
*/
processedRange.length -= remainingRange.length;
processedRange.addTag(tag);
res.add(processedRange);
res.add(remainingRange);
processingStart = selectionEnd + 1;
}
}
}
if (processingStart < selectionEnd) {
/*
* there is a gap between the last range inside the selection and
* the selection end that needs to be filled here
*/
TagableStyleRange fillRange = new TagableStyleRange(processingStart, selectionEnd-processingStart, new TagNode[] { tag });
res.add(fillRange);
}
ArrayList<TagableStyleRange> resultArrayList = new ArrayList<TagableStyleRange>(res);
documentData.setWorkingStyleRanges(resultArrayList);
/*
* notify observer
*/
fireDocumentSelectionManipulationEvent(textNode, resultArrayList, selectionStart, selectionLength);
textNode.updateCodeStats();
tag.updateCodeStats();
ProjectManager.getInstance().fireProjectDataModifiedEvent();
}
public void removeCodeFromRange(TextNode textNode, TagNode code, int selectionStart, int selectionLength) {
DocumentData documentData = documentDataMap.get(textNode);
if (documentData == null) {
documentData = new DocumentData(textNode);
documentDataMap.put(textNode, documentData);
}
if (code != null) {
/*
* remove the desired range from the list
*/
HashMap<TagNode,SimpleRangeList> codedRanges = documentData.getCodedRanges();
SimpleRangeList tagRangeList = codedRanges.get(code);
if (tagRangeList == null) {
/*
* Nothing to do - return
*/
return;
}
tagRangeList.remove(new SimpleRange(selectionStart, selectionLength));
}
else {
/*
* remove all tags from the range
*/
HashMap<TagNode,SimpleRangeList> codedRanges = documentData.getCodedRanges();
for (SimpleRangeList rangeList: codedRanges.values()) {
rangeList.remove(new SimpleRange(selectionStart, selectionLength));
}
}
LinkedList<TagableStyleRange> res = new LinkedList<TagableStyleRange>();
int processingStart = selectionStart;
int selectionEnd = selectionStart + selectionLength;
/*
*/
for (TagableStyleRange range: documentData.getWorkingStyleRanges()) {
int rangeEnd = range.start + range.length - 1;
/*
* check if the range is completed before or the selection or starts after it
*/
if ((rangeEnd < processingStart) || (range.start > selectionEnd)) {
res.add(range);
}
/*
* Selection overlaps the range
*/
else {
/*
* check if the range contains the code to remove
*/
if ((code == null) || (range.containsTag(code))) {
/*
* check if the front, end or both overlap with the selection
*/
TagableStyleRange processedRange = (TagableStyleRange) range.clone();
int processedRangeEnd = processedRange.start + processedRange.length;
if (processedRange.start < selectionStart) {
/*
* Split the range apart, add the first part unmodified and continue with the second one
*/
TagableStyleRange frontRange = (TagableStyleRange) processedRange.clone();
frontRange.length = selectionStart - processedRange.start;
/*
* commit changes to the processed range
*/
processedRange.length -= frontRange.length;
processedRange.start = selectionStart;
res.add(frontRange);
}
TagableStyleRange remainingRange = null;
if (processedRangeEnd > selectionEnd) {
remainingRange = (TagableStyleRange) processedRange.clone();
remainingRange.length = processedRangeEnd - selectionEnd;
remainingRange.start = selectionEnd;
/*
* modify range inside the selection
*/
processedRange.length -= remainingRange.length;
}
if (code != null) {
/*
* remove the code from the (remaining) range
*/
processedRange.removeTag(code);
}
else {
/*
* remove the all codes from the (remaining) range
*/
processedRange.removeAllTags();
}
res.add(processedRange);
if (remainingRange != null) res.add(remainingRange);
}
else {
res.add(range);
}
}
}
ArrayList<TagableStyleRange> resultArrayList = new ArrayList<TagableStyleRange>(res);
documentData.setWorkingStyleRanges(resultArrayList);
/*
* notify observer
*/
fireDocumentSelectionManipulationEvent(textNode, resultArrayList, selectionStart, selectionLength);
textNode.updateCodeStats();
code.updateCodeStats();
ProjectManager.getInstance().fireProjectDataModifiedEvent();
}
private void fireDocumentSelectionManipulationEvent(TextNode textNode, ArrayList<TagableStyleRange> affectedRanges, int start, int length) {
if ((textNode != null) && (affectedRanges != null)){
DocumentSelectionAddTagEvent event = new DocumentSelectionAddTagEvent(this, affectedRanges, start, length);
LinkedList<DocumentSelectionManipulateTagsListener> listeners = tagAddedListener.get(textNode);
if (listeners == null) return;
for (DocumentSelectionManipulateTagsListener listener: listeners) {
listener.DocumentSelectionTagAdded(event);
}
}
}
public SimpleRangeList getPassagesFor(TextNode textNode, TagNode tag) {
HashMap<TagNode, SimpleRangeList> tagMap = textNode.getCodedRangesList();
if (tagMap.containsKey(tag)) {
return tagMap.get(tag);
}
else return new SimpleRangeList();
}
public HashMap<TagNode, SimpleRange> getPassagesAt(TextNode textNode, int charOffset) {
HashMap<TagNode, SimpleRange> res = new HashMap<TagNode, SimpleRange>();
DocumentData docData = documentDataMap.get(textNode);
HashMap<TagNode, SimpleRangeList> codedRangesMap = docData.getCodedRanges();
for (TagNode code: codedRangesMap.keySet()) {
SimpleRangeList codedRangesList = codedRangesMap.get(code);
SimpleRange fittingRange = codedRangesList.getRangeAt(charOffset);
if (fittingRange != null) {
res.put(code, fittingRange);
}
}
return res;
}
public DocumentData getDocumentData(TextNode textNode) {
return documentDataMap.get(textNode);
}
public HashMap<TextNode, DocumentData> getDocumentDataMap() {
return documentDataMap;
}
public Set<TextNode> getTextNodes() {
return documentDataMap.keySet();
}
public void resetDocumentData(TextNode textNode) {
DocumentData docData = new DocumentData(textNode);
documentDataMap.put(textNode, docData);
textNode.updateCodeStats();
ProjectManager.getInstance().fireProjectDataModifiedEvent();
}
public String dump() {
StringBuilder sb = new StringBuilder();
for (TextNode node: documentDataMap.keySet()) {
sb.append(MessageFormat.format("TextNode: %1\nDocument Data: %2", new Object[] {node.getName(), documentDataMap.get(node)})); //$NON-NLS-1$
}
return documentDataMap.toString();
}
public void updateCodeStats() {
for (TextNode textNode: documentDataMap.keySet()) {
textNode.updateCodeStats();
}
}
}