1 package uk.ac.cam.ch.wwmm.opsin;
2 
3 import java.util.ArrayList;
4 import java.util.Arrays;
5 import java.util.Collections;
6 import java.util.HashMap;
7 import java.util.HashSet;
8 import java.util.LinkedHashSet;
9 import java.util.List;
10 import java.util.Locale;
11 import java.util.Map;
12 import java.util.Set;
13 
14 import static uk.ac.cam.ch.wwmm.opsin.OpsinTools.*;
15 import static uk.ac.cam.ch.wwmm.opsin.XmlDeclarations.*;
16 
17 /**
18  * Assembles fused rings named using fusion nomenclature
19  * @author dl387
20  *
21  */
22 class FusedRingBuilder {
23 	private final BuildState state;
24 	private final List<Element> groupsInFusedRing;
25 	private final Element lastGroup;
26 	private final Fragment parentRing;
27 	private final Map<Integer,Fragment> fragmentInScopeForEachFusionLevel = new HashMap<Integer,Fragment>();
28 	private final Map<Atom, Atom> atomsToRemoveToReplacementAtom = new HashMap<Atom, Atom>();
29 
FusedRingBuilder(BuildState state, List<Element> groupsInFusedRing)30 	private FusedRingBuilder(BuildState state, List<Element> groupsInFusedRing) {
31 		this.state = state;
32 		this.groupsInFusedRing = groupsInFusedRing;
33 		lastGroup = groupsInFusedRing.get(groupsInFusedRing.size()-1);
34 		parentRing = lastGroup.getFrag();
35 		fragmentInScopeForEachFusionLevel.put(0, parentRing);
36 	}
37 
38 	/**
39 	 * Master method for processing fused rings. Fuses groups together
40 	 * @param state: contains the current id and fragment manager
41 	 * @param subOrRoot Element (substituent or root)
42 	 * @throws StructureBuildingException
43 	 */
processFusedRings(BuildState state, Element subOrRoot)44 	static void processFusedRings(BuildState state, Element subOrRoot) throws  StructureBuildingException {
45 		List<Element> groups = subOrRoot.getChildElements(GROUP_EL);
46 		if (groups.size() < 2){
47 			return;//nothing to fuse
48 		}
49 		List<Element> groupsInFusedRing =new ArrayList<Element>();
50 		for (int i = groups.size()-1; i >=0; i--) {//group groups into fused rings
51 			Element group =groups.get(i);
52 			groupsInFusedRing.add(0, group);
53 			if (i!=0){
54 				Element startingEl = group;
55 				if ((group.getValue().equals("benz") || group.getValue().equals("benzo")) && FUSIONRING_SUBTYPE_VAL.equals(group.getAttributeValue(SUBTYPE_ATR))){
56 					Element beforeBenzo = OpsinTools.getPreviousSibling(group);
57 					if (beforeBenzo !=null && beforeBenzo.getName().equals(LOCANT_EL)){
58 						startingEl = beforeBenzo;
59 					}
60 				}
61 				Element possibleGroup = OpsinTools.getPreviousSiblingIgnoringCertainElements(startingEl, new String[]{MULTIPLIER_EL, FUSION_EL});
62 				if (!groups.get(i-1).equals(possibleGroup)){//end of fused ring system
63 					if (groupsInFusedRing.size()>=2){
64 						//This will be invoked in cases where there are multiple fused ring systems in the same subOrRoot such as some spiro systems
65 						new FusedRingBuilder(state, groupsInFusedRing).buildFusedRing();
66 					}
67 					groupsInFusedRing.clear();
68 				}
69 			}
70 		}
71 		if (groupsInFusedRing.size()>=2){
72 			new FusedRingBuilder(state, groupsInFusedRing).buildFusedRing();
73 		}
74 	}
75 
76 	/**
77 	 * Combines the groups given in the {@link FusedRingBuilder} constructor to destructively create the fused ring system
78 	 * This fused ring is then numbered
79 	 * @throws StructureBuildingException
80 	 */
buildFusedRing()81 	void buildFusedRing() throws StructureBuildingException{
82 		/*
83 		 * Apply any nonstandard ring numbering, sorts atomOrder by locant
84 		 * Aromatises appropriate cycloalkane rings, Rejects groups with acyclic atoms
85 		 */
86         processRingNumberingAndIrregularities();
87 		processBenzoFusions();//FR-2.2.8  e.g. in 2H-[1,3]benzodioxino[6',5',4':10,5,6]anthra[2,3-b]azepine  benzodioxino is one component
88 		List<Element> nameComponents = formNameComponentList();
89 		nameComponents.remove(lastGroup);
90 
91 		List<Fragment> componentFragments = new ArrayList<Fragment>();//all the ring fragments (other than the parentRing). These will later be merged into the parentRing
92 		List<Fragment> parentFragments = new ArrayList<Fragment>();
93 		parentFragments.add(parentRing);
94 
95 		int numberOfParents = 1;
96 		Element possibleMultiplier = OpsinTools.getPreviousSibling(lastGroup);
97 		if (nameComponents.size()>0 && possibleMultiplier !=null && possibleMultiplier.getName().equals(MULTIPLIER_EL)){
98 			numberOfParents = Integer.parseInt(possibleMultiplier.getAttributeValue(VALUE_ATR));
99 			possibleMultiplier.detach();
100 			for (int j = 1; j < numberOfParents; j++) {
101 				Fragment copyOfParentRing =state.fragManager.copyFragment(parentRing);
102 				parentFragments.add(copyOfParentRing);
103 				componentFragments.add(copyOfParentRing);
104 			}
105 		}
106 
107 		/*The indice from nameComponents to use next. Work from right to left i.e. starts at nameComponents.size()-1*/
108 		int ncIndice = processMultiParentSystem(parentFragments, nameComponents, componentFragments);//handle multiparent systems
109 		/*
110 		 * The number of primes on the component to be connected.
111 		 * This is initially 0 indicating fusion of unprimed locants with the letter locants of the parentRing
112 		 * Subsequently it will switch to 1 indicating fusion of a second order component (primed locants) with a
113 		 * first order component (unprimed locants)
114 		 * Next would be double primed fusing to single primed locants etc.
115 		 *
116 		 */
117 		int fusionLevel = (nameComponents.size()-1 -ncIndice)/2;
118 		for (; ncIndice>=0; ncIndice--) {
119 			Element fusion = null;
120 			if (nameComponents.get(ncIndice).getName().equals(FUSION_EL)){
121 				fusion = nameComponents.get(ncIndice--);
122 			}
123 			if (ncIndice <0 || !nameComponents.get(ncIndice).getName().equals(GROUP_EL)){
124 				throw new StructureBuildingException("Group not found where group expected. This is probably a bug");
125 			}
126 			Fragment nextComponent = nameComponents.get(ncIndice).getFrag();
127 			int multiplier = 1;
128 			Element possibleMultiplierEl = OpsinTools.getPreviousSibling(nameComponents.get(ncIndice));//e.g. the di of difuro
129 			if (possibleMultiplierEl != null && possibleMultiplierEl.getName().equals(MULTIPLIER_EL)){
130 				multiplier = Integer.parseInt(possibleMultiplierEl.getAttributeValue(VALUE_ATR));
131 			}
132 			String[] fusionDescriptors =null;
133 			if (fusion !=null){
134 				String fusionDescriptorString = fusion.getValue().toLowerCase(Locale.ROOT).substring(1, fusion.getValue().length()-1);
135 				if (multiplier ==1){
136 					fusionDescriptors = new String[]{fusionDescriptorString};
137 				}
138 				else{
139 					if (fusionDescriptorString.split(";").length >1){
140 						fusionDescriptors = fusionDescriptorString.split(";");
141 					}
142 					else if (fusionDescriptorString.split(":").length >1){
143 						fusionDescriptors = fusionDescriptorString.split(":");
144 					}
145 					else if (fusionDescriptorString.split(",").length >1){
146 						fusionDescriptors = fusionDescriptorString.split(",");
147 					}
148 					else{//multiplier does not appear to mean multiplied component. Could be indicating multiplication of the whole fused ring system
149 						if (ncIndice!=0){
150 							throw new StructureBuildingException("Unexpected multiplier: " + possibleMultiplierEl.getValue() +" or incorrect fusion descriptor: " + fusionDescriptorString);
151 						}
152 						multiplier =1;
153 						fusionDescriptors = new String[]{fusionDescriptorString};
154 					}
155 				}
156 			}
157 			if (multiplier >1){
158 				possibleMultiplierEl.detach();
159 			}
160 			Fragment[] fusionComponents = new Fragment[multiplier];
161 			for (int j = 0; j < multiplier; j++) {
162 				if (j>0){
163 					fusionComponents[j] = state.fragManager.copyAndRelabelFragment(nextComponent,  j);
164 				}
165 				else{
166 					fusionComponents[j] = nextComponent;
167 				}
168 			}
169 
170 			for (int j = 0; j < multiplier; j++) {
171 				Fragment component = fusionComponents[j];
172 				componentFragments.add(component);
173 				if (fusion !=null){
174 					if (fusionDescriptors[j].split(":").length==1){//A fusion bracket without a colon is used when applying to the parent component (except in a special case where locants are ommitted)
175 						//check for case of omitted locant from a higher order fusion bracket e.g. cyclopenta[4,5]pyrrolo[2,3-c]pyridine
176 						if (fusionDescriptors[j].split("-").length==1 &&
177 								fusionDescriptors[j].split(",").length >1 &&
178 								FragmentTools.allAtomsInRingAreIdentical(component)
179 								&& ((StringTools.countTerminalPrimes(fusionDescriptors[j].split(",")[0])) != fusionLevel) ){//Could be like cyclopenta[3,4]cyclobuta[1,2]benzene where the first fusion to occur has parent locants omitted not child locants
180 							int numberOfPrimes = StringTools.countTerminalPrimes(fusionDescriptors[j].split(",")[0]);
181 							//note that this is the number of primes on the parent ring. So would expect the child ring and hence the fusionLevel to be 1 higher
182 							if (numberOfPrimes + 1 != fusionLevel){
183 								if (numberOfPrimes + 2 == fusionLevel){//ring could be in previous fusion level e.g. the benzo in benzo[10,11]phenanthro[2',3',4',5',6':4,5,6,7]chryseno[1,2,3-bc]coronene
184 									fusionLevel--;
185 								}
186 								else{
187 									throw new StructureBuildingException("Incorrect number of primes in fusion bracket: " +fusionDescriptors[j]);
188 								}
189 							}
190 							relabelAccordingToFusionLevel(component, fusionLevel);
191 							List<String> numericalLocantsOfParent = Arrays.asList(fusionDescriptors[j].split(","));
192 							List<String> numericalLocantsOfChild = findPossibleNumericalLocants(component, determineAtomsToFuse(fragmentInScopeForEachFusionLevel.get(fusionLevel), numericalLocantsOfParent, null).size()-1);
193 							processHigherOrderFusionDescriptors(component, fragmentInScopeForEachFusionLevel.get(fusionLevel), numericalLocantsOfChild, numericalLocantsOfParent);
194 						}
195 						else{
196 							fusionLevel = 0;
197 							relabelAccordingToFusionLevel(component, fusionLevel);
198 							String fusionDescriptor = fusionDescriptors[j];
199 							String[] fusionArray = determineNumericalAndLetterComponents(fusionDescriptor);
200 							int numberOfPrimes =0;
201 							if (!fusionArray[1].equals("")){
202 								numberOfPrimes =StringTools.countTerminalPrimes(fusionArray[1]);
203 								if (fusionArray[0].equals("")){
204 									fusionDescriptor = fusionArray[1].replaceAll("'", "");
205 								}
206 								else{
207 									fusionDescriptor = fusionArray[0]+ "-" +fusionArray[1].replaceAll("'", "");
208 								}
209 								if (numberOfPrimes >= parentFragments.size()){
210 									throw new StructureBuildingException("Unexpected prime in fusion descriptor");
211 								}
212 							}
213 							performSimpleFusion(fusionDescriptor, component, parentFragments.get(numberOfPrimes));//e.g. pyrano[3,2-b]imidazo[4,5-e]pyridine where both are level 0 fusions
214 						}
215 					}
216 					else{
217 						//determine number of primes in fusor and hence determine fusion level
218 						int numberOfPrimes = -j + StringTools.countTerminalPrimes(fusionDescriptors[j].split(",")[0]);
219 						if (numberOfPrimes != fusionLevel){
220 							if (fusionLevel == numberOfPrimes +1){
221 								fusionLevel--;
222 							}
223 							else{
224 								throw new StructureBuildingException("Incorrect number of primes in fusion bracket: " +fusionDescriptors[j]);
225 							}
226 						}
227 						relabelAccordingToFusionLevel(component, fusionLevel);
228 						performHigherOrderFusion(fusionDescriptors[j], component, fragmentInScopeForEachFusionLevel.get(fusionLevel));
229 					}
230 				}
231 				else{
232 					relabelAccordingToFusionLevel(component, fusionLevel);
233 					performSimpleFusion(null, component, fragmentInScopeForEachFusionLevel.get(fusionLevel));
234 				}
235 			}
236 			fusionLevel++;
237 			if (multiplier ==1){//multiplied components may not be substituted onto
238 				fragmentInScopeForEachFusionLevel.put(fusionLevel, fusionComponents[0]);
239 			}
240 		}
241 		for (Fragment ring : componentFragments) {
242 			state.fragManager.incorporateFragment(ring, parentRing);
243 		}
244 		removeMergedAtoms();
245 
246 		FusedRingNumberer.numberFusedRing(parentRing);//numbers the fused ring;
247 
248 		StringBuilder fusedRingName = new StringBuilder();
249 		for (Element element : nameComponents) {
250 			fusedRingName.append(element.getValue());
251 		}
252 		fusedRingName.append(lastGroup.getValue());
253 
254 		Element fusedRingEl =lastGroup;//reuse this element to save having to remap suffixes...
255 		fusedRingEl.getAttribute(VALUE_ATR).setValue(fusedRingName.toString());
256 		fusedRingEl.getAttribute(TYPE_ATR).setValue(RING_TYPE_VAL);
257 		fusedRingEl.setValue(fusedRingName.toString());
258 
259 		for (Element element : nameComponents) {
260 			element.detach();
261 		}
262 	}
263 
removeMergedAtoms()264 	private void removeMergedAtoms() {
265 		for (Atom a : atomsToRemoveToReplacementAtom.keySet()) {
266 			state.fragManager.removeAtomAndAssociatedBonds(a);
267 		}
268 		atomsToRemoveToReplacementAtom.clear();
269 	}
270 
271 	/**
272 	 * Forms a list a list of all group and fusion elements between the first and last group in the fused ring
273 	 * @return
274 	 */
formNameComponentList()275 	private List<Element> formNameComponentList() {
276 		List<Element> nameComponents  = new ArrayList<Element>();
277 		Element currentEl = groupsInFusedRing.get(0);
278 		while(currentEl != lastGroup){
279 			if (currentEl.getName().equals(GROUP_EL) || currentEl.getName().equals(FUSION_EL)){
280 				nameComponents.add(currentEl);
281 			}
282 			currentEl = OpsinTools.getNextSibling(currentEl);
283 		}
284 		return nameComponents;
285 	}
286 
processRingNumberingAndIrregularities()287 	private void processRingNumberingAndIrregularities() throws StructureBuildingException {
288 		for (Element group : groupsInFusedRing) {
289             Fragment ring = group.getFrag();
290             if (ALKANESTEM_SUBTYPE_VAL.equals(group.getAttributeValue(SUBTYPE_ATR))){
291             	aromatiseCyclicAlkane(group);
292             }
293             processPartiallyUnsaturatedHWSystems(group, ring);
294             if (group == lastGroup) {
295                 //perform a quick check that every atom in this group is infact cyclic. Fusion components are enumerated and hence all guaranteed to be purely cyclic
296                 List<Atom> atomList = ring.getAtomList();
297                 for (Atom atom : atomList) {
298                     if (!atom.getAtomIsInACycle()) {
299                         throw new StructureBuildingException("Inappropriate group used in fusion nomenclature. Only groups composed entirely of atoms in cycles may be used. i.e. not: " + group.getValue());
300                     }
301                 }
302                 if (group.getAttribute(FUSEDRINGNUMBERING_ATR) != null) {
303                     String[] standardNumbering = group.getAttributeValue(FUSEDRINGNUMBERING_ATR).split("/", -1);
304                     for (int j = 0; j < standardNumbering.length; j++) {
305                         atomList.get(j).replaceLocants(standardNumbering[j]);
306                     }
307                 } else {
308                     ring.sortAtomListByLocant();//for those where the order the locants are in is sensible					}
309                 }
310                 for (Atom atom : atomList) {
311                     atom.clearLocants();//the parentRing does not have locants, letters are used to indicate the edges
312                 }
313             } else if (group.getAttribute(FUSEDRINGNUMBERING_ATR) == null) {
314                 ring.sortAtomListByLocant();//for those where the order the locants are in is sensible
315             }
316         }
317 	}
318 
319 	/**
320 	 * Interprets the unlocanted unsaturator after a partially unsaturated HW Rings as indication of spare valency and detaches it
321      * This is necessary as this unsaturator can only refer to the HW ring and for names like 2-Benzoxazolinone to avoid confusion as to what the 2 refers to.
322 	 * @param group
323 	 * @param ring
324 	 */
processPartiallyUnsaturatedHWSystems(Element group, Fragment ring)325 	private void processPartiallyUnsaturatedHWSystems(Element group, Fragment ring) {
326 		if (HANTZSCHWIDMAN_SUBTYPE_VAL.equals(group.getAttributeValue(SUBTYPE_ATR)) && group.getAttribute(ADDBOND_ATR)!=null){
327 			List<Element> unsaturators = OpsinTools.getNextAdjacentSiblingsOfType(group, UNSATURATOR_EL);
328 			if (unsaturators.size()>0){
329 				Element unsaturator = unsaturators.get(0);
330 				if (unsaturator.getAttribute(LOCANT_ATR)==null && unsaturator.getAttributeValue(VALUE_ATR).equals("2")){
331 					unsaturator.detach();
332 					List<Bond> bondsToUnsaturate = StructureBuildingMethods.findBondsToUnSaturate(ring, 2, true);
333 					if (bondsToUnsaturate.size() == 0) {
334 						throw new RuntimeException("Failed to find bond to unsaturate on partially saturated HW ring");
335 					}
336 					Bond b = bondsToUnsaturate.get(0);
337 					b.getFromAtom().setSpareValency(true);
338 					b.getToAtom().setSpareValency(true);
339 				}
340 			}
341 		}
342 	}
343 
344 	/**
345 	 * Given a cyclicAlkaneGroup determines whether or not it should be aromatised. Unlocanted ene will be detached if it is an aromatisation hint
346 	 * No unsaturators -->aromatise
347 	 * Just ane -->don't
348 	 * More than 1 ene or locants on ene -->don't
349 	 * yne --> don't
350 	 * @param cyclicAlkaneGroup
351 	 */
aromatiseCyclicAlkane(Element cyclicAlkaneGroup)352 	private void aromatiseCyclicAlkane(Element cyclicAlkaneGroup) {
353 		Element next = OpsinTools.getNextSibling(cyclicAlkaneGroup);
354 		List<Element> unsaturators = new ArrayList<Element>();
355 		while (next!=null && next.getName().equals(UNSATURATOR_EL)){
356 			unsaturators.add(next);
357 			next = OpsinTools.getNextSibling(next);
358 		}
359 		boolean conjugate =true;
360 		if (unsaturators.size()==1){
361 			int value = Integer.parseInt(unsaturators.get(0).getAttributeValue(VALUE_ATR));
362 			if (value !=2){
363 				conjugate =false;
364 			}
365 			else if (unsaturators.get(0).getAttribute(LOCANT_ATR)!=null){
366 				conjugate =false;
367 			}
368 		}
369 		else if (unsaturators.size()==2){
370 			int value1 = Integer.parseInt(unsaturators.get(0).getAttributeValue(VALUE_ATR));
371 			if (value1 !=1){
372 				conjugate =false;
373 			}
374 			else{
375 				int value2 = Integer.parseInt(unsaturators.get(1).getAttributeValue(VALUE_ATR));
376 				if (value2 !=2 || unsaturators.get(1).getAttribute(LOCANT_ATR)!=null){
377 					conjugate =false;
378 				}
379 			}
380 		}
381 		else if (unsaturators.size() >2){
382 			conjugate =false;
383 		}
384 		if (conjugate){
385 			for (Element unsaturator : unsaturators) {
386 				unsaturator.detach();
387 			}
388 			List<Atom> atomList = cyclicAlkaneGroup.getFrag().getAtomList();
389 		    for (Atom atom : atomList) {
390 		        atom.setSpareValency(true);
391 		    }
392 		}
393 	}
394 
processMultiParentSystem(List<Fragment> parentFragments, List<Element> nameComponents, List<Fragment> componentFragments)395 	private int processMultiParentSystem(List<Fragment> parentFragments, List<Element> nameComponents, List<Fragment> componentFragments) throws StructureBuildingException {
396 		int i = nameComponents.size()-1;
397 		int fusionLevel =0;
398 		if (i>=0 && parentFragments.size()>1){
399 			List<Fragment> previousFusionLevelFragments = parentFragments;
400 			for (; i>=0; i--) {
401 				if (previousFusionLevelFragments.size()==1){//completed multi parent system
402 					fragmentInScopeForEachFusionLevel.put(fusionLevel, previousFusionLevelFragments.get(0));
403 					break;
404 				}
405 				Element fusion = null;
406 				if (nameComponents.get(i).getName().equals(FUSION_EL)){
407 					fusion = nameComponents.get(i--);
408 				}
409 				else{
410 					throw new StructureBuildingException("Fusion bracket not found where fusion bracket expected");
411 				}
412 				if (i <0 || !nameComponents.get(i).getName().equals(GROUP_EL)){
413 					throw new StructureBuildingException("Group not found where group expected. This is probably a bug");
414 				}
415 				Fragment nextComponent = nameComponents.get(i).getFrag();
416 				relabelAccordingToFusionLevel(nextComponent, fusionLevel);
417 				int multiplier = 1;
418 				Element possibleMultiplierEl = OpsinTools.getPreviousSibling(nameComponents.get(i));
419 				if (possibleMultiplierEl != null && possibleMultiplierEl.getName().equals(MULTIPLIER_EL)){
420 					multiplier = Integer.parseInt(possibleMultiplierEl.getAttributeValue(VALUE_ATR));
421 					possibleMultiplierEl.detach();
422 				}
423 				List<Fragment> fusionComponents = new ArrayList<Fragment>();
424 				for (int j = 0; j < multiplier; j++) {
425 					if (j>0){
426 						Fragment clonedFrag = state.fragManager.copyFragment(nextComponent);
427 						relabelAccordingToFusionLevel(clonedFrag, j);//fusionLevels worth of primes already added
428 						fusionComponents.add(clonedFrag);
429 					}
430 					else{
431 						fusionComponents.add(nextComponent);
432 					}
433 				}
434 				fusionLevel+=multiplier;
435 				if (multiplier>1 && multiplier != previousFusionLevelFragments.size()){
436 					throw new StructureBuildingException("Mismatch between number of components and number of parents in fused ring system");
437 				}
438 				String fusionDescriptorString = fusion.getValue().toLowerCase(Locale.ROOT).substring(1, fusion.getValue().length()-1);
439 				String[] fusionDescriptors =null;
440 				if (fusionDescriptorString.split(";").length >1){
441 					fusionDescriptors = fusionDescriptorString.split(";");
442 				}
443 				else if (fusionDescriptorString.split(":").length >1){
444 					fusionDescriptors = fusionDescriptorString.split(":");
445 				}
446 				else if (fusionDescriptorString.split(",").length >1){
447 					fusionDescriptors = fusionDescriptorString.split(",");
448 				}
449 				else{
450 					throw new StructureBuildingException("Invalid fusion descriptor: " + fusionDescriptorString);
451 				}
452 				if (fusionDescriptors.length != previousFusionLevelFragments.size()){
453 					throw new StructureBuildingException("Invalid fusion descriptor: "+fusionDescriptorString +"(Number of locants disagrees with number of parents)");
454 				}
455 				for (int j = 0; j < fusionDescriptors.length; j++) {
456 					String fusionDescriptor = fusionDescriptors[j];
457 					Fragment component = multiplier>1 ? fusionComponents.get(j) : nextComponent;
458 					Fragment parentToUse = previousFusionLevelFragments.get(j);
459 					boolean simpleFusion = fusionDescriptor.split(":").length <= 1;
460 					if (simpleFusion){
461 						String[] fusionArray = determineNumericalAndLetterComponents(fusionDescriptor);
462 						if (fusionArray[1].length() != 0){
463 							int numberOfPrimes =StringTools.countTerminalPrimes(fusionArray[1]);
464 							if (fusionArray[0].length() == 0){
465 								fusionDescriptor = fusionArray[1].replaceAll("'", "");
466 							}
467 							else{
468 								fusionDescriptor = fusionArray[0]+ "-" +fusionArray[1].replaceAll("'", "");
469 							}
470 							if (numberOfPrimes !=j){//check the number of primes on the letter part agree with the parent to use e.g.[4,5-bcd:1,2-c']difuran
471 								throw new StructureBuildingException("Incorrect number of primes in fusion descriptor: " + fusionDescriptor);
472 							}
473 						}
474 						performSimpleFusion(fusionDescriptor, component, parentToUse);
475 					}
476 					else{
477 						performHigherOrderFusion(fusionDescriptor, component, parentToUse);
478 					}
479 				}
480 				previousFusionLevelFragments = fusionComponents;
481 				componentFragments.addAll(fusionComponents);
482 			}
483 			if (previousFusionLevelFragments.size()!=1){
484 				throw new StructureBuildingException("Invalid fused ring system. Incomplete multiparent system");
485 			}
486 		}
487 		return i;
488 	}
489 
490 	/**
491 	 * Splits a first order fusion component into it's numerical and letter parts
492 	 * Either one of these can be the blank string as they may have been omitted
493 	 * The first entry in the array is the numbers and the second the letters
494 	 * @param fusionDescriptor
495 	 * @return
496 	 */
determineNumericalAndLetterComponents(String fusionDescriptor)497 	private String[] determineNumericalAndLetterComponents(String fusionDescriptor) {
498 		String[] fusionArray = fusionDescriptor.split("-");
499 		if (fusionArray.length ==2){
500 			return fusionArray;
501 		}
502 		else{
503 			String[] components = new String[2];
504 			if (fusionArray[0].contains(",")){//the digit section
505 				components[0]=fusionArray[0];
506 				components[1]="";
507 			}
508 			else{
509 				components[0]="";
510 				components[1]=fusionArray[0];
511 			}
512 			return components;
513 		}
514 	}
515 
516 	/**
517 	 * Searches groups for benz(o) components and fuses them in accordance with
518 	 * FR-2.2.8 Heterobicyclic components with a benzene ring
519 	 * @throws StructureBuildingException
520 	 */
processBenzoFusions()521 	private void processBenzoFusions() throws StructureBuildingException {
522 		for(int i = groupsInFusedRing.size() - 2; i >= 0; i--) {
523 			Element group = groupsInFusedRing.get(i);
524 			if (group.getValue().equals("benz") || group.getValue().equals("benzo")) {
525 				Element possibleFusionbracket = OpsinTools.getNextSibling(group);
526 				if (!possibleFusionbracket.getName().equals(FUSION_EL)) {
527 					Element possibleMultiplier = OpsinTools.getPreviousSibling(group);
528 					if (possibleMultiplier == null || !possibleMultiplier.getName().equals(MULTIPLIER_EL) || possibleMultiplier.getAttributeValue(TYPE_ATR).equals(GROUP_TYPE_VAL)) {
529 						//e.g. 2-benzofuran. Fused rings of this type are a special case treated as being a single component
530 						//and have a special convention for indicating the position of heteroatoms
531 						benzoSpecificFusion(group, groupsInFusedRing.get(i + 1));
532 						group.detach();
533 						groupsInFusedRing.remove(i);
534 					}
535 				}
536 			}
537 		}
538 	}
539 
540 	/**
541 	 * Modifies nextComponent's locants according to the fusionLevel.
542 	 * @param component
543 	 * @param fusionLevel
544 	 */
relabelAccordingToFusionLevel(Fragment component, int fusionLevel)545 	private void relabelAccordingToFusionLevel(Fragment component, int fusionLevel)  {
546 		if (fusionLevel > 0){
547 			FragmentTools.relabelNumericLocants(component.getAtomList(), StringTools.multiplyString("'", fusionLevel));
548 		}
549 	}
550 
551 	/**
552 	 * Handles fusion between components where the fusion descriptor is of the form:
553 	 * comma separated locants dash letters
554 	 * e.g imidazo[4,5-d]pyridine
555 	 * The fusionDescriptor may be given as null or the letter/numerical part omitted.
556 	 * Sensible defaults will be found instead
557 	 * @param fusionDescriptor
558 	 * @param childRing
559 	 * @param parentRing
560 	 * @throws StructureBuildingException
561 	 */
performSimpleFusion(String fusionDescriptor, Fragment childRing, Fragment parentRing)562 	private void performSimpleFusion(String fusionDescriptor, Fragment childRing, Fragment parentRing) throws StructureBuildingException {
563 		List<String> numericalLocantsOfChild = null;
564 		List<String> letterLocantsOfParent = null;
565 		if (fusionDescriptor != null){
566 			String[] fusionArray = fusionDescriptor.split("-");
567 			if (fusionArray.length ==2){
568 				numericalLocantsOfChild = Arrays.asList(fusionArray[0].split(","));
569 				char[] tempLetterLocantsOfParent = fusionArray[1].toCharArray();
570 				letterLocantsOfParent = new ArrayList<String>();
571                 for (char letterLocantOfParent : tempLetterLocantsOfParent) {
572                     letterLocantsOfParent.add(String.valueOf(letterLocantOfParent));
573                 }
574 			}
575 			else{
576 				if (fusionArray[0].contains(",")){//only has digits
577 					String[] numericalLocantsOfChildTemp = fusionArray[0].split(",");
578 					numericalLocantsOfChild = Arrays.asList(numericalLocantsOfChildTemp);
579 				}
580 				else{//only has letters
581 					char[] tempLetterLocantsOfParentCharArray = fusionArray[0].toCharArray();
582 					letterLocantsOfParent = new ArrayList<String>();
583                     for (char letterLocantOfParentCharArray : tempLetterLocantsOfParentCharArray) {
584                         letterLocantsOfParent.add(String.valueOf(letterLocantOfParentCharArray));
585                     }
586 				}
587 			}
588 		}
589 
590 		int edgeLength =1;
591 		if (numericalLocantsOfChild != null){
592 			if (numericalLocantsOfChild.size() <=1){
593 				throw new StructureBuildingException("At least two numerical locants must be provided to perform fusion!");
594 			}
595 			edgeLength = numericalLocantsOfChild.size()-1;
596 		}
597 		else if (letterLocantsOfParent != null){
598 			edgeLength = letterLocantsOfParent.size();
599 		}
600 
601 		if (numericalLocantsOfChild == null){
602 			numericalLocantsOfChild = findPossibleNumericalLocants(childRing, edgeLength);
603 		}
604 
605 		if (letterLocantsOfParent == null){
606 			letterLocantsOfParent = findPossibleLetterLocants(parentRing, edgeLength);
607 		}
608 		if (numericalLocantsOfChild == null || letterLocantsOfParent ==null){
609 			throw new StructureBuildingException("Unable to find bond to form fused ring system. Some information for forming fused ring system was only supplyed implicitly");
610 		}
611 
612 		processFirstOrderFusionDescriptors(childRing, parentRing, numericalLocantsOfChild, letterLocantsOfParent);//fuse the rings
613 	}
614 
615 	/**
616 	 * Takes a ring an returns and array with one letter corresponding to a side/s
617 	 * that contains two adjacent non bridgehead carbons
618 	 * The number of sides is specified by edgeLength
619 	 * @param ring
620 	 * @param edgeLength The number of bonds to be fused along
621 	 * @return
622 	 */
findPossibleLetterLocants(Fragment ring, int edgeLength)623 	private List<String> findPossibleLetterLocants(Fragment ring, int edgeLength) {
624 		List<Integer> carbonAtomIndexes = new ArrayList<Integer>();
625 		int numberOfAtoms = ring.getAtomCount();
626 		CyclicAtomList cyclicAtomList = new CyclicAtomList(ring.getAtomList());
627 		for (int i = 0; i <= numberOfAtoms; i++) {
628 			//iterate backwards in list to use highest locanted edge in preference.
629 			//this retains what is currently locant 1 on the parent ring as locant 1 if the first two atoms found match
630 			//the last atom in the list is potentially tested twice e.g. on a 6 membered ring, 6-5 and 1-6 are both possible
631 			Atom atom = cyclicAtomList.previous();
632 			//want non-bridgehead carbon atoms. Double-check that these carbon atoms are actually bonded (e.g. von baeyer systems have non-consecutive atom numbering!)
633 			if (atom.getElement() == ChemEl.C && atom.getBondCount() == 2
634 					&& (carbonAtomIndexes.size() == 0 || atom.getAtomNeighbours().contains(cyclicAtomList.peekNext()))){
635 				carbonAtomIndexes.add(cyclicAtomList.getIndex());
636 				if (carbonAtomIndexes.size() == edgeLength + 1){//as many carbons in a row as to give that edgelength ->use these side/s
637 					Collections.reverse(carbonAtomIndexes);
638 					List<String> letterLocantsOfParent = new ArrayList<String>();
639 					for (int j = 0; j < edgeLength; j++) {
640 						letterLocantsOfParent.add(String.valueOf((char)(97 + carbonAtomIndexes.get(j))));//97 is ascii for a
641 					}
642 					return letterLocantsOfParent;
643 				}
644 			}
645 			else{
646 				carbonAtomIndexes.clear();
647 			}
648 		}
649 		return null;
650 	}
651 
652 	/**
653 	 * Takes a ring and returns an array of numbers corresponding to a side/s
654 	 * that contains two adjacent non bridgehead carbons
655 	 * The number of sides is specified by edgeLength
656 	 * @param ring
657 	 * @param edgeLength The number of bonds to be fused along
658 	 * @return
659 	 */
findPossibleNumericalLocants(Fragment ring, int edgeLength)660 	private List<String> findPossibleNumericalLocants(Fragment ring, int edgeLength) {
661 		List<String> carbonLocants = new ArrayList<String>();
662 		int numberOfAtoms = ring.getAtomCount();
663 		CyclicAtomList cyclicAtomList = new CyclicAtomList(ring.getAtomList());
664 		for (int i = 0; i <= numberOfAtoms; i++) {
665 			//the last atom in the list is potentially tested twice e.g. on a 6 membered ring, 1-2 and 6-1 are both possible
666 			Atom atom = cyclicAtomList.next();
667 			//want non-bridgehead carbon atoms. Double-check that these carbon atoms are actually bonded (e.g. von baeyer systems have non-consecutive atom numbering!)
668 			if (atom.getElement() == ChemEl.C && atom.getBondCount() == 2
669 					&& (carbonLocants.size() == 0 || atom.getAtomNeighbours().contains(cyclicAtomList.peekPrevious()))){
670 				carbonLocants.add(atom.getFirstLocant());
671 				if (carbonLocants.size() == edgeLength + 1){//as many carbons in a row as to give that edgelength ->use these side/s
672 					List<String> numericalLocantsOfChild = new ArrayList<String>();
673 					for (String locant : carbonLocants) {
674 						numericalLocantsOfChild.add(locant);
675 					}
676 					return numericalLocantsOfChild;
677 				}
678 			}
679 			else{
680 				carbonLocants.clear();
681 			}
682 		}
683 		return null;
684 	}
685 
686 	/**
687 	 * Performs a single ring fusion using the values in numericalLocantsOfChild/letterLocantsOfParent
688 	 * @param childRing
689 	 * @param parentRing
690 	 * @param numericalLocantsOfChild
691 	 * @param letterLocantsOfParent
692 	 * @throws StructureBuildingException
693 	 */
processFirstOrderFusionDescriptors(Fragment childRing, Fragment parentRing, List<String> numericalLocantsOfChild, List<String> letterLocantsOfParent)694 	private void processFirstOrderFusionDescriptors(Fragment childRing, Fragment parentRing, List<String> numericalLocantsOfChild, List<String> letterLocantsOfParent) throws StructureBuildingException {
695 		List<Atom> childAtoms = determineAtomsToFuse(childRing, numericalLocantsOfChild, letterLocantsOfParent.size() +1);
696 		if (childAtoms ==null){
697 			throw new StructureBuildingException("Malformed fusion bracket!");
698 		}
699 
700 		List<Atom> parentAtoms = new ArrayList<Atom>();
701 		List<Atom> parentPeripheralAtomList = getPeripheralAtoms(parentRing.getAtomList());
702 		CyclicAtomList cyclicListAtomsOnSurfaceOfParent = new CyclicAtomList(parentPeripheralAtomList, (int)letterLocantsOfParent.get(0).charAt(0) -97);//convert from lower case character through ascii to 0-23
703 		parentAtoms.add(cyclicListAtomsOnSurfaceOfParent.getCurrent());
704 		for (int i = 0; i < letterLocantsOfParent.size(); i++) {
705 			parentAtoms.add(cyclicListAtomsOnSurfaceOfParent.next());
706 		}
707 		fuseRings(childAtoms, parentAtoms);
708 	}
709 
710 	/**
711 	 * Returns the sublist of the given atoms that are peripheral atoms given that the list is ordered such that the interior atoms are at the end of the list
712 	 * @param atomList
713 	 * @return
714 	 */
getPeripheralAtoms(List<Atom> atomList)715 	private List<Atom> getPeripheralAtoms(List<Atom> atomList) {
716 		//find the indice of the last atom on the surface of the ring. This obviously connects to the first atom. The objective is to exclude any interior atoms.
717 		List<Atom> neighbours = atomList.get(0).getAtomNeighbours();
718 		int indice = Integer.MAX_VALUE;
719 		for (Atom atom : neighbours) {
720 			int indexOfAtom =atomList.indexOf(atom);
721 			if (indexOfAtom ==1){//not the next atom
722 				continue;
723 			}
724 			else if (indexOfAtom ==-1){//not in parentRing
725 				continue;
726 			}
727 			if (atomList.indexOf(atom)< indice){
728 				indice = indexOfAtom;
729 			}
730 		}
731 		return atomList.subList(0, indice +1);
732 	}
733 
734 	/**
735 	 * Handles fusion between components where the fusion descriptor is of the form:
736 	 * comma separated locants colon comma separated locants
737 	 * e.g pyrido[1'',2'':1',2']imidazo
738 	 * @param fusionDescriptor
739 	 * @param nextComponent
740 	 * @param fusedRing
741 	 * @throws StructureBuildingException
742 	 */
performHigherOrderFusion(String fusionDescriptor, Fragment nextComponent, Fragment fusedRing)743 	private void performHigherOrderFusion(String fusionDescriptor, Fragment nextComponent, Fragment fusedRing) throws StructureBuildingException {
744 		List<String> numericalLocantsOfChild = null;
745 		List<String> numericalLocantsOfParent = null;
746 		String[] fusionArray = fusionDescriptor.split(":");
747 		if (fusionArray.length ==2){
748 			numericalLocantsOfChild = Arrays.asList(fusionArray[0].split(","));
749 			numericalLocantsOfParent = Arrays.asList(fusionArray[1].split(","));
750 		}
751 		else{
752 			throw new StructureBuildingException("Malformed fusion bracket: This is an OPSIN bug, check regexTokens.xml");
753 		}
754 		processHigherOrderFusionDescriptors(nextComponent, fusedRing, numericalLocantsOfChild, numericalLocantsOfParent);//fuse the rings
755 	}
756 
757 	/**
758 	 * Performs a single ring fusion using the values in numericalLocantsOfChild/numericalLocantsOfParent
759 	 * @param childRing
760 	 * @param parentRing
761 	 * @param numericalLocantsOfChild
762 	 * @param numericalLocantsOfParent
763 	 * @throws StructureBuildingException
764 	 */
processHigherOrderFusionDescriptors(Fragment childRing, Fragment parentRing, List<String> numericalLocantsOfChild, List<String> numericalLocantsOfParent)765 	private void processHigherOrderFusionDescriptors(Fragment childRing, Fragment parentRing, List<String> numericalLocantsOfChild, List<String> numericalLocantsOfParent) throws StructureBuildingException {
766 		List<Atom> childAtoms =determineAtomsToFuse(childRing, numericalLocantsOfChild, null);
767 		if (childAtoms ==null){
768 			throw new StructureBuildingException("Malformed fusion bracket!");
769 		}
770 
771 		List<Atom> parentAtoms = determineAtomsToFuse(parentRing, numericalLocantsOfParent, childAtoms.size());
772 		if (parentAtoms ==null){
773 			throw new StructureBuildingException("Malformed fusion bracket!");
774 		}
775 		fuseRings(childAtoms, parentAtoms);
776 	}
777 
778 	/**
779 	 * Determines which atoms on a ring should be used for fusion given a set of numerical locants.
780 	 * If from the other ring involved in the fusion it is known how many atoms are expected to be found this should be provided
781 	 * If this is not known it should be set to null and the smallest number of fusion atoms will be returned.
782 	 * @param ring
783 	 * @param numericalLocantsOnRing
784 	 * @param expectedNumberOfAtomsToBeUsedForFusion
785 	 * @return
786 	 * @throws StructureBuildingException
787 	 */
determineAtomsToFuse(Fragment ring, List<String> numericalLocantsOnRing, Integer expectedNumberOfAtomsToBeUsedForFusion)788 	private List<Atom> determineAtomsToFuse(Fragment ring, List<String> numericalLocantsOnRing, Integer expectedNumberOfAtomsToBeUsedForFusion) throws StructureBuildingException {
789 		List<Atom> parentPeripheralAtomList = getPeripheralAtoms(ring.getAtomList());
790 		int indexfirst = parentPeripheralAtomList.indexOf(ring.getAtomByLocantOrThrow(numericalLocantsOnRing.get(0)));
791 		int indexfinal = parentPeripheralAtomList.indexOf(ring.getAtomByLocantOrThrow(numericalLocantsOnRing.get(numericalLocantsOnRing.size()-1)));
792 		CyclicAtomList cyclicRingAtomList = new CyclicAtomList(parentPeripheralAtomList, indexfirst);
793 		List<Atom> fusionAtoms = null;
794 
795 		List<Atom> potentialFusionAtomsAscending = new ArrayList<Atom>();
796 		potentialFusionAtomsAscending.add(cyclicRingAtomList.getCurrent());
797 		while (cyclicRingAtomList.getIndex() != indexfinal){//assume numbers are ascending
798 			potentialFusionAtomsAscending.add(cyclicRingAtomList.next());
799 		}
800 		if (expectedNumberOfAtomsToBeUsedForFusion ==null ||expectedNumberOfAtomsToBeUsedForFusion == potentialFusionAtomsAscending.size()){
801 			boolean notInPotentialParentAtoms =false;
802 			for (int i =1; i < numericalLocantsOnRing.size()-1 ; i ++){
803 				if (!potentialFusionAtomsAscending.contains(ring.getAtomByLocantOrThrow(numericalLocantsOnRing.get(i)))){
804 					notInPotentialParentAtoms =true;
805 				}
806 			}
807 			if (!notInPotentialParentAtoms){
808 				fusionAtoms = potentialFusionAtomsAscending;
809 			}
810 		}
811 
812 		if (fusionAtoms ==null || expectedNumberOfAtomsToBeUsedForFusion ==null){//that didn't work, so try assuming the numbers are descending
813 			cyclicRingAtomList.setIndex(indexfirst);
814 			List<Atom> potentialFusionAtomsDescending = new ArrayList<Atom>();
815 			potentialFusionAtomsDescending.add(cyclicRingAtomList.getCurrent());
816 			while (cyclicRingAtomList.getIndex() != indexfinal){//assume numbers are descending
817 				potentialFusionAtomsDescending.add(cyclicRingAtomList.previous());
818 			}
819 			if (expectedNumberOfAtomsToBeUsedForFusion ==null || expectedNumberOfAtomsToBeUsedForFusion == potentialFusionAtomsDescending.size()){
820 				boolean notInPotentialParentAtoms =false;
821 				for (int i =1; i < numericalLocantsOnRing.size()-1 ; i ++){
822 					if (!potentialFusionAtomsDescending.contains(ring.getAtomByLocantOrThrow(numericalLocantsOnRing.get(i)))){
823 						notInPotentialParentAtoms =true;
824 					}
825 				}
826 				if (!notInPotentialParentAtoms){
827 					if (fusionAtoms!=null && expectedNumberOfAtomsToBeUsedForFusion ==null){
828 						//prefer less fusion atoms
829 						if (potentialFusionAtomsDescending.size()< fusionAtoms.size()){
830 							fusionAtoms = potentialFusionAtomsDescending;
831 						}
832 					}
833 					else{
834 						fusionAtoms = potentialFusionAtomsDescending;
835 					}
836 				}
837 			}
838 		}
839 		return fusionAtoms;
840 	}
841 
842 	/**
843 	 * Creates the bonds required to fuse two rings together.
844 	 * The child atoms are recorded as atoms that should be removed later
845 	 * @param childAtoms
846 	 * @param parentAtoms
847 	 * @throws StructureBuildingException
848 	 */
fuseRings(List<Atom> childAtoms, List<Atom> parentAtoms)849 	private void fuseRings(List<Atom> childAtoms, List<Atom> parentAtoms) throws StructureBuildingException {
850 		if (parentAtoms.size()!=childAtoms.size()){
851 			throw new StructureBuildingException("Problem with fusion descriptors: Parent atoms specified: " + parentAtoms.size() +" Child atoms specified: " + childAtoms.size() + " These should have been identical!");
852 		}
853 		//replace parent atoms if the atom has already been used in fusion with the original atom
854 		//This will occur if fusion has resulted in something resembling a spiro centre e.g. cyclopenta[1,2-b:5,1-b']bis[1,4]oxathiine
855 		for (int i = parentAtoms.size() -1; i >=0; i--) {
856 			if (atomsToRemoveToReplacementAtom.get(parentAtoms.get(i))!=null){
857 				parentAtoms.set(i, atomsToRemoveToReplacementAtom.get(parentAtoms.get(i)));
858 			}
859 			if (atomsToRemoveToReplacementAtom.get(childAtoms.get(i))!=null){
860 				childAtoms.set(i, atomsToRemoveToReplacementAtom.get(childAtoms.get(i)));
861 			}
862 		}
863 
864 		//sync spareValency and check that element type matches
865 		for (int i = 0; i < childAtoms.size(); i++) {
866 			Atom parentAtom = parentAtoms.get(i);
867 			Atom childAtom = childAtoms.get(i);
868 			if (childAtom.hasSpareValency()){
869 				parentAtom.setSpareValency(true);
870 			}
871 			if (parentAtom.getElement() != childAtom.getElement()){
872 				throw new StructureBuildingException("Invalid fusion descriptor: Heteroatom placement is ambiguous as it is not present in both components of the fusion");
873 			}
874 			atomsToRemoveToReplacementAtom.put(childAtom, parentAtom);
875 		}
876 
877 		Set<Bond> fusionEdgeBonds  = new HashSet<Bond>();//these bonds already exist in both the child and parent atoms
878 		for (int i = 0; i < childAtoms.size() -1; i++) {
879 			fusionEdgeBonds.add(childAtoms.get(i).getBondToAtomOrThrow(childAtoms.get(i+1)));
880 			fusionEdgeBonds.add(parentAtoms.get(i).getBondToAtomOrThrow(parentAtoms.get(i+1)));
881 		}
882 
883 		Set<Bond> bondsToAddToParentAtoms = new LinkedHashSet<Bond>();
884 		for (Atom childAtom : childAtoms) {
885 			for (Bond b : childAtom.getBonds()) {
886 				if (!fusionEdgeBonds.contains(b)){
887 					bondsToAddToParentAtoms.add(b);
888 				}
889 			}
890 		}
891 
892 		Set<Bond> bondsToAddToChildAtoms = new LinkedHashSet<Bond>();
893 		for (Atom parentAtom : parentAtoms) {
894 			for (Bond b : parentAtom.getBonds()) {
895 				if (!fusionEdgeBonds.contains(b)){
896 					bondsToAddToChildAtoms.add(b);
897 				}
898 			}
899 		}
900 
901 		for (Bond bond : bondsToAddToParentAtoms) {
902 			Atom from = bond.getFromAtom();
903 			int indiceInChildAtoms = childAtoms.indexOf(from);
904 			if (indiceInChildAtoms !=-1){
905 				from = parentAtoms.get(indiceInChildAtoms);
906 			}
907 			Atom to = bond.getToAtom();
908 			indiceInChildAtoms = childAtoms.indexOf(to);
909 			if (indiceInChildAtoms !=-1){
910 				to = parentAtoms.get(indiceInChildAtoms);
911 			}
912 			state.fragManager.createBond(from, to, 1);
913 		}
914 
915 		for (Bond bond : bondsToAddToChildAtoms) {
916 			Atom from = bond.getFromAtom();
917 			int indiceInParentAtoms = parentAtoms.indexOf(from);
918 			if (indiceInParentAtoms !=-1){
919 				from = childAtoms.get(indiceInParentAtoms);
920 			}
921 			Atom to = bond.getToAtom();
922 			indiceInParentAtoms = parentAtoms.indexOf(to);
923 			if (indiceInParentAtoms !=-1){
924 				to = childAtoms.get(indiceInParentAtoms);
925 			}
926 			Bond newBond = new Bond(from, to, 1);
927 			if (childAtoms.contains(from)){
928 				from.addBond(newBond);
929 			}
930 			else{
931 				to.addBond(newBond);
932 			}
933 		}
934 	}
935 
936 	/**
937 	 * Fuse the benzo with the subsequent ring
938 	 * Uses locants in front of the benz/benzo group to assign heteroatoms on the now numbered fused ring system
939 	 * @param benzoEl
940 	 * @param parentEl
941 	 * @throws StructureBuildingException
942 	 */
benzoSpecificFusion(Element benzoEl, Element parentEl)943 	private void benzoSpecificFusion(Element benzoEl, Element parentEl) throws StructureBuildingException {
944 		/*
945 		 * Perform the fusion, number it and associate it with the parentEl
946 		 */
947 		Fragment benzoRing = benzoEl.getFrag();
948 		Fragment parentRing = parentEl.getFrag();
949 		performSimpleFusion(null, benzoRing , parentRing);
950 		state.fragManager.incorporateFragment(benzoRing, parentRing);
951 		removeMergedAtoms();
952 		FusedRingNumberer.numberFusedRing(parentRing);//numbers the fused ring;
953 		Fragment fusedRing =parentRing;
954 		setBenzoHeteroatomPositioning(benzoEl, fusedRing);
955 	}
956 
957 	/**
958 	 * Checks for locant(s) before benzo and uses these to set
959 	 * @param benzoEl
960 	 * @param fusedRing
961 	 * @throws StructureBuildingException
962 	 */
setBenzoHeteroatomPositioning(Element benzoEl, Fragment fusedRing)963 	private void setBenzoHeteroatomPositioning(Element benzoEl, Fragment fusedRing) throws StructureBuildingException {
964 		Element locantEl = OpsinTools.getPreviousSibling(benzoEl);
965 		if (locantEl != null && locantEl.getName().equals(LOCANT_EL)) {
966 			String[] locants = locantEl.getValue().split(",");
967 			if (locantsCouldApplyToHeteroatomPositions(locants, benzoEl)) {
968 				List<Atom> atomList =fusedRing.getAtomList();
969 				List<Atom> heteroatoms = new ArrayList<Atom>();
970 				List<ChemEl> elementOfHeteroAtom = new ArrayList<ChemEl>();
971 				for (Atom atom : atomList) {//this iterates in the same order as the numbering system
972 					if (atom.getElement() != ChemEl.C){
973 						heteroatoms.add(atom);
974 						elementOfHeteroAtom.add(atom.getElement());
975 					}
976 				}
977 				if (locants.length == heteroatoms.size()){//as many locants as there are heteroatoms to assign
978 					//check for special case of a single locant indicating where the group substitutes e.g. 4-benzofuran-2-yl
979 					if (!(locants.length == 1 && OpsinTools.getPreviousSibling(locantEl) == null
980 							 && ComponentProcessor.checkLocantPresentOnPotentialRoot(state, benzoEl.getParent(), locants[0]))) {
981 						for (Atom atom : heteroatoms) {
982 							atom.setElement(ChemEl.C);
983 						}
984 						for (int i=0; i< heteroatoms.size(); i++) {
985 							fusedRing.getAtomByLocantOrThrow(locants[i]).setElement(elementOfHeteroAtom.get(i));
986 						}
987 						locantEl.detach();
988 					}
989 				}
990 				else if (locants.length > 1){
991 					throw new StructureBuildingException("Unable to assign all locants to benzo-fused ring or multiplier was mising");
992 				}
993 			}
994 		}
995 	}
996 
locantsCouldApplyToHeteroatomPositions(String[] locants, Element benzoEl)997 	private boolean locantsCouldApplyToHeteroatomPositions(String[] locants, Element benzoEl) {
998 		if (!locantsAreAllNumeric(locants)) {
999 			return false;
1000 		}
1001 		List<Element> suffixes = benzoEl.getParent().getChildElements(SUFFIX_EL);
1002 		int suffixesWithoutLocants = 0;
1003 		for (Element suffix : suffixes) {
1004 			if (suffix.getAttribute(LOCANT_ATR)==null){
1005 				suffixesWithoutLocants++;
1006 			}
1007 		}
1008 		if (locants.length == suffixesWithoutLocants){//In preference locants will be assigned to suffixes rather than to this nomenclature
1009 			return false;
1010 		}
1011 		return true;
1012 	}
1013 
locantsAreAllNumeric(String[] locants)1014 	private boolean locantsAreAllNumeric(String[] locants) {
1015 		for (String locant : locants) {
1016 			if (!MATCH_NUMERIC_LOCANT.matcher(locant).matches()){
1017 				return false;
1018 			}
1019 		}
1020 		return true;
1021 	}
1022 }
1023