Blob Blame History Raw
diff -r bcb9acfa66de -r effd078610a7 .hgtags
--- a/.hgtags	Tue May 31 13:04:58 2016 +1000
+++ b/.hgtags	Tue Jun 21 12:45:20 2016 +0100
@@ -16,3 +16,4 @@
 9fcee7d53bc2a2b69db9dbe59c6f1264615a54e4 3Depict-0.0.16
 19a65d51c5d65ebf3d8b7760cfa4edde36c251c0 3Depict-0.0.17
 910618ab1f369c5a9253965e12f5d8f75fceb0c6 3Depict-0.0.18
+bcb9acfa66de56f6531016d1e81a68d7118b7b35 3Depict-0.0.19
diff -r bcb9acfa66de -r effd078610a7 docs/manual-latex/build-latex.sh
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/docs/manual-latex/build-latex.sh	Tue Jun 21 12:45:20 2016 +0100
@@ -0,0 +1,37 @@
+#!/bin/bash
+LATEXBIN=pdflatex
+BIBTEXBIN=bibtex
+if [ x`which $LATEXBIN` == x"" ] ; then
+	echo "no $LATEXBIN" 
+	exit 1
+fi
+
+if [ x`which $BIBTEXBIN` == x"" ] ; then
+	echo "no $BIBTEXBIN" 
+	exit 1
+fi
+
+MANUAL=manual.tex
+
+#check to see if the manual and its bib file are around
+if [ ! -f $MANUAL  ] ; then
+	echo "$MANUAL is missing"
+	exit 1
+fi
+
+#Remove the old manul
+rm -f manual.pdf
+
+
+
+#run the multi-pass latex build. 
+$LATEXBIN $MANUAL || { echo "failed latex build (pass 1 of 2)"; exit 1 ; }
+$BIBTEXBIN `basename -s .tex $MANUAL` || { echo "failed bibtex build"; exit 1 ; }
+$LATEXBIN $MANUAL || { echo "failed latex build (pass 2 of 2)"; exit 1 ; }
+
+
+if [ ! -f manual.pdf ] ; then
+	echo "latex ran, but somehow we did not output the PDF...."
+	exit 1
+fi
+
diff -r bcb9acfa66de -r effd078610a7 src/backend/filter.cpp
--- a/src/backend/filter.cpp	Tue May 31 13:04:58 2016 +1000
+++ b/src/backend/filter.cpp	Tue Jun 21 12:45:20 2016 +0100
@@ -553,7 +553,7 @@
 		ASSERT(scatterIntensity.empty());
 
 
-	ASSERT(plotType < PLOT_TYPE_ENUM_END);
+	ASSERT(plotStyle < PLOT_TYPE_ENUM_END);
 }
 void RangeStreamData::checkSelfConsistent() const
 {
diff -r bcb9acfa66de -r effd078610a7 src/backend/filter.h
--- a/src/backend/filter.h	Tue May 31 13:04:58 2016 +1000
+++ b/src/backend/filter.h	Tue Jun 21 12:45:20 2016 +0100
@@ -395,7 +395,7 @@
 		//Label for X, Y axes
 		std::string xLabel,yLabel;
 
-		unsigned int plotType;
+		unsigned int plotStyle;
 
 		//!Structured XY data pairs for plotting curve
 		Array2D<float> xyData;
diff -r bcb9acfa66de -r effd078610a7 src/backend/filters/algorithms/rdf.cpp
--- a/src/backend/filters/algorithms/rdf.cpp	Tue May 31 13:04:58 2016 +1000
+++ b/src/backend/filters/algorithms/rdf.cpp	Tue Jun 21 12:45:20 2016 +0100
@@ -862,6 +862,8 @@
 		return RDF_ABORT_FAIL;
 #endif
 
+	*progressPtr=100;
+
 	return 0;
 }
 
@@ -1003,6 +1005,8 @@
 #endif
 
 	//Calculations complete!
+	*progressPtr=100;
+
 	return 0;
 }
 
diff -r bcb9acfa66de -r effd078610a7 src/backend/filters/annotation.cpp
--- a/src/backend/filters/annotation.cpp	Tue May 31 13:04:58 2016 +1000
+++ b/src/backend/filters/annotation.cpp	Tue Jun 21 12:45:20 2016 +0100
@@ -167,8 +167,15 @@
 
 	//If we are not enabled, do not draw anything into the output
 	if(!active)
+	{
+		progress.filterProgress=100;
 		return 0;
+	}
 
+	progress.step=1;
+	progress.maxStep=1;
+	progress.stepName=TRANS("Draw");
+	
 	DrawStreamData *d; 
 	d = new DrawStreamData;
 	d->parent=this;
@@ -505,6 +512,7 @@
 	d->cached=0;
 	getOut.push_back(d);
 
+	progress.filterProgress=100;
 	return 0;
 }
 
diff -r bcb9acfa66de -r effd078610a7 src/backend/filters/boundingBox.cpp
--- a/src/backend/filters/boundingBox.cpp	Tue May 31 13:04:58 2016 +1000
+++ b/src/backend/filters/boundingBox.cpp	Tue Jun 21 12:45:20 2016 +0100
@@ -510,7 +510,6 @@
 				}
 #endif
 				bTotal.expand(bThis);
-				progress.filterProgress=100;
 				break;
 			}
 			default:
@@ -521,6 +520,7 @@
 		getOut.push_back(dataIn[ui]);	
 	}
 
+	progress.filterProgress=100;
 	//Append the bounding box if it is valid
 	if(bTotal.isValid())
 	{
diff -r bcb9acfa66de -r effd078610a7 src/backend/filters/clusterAnalysis.cpp
--- a/src/backend/filters/clusterAnalysis.cpp	Tue May 31 13:04:58 2016 +1000
+++ b/src/backend/filters/clusterAnalysis.cpp	Tue Jun 21 12:45:20 2016 +0100
@@ -685,7 +685,7 @@
 		Plot2DStreamData *p = new Plot2DStreamData;
 		p->parent=this;
 
-		p->plotType=PLOT_2D_SCATTER;
+		p->plotStyle=PLOT_2D_SCATTER;
 		p->dataLabel=TRANS("Morphology Plot");
 		p->xLabel=TRANS("\\lambda_1:\\lambda_2 ratio");
 		p->yLabel=TRANS("\\lambda_2:\\lambda_3 ratio");
diff -r bcb9acfa66de -r effd078610a7 src/backend/filters/ionColour.cpp
--- a/src/backend/filters/ionColour.cpp	Tue May 31 13:04:58 2016 +1000
+++ b/src/backend/filters/ionColour.cpp	Tue Jun 21 12:45:20 2016 +0100
@@ -271,12 +271,14 @@
 	
 
 	p.name=TRANS("Show Bar");
+	p.helpText=TRANS("Display the colour legend in the 3D view");
 	p.key=KEY_IONCOLOURFILTER_SHOWBAR;
 	p.data=boolStrEnc(showColourBar);
 	p.type=PROPERTY_TYPE_BOOL;
 	propertyList.addProperty(p,curGroup);
 	
 	p.name=TRANS("Opacity");
+	p.helpText=TRANS("How see-through to make the legend (0- transparent, 1- solid)");
 	p.key=KEY_IONCOLOURFILTER_ALPHA;
 	stream_cast(p.data,alpha);
 	p.type=PROPERTY_TYPE_REAL;
diff -r bcb9acfa66de -r effd078610a7 src/backend/filters/ionDownsample.cpp
--- a/src/backend/filters/ionDownsample.cpp	Tue May 31 13:04:58 2016 +1000
+++ b/src/backend/filters/ionDownsample.cpp	Tue Jun 21 12:45:20 2016 +0100
@@ -88,7 +88,7 @@
 			rsdIncoming = new RangeStreamData;
 			*rsdIncoming=*c;
 
-			if(ionFractions.size() != c->rangeFile->getNumIons())
+			if(ionFractions.size() != c->rangeFile->getNumIons()+1)
 			{
 				//set up some defaults; seeded from normal
 				ionFractions.resize(c->rangeFile->getNumIons()+1,fraction);
@@ -464,7 +464,8 @@
 
 	propertyList.setGroupTitle(curGroup,TRANS("Mode"));
 	curGroup++;
-	if(rsdIncoming && perSpecies)
+
+	if(rsdIncoming && perSpecies && rsdIncoming->enabledIons.size())
 	{
 		unsigned int typeVal;
 		if(fixedNumOut)
diff -r bcb9acfa66de -r effd078610a7 src/backend/filters/ionInfo.cpp
--- a/src/backend/filters/ionInfo.cpp	Tue May 31 13:04:58 2016 +1000
+++ b/src/backend/filters/ionInfo.cpp	Tue Jun 21 12:45:20 2016 +0100
@@ -398,7 +398,7 @@
 
 
 	//"Pairwise events" - where we perform an action if both 
-	//These
+	//these are set
 	if(wantIonCounts && wantVolume)
 	{
 		if(computedVol > sqrtf(std::numeric_limits<float>::epsilon()))
@@ -420,6 +420,9 @@
 		}
 	}
 
+
+	progress.filterProgress=100;
+	
 	return 0;
 }
 
diff -r bcb9acfa66de -r effd078610a7 src/backend/filters/spatialAnalysis.cpp
--- a/src/backend/filters/spatialAnalysis.cpp	Tue May 31 13:04:58 2016 +1000
+++ b/src/backend/filters/spatialAnalysis.cpp	Tue Jun 21 12:45:20 2016 +0100
@@ -655,115 +655,180 @@
 		return ERR_FILE_READ_FAIL;
 
 
-
-	progress.step=3;
-	progress.stepName=TRANS("Build");
-	progress.filterProgress=0;
-
-	//Build the search tree we will use to perform replacement
-	K3DTreeMk2 tree;
-	tree.resetPts(fileIons,false);
-	if(!tree.build())
-		return ERR_ABORT_FAIL;
-	BoundCube b;
-	tree.getBoundCube(b);
-
-	//map the offset of the nearest to
-	//the tree ID 
-	vector<size_t > nearestVec;
-	nearestVec.resize(inIons.size());
-
-	//TODO: pair vector might be faster
-	// as we can use it in sequence, and can use openmp
-	map<size_t,size_t> matchedMap;
-
-	//Find the nearest point for all points in the dataset
-
-	#pragma omp parallel for 
-	for(size_t ui=0;ui<inIons.size();ui++)
+	vector<IonHit> outIons;
+	if(inIons.empty() || fileIons.empty())
 	{
-		nearestVec[ui]=tree.findNearestUntagged(inIons[ui].getPos(),b,false);
-	}
-
-	float sqrReplaceTol=replaceTolerance*replaceTolerance;
-
-	//Filter this to only points that had an NN within range
-	#pragma omp parallel for 
-	for(size_t ui=0;ui<inIons.size();ui++)
-	{
-		if(nearestVec[ui]!=(size_t)-1 && inIons[ui].getPos().sqrDist(*tree.getPt(nearestVec[ui])) <=sqrReplaceTol)
+		//Performance increase if we have an empty item.
+		// - in this case we can swap sets around
+		switch(replaceMode)
 		{
-			#pragma omp critical
-			matchedMap[ui]=tree.getOrigIndex(nearestVec[ui]);
+			case REPLACE_MODE_UNION:
+			{
+				//If the local data is empty, then the union is just the "b" data (swap).
+				// if nonempty, then it is simply the "a" data 
+				if(inIons.empty())
+					outIons.swap(fileIons);
+				else
+					outIons.swap(inIons);
+				break;
+			}
+			case REPLACE_MODE_SUBTRACT:
+			{
+				//if either localdata OR bdata is empty, then we don't need to do anything.
+				// either way, input stays as it is
+				outIons.swap(inIons);
+				break;
+			}
+			case REPLACE_MODE_INTERSECT:
+			{
+				//intersection with empty set is empty set.
+				// might as well clear the ions incoming
+				inIons.clear();
+				break;
+			}
+			default:
+				ASSERT(false);
+
 		}
 	}
-
-	nearestVec.clear();
-
-
-	progress.step=4;
-	progress.stepName=TRANS("Compute");
-	progress.filterProgress=0;
-
-	//Finish if no matches
-	if(matchedMap.empty())
+	else
 	{
-		progress.filterProgress=100;
-		return 0;
-	}
-
-	vector<IonHit> outIons;
-	switch(replaceMode)
-	{
-		case REPLACE_MODE_SUBTRACT:
+
+		progress.step=3;
+		progress.stepName=TRANS("Build");
+		progress.filterProgress=0;
+
+		//TODO: Possible speed increase by finding the smaller of
+		// the two inputs, and using that to build the tree
+
+		//Build the search tree we will use to perform replacement
+		K3DTreeMk2 tree;
+		tree.resetPts(fileIons,false);
+		if(!tree.build())
+			return ERR_ABORT_FAIL;
+		BoundCube b;
+		tree.getBoundCube(b);
+
+		//map the offset of the nearest to
+		//the tree ID 
+		vector<size_t > nearestVec;
+		nearestVec.resize(inIons.size());
+
+		//TODO: pair vector might be faster
+		// as we can use it in sequence, and can use openmp
+		map<size_t,size_t> matchedMap;
+
+		//Find the nearest point for all points in the dataset
+		// maps the ith ion in "inions" to the tree value
+		#pragma omp parallel for 
+		for(size_t ui=0;ui<inIons.size();ui++)
 		{
-			//In subtraction mode, we should have
-			// at least this many ions
-			if(inIons.size() > matchedMap.size())
-				outIons.reserve(inIons.size()-matchedMap.size());
-			
-			//
-			#pragma omp parallel for
-			for(unsigned int ui=0;ui<inIons.size();ui++)
+			nearestVec[ui]=tree.findNearestUntagged(inIons[ui].getPos(),b,false);
+		}
+
+		float sqrReplaceTol=replaceTolerance*replaceTolerance;
+
+		//Filter this to only points that had an NN within range
+		#pragma omp parallel for 
+		for(size_t ui=0;ui<inIons.size();ui++)
+		{
+			if(nearestVec[ui]!=(size_t)-1 && inIons[ui].getPos().sqrDist(*tree.getPt(nearestVec[ui])) <=sqrReplaceTol)
 			{
-				map<size_t,size_t>::iterator it;
-				it=matchedMap.find(ui);
-				if(it != matchedMap.end())
-					continue;
-
 				#pragma omp critical
-				outIons.push_back(inIons[ui]);
+				matchedMap[ui]=tree.getOrigIndex(nearestVec[ui]);
 			}
-			break;
 		}
-		case REPLACE_MODE_INTERSECT:
+
+		nearestVec.clear();
+
+
+		progress.step=4;
+		progress.stepName=TRANS("Compute");
+		progress.filterProgress=0;
+
+
+		//now we have a map that matches as so:
+		// map ( "inIon" ID -> "fileIon" ID)
+		// inIon should be our "A" in "A operator B"
+		switch(replaceMode)
 		{
-			outIons.reserve(matchedMap.size());
-
-			if(replaceMass)
+			case REPLACE_MODE_SUBTRACT:
 			{
-				for(map<size_t,size_t>::const_iterator it=matchedMap.begin();it!=matchedMap.end();++it)
+				//If no matches, A-0 = A. Just return input
+				if(matchedMap.empty())
 				{
-					outIons.push_back(fileIons[it->second]);
-					ASSERT(fileIons[it->second].getPosRef().sqrDist(inIons[it->first].getPosRef()) < sqrReplaceTol);
+					outIons.swap(inIons);
+					break;
 				}
+				//In subtraction mode, we should have
+				// at least this many ions
+				if(inIons.size() > matchedMap.size())
+					outIons.reserve(inIons.size()-matchedMap.size());
+				
+				//
+				#pragma omp parallel for
+				for(unsigned int ui=0;ui<inIons.size();ui++)
+				{
+					map<size_t,size_t>::iterator it;
+					it=matchedMap.find(ui);
+					if(it != matchedMap.end())
+						continue;
+
+					#pragma omp critical
+					outIons.push_back(inIons[ui]);
+				}
+				break;
 			}
-			else
+			case REPLACE_MODE_INTERSECT:
 			{
-				for(map<size_t,size_t>::const_iterator it=matchedMap.begin();it!=matchedMap.end();++it)
+				//Finish if no matches
+				if(matchedMap.empty())
+					break;
+				
+				outIons.reserve(matchedMap.size());
+
+				if(replaceMass)
 				{
-					outIons.push_back(inIons[it->first]);
+					for(map<size_t,size_t>::const_iterator it=matchedMap.begin();it!=matchedMap.end();++it)
+					{
+						outIons.push_back(fileIons[it->second]);
+						ASSERT(fileIons[it->second].getPosRef().sqrDist(inIons[it->first].getPosRef()) < sqrReplaceTol);
+					}
 				}
+				else
+				{
+					for(map<size_t,size_t>::const_iterator it=matchedMap.begin();it!=matchedMap.end();++it)
+					{
+						outIons.push_back(inIons[it->first]);
+					}
+				}
+				break;
 			}
-			break;
+			case REPLACE_MODE_UNION:
+			{
+				outIons.swap(fileIons);
+				outIons.reserve(outIons.size() + fileIons.size() - matchedMap.size());
+				map<size_t,size_t>::const_iterator it=matchedMap.begin();
+				
+
+				for(unsigned int ui=0;ui<inIons.size();ui++)
+				{
+					if(it !=matchedMap.end() && (it->first == ui) )
+					{
+						it++;
+						continue;
+					}
+
+
+					outIons.push_back(inIons[ui]);
+				}
+
+
+				break;
+			}
+			default:
+				ASSERT(false);
 		}
-		case REPLACE_MODE_UNION:
-		{
-			ASSERT(false);
-			break;
-		}
-		default:
-			ASSERT(false);
 	}
 
 	//Only output ions if any were found
@@ -2779,6 +2844,8 @@
 				newD->data.resize(d->data.size());
 				if(stopMode == STOP_MODE_NEIGHBOUR)
 				{
+					unsigned int nProg=0;
+
 					bool spin=false;
 					#pragma omp parallel for shared(spin)
 					for(size_t uj=0;uj<d->data.size();uj++)
@@ -2811,14 +2878,15 @@
 						}
 
 						res.clear();
+						#pragma atomic
+						nProg++;
 						
 						//Update progress as needed
 						if(!curProg--)
 						{
 							#pragma omp critical 
 							{
-							n+=NUM_CALLBACK/(nnMax);
-							progress.filterProgress= (unsigned int)(((float)n/(float)totalDataSize)*100.0f);
+							progress.filterProgress= (unsigned int)(((float)nProg/(float)totalDataSize)*100.0f);
 							if(*Filter::wantAbort)
 								spin=true;
 							curProg=NUM_CALLBACK/(nnMax);
@@ -3063,9 +3131,10 @@
 				IonStreamData *newD = new IonStreamData;
 				newD->parent=this;
 
-				//Adjust this number to provide more update thanusual, because we
+				//Adjust this number to provide more update than usual, because we
 				//are not doing an o(1) task between updates; yes, it is a hack
-				unsigned int curProg=NUM_CALLBACK/(10*nnMax);
+				const unsigned int PROG_PER_PASS=NUM_CALLBACK/(10*nnMax);
+				unsigned int curProg=PROG_PER_PASS;
 				newD->data.reserve(d->data.size());
 				if(stopMode == STOP_MODE_NEIGHBOUR)
 				{
@@ -3113,11 +3182,11 @@
 						{
 							#pragma omp critical 
 							{
-							n+=NUM_CALLBACK/(nnMax);
+							n+=PROG_PER_PASS;
 							progress.filterProgress= (unsigned int)(((float)n/(float)totalDataSize)*100.0f);
 							if(*Filter::wantAbort)
 								spin=true;
-							curProg=NUM_CALLBACK/(nnMax);
+							curProg=PROG_PER_PASS;
 							}
 						}
 					}
@@ -3263,6 +3332,8 @@
 				break;
 		}
 	}
+	progress.filterProgress=100;
+
 	//If we have bad points, let the user know.
 	if(!badPts.empty())
 	{
@@ -4230,11 +4301,8 @@
 				}
 
 				//distance between search pt and found pt
-				float sqrDistance;
-				sqrDistance = searchTree.getPtRef(ptIdx).sqrDist(pSource[ui].getPosRef());
-
-				if(sqrDistance > DISTANCE_EPSILON)
-					ptsFound.insert(ptIdx);
+
+				ptsFound.insert(ptIdx);
 			}
 
 
@@ -4244,8 +4312,14 @@
 			//Count the number of numerator and denominator ions, using the masses we set aside earlier
 			for(set<size_t>::iterator it=ptsFound.begin(); it!=ptsFound.end(); ++it)
 			{
+
+				//check that the distance is non-zero, to force no self-matching
+				float sqrDistance;
+				sqrDistance = searchTree.getPtRef(*it).sqrDist(pSource[ui].getPosRef());
+				if(sqrDistance < DISTANCE_EPSILON)
+					continue;
+
 				float ionMass;
-				//check that the distance is non-zero, to force no self-matching
 				ionMass = dataMasses[searchTree.getOrigIndex(*it)];
 
 				unsigned int ionID;
@@ -4326,9 +4400,11 @@
 bool nnHistogramTest();
 bool rdfPlotTest();
 bool axialDistTest();
-bool replaceTest();
+bool replaceIntersectAndUnionTest();
 bool localConcTestRadius();
 bool localConcTestNN();
+bool replaceSubtractTest();
+bool replaceUnionTest();
 
 bool SpatialAnalysisFilter::runUnitTests()
 {
@@ -4343,8 +4419,15 @@
 
 	if(!axialDistTest())
 		return false;
-	if(!replaceTest())
+	if(!replaceIntersectAndUnionTest())
 		return false;
+
+	if(!replaceSubtractTest())
+		return false;
+
+	if(!replaceUnionTest())
+		return false;
+
 	if(!localConcTestRadius())
 		return false;
 
@@ -4617,7 +4700,7 @@
 	return true;
 }
 
-bool replaceTest()
+bool replaceIntersectAndUnionTest()
 {
 	std::string ionFile=createTmpFilename(NULL,".pos");
 		
@@ -4637,6 +4720,157 @@
 	//Create a spatial analysis filter
 	SpatialAnalysisFilter *f=new SpatialAnalysisFilter;
 	f->setCaching(false);	
+	//Set it to do a union calculation 
+	bool needUp;
+	string s;
+	s=TRANS(SPATIAL_ALGORITHMS[ALGORITHM_REPLACE]);
+	TEST(f->setProperty(KEY_ALGORITHM,s,needUp),"Set prop");
+	TEST(f->setProperty(KEY_REPLACE_FILE,ionFile,needUp),"Set prop");
+	s="1";
+	TEST(f->setProperty(KEY_REPLACE_VALUE,s,needUp),"Set prop");
+
+	vector<unsigned int> opVec;
+	opVec.push_back(REPLACE_MODE_INTERSECT);
+	opVec.push_back(REPLACE_MODE_UNION);
+	
+	ProgressData p;
+	vector<const FilterStreamData*> streamIn,streamOut;
+	streamIn.push_back(d);
+	for(unsigned int opId=0;opId<opVec.size();opId++)
+	{
+		s=TRANS(REPLACE_ALGORITHMS[opVec[opId]]);
+		TEST(f->setProperty(KEY_REPLACE_ALGORITHM,s,needUp),"Set prop");
+
+		//Do the refresh
+		TEST(!f->refresh(streamIn,streamOut,p),"refresh OK");
+
+		TEST(streamOut.size() == 1,"stream count");
+		TEST(streamOut[0]->getStreamType() == STREAM_TYPE_IONS,"stream type");
+		TEST(streamOut[0]->getNumBasicObjects() == NIONS,"Number objects");
+
+		//we should have taken the mass-to-charge from the file
+		const IonStreamData *outIons = (const IonStreamData*)streamOut[0];
+		for(unsigned int ui=0;ui<NIONS; ui++)
+		{
+			ASSERT(outIons->data[ui].getMassToCharge() == 1); 
+		}
+		delete streamOut[0];
+		streamOut.clear();
+	}
+	delete f;
+	delete d;
+	
+	wxRemoveFile(ionFile);
+
+	
+	return true;
+}
+
+bool replaceSubtractTest()
+{
+	std::string ionFile=createTmpFilename(NULL,".pos");
+		
+	vector<IonHit> ions;
+	const unsigned int NIONS=10;
+	const unsigned int DIFF_COUNT=5;	
+	for(unsigned int ui=0;ui<NIONS;ui++)
+	{
+		IonHit h;
+		h = IonHit(Point3D(ui,ui,ui),1);
+
+		//make some ions different to the (x,x,x) pattern
+		if(ui < DIFF_COUNT)
+		{
+			h.setPos(h.getPos() - Point3D(0,0,100));
+			ions.push_back(h);
+		}
+		else
+			ions.push_back(h);
+	}
+
+	vector<IonHit> tmpI;
+	for(unsigned int ui=0;ui<NIONS;ui++)
+	{
+		if(ions[ui][2] < 0 )
+		{
+			tmpI.push_back(ions[ui]);
+			tmpI.back().setMassToCharge(2);
+		}
+	}
+
+	IonHit::makePos(tmpI,ionFile.c_str());
+	tmpI.clear();
+
+	IonStreamData *d = new IonStreamData;
+	d->data.swap(ions);
+
+	//Create a spatial analysis filter
+	SpatialAnalysisFilter *f=new SpatialAnalysisFilter;
+	f->setCaching(false);	
+	
+	//Set it to do a subtraction calculation 
+	bool needUp;
+	string s;
+	s=TRANS(SPATIAL_ALGORITHMS[ALGORITHM_REPLACE]);
+	TEST(f->setProperty(KEY_ALGORITHM,s,needUp),"Set prop");
+	TEST(f->setProperty(KEY_REPLACE_FILE,ionFile,needUp),"Set prop");
+	s=TRANS(REPLACE_ALGORITHMS[REPLACE_MODE_SUBTRACT]);
+	TEST(f->setProperty(KEY_REPLACE_ALGORITHM,s,needUp),"Set prop");
+
+	//Do the refresh
+	ProgressData p;
+	vector<const FilterStreamData*> streamIn,streamOut;
+	streamIn.push_back(d);
+	TEST(!f->refresh(streamIn,streamOut,p),"refresh OK");
+	delete f;
+	delete d;
+	streamIn.clear();
+
+	TEST(streamOut.size() == 1,"stream count");
+	TEST(streamOut[0]->getStreamType() == STREAM_TYPE_IONS,"stream type");
+	TEST(streamOut[0]->getNumBasicObjects() == DIFF_COUNT,"Number objects");
+
+	//we should have taken the mass-to-charge from the original data,
+	// not the file
+	const IonStreamData *outIons = (const IonStreamData*)streamOut[0];
+	for(unsigned int ui=0;ui<outIons->getNumBasicObjects(); ui++)
+	{
+		ASSERT(outIons->data[ui].getMassToCharge() == 1); 
+		ASSERT(outIons->data[ui].getPos()[2] >= 0); 
+	}
+
+	wxRemoveFile(ionFile);
+
+	delete streamOut[0];
+	
+	return true;
+}
+
+bool replaceUnionTest()
+{
+	std::string ionFile=createTmpFilename(NULL,".pos");
+	
+	//"B" dataset	
+	vector<IonHit> ions;
+	ions.push_back(IonHit(Point3D(0,0,0),1));
+	ions.push_back(IonHit(Point3D(1,0,1),1));
+	ions.push_back(IonHit(Point3D(0,1,1),1));
+
+	IonHit::makePos(ions,ionFile.c_str());
+	ions.clear();
+
+	//"A" dataset	
+	ions.push_back(IonHit(Point3D(0,0,0),2));
+	ions.push_back(IonHit(Point3D(1,0,-1),2));
+	ions.push_back(IonHit(Point3D(0,1,-1),2));
+
+
+	IonStreamData *d = new IonStreamData;
+	d->data.swap(ions);
+
+	//Create a spatial analysis filter
+	SpatialAnalysisFilter *f=new SpatialAnalysisFilter;
+	f->setCaching(false);	
 	
 	//Set it to do a union calculation 
 	bool needUp;
@@ -4644,13 +4878,11 @@
 	s=TRANS(SPATIAL_ALGORITHMS[ALGORITHM_REPLACE]);
 	TEST(f->setProperty(KEY_ALGORITHM,s,needUp),"Set prop");
 	TEST(f->setProperty(KEY_REPLACE_FILE,ionFile,needUp),"Set prop");
-	s=TRANS(REPLACE_ALGORITHMS[REPLACE_MODE_INTERSECT]);
+	s=TRANS(REPLACE_ALGORITHMS[REPLACE_MODE_UNION]);
 	TEST(f->setProperty(KEY_REPLACE_ALGORITHM,s,needUp),"Set prop");
-	
 	s="1";
 	TEST(f->setProperty(KEY_REPLACE_VALUE,s,needUp),"Set prop");
 
-
 	//Do the refresh
 	ProgressData p;
 	vector<const FilterStreamData*> streamIn,streamOut;
@@ -4662,14 +4894,16 @@
 
 	TEST(streamOut.size() == 1,"stream count");
 	TEST(streamOut[0]->getStreamType() == STREAM_TYPE_IONS,"stream type");
-	TEST(streamOut[0]->getNumBasicObjects() == NIONS,"Number objects");
-
-	//we should have taken the mass-to-charge from the file
+	TEST(streamOut[0]->getNumBasicObjects() == 5,"Number objects");
+
+	//There should be
 	const IonStreamData *outIons = (const IonStreamData*)streamOut[0];
-	for(unsigned int ui=0;ui<NIONS; ui++)
+	float sumV=0;
+	for(unsigned int ui=0;ui<outIons->getNumBasicObjects(); ui++)
 	{
-		ASSERT(outIons->data[ui].getMassToCharge() == 1); 
+		sumV+=outIons->data[ui].getMassToCharge();
 	}
+	TEST( EQ_TOL(sumV,7.0f),"mass-to-charge check");
 
 	wxRemoveFile(ionFile);
 
@@ -4678,7 +4912,6 @@
 	return true;
 }
 
-
 //--- Local concentration tests --
 const IonStreamData *createLCIonStream()
 {
diff -r bcb9acfa66de -r effd078610a7 src/backend/filters/spectrumPlot.cpp
--- a/src/backend/filters/spectrumPlot.cpp	Tue May 31 13:04:58 2016 +1000
+++ b/src/backend/filters/spectrumPlot.cpp	Tue Jun 21 12:45:20 2016 +0100
@@ -517,6 +517,8 @@
 	
 	getOut.push_back(d);
 
+	progress.filterProgress=100;
+
 	return 0;
 }
 
diff -r bcb9acfa66de -r effd078610a7 src/backend/filters/transform.cpp
--- a/src/backend/filters/transform.cpp	Tue May 31 13:04:58 2016 +1000
+++ b/src/backend/filters/transform.cpp	Tue Jun 21 12:45:20 2016 +0100
@@ -998,8 +998,10 @@
 					break;
 				}
 			
+			}
 		}
-		}
+
+		progress.filterProgress=100;
 	}
 	else
 	{
@@ -1526,6 +1528,11 @@
 		case KEY_CROP_MINIMUM:
 		{
 			ASSERT(scalarParams.size() ==2);
+			float tmp;
+			if(stream_cast(tmp,value) || tmp >=scalarParams[1])
+				return false;
+
+
 			if(!applyPropertyNow(scalarParams[0],value,needUpdate))
 				return false;
 			break;
@@ -1533,6 +1540,9 @@
 		case KEY_CROP_MAXIMUM:
 		{
 			ASSERT(scalarParams.size() ==2);
+			float tmp;
+			if(stream_cast(tmp,value) || tmp <=scalarParams[0])
+				return false;
 			if(!applyPropertyNow(scalarParams[1],value,needUpdate))
 				return false;
 			break;
diff -r bcb9acfa66de -r effd078610a7 src/backend/plot.cpp
--- a/src/backend/plot.cpp	Tue May 31 13:04:58 2016 +1000
+++ b/src/backend/plot.cpp	Tue Jun 21 12:45:20 2016 +0100
@@ -29,7 +29,7 @@
 				NTRANS("Moving avg.")
 				};
 
-const char *plotTypeStrings[]= {
+const char *traceStyleStrings[]= {
 	NTRANS("Lines"),
 	NTRANS("Bars"),
 	NTRANS("Steps"),
@@ -119,15 +119,15 @@
 string plotString(unsigned int plotMode)
 {
 	ASSERT(plotMode< PLOT_TYPE_ENUM_END);
-	return TRANS(plotTypeStrings[plotMode]); 
+	return TRANS(traceStyleStrings[plotMode]); 
 }
 
 unsigned int plotID(const std::string &plotString)
 {
-	COMPILE_ASSERT(THREEDEP_ARRAYSIZE(plotTypeStrings) == PLOT_TYPE_ENUM_END);
+	COMPILE_ASSERT(THREEDEP_ARRAYSIZE(traceStyleStrings) == PLOT_TYPE_ENUM_END);
 	for(unsigned int ui=0;ui<PLOT_TYPE_ENUM_END; ui++)
 	{
-		if(plotString==TRANS(plotTypeStrings[ui]))
+		if(plotString==TRANS(traceStyleStrings[ui]))
 			return ui;
 	}
 
@@ -298,7 +298,7 @@
 
 PlotWrapper::PlotWrapper()
 {
-	//COMPILE_ASSERT(THREEDEP_ARRAYSIZE(plotTypeStrings) == PLOT_TYPE_ENUM_END);
+	//COMPILE_ASSERT(THREEDEP_ARRAYSIZE(traceStyleStrings) == PLOT_TYPE_ENUM_END);
 
 	applyUserBounds=false;
 	plotChanged=true;
@@ -684,6 +684,12 @@
 	return visibleMode;
 }
 
+unsigned int PlotWrapper::getPlotMode(unsigned int plotId) const
+{
+	ASSERT(plotId < plottingData.size());
+	return plottingData[plotId]->getPlotMode();
+}
+
 void PlotWrapper::getVisibleIDs(vector<unsigned int> &visiblePlotIDs ) const
 {
 
@@ -791,7 +797,7 @@
 				if(!plottingData[ui]->visible)
 					continue;
 
-				if(plottingData[ui]->getType()!= PLOT_MODE_1D)
+				if(plottingData[ui]->getMode()!= PLOT_MODE_1D)
 					continue;
 			
 				if(((Plot1D*)plottingData[ui])->wantLogPlot()) 
@@ -809,7 +815,7 @@
 				float minYVal=0.1;
 				for(size_t ui=0;ui<plottingData.size();ui++)
 				{
-					if(!plottingData[ui]->visible || plottingData[ui]->getType() !=PLOT_MODE_1D)
+					if(!plottingData[ui]->visible || plottingData[ui]->getMode() !=PLOT_MODE_1D)
 						continue;
 
 					float tmp ;
@@ -929,7 +935,7 @@
 				Plot2DFunc *curPlot;
 				curPlot=(Plot2DFunc*)plottingData[ui];
 
-				if(curPlot->getType() == PLOT_2D_DENS)
+				if(curPlot->getMode() == PLOT_2D_DENS)
 				{
 					wantColourbar=true;
 				}
@@ -1047,11 +1053,6 @@
 	plottingData[plotIDHandler.getPos(plotId)]->regionGroup.getRegion(regionId,region);
 }
 
-unsigned int PlotWrapper::plotType(unsigned int plotId) const
-{
-	return plottingData[plotIDHandler.getPos(plotId)]->getPlotMode();
-}
-
 
 void PlotWrapper::moveRegion(unsigned int plotID, unsigned int regionId, bool regionSelfUpdate,
 		unsigned int movementType, float newX, float newY) const
@@ -1135,7 +1136,7 @@
 
 void PlotBase::copyBase(PlotBase *target) const
 {
-	target->plotType=plotType;
+	target->traceStyle=traceStyle;
 	target->minX=minX;
 	target->maxX=maxX;
 	target->minY=minY;
@@ -1157,12 +1158,12 @@
 
 unsigned int PlotBase::getType() const
 {
-	return plotType;
+	return traceStyle;
 }
 
 unsigned int PlotBase::getMode() const
 {
-	switch(plotType)
+	switch(traceStyle)
 	{
 		case PLOT_LINE_LINES:
 		case PLOT_LINE_BARS:
@@ -1181,7 +1182,7 @@
 Plot1D::Plot1D()
 {
 	//Set the default plot properties
-	plotType=PLOT_LINE_LINES;
+	traceStyle=PLOT_LINE_LINES;
 	plotMode=PLOT_MODE_1D;
 	xLabel="";
 	yLabel="";
@@ -1489,7 +1490,7 @@
 
 
 	//Plot the appropriate form	
-	switch(plotMode)
+	switch(traceStyle)
 	{
 		case PLOT_LINE_LINES:
 			//Unfortunately, when using line plots, mathgl moves the data points to the plot boundary,
@@ -1632,7 +1633,7 @@
 Plot2DFunc::Plot2DFunc()
 {
 	plotMode = PLOT_MODE_2D;
-	plotType=PLOT_2D_DENS;
+	traceStyle=PLOT_2D_DENS;
 }
 
 void Plot2DFunc::setData(const Array2D<float> &a,
@@ -1705,7 +1706,8 @@
 
 Plot2DScatter::Plot2DScatter()
 {
-	plotType=PLOT_2D_SCATTER;
+	plotMode=PLOT_2D_SCATTER;
+	traceStyle=PLOT_LINE_POINTS;
 	scatterIntensityLog=false;
 }
 
diff -r bcb9acfa66de -r effd078610a7 src/backend/plot.h
--- a/src/backend/plot.h	Tue May 31 13:04:58 2016 +1000
+++ b/src/backend/plot.h	Tue Jun 21 12:45:20 2016 +0100
@@ -217,7 +217,7 @@
 class PlotBase
 {
 	protected:
-		//!Sub type of plot (eg lines, bars for 1D)
+		//!Type of plot 
 		unsigned int plotMode;
 		//!xaxis label
 		std::string xLabel;
@@ -229,8 +229,9 @@
 		//plot colour (for single coloured plots)
 		float r,g,b;
 		
-		//The type of plot (ie what class is it?)	
-		unsigned int plotType;
+		//The sub-style of the plot trace (eg lines, points, bars, etc)
+		// FIXME: This is badly named, change to traceStyle, or dataStyle, or something
+		unsigned int traceStyle;
 		
 		void copyBase(PlotBase *target) const;
 
@@ -297,8 +298,12 @@
 		void setStrings(const std::string &x, 
 			const std::string &y,const std::string &t);
 
+		//Set the colour of the plot trace
 		void setColour(float rNew, float gNew, float bNew);
 
+		//set the visual style for the trace (dots, lines, etc)
+		void setTraceStyle(unsigned int newStyle) { traceStyle=newStyle;}
+
 		std::string getXLabel() const { return xLabel;}
 		std::string getTitle() const { return title;}
 		std::string getYLabel() const { return yLabel;}
@@ -309,6 +314,7 @@
 		void setPlotMode(unsigned int newMode) { plotMode= newMode;}
 
 
+		//get the  colour of the trace
 		void getColour(float &r, float &g, float &b) const ;
 
 #ifdef DEBUG
@@ -613,7 +619,7 @@
 	
 
 		//!obtain the type of a plot, given the plot's uniqueID
-		unsigned int plotType(unsigned int plotId) const;
+		unsigned int getPlotMode(unsigned int plotId) const;
 
 		//Retrieve the types of visible plots
 		unsigned int getVisibleMode() const;
diff -r bcb9acfa66de -r effd078610a7 src/backend/viscontrol.cpp
--- a/src/backend/viscontrol.cpp	Tue May 31 13:04:58 2016 +1000
+++ b/src/backend/viscontrol.cpp	Tue Jun 21 12:45:20 2016 +0100
@@ -249,7 +249,7 @@
 						plotData->yLabel,plotData->dataLabel);
 					
 					//set the appearance of the plot
-					//plotNew->setTraceStyle(plotStyle);
+					plotNew->setTraceStyle(plotData->plotStyle);
 					plotNew->setColour(plotData->r,plotData->g,plotData->b);
 					
 					
@@ -275,7 +275,7 @@
 					unsigned int plotID;
 		
 					PlotBase *plotNew;
-					switch(plotData->plotType) 
+					switch(plotData->plotStyle) 
 					{
 						case PLOT_2D_DENS:
 						{
diff -r bcb9acfa66de -r effd078610a7 src/common/basics.cpp
--- a/src/common/basics.cpp	Tue May 31 13:04:58 2016 +1000
+++ b/src/common/basics.cpp	Tue Jun 21 12:45:20 2016 +0100
@@ -1347,7 +1347,8 @@
 	while(CFile.good() && !CFile.eof() && atHeader)
 	{
 		//Grab a line from the file
-		CFile.getline(inBuffer,BUFFER_SIZE);
+		if(!CFile.getline(inBuffer,BUFFER_SIZE))
+			break;
 
 		if(!CFile.good())
 			return ERR_FILE_FORMAT;
@@ -1457,10 +1458,8 @@
 			
 		}
 		//Grab a line from the file
-		CFile.getline(inBuffer,BUFFER_SIZE);
-		
-		if(!CFile.good() && !CFile.eof())
-			return ERR_FILE_FORMAT;
+		if(!CFile.getline(inBuffer,BUFFER_SIZE))
+			break;
 	}
 
 	return 0;
diff -r bcb9acfa66de -r effd078610a7 src/gui/mainFrame.cpp
--- a/src/gui/mainFrame.cpp	Tue May 31 13:04:58 2016 +1000
+++ b/src/gui/mainFrame.cpp	Tue Jun 21 12:45:20 2016 +0100
@@ -1391,7 +1391,7 @@
 	updateWxTreeCtrl(treeFilters);
 
 	if(!noUpdate)
-		return doSceneUpdate(true);
+		doSceneUpdate(true);
 
 	return true;
 }	
@@ -3834,7 +3834,7 @@
 	
 }
 
-bool MainWindowFrame::doSceneUpdate(bool ensureVisible)
+void MainWindowFrame::doSceneUpdate(bool ensureVisible)
 {
 	//Update scene
 	ASSERT(!currentlyUpdatingScene);
@@ -3864,6 +3864,11 @@
 	ensureResultVisible=ensureVisible;
 
 	ASSERT(!refreshControl);
+
+	//Hack to prevent crash on double-refresh
+	if(refreshControl)
+		return;
+
 	refreshControl = new RefreshController(visControl.state.treeState);
 	refreshThread=new RefreshThread(this,refreshControl);
 	progressTimer->Start(PROGRESS_TIMER_DELAY);
@@ -3871,7 +3876,8 @@
 	refreshThread->Create();
 	refreshThread->Run();
 
-	return true;
+	cerr << "Updating scene complete"<< endl;
+	return;
 }
 
 void MainWindowFrame::updateWxTreeCtrl( wxTreeCtrl *t, const Filter *f)
@@ -3971,6 +3977,11 @@
 	ASSERT(!visControl.state.treeState.isRefreshing());
 	progressTimer->Stop();
 
+	//Hack to prevent crash on re-entry during refresh. Should never trigger.
+	if(!refreshControl)
+		return;
+
+
 	vector<std::pair<const Filter*, std::string> > consoleMessages;
 	consoleMessages=refreshControl->getConsoleMessages();
 
diff -r bcb9acfa66de -r effd078610a7 src/gui/mainFrame.h
--- a/src/gui/mainFrame.h	Tue May 31 13:04:58 2016 +1000
+++ b/src/gui/mainFrame.h	Tue Jun 21 12:45:20 2016 +0100
@@ -124,7 +124,7 @@
 	//!Update the progress information in the status bar
 	void updateProgressStatus();
 	//!Perform an update to the 3D Scene. Returns false if refresh failed
-	bool doSceneUpdate(bool ensureResultVisible=false);
+	void doSceneUpdate(bool ensureResultVisible=false);
 	
 	//!Complete the scene update. Returns false if failed
 	void finishSceneUpdate(unsigned int errCode);
diff -r bcb9acfa66de -r effd078610a7 src/gui/mathglPane.cpp
--- a/src/gui/mathglPane.cpp	Tue May 31 13:04:58 2016 +1000
+++ b/src/gui/mathglPane.cpp	Tue Jun 21 12:45:20 2016 +0100
@@ -627,7 +627,7 @@
 			thePlot->getRegion(plotId,regionId,r);
 
 			//TODO: Implement a more generic region handler?
-			ASSERT(thePlot->plotType(plotId) == PLOT_MODE_1D);
+			ASSERT(thePlot->getPlotMode(plotId) == PLOT_MODE_1D);
 
 			float mglStartX,mglStartY;
 			toPlotCoords(draggingStart.x, draggingStart.y,mglStartX,mglStartY);
@@ -1471,7 +1471,7 @@
 		return;
 
 
-	ASSERT(thePlot->plotType(startMousePlot) == PLOT_MODE_1D);
+	ASSERT(thePlot->getPlotMode(startMousePlot) == PLOT_MODE_1D);
 
 	//See where extending the region is allowed up to.
 	thePlot->findRegionLimit(startMousePlot,startMouseRegion,
@@ -1550,7 +1550,7 @@
 		{
 			//This needs to be extended to support more
 			//plot types.
-			ASSERT(thePlot->plotType(startMousePlot) == PLOT_MODE_1D);
+			ASSERT(thePlot->getPlotMode(startMousePlot) == PLOT_MODE_1D);
 			
 			//Draw "ghost" limits markers for move,
 			//these appear as moving vertical bars to outline
diff -r bcb9acfa66de -r effd078610a7 src/wx/wxcomponents.cpp
--- a/src/wx/wxcomponents.cpp	Tue May 31 13:04:58 2016 +1000
+++ b/src/wx/wxcomponents.cpp	Tue Jun 21 12:45:20 2016 +0100
@@ -188,14 +188,14 @@
 
 void CopyGrid::saveData()
 {
-	wxFileDialog *wxF = new wxFileDialog(this,TRANS("Save Data..."), wxT(""),
+	wxFileDialog wxF(this,TRANS("Save Data..."), wxT(""),
 		wxT(""),TRANS("Text File (*.txt)|*.txt|All Files (*)|*"),wxFD_SAVE);
 
-	if( (wxF->ShowModal() == wxID_CANCEL))
+	if( (wxF.ShowModal() == wxID_CANCEL))
 		return;
 	
 
-	std::string dataFile = stlStr(wxF->GetPath());
+	std::string dataFile = stlStr(wxF.GetPath());
 	ofstream f(dataFile.c_str());
 
 	if(!f)