Coverage Report - net.cscott.sdr.calls.GeneralFormationMatcher
 
Classes in this File Line Coverage Branch Coverage Complexity
GeneralFormationMatcher
98%
158/161
78%
85/108
3.25
GeneralFormationMatcher$1
100%
3/3
N/A
3.25
GeneralFormationMatcher$2
100%
4/4
50%
1/2
3.25
GeneralFormationMatcher$3
100%
17/17
100%
8/8
3.25
GeneralFormationMatcher$4
83%
5/6
100%
2/2
3.25
GeneralFormationMatcher$5
100%
2/2
N/A
3.25
GeneralFormationMatcher$6
75%
3/4
50%
1/2
3.25
GeneralFormationMatcher$GoalInfo
100%
25/25
81%
18/22
3.25
GeneralFormationMatcher$GoalInfo$1
100%
2/2
N/A
3.25
GeneralFormationMatcher$MatchInfo
100%
17/17
100%
2/2
3.25
GeneralFormationMatcher$OneMatch
100%
3/3
N/A
3.25
GeneralFormationMatcher$Warp
100%
14/14
57%
8/14
3.25
GeneralFormationMatcher$Warp$1
100%
2/2
N/A
3.25
GeneralFormationMatcher$Warp$2
100%
3/3
N/A
3.25
 
 1  
 package net.cscott.sdr.calls;
 2  
 
 3  
 import static net.cscott.sdr.util.Tools.m;
 4  
 import static net.cscott.sdr.util.Tools.p;
 5  
 
 6  
 import java.util.ArrayList;
 7  
 import java.util.Arrays;
 8  
 import java.util.Collections;
 9  
 import java.util.Comparator;
 10  
 import java.util.HashMap;
 11  
 import java.util.LinkedHashMap;
 12  
 import java.util.LinkedHashSet;
 13  
 import java.util.List;
 14  
 import java.util.Map;
 15  
 import java.util.Set;
 16  
 
 17  
 import net.cscott.jdoctest.JDoctestRunner;
 18  
 import net.cscott.jutil.BitSetFactory;
 19  
 import net.cscott.jutil.GenericMultiMap;
 20  
 import net.cscott.jutil.Indexer;
 21  
 import net.cscott.jutil.MultiMap;
 22  
 import net.cscott.jutil.PersistentSet;
 23  
 import net.cscott.jutil.SetFactory;
 24  
 import net.cscott.sdr.calls.Breather.FormationPiece;
 25  
 import net.cscott.sdr.calls.Position.Flag;
 26  
 import net.cscott.sdr.calls.TaggedFormation.Tag;
 27  
 import net.cscott.sdr.calls.TaggedFormation.TaggedDancerInfo;
 28  
 import net.cscott.sdr.util.Fraction;
 29  
 
 30  
 import org.junit.runner.RunWith;
 31  
 
 32  
 /**
 33  
  * {@link GeneralFormationMatcher} produces a {@link FormationMatch}
 34  
  * given an input {@link Formation} and a goal {@link TaggedFormation}.
 35  
  * This can be used to make {@link Matcher}s out of {@link TaggedFormation}s,
 36  
  * via the {@link #makeMatcher} method.
 37  
  *
 38  
  * @author C. Scott Ananian
 39  
  */
 40  1
 @RunWith(value=JDoctestRunner.class)
 41  540673
 public class GeneralFormationMatcher {
 42  0
     private GeneralFormationMatcher() {}
 43  
     // currying, oh, my
 44  
     public static Matcher makeMatcher(TaggedFormation... goals) {
 45  1955
         return makeMatcher(Arrays.asList(goals));
 46  
     }
 47  
     public static Matcher makeMatcher(List<TaggedFormation> goals) {
 48  1955
         String name = targetName(goals);
 49  1955
         return makeMatcher(name, goals);
 50  
     }
 51  
     public static Matcher makeMatcher(final String name, final List<TaggedFormation> goals) {
 52  1957
         return new Matcher() {
 53  
             @Override
 54  
             public FormationMatch match(Formation f) throws NoMatchException {
 55  20990
                 return doMatch(f, goals, false, false);
 56  
             }
 57  
             @Override
 58  480
             public String getName() { return name; }
 59  
         };
 60  
     }
 61  
 
 62  
     /**
 63  
      * Attempt to match the input formation against the goal formation; you can
 64  
      * have multiple rotated copies of the goal formation in the input.
 65  
      * Allow dancers who are not part of copies of the goal formation if
 66  
      * allowUnmatchedDancers is true; allow copies of the goal formation
 67  
      * with phantoms in them if usePhantoms is true.  Returns the best
 68  
      * such match (ie, most copies of the goal formation).
 69  
      * @param input An untagged formation to match against.
 70  
      * @param goal A tagged goal formation
 71  
      * @param allowUnmatchedDancers allow dancers in the input formation not to
 72  
      *        match dancers in (copies of) the goal
 73  
      * @param usePhantoms allow dancers in the goal formation not to match
 74  
      *        dancers in the input
 75  
      * @return the match result
 76  
      * @throws NoMatchException if there is no way to match the goal formation
 77  
      *   with the given input
 78  
      * @doc.test A successful match with no phantoms or unmatched dancers:
 79  
      *  js> FormationList = FormationListJS.initJS(this); undefined;
 80  
      *  js> GeneralFormationMatcher.doMatch(Formation.SQUARED_SET,
 81  
      *    >                                 FormationList.COUPLE,
 82  
      *    >                                 false, false)
 83  
      *       AAv
 84  
      *  
 85  
      *  BB>       CC<
 86  
      *  
 87  
      *       DD^
 88  
      *  AA:
 89  
      *     3B^  3G^
 90  
      *   [3B: BEAU; 3G: BELLE]
 91  
      *  BB:
 92  
      *     4B^  4G^
 93  
      *   [4B: BEAU; 4G: BELLE]
 94  
      *  CC:
 95  
      *     2B^  2G^
 96  
      *   [2B: BEAU; 2G: BELLE]
 97  
      *  DD:
 98  
      *     1B^  1G^
 99  
      *   [1B: BEAU; 1G: BELLE]
 100  
      * @doc.test A successful match with some unmatched dancers:
 101  
      *  js> FormationList = FormationListJS.initJS(this); undefined;
 102  
      *  js> GeneralFormationMatcher.doMatch(FormationList.RH_TWIN_DIAMONDS,
 103  
      *    >                                 FormationList.RH_MINIWAVE,
 104  
      *    >                                 true, false)
 105  
      *  AA>  BB>
 106  
      *  
 107  
      *  CC^  DDv
 108  
      *  
 109  
      *  EE<  FF<
 110  
      *  AA: (unmatched)
 111  
      *     ^
 112  
      *  BB: (unmatched)
 113  
      *     ^
 114  
      *  CC:
 115  
      *     ^    v
 116  
      *   [ph: BEAU; ph: BEAU]
 117  
      *  DD:
 118  
      *     ^    v
 119  
      *   [ph: BEAU; ph: BEAU]
 120  
      *  EE: (unmatched)
 121  
      *     ^
 122  
      *  FF: (unmatched)
 123  
      *     ^
 124  
      * @doc.test When possible, symmetry is preserved in the result:
 125  
      *  js> FormationList = FormationListJS.initJS(this); undefined;
 126  
      *  js> GeneralFormationMatcher.doMatch(FormationList.PARALLEL_RH_WAVES,
 127  
      *    >                                 FormationList.RH_MINIWAVE,
 128  
      *    >                                 false, false)
 129  
      *  AA^  BBv
 130  
      *  
 131  
      *  CC^  DDv
 132  
      *  AA:
 133  
      *     ^    v
 134  
      *   [ph: BEAU; ph: BEAU]
 135  
      *  BB:
 136  
      *     ^    v
 137  
      *   [ph: BEAU; ph: BEAU]
 138  
      *  CC:
 139  
      *     ^    v
 140  
      *   [ph: BEAU; ph: BEAU]
 141  
      *  DD:
 142  
      *     ^    v
 143  
      *   [ph: BEAU; ph: BEAU]
 144  
      */
 145  
     // booleans for 'allow unmatched dancers' and
 146  
     // 'use phantoms' allow dancers in the input and result formations,
 147  
     // respectively, not to match up.
 148  
     // XXX: implement usePhantoms
 149  
     // NOTE THAT result will include 1-dancer formations if
 150  
     // allowUnmatchedDancers is true; see the contract of FormationMatch.
 151  
     public static FormationMatch doMatch(
 152  
             final Formation input, 
 153  
             final TaggedFormation goal,
 154  
             boolean allowUnmatchedDancers,
 155  
             boolean usePhantoms)
 156  
     throws NoMatchException {
 157  274
         return doMatch(input, Collections.singletonList(goal),
 158  
                        allowUnmatchedDancers, usePhantoms);
 159  
     }
 160  
     private static String targetName(List<TaggedFormation> goals) {
 161  23220
         StringBuilder sb = new StringBuilder();
 162  23220
         assert goals.size() > 0;
 163  23220
         for (TaggedFormation goal : goals) {
 164  30468
             String name = (goal instanceof NamedTaggedFormation) ?
 165  
                     ((NamedTaggedFormation) goal).getName() : goal.toString();
 166  30468
             if (sb.length()>0) sb.append(',');
 167  30468
             sb.append(name);
 168  30468
         }
 169  23220
         if (goals.size()>1)
 170  3712
             return "MIXED("+sb.toString()+")";
 171  19508
         return sb.toString();
 172  
     }
 173  
     /** sort so that first dancers' target rotations are most constrained,
 174  
      *  for efficiency. (ie largest rotation moduli are first) */
 175  1
     private static Comparator<Position> PCOMP = new Comparator<Position>() {
 176  
         public int compare(Position p1, Position p2) {
 177  467522
             int c = -p1.facing.modulus.compareTo(p2.facing.modulus);
 178  467522
             if (c!=0) return c;
 179  467522
             return p1.compareTo(p2);
 180  
         }
 181  
     };
 182  
     /** Allow multiple simultaneous goal formations.
 183  
      * @doc.test A successful match on a Siamese Diamond.
 184  
      *  js> importPackage(net.cscott.sdr.util); // for Fraction
 185  
      *  js> FormationList = FormationListJS.initJS(this); undefined;
 186  
      *  js> f = Formation.SQUARED_SET; undefined
 187  
      *  js> for each (d in StandardDancer.values()) {
 188  
      *    >   if (d.isHead()) continue;
 189  
      *    >   p = f.location(d).forwardStep(Fraction.valueOf(2), true).
 190  
      *    >                     turn(Fraction.valueOf(1,4), false);
 191  
      *    >   f = f.move(d, p);
 192  
      *    > }; f.toStringDiagram('|');
 193  
      *  |3Gv  3Bv
 194  
      *  |
 195  
      *  |4Bv  2G^
 196  
      *  |
 197  
      *  |4Gv  2B^
 198  
      *  |
 199  
      *  |1B^  1G^
 200  
      *  js> goals = java.util.Arrays.asList(FormationList.TANDEM,
 201  
      *    >                                 FormationList.COUPLE); undefined
 202  
      *  js> GeneralFormationMatcher.doMatch(f, goals, false, false)
 203  
      *    AAv
 204  
      *  
 205  
      *  BBv  CC^
 206  
      *  
 207  
      *    DD^
 208  
      *  AA:
 209  
      *     3B^  3G^
 210  
      *   [3B: BEAU; 3G: BELLE]
 211  
      *  BB:
 212  
      *     4G^
 213  
      *     
 214  
      *     4B^
 215  
      *   [4G: LEADER; 4B: TRAILER]
 216  
      *  CC:
 217  
      *     2G^
 218  
      *     
 219  
      *     2B^
 220  
      *   [2G: LEADER; 2B: TRAILER]
 221  
      *  DD:
 222  
      *     1B^  1G^
 223  
      *   [1B: BEAU; 1G: BELLE]
 224  
      */
 225  
     public static FormationMatch doMatch(
 226  
                 final Formation input,
 227  
                 final List<TaggedFormation> goals,
 228  
                 boolean allowUnmatchedDancers,
 229  
                 boolean usePhantoms)
 230  
         throws NoMatchException {
 231  21265
         assert !usePhantoms : "matching with phantoms is not implemented";
 232  
         // get an appropriate formation name
 233  21265
         String target = targetName(goals);
 234  
 
 235  
         // okay, try to perform match by trying to use each dancer in turn
 236  
         // as dancer #1 in the goal formation.  We then validate the match:
 237  
         // make sure that there is a dancer in each position, that no dancer
 238  
         // in the match is already in another match, and that the state isn't
 239  
         // redundant due to symmetry.  (To determine this last, we identify
 240  
         // those 'other dancers' in the goal formation which are rotationally
 241  
         // symmetric to dancer #1, and make sure that the proposed match
 242  
         // doesn't assign any of these positions to dancers we've already
 243  
         // tried as #1.)  Finally, we'll have a list of matches.  We
 244  
         // identify the ones with a maximum number of the goal formation,
 245  
         // and assert that this maximal match is unique; otherwise the
 246  
         // match is ambiguous and we throw NoMatchException.
 247  
         // (note that we ignore unselected dancers in formation f)
 248  
         // (note that 'dancer #1' is really 'dancer #0' below)
 249  21265
         assert goals.size() > 0;
 250  21265
         int minGoalDancers = goals.get(0).dancers().size();
 251  21265
         List<GoalInfo> goalInfo = new ArrayList<GoalInfo>(goals.size());
 252  21265
         for (TaggedFormation goal : goals) {
 253  
             // create GoalInfo
 254  24891
             goalInfo.add(new GoalInfo(goal));
 255  24891
             minGoalDancers = Math.min(minGoalDancers, goal.dancers().size());
 256  
         }
 257  21265
         if (minGoalDancers > input.dancers().size())
 258  1865
             throw new NoMatchException(target, "goal is too large");
 259  
 
 260  
         // sort the input dancers the same as the goal dancers: real dancers
 261  
         // before phantoms.
 262  
         // there must be at least one non-phantom dancer in the formation.
 263  
         // in addition, group symmetric dancers together in the order, so
 264  
         // that the resulting matches tend to symmetry.
 265  19400
         final List<Dancer> inputDancers=new ArrayList<Dancer>(input.dancers());
 266  19400
         Collections.sort(inputDancers, new Comparator<Dancer>() {
 267  
             /** minimum of position rotated through 4 quarter rotations */
 268  
             private Position qtrMin(Position p) {
 269  193106
                 if (!qtrMinCache.containsKey(p))
 270  67090
                     qtrMinCache.put(p, Collections.min(rotated(p), PCOMP));
 271  193106
                 return qtrMinCache.get(p);
 272  
             }
 273  
             /** minimum of position rotated by 180 degrees */
 274  
             private Position halfMin(Position p) {
 275  70000
                 if (!halfMinCache.containsKey(p)) {
 276  57626
                     Position pprime = p.rotateAroundOrigin(ExactRotation.ONE_HALF);
 277  57626
                     Position r = Collections.min(Arrays.asList(p,pprime), PCOMP);
 278  57626
                     halfMinCache.put(p, r);
 279  
                 }
 280  70000
                 return halfMinCache.get(p);
 281  
             }
 282  
             public int compare(Dancer d1, Dancer d2) {
 283  96553
                 Position p1 = input.location(d1), p2 = input.location(d2);
 284  
                 // first comparison is against min of quarter-rotated versions
 285  96553
                 int c = PCOMP.compare(qtrMin(p1), qtrMin(p2));
 286  96553
                 if (c!=0) return c;
 287  
                 // now, compare against min of half-rotated versions
 288  35000
                 c = PCOMP.compare(halfMin(p1), halfMin(p2));
 289  35000
                 if (c!=0) return c;
 290  
                 // finally, break ties by comparing against "real" position
 291  28813
                 return PCOMP.compare(p1, p2);
 292  
             }
 293  19400
             private Map<Position,Position> halfMinCache = new HashMap<Position,Position>();
 294  19400
             private Map<Position,Position> qtrMinCache = new HashMap<Position,Position>();
 295  
         });
 296  19400
         final Indexer<Dancer> inputIndex = new Indexer<Dancer>() {
 297  19400
             Map<Dancer,Integer> index = new HashMap<Dancer,Integer>();
 298  19400
             { int i=0; for (Dancer d: inputDancers) index.put(d, i++); }
 299  
             @Override
 300  1791959
             public int getID(Dancer d) { return index.get(d); }
 301  
             @Override
 302  132978
             public Dancer getByID(int id) { return inputDancers.get(id); }
 303  
             @Override
 304  0
             public boolean implementsReverseMapping() { return true; }
 305  
         };
 306  19400
         final PersistentSet<Dancer> inputEmpty = new PersistentSet<Dancer>
 307  743939
         (new Comparator<Dancer>() {
 308  
            public int compare(Dancer d1, Dancer d2) {
 309  724539
                return inputIndex.getID(d1) - inputIndex.getID(d2);
 310  
             } 
 311  
         });
 312  
         
 313  
         // now try setting each dancer in 'f' to d0 in the goal formation.
 314  
 
 315  
         // Construct MatchInfo & initial (empty) assignment
 316  19400
         MatchInfo mi = new MatchInfo(input, goalInfo, minGoalDancers,
 317  
                                      inputDancers, inputIndex, inputEmpty);
 318  19400
         PersistentSet<OneMatch> initialAssignment = new PersistentSet<OneMatch>
 319  62206
         (new Comparator<OneMatch>(){
 320  
             public int compare(OneMatch o1, OneMatch o2) {
 321  42806
                 int c=inputIndex.getID(o1.dancer)-inputIndex.getID(o2.dancer);
 322  42806
                 if (c!=0) return c;
 323  0
                 return o1.extraRot.compareTo(o2.extraRot);
 324  
             }
 325  
         }); 
 326  
         // Do the match
 327  19400
         tryOne(mi, 0, initialAssignment, inputEmpty, allowUnmatchedDancers);
 328  19400
         if (mi.matches.isEmpty())
 329  5465
             throw new NoMatchException(target, "no matches");
 330  
         
 331  
         // Filter out the max
 332  13935
         int max = 0;
 333  13935
         for (PersistentSet<OneMatch> match: mi.matches)
 334  13946
             max = Math.max(max,match.size());
 335  13935
         assert max > 0;
 336  
         // Is it unique?
 337  13935
         PersistentSet<OneMatch> bestMatch=null; boolean found = false;
 338  13935
         for (PersistentSet<OneMatch> match: mi.matches)
 339  13946
             if (match.size()==max)
 340  13935
                 if (found) // ambiguous match.
 341  0
                     throw new NoMatchException(target, "ambiguous");
 342  
                 else {
 343  13935
                     bestMatch = match;
 344  13935
                     found = true;
 345  
                 }
 346  13935
         assert found;
 347  
         // track the input dancers who aren't involved in matches
 348  13935
         Set<Dancer> unmappedInputDancers = new LinkedHashSet<Dancer>(inputDancers);
 349  
         // Create a FormationMatch object from FormationPieces.
 350  13935
         List<FormationPiece> pieces = new ArrayList<FormationPiece>(max);
 351  13935
         Map<Dancer,TaggedFormation> canonical=new LinkedHashMap<Dancer,TaggedFormation>();
 352  13935
         for (OneMatch om : bestMatch) {
 353  17657
             Dancer id0 = om.dancer;//input dancer who's #1 in the goal formation
 354  17657
             int dn0 = inputIndex.getID(id0);
 355  17657
             Position inP = mi.inputPositions.get(dn0);
 356  
             assert inP.facing instanceof ExactRotation :
 357  17657
                 "at least one real dancer must be in formation";
 358  
             // make an ExactRotation for pGoal, given the extraRot
 359  17657
             Position goP = makeExact(om.gi.goalPositions.get(0), om.extraRot);
 360  17657
             Warp warpF = Warp.rotateAndMove(goP, inP);
 361  17657
             Warp warpB = Warp.rotateAndMove(inP, goP);
 362  17657
             ExactRotation rr = (ExactRotation) inP.facing.subtract(goP.facing.amount);
 363  17657
             Map<Dancer,Position> subPos = new LinkedHashMap<Dancer,Position>();
 364  17657
             MultiMap<Dancer,Tag> subTag = new GenericMultiMap<Dancer,Tag>();
 365  17657
             for (Dancer goD : om.gi.goalDancers) {
 366  48726
                 goP = om.gi.goal.location(goD);
 367  
                 // warp to find which input dancer corresponds to this one
 368  48726
                 inP = warpF.warp(goP, Fraction.ZERO);
 369  48726
                 Dancer inD = mi.inputPositionMap.get(zeroRotation(inP));
 370  
                 // warp back to get an exact rotation for this version of goal
 371  48726
                 Position goPr = warpB.warp(input.location(inD), Fraction.ZERO);
 372  
                 // to avoid distortion for 1/8 off formations, take only the
 373  
                 // rotation (and flags) from this new goP
 374  48726
                 goP = goPr.relocate(goP.x, goP.y, goPr.facing);
 375  
                 // add to this subformation.
 376  48726
                 subPos.put(inD, goP);
 377  48726
                 subTag.addAll(inD, om.gi.goal.tags(goD));
 378  48726
                 unmappedInputDancers.remove(inD);
 379  48726
             }
 380  17657
             TaggedFormation tf =
 381  
                 new TaggedFormation(new Formation(subPos), subTag);
 382  17657
             Dancer dd = new PhantomDancer();
 383  17657
             canonical.put(dd, tf);
 384  
 
 385  17657
             Formation pieceI = input.select(tf.dancers()).onlySelected();
 386  17657
             Formation pieceO = new Formation(m(p(dd, new Position(0,0,rr))));
 387  17657
             pieces.add(new FormationPiece(pieceI, pieceO));
 388  17657
         }
 389  
         // add pieces for unmapped dancers (see spec for FormationMatch.meta)
 390  13935
         Set<Dancer> unmatchedMetaDancers = new LinkedHashSet<Dancer>();
 391  13935
         for (Dancer d : unmappedInputDancers) {
 392  
             // these clauses are parallel to the ones above for matched dancers
 393  4
             Position inP = input.location(d);
 394  4
             Position goP = Position.getGrid(0,0,"n");
 395  4
             ExactRotation rr = (ExactRotation) // i know this is a no-op.
 396  
                 inP.facing.subtract(goP.facing.amount);
 397  
 
 398  4
             Dancer dd = new PhantomDancer();
 399  4
             TaggedFormation tf = new TaggedFormation
 400  
                 (new TaggedDancerInfo(d, goP));
 401  4
             canonical.put(dd, tf);
 402  4
             unmatchedMetaDancers.add(dd);
 403  
 
 404  4
             Formation pieceI = input.select(tf.dancers()).onlySelected();
 405  4
             Formation pieceO = new Formation(m(p(dd, new Position(0,0,rr))));
 406  4
             pieces.add(new FormationPiece(pieceI, pieceO));
 407  4
         }
 408  
         // the components formations are the warped & rotated version.
 409  
         // the rotation in 'components' tells how much they were rotated.
 410  
         // the canonical formations have the input dancers, and the formations
 411  
         // are unwarped and unrotated.  The key dancers in the canonical map
 412  
         // are the phantoms from the meta formation.
 413  13935
         return new FormationMatch(Breather.breathe(pieces), canonical,
 414  
                                   unmatchedMetaDancers);
 415  
     }
 416  
     private static class OneMatch {
 417  
         /** Which goal formation. */
 418  
         public final GoalInfo gi;
 419  
         /** This input dancer is #1 in the goal formation. */
 420  
         public final Dancer dancer;
 421  
         /** This is the 'extra rotation' needed to align the goal formation,
 422  
          * if dancer #1 in the goal formation allows multiple orientations. */
 423  
         public final Fraction extraRot;
 424  116928
         OneMatch(GoalInfo gi, Dancer dancer, Fraction extraRot) {
 425  116928
             this.gi = gi; this.dancer = dancer; this.extraRot = extraRot;
 426  116928
         }
 427  
     }
 428  1
     private static class GoalInfo {
 429  
         final List<Dancer> goalDancers;
 430  
         final List<Position> goalPositions;
 431  
         final TaggedFormation goal;
 432  
         final Set<Dancer> eq0; // goal dancers who are symmetric to goal dancer #0
 433  
         final int numExtra; // number of 'extra' rotations we'll try to match
 434  24891
         GoalInfo(final TaggedFormation goal) {
 435  24891
             this.goal = goal;
 436  
             // make a canonical ordering for the goal dancers
 437  24891
             this.goalDancers=new ArrayList<Dancer>(goal.dancers());
 438  73151
             Collections.sort(this.goalDancers, new Comparator<Dancer>() {
 439  
                 public int compare(Dancer d1, Dancer d2) {
 440  48260
                     return PCOMP.compare(goal.location(d1), goal.location(d2));
 441  
                 }
 442  
             });
 443  24891
             SetFactory<Dancer> gsf = new BitSetFactory<Dancer>(goalDancers);
 444  
             // Identify dancers who are symmetric to dancer #0
 445  24891
             assert goal.isCentered(); // Assumes center of goal formation is 0,0
 446  24891
             Dancer gd0 = goalDancers.get(0);
 447  24891
             this.eq0 = gsf.makeSet();
 448  24891
             Position p0 = goal.location(gd0).normalize(); // remember p0.facing is most exact
 449  24891
             for (Dancer gd : goalDancers)
 450  73151
                 for (Position rp: rotated(goal.location(gd)))
 451  292604
                         if (rp.x.equals(p0.x) && rp.y.equals(p0.y)&&
 452  
                                 rp.facing.includes(p0.facing))
 453  50887
                             eq0.add(gd);
 454  24891
             assert eq0.contains(gd0);//at the very least, gd0 is symmetric to itself
 455  
             // map dancer # to position
 456  24891
             this.goalPositions = new ArrayList<Position>(goalDancers.size());
 457  24891
             for (Dancer d : goalDancers) {
 458  73151
                 Position p = goal.location(d);
 459  73151
                 this.goalPositions.add(p);
 460  73151
             }
 461  
             // first goal dancer has a rotation modulus which is 1/N for some
 462  
             // N.  This means we need to try N other rotations for matches.
 463  24891
             Fraction highestModulus =
 464  
                 goal.location(goalDancers.get(0)).facing.modulus;
 465  24891
             if (highestModulus.compareTo(Fraction.ZERO) != 0) {
 466  24889
                 this.numExtra = highestModulus.getDenominator();
 467  
             } else {
 468  
                 // all phantoms.  Try all rotations by 1/8.
 469  2
                 this.numExtra = 8;
 470  
             }
 471  24891
         }
 472  
     }
 473  
     private static class MatchInfo {
 474  19400
         final List<PersistentSet<OneMatch>> matches = new ArrayList<PersistentSet<OneMatch>>();
 475  
         final Indexer<Dancer> inputIndex;
 476  19400
         final Map<Position,Dancer> inputPositionMap = new HashMap<Position,Dancer>();
 477  19400
         final List<Position> inputPositions = new ArrayList<Position>();
 478  
         final List<GoalInfo> goals;
 479  
         final int minGoalDancers;
 480  
         final int numInput;
 481  
         final Set<Dancer> sel; // input dancers who are selected
 482  
         // these next one is used to pass info into validate & back:
 483  
         /** Input dancers who are already assigned to a formation. */
 484  
         PersistentSet<Dancer> inFormation;
 485  
         /** Size of the current best match. */
 486  19400
         int bestMatchSize = 0;
 487  
         MatchInfo(Formation f, List<GoalInfo> goals, int minGoalDancers,
 488  
                   List<Dancer> inputDancers, Indexer<Dancer> inputIndex,
 489  19400
                   PersistentSet<Dancer> inputEmpty) {
 490  19400
             for (Dancer d : inputDancers) {
 491  67090
                 Position p = f.location(d);
 492  67090
                 this.inputPositions.add(p);
 493  67090
                 this.inputPositionMap.put(zeroRotation(p), d);
 494  67090
             }
 495  19400
             this.numInput = inputDancers.size();
 496  19400
             this.inputIndex = inputIndex;
 497  19400
             this.sel = f.selected;
 498  19400
             this.inFormation = inputEmpty;
 499  19400
             this.goals = goals;
 500  19400
             this.minGoalDancers = minGoalDancers;
 501  19400
         }
 502  
     }
 503  
     private static boolean validate(MatchInfo mi, GoalInfo goal, int dancerNum, Fraction extraRot) {
 504  116928
         PersistentSet<Dancer> inFormation = mi.inFormation;
 505  116928
         Set<Dancer> eq0 = goal.eq0;
 506  
         // find some Dancer in the input formation to correspond to each
 507  
         // Dancer in the goal formation.  Each such dancer must not already
 508  
         // be assigned.
 509  116928
         Position pIn = mi.inputPositions.get(dancerNum);
 510  
         assert pIn.facing instanceof ExactRotation :
 511  116928
             "at least one dancer in the input formation must be non-phantom";
 512  116928
         Position pGoal = makeExact(goal.goalPositions.get(0), extraRot);
 513  116928
         Warp warp = Warp.rotateAndMove(pGoal, pIn);
 514  116928
         int gNum = 0;
 515  116928
         for (Position gp : goal.goalPositions) {
 516  
             // compute warped position.
 517  291583
             gp = warp.warp(gp, Fraction.ZERO);
 518  291583
             Position key = zeroRotation(gp);
 519  291583
             if (!mi.inputPositionMap.containsKey(key))
 520  51971
                 return false; // no input dancer at this goal position.
 521  
             // okay, there is an input dancer:
 522  239612
             Dancer iDan = mi.inputPositionMap.get(key);
 523  239612
             int iNum = mi.inputIndex.getID(iDan);
 524  
             // if this dancer selected?
 525  239612
             if (!mi.sel.contains(iDan))
 526  0
                 return false; // this dancer isn't selected.
 527  
             // is he free to be assigned to this formation?
 528  239612
             if (inFormation.contains(iDan))
 529  3497
                 return false; // this dancer is already in some match
 530  
             // is his facing direction consistent?
 531  236115
             Position ip = mi.inputPositions.get(iNum);
 532  236115
             assert ip.x.equals(gp.x) && ip.y.equals(gp.y);
 533  236115
             if (!gp.facing.includes(ip.facing))
 534  9090
                 return false; // rotations aren't correct.
 535  
             // check for symmetry: if this goal position is 'eq0' (ie,
 536  
             // symmetric with the 0 dancer's position), then this dancer #
 537  
             // must be >= the 0 dancer's input # (ie, dancerNum)
 538  227025
             if (eq0.contains(goal.goalDancers.get(gNum)))
 539  170884
                 if (iNum < dancerNum) {
 540  
                     // check that our matching rotation is really symmetric,
 541  
                     // since the goal dancer may have a vague direction which
 542  
                     // includes an asymmetric alternative (ie, "n|" as a target)
 543  26907
                     for (Position gp0 : rotated(goal.goalPositions.get(0))) {
 544  78881
                         gp0 = warp.warp(gp0, Fraction.ZERO);
 545  78881
                         if (ip.x.equals(gp0.x) &&
 546  
                             ip.y.equals(gp0.y) &&
 547  
                             gp0.facing.includes(ip.facing))
 548  26907
                             return false; // symmetric to some other canonical formation
 549  
                     }
 550  
                 }
 551  
             // update 'in formation' and 'gNum'
 552  200118
             inFormation = inFormation.add(iDan);
 553  200118
             gNum++;
 554  200118
         }
 555  
         // return updates to inFormation
 556  25463
         mi.inFormation = inFormation;
 557  25463
         return true; // this is a valid match.
 558  
     }
 559  
         
 560  
     private static void tryOne(MatchInfo mi, int dancerNum,
 561  
             PersistentSet<OneMatch> currentAssignment,
 562  
             PersistentSet<Dancer> inFormation,
 563  
             boolean allowUnmatchedDancers) {
 564  177841
         if (dancerNum >= mi.numInput) {
 565  37372
             if (inFormation.size() != mi.numInput)
 566  23427
                 if (!allowUnmatchedDancers)
 567  23424
                     return; // not a good assignment
 568  
             // we've got a complete assignment; save it.
 569  13948
             if (!currentAssignment.isEmpty() &&
 570  
                 currentAssignment.size() >= mi.bestMatchSize) {
 571  13946
                 mi.bestMatchSize = currentAssignment.size();
 572  13946
                 mi.matches.add(currentAssignment);
 573  
             }
 574  13948
             return;
 575  
         }
 576  
 
 577  
         // is there any way we can still match the bestMatchSize?
 578  140469
         int dancer0sLeftToAssign = mi.numInput - dancerNum;
 579  140469
         if (currentAssignment.size() + dancer0sLeftToAssign < mi.bestMatchSize)
 580  7480
             return; // not enough dancer 0's to beat the current best match
 581  132989
         int dancersLeftToAssign = mi.numInput - inFormation.size();
 582  132989
         int goalsLeftToAssign = mi.bestMatchSize - currentAssignment.size();
 583  132989
         if (mi.minGoalDancers*goalsLeftToAssign > dancersLeftToAssign)
 584  11
             return; // not enough unassigned dancers to beat the best match
 585  
 
 586  
         // is this dancer available to be assigned?
 587  132978
         Dancer thisDancer = mi.inputIndex.getByID(dancerNum);
 588  132978
         if (!inFormation.contains(thisDancer)) {
 589  
             // okay, try to assign the thisDancer, possibly w/ some extra rotation
 590  85817
             for (GoalInfo gi : mi.goals) {
 591  220812
                 for (int i=0; i < gi.numExtra; i++) {
 592  116928
                     Fraction extraRot = Fraction.valueOf(i, gi.numExtra);
 593  116928
                     PersistentSet<OneMatch> newAssignment =
 594  
                         currentAssignment.add(new OneMatch(gi, thisDancer, extraRot));
 595  116928
                     mi.inFormation = inFormation;
 596  116928
                     if (validate(mi, gi, dancerNum, extraRot)) // sets mi.inFormation
 597  
                         // try to extend this match!
 598  25463
                         tryOne(mi, dancerNum+1, newAssignment, mi.inFormation,
 599  
                                 allowUnmatchedDancers);
 600  
                 }
 601  
             }
 602  
         }
 603  
 
 604  
         // try NOT assigning this dancer
 605  132978
         tryOne(mi, dancerNum+1, currentAssignment, inFormation,
 606  
                allowUnmatchedDancers);
 607  132978
     }
 608  
 
 609  
     /** Make a position with an ExactRotation from the given position with a
 610  
      * general rotation and an 'extra rotation' amount. */
 611  
     private static Position makeExact(Position p, Fraction extraRot) {
 612  134585
         return new Position(p.x, p.y, 
 613  
                 new ExactRotation(p.facing.amount.add(extraRot)));
 614  
     }
 615  
     /** Make a position with exact zero rotation from the given position. */
 616  
     private static Position zeroRotation(Position p) {
 617  407399
         return new Position(p.x, p.y, ExactRotation.ZERO);
 618  
     }
 619  
     private static Set<Position> rotated(Position p) {
 620  167148
         Set<Position> s = new LinkedHashSet<Position>(4);
 621  835740
         for (int i=0; i<4; i++) {
 622  668592
             s.add(p);
 623  668592
             p = p.rotateAroundOrigin(ExactRotation.ONE_QUARTER);
 624  
         }
 625  167148
         return s;
 626  
     }
 627  
     /** @deprecated XXX: rewrite to remove dependency on old Warp class */
 628  876308
     private static abstract class Warp {
 629  
         public abstract Position warp(Position p, Fraction time);
 630  
         /** A <code>Warp</code> which returns points unchanged. */
 631  1
         public static final Warp NONE = new Warp() {
 632  27409
             public Position warp(Position p, Fraction time) { return p; }
 633  
         };
 634  
         /** Returns a <code>Warp</code> which will rotate and translate
 635  
          * points such that <code>from</code> is warped to <code>to</code>.
 636  
          * Requires that both {@code from.facing} and {@code to.facing} are
 637  
          * {@link ExactRotation}s.
 638  
          */
 639  
         // XXX is this the right spec?  Should we allow general Rotations?
 640  
         public static Warp rotateAndMove(Position from, Position to) {
 641  152242
             assert from.facing instanceof ExactRotation;
 642  152242
             assert to.facing instanceof ExactRotation;
 643  152242
             if (from.equals(to)) return NONE;
 644  145266
             ExactRotation rot = (ExactRotation) to.facing.add(from.facing.amount.negate());
 645  145266
             Position nFrom = rotateCWAroundOrigin(from,rot);
 646  145266
             final Position warp = new Position
 647  
                 (to.x.subtract(nFrom.x), to.y.subtract(nFrom.y),
 648  
                  rot);
 649  145266
             Warp w = new Warp() {
 650  
                 public Position warp(Position p, Fraction time) {
 651  585773
                     p = rotateCWAroundOrigin(p, (ExactRotation) warp.facing);
 652  585773
                     return p.relocate
 653  
                     (p.x.add(warp.x), p.y.add(warp.y), p.facing);
 654  
                 }
 655  
             };
 656  145266
             assert to.setFlags(from.flags.toArray(new Flag[0])).equals(w.warp(from,Fraction.ZERO)) : "bad warp "+to+" vs "+w.warp(from, Fraction.ZERO);
 657  145266
             return w;
 658  
         }
 659  
         // helper method for rotateAndMove
 660  
         private static Position rotateCWAroundOrigin(Position p, ExactRotation amt) {
 661  731039
             Fraction x = p.x.multiply(amt.toY()).add(p.y.multiply(amt.toX()));
 662  731039
             Fraction y = p.y.multiply(amt.toY()).subtract(p.x.multiply(amt.toX()));
 663  731039
             return p.relocate(x, y, p.facing.add(amt.amount));
 664  
         }
 665  
     }
 666  
 }