001/* 002 * BioJava development code 003 * 004 * This code may be freely distributed and modified under the 005 * terms of the GNU Lesser General Public Licence. This should 006 * be distributed with the code. If you do not have a copy, 007 * see: 008 * 009 * http://www.gnu.org/copyleft/lesser.html 010 * 011 * Copyright for this code is held jointly by the individual 012 * authors. These should be listed in @author doc comments. 013 * 014 * For more information on the BioJava project and its aims, 015 * or to join the biojava-l mailing list, visit the home page 016 * at: 017 * 018 * http://www.biojava.org/ 019 * 020 * Created on 2012-11-20 021 * 022 */ 023 024package org.biojava.nbio.structure; 025 026import java.util.*; 027import java.util.Map.Entry; 028import java.util.regex.Matcher; 029import java.util.regex.Pattern; 030 031/** 032 * A chainName, a start residue, and an end residue. The chainName is matched 033 * to {@link Chain#getName()}, so for mmCIF files it indicates the authorId 034 * rather than the asymId. 035 * 036 * Chain may be null when referencing a single-chainName structure; for multi-chainName 037 * structures omitting the chainName is an error. Start and/or end may also be null, 038 * which is interpreted as the first and last residues in the chainName, respectively. 039 * 040 * @author dmyerstu 041 * @see ResidueNumber 042 * @see org.biojava.nbio.structure.ResidueRangeAndLength 043 */ 044public class ResidueRange { 045 046 private final String chainName; 047 private final ResidueNumber end; 048 private final ResidueNumber start; 049 050 public static final Pattern RANGE_REGEX = Pattern.compile( 051 "^\\s*([a-zA-Z0-9]+|_)" + //chainName ID. Be flexible here, rather than restricting to 4-char IDs 052 "(?:" + //begin range, this is a "non-capturing group" 053 "(?::|_|:$|_$|$)" + //colon or underscore, could be at the end of a line, another non-capt. group. 054 "(?:"+ // another non capturing group for the residue range 055 "([-+]?[0-9]+[A-Za-z]?|\\^)?" + // first residue 056 "(?:" + 057 "\\s*(-)\\s*" + // hyphen indicates a range was intended 058 "([-+]?[0-9]+[A-Za-z]?|\\$)?" + // second residue 059 ")?+"+ 060 ")?+"+ 061 ")?" + //end range 062 "\\s*"); 063 064 public static final Pattern CHAIN_REGEX = Pattern.compile("^\\s*([a-zA-Z0-9]+|_)$"); 065 066 /** 067 * Parse the residue range from a string. Several formats are accepted: 068 * <ul> 069 * <li> chainName.start-end 070 * <li> chainName.residue 071 * <li> chain_start-end (for better filename compatibility) 072 * </ul> 073 * 074 * <p>Residues can be positive or negative and may include insertion codes. 075 * See {@link ResidueNumber#fromString(String)}. 076 * 077 * <p>Examples: 078 * <ul> 079 * <li><code>A:5-100</code> 080 * <li><code>A_5-100</code> 081 * <li><code>A_-5</code> 082 * <li><code>A:-12I-+12I</code> 083 * <li><code>A:^-$</code> 084 * </ul> 085 * 086 * @param s residue string to parse 087 * @return The unique ResidueRange corresponding to {@code s} 088 */ 089 public static ResidueRange parse(String s) { 090 Matcher matcher = RANGE_REGEX.matcher(s); 091 if (matcher.matches()) { 092 ResidueNumber start = null, end = null; 093 String chain = null; 094 try { 095 chain = matcher.group(1); 096 if (matcher.group(2) != null) { 097 // ^ indicates first res (start==null) 098 if(!"^".equals(matcher.group(2)) ) { 099 start = ResidueNumber.fromString(matcher.group(2)); 100 start.setChainName(chain); 101 } 102 } 103 if(matcher.group(3) == null) { 104 // single-residue range 105 end = start; 106 } else 107 // $ indicates last res (end==null) 108 if( matcher.group(4) != null && !"$".equals(matcher.group(4)) ){ 109 end = ResidueNumber.fromString(matcher.group(4)); 110 end.setChainName(chain); 111 } 112 113 return new ResidueRange(chain, start, end); 114 } catch (IllegalStateException e) { 115 throw new IllegalArgumentException("Range " + s + " was not valid", e); 116 } 117 } else if (CHAIN_REGEX.matcher(s).matches()) { 118 return new ResidueRange(s, (ResidueNumber)null, null); 119 } 120 throw new IllegalArgumentException("Illegal ResidueRange format:" + s); 121 } 122 123 /** 124 * @param s 125 * A string of the form chain_start-end,chain_start-end, ... For example: 126 * <code>A.5-100,R_110-190,Z_200-250</code>. 127 * @return The unique ResidueRange corresponding to {@code s}. 128 * @see #parse(String) 129 */ 130 public static List<ResidueRange> parseMultiple(String s) { 131 s = s.trim(); 132 // trim parentheses, for backwards compatibility 133 if ( s.startsWith("(")) 134 s = s.substring(1); 135 if ( s.endsWith(")")) { 136 s = s.substring(0,s.length()-1); 137 } 138 139 String[] parts = s.split(","); 140 List<ResidueRange> list = new ArrayList<ResidueRange>(parts.length); 141 for (String part : parts) { 142 list.add(parse(part)); 143 } 144 return list; 145 } 146 147 public ResidueRange(String chainName, String start, String end) { 148 this.chainName = chainName; 149 this.start = ResidueNumber.fromString(start); 150 this.start.setChainName(chainName); 151 this.end = ResidueNumber.fromString(end); 152 this.end.setChainName(chainName); 153 } 154 155 public ResidueRange(String chainName, ResidueNumber start, ResidueNumber end) { 156 this.chainName = chainName; 157 this.start = start; 158 this.end = end; 159 } 160 161 @Override 162 public boolean equals(Object obj) { 163 if (this == obj) return true; 164 if (obj == null) return false; 165 if (getClass() != obj.getClass()) return false; 166 ResidueRange other = (ResidueRange) obj; 167 if (chainName == null) { 168 if (other.chainName != null) return false; 169 } else if (!chainName.equals(other.chainName)) return false; 170 if (end == null) { 171 if (other.end != null) return false; 172 } else if (!end.equals(other.end)) return false; 173 if (start == null) { 174 if (other.start != null) return false; 175 } else if (!start.equals(other.start)) return false; 176 return true; 177 } 178 179 public String getChainName() { 180 return chainName; 181 } 182 183 public ResidueNumber getEnd() { 184 return end; 185 } 186 187 public ResidueNumber getStart() { 188 return start; 189 } 190 191 @Override 192 public int hashCode() { 193 final int prime = 31; 194 int result = 1; 195 result = prime * result + (chainName == null ? 0 : chainName.hashCode()); 196 result = prime * result + (end == null ? 0 : end.hashCode()); 197 result = prime * result + (start == null ? 0 : start.hashCode()); 198 return result; 199 } 200 201 @Override 202 public String toString() { 203 if( start == null && end == null) { 204 // Indicates the full chainName 205 return chainName; 206 } 207 return chainName + "_" + start + "-" + end; 208 } 209 210 /** 211 * Returns the ResidueNumber that is at position {@code positionInRange} in 212 * <em>this</em> ResidueRange. 213 * @return The ResidueNumber, or false if it does not exist or is not within this ResidueRange 214 */ 215 public ResidueNumber getResidue(int positionInRange, AtomPositionMap map) { 216 if (map == null) throw new NullPointerException("The AtomPositionMap must be non-null"); 217 int i = 0; 218 for (Map.Entry<ResidueNumber, Integer> entry : map.getNavMap().entrySet()) { 219 if (i == positionInRange) return entry.getKey(); 220 if (contains(entry.getKey(), map)) { 221 i++; 222 } 223 } 224 return null; 225 } 226 227 /** 228 * @return True if and only if {@code residueNumber} is within this ResidueRange 229 */ 230 public boolean contains(ResidueNumber residueNumber, AtomPositionMap map) { 231 232 if (residueNumber == null) 233 throw new NullPointerException("Can't find the ResidueNumber because it is null"); 234 235 if (map == null) 236 throw new NullPointerException("The AtomPositionMap must be non-null"); 237 238 Integer pos = map.getPosition(residueNumber); 239 if (pos == null) throw new IllegalArgumentException("Couldn't find residue " + residueNumber.printFull()); 240 241 ResidueNumber startResidue = getStart()==null? map.getFirst(getChainName()) : getStart(); 242 Integer startPos = map.getPosition(startResidue); 243 if (startPos == null) throw new IllegalArgumentException("Couldn't find the start position"); 244 245 ResidueNumber endResidue = getEnd()==null? map.getLast(getChainName()) : getEnd(); 246 Integer endPos = map.getPosition(endResidue); 247 if (endPos == null) throw new IllegalArgumentException("Couldn't find the end position"); 248 return pos >= startPos && pos <= endPos; 249 } 250 251 /** 252 * Returns a new Iterator over every {@link ResidueNumber} in this ResidueRange. 253 * Stores the contents of {@code map} until the iterator is finished, so calling code should set the iterator to {@code null} if it did not finish. 254 */ 255 public Iterator<ResidueNumber> iterator(final AtomPositionMap map) { 256 //Use Entries to guarentee not null 257 final Iterator<Entry<ResidueNumber, Integer>> entryIt = map.getNavMap().entrySet().iterator(); 258 if(! entryIt.hasNext()) { 259 // empty iterator 260 return Arrays.asList(new ResidueNumber[0]).iterator(); 261 } 262 // Peek at upcoming entry 263 264 return new Iterator<ResidueNumber>() { 265 Entry<ResidueNumber,Integer> next = loadNext(); 266 267 private Entry<ResidueNumber,Integer> loadNext() { 268 269 while( entryIt.hasNext() ) { 270 next = entryIt.next(); 271 ResidueNumber nextPos = next.getKey(); 272 if( contains(nextPos, map)) { 273 // loaded a valid next value 274 return next; 275 } 276 } 277 next = null; 278 return next; 279 } 280 281 @Override 282 public boolean hasNext() { 283 return next != null; 284 } 285 286 @Override 287 public ResidueNumber next() { 288 if(!hasNext()){ 289 throw new NoSuchElementException(); 290 } 291 ResidueNumber pos = next.getKey(); 292 loadNext(); 293 return pos; 294 } 295 296 @Override 297 public void remove() { 298 throw new UnsupportedOperationException("Not modifiable"); 299 } 300 }; 301 } 302 303 /** 304 * Returns a new Iterator over every {@link ResidueNumber} in the list of ResidueRanges. 305 * Stores the contents of {@code map} until the iterator is finished, so calling code should set the iterator to {@code null} if it did not finish. 306 */ 307 public static Iterator<ResidueNumber> multiIterator(final AtomPositionMap map, final ResidueRange... rrs) { 308 return new Iterator<ResidueNumber>() { 309 private int r = 0; 310 private Iterator<ResidueNumber> internal; 311 @Override 312 public boolean hasNext() { 313 if (r == rrs.length - 1) { 314 init(); 315 return internal.hasNext(); 316 } 317 return true; 318 } 319 private void init() { 320 if (internal == null) { 321 internal = rrs[r].iterator(map); 322 } 323 } 324 @Override 325 public ResidueNumber next() { 326 if (rrs.length == 0) throw new NoSuchElementException(); 327 init(); 328 if (!hasNext()) throw new NoSuchElementException(); 329 if (!internal.hasNext()) { 330 r++; 331 internal = rrs[r].iterator(map); 332 } 333 return internal.next(); 334 } 335 @Override 336 public void remove() { 337 throw new UnsupportedOperationException("Not modifiable"); 338 } 339 }; 340 } 341 342 /** 343 * Returns a new Iterator over every {@link ResidueNumber} in the list of ResidueRanges. 344 * Stores the contents of {@code map} until the iterator is finished, so calling code should set the iterator to {@code null} if it did not finish. 345 */ 346 public static Iterator<ResidueNumber> multiIterator(AtomPositionMap map, List<? extends ResidueRange> rrs) { 347 ResidueRange[] ranges = new ResidueRange[rrs.size()]; 348 for (int i = 0; i < rrs.size(); i++) { 349 ranges[i] = rrs.get(i); 350 } 351 return multiIterator(map, ranges); 352 } 353 354 public static List<ResidueRange> parseMultiple(List<String> ranges) { 355 List<ResidueRange> rrs = new ArrayList<ResidueRange>(ranges.size()); 356 for (String range : ranges) { 357 ResidueRange rr = ResidueRange.parse(range); 358 if (rr != null) rrs.add(rr); 359 } 360 return rrs; 361 } 362 363 public static List<String> toStrings(List<? extends ResidueRange> ranges) { 364 List<String> list = new ArrayList<String>(ranges.size()); 365 for (ResidueRange range : ranges) { 366 list.add(range.toString()); 367 } 368 return list; 369 } 370 371 public static String toString(List<? extends ResidueRange> ranges) { 372 StringBuilder sb = new StringBuilder(); 373 for (int i = 0; i < ranges.size(); i++) { 374 sb.append(ranges.get(i)); 375 if (i < ranges.size() - 1) sb.append(","); 376 } 377 return sb.toString(); 378 } 379 380}