View Javadoc

1   /*
2   
3       dsh-piccolo-physics  Piccolo2D particle system physics integration.
4       Copyright (c) 2009-2013 held jointly by the individual authors.
5   
6       This library is free software; you can redistribute it and/or modify it
7       under the terms of the GNU Lesser General Public License as published
8       by the Free Software Foundation; either version 3 of the License, or (at
9       your option) any later version.
10  
11      This library is distributed in the hope that it will be useful, but WITHOUT
12      ANY WARRANTY; with out even the implied warranty of MERCHANTABILITY or
13      FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
14      License for more details.
15  
16      You should have received a copy of the GNU Lesser General Public License
17      along with this library;  if not, write to the Free Software Foundation,
18      Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA.
19  
20      > http://www.fsf.org/licensing/licenses/lgpl.html
21      > http://www.opensource.org/licenses/lgpl-license.php
22  
23  */
24  package org.dishevelled.piccolo.physics;
25  
26  import java.awt.geom.Point2D;
27  
28  import java.util.HashMap;
29  import java.util.Map;
30  
31  import org.piccolo2d.PNode;
32  
33  import org.piccolo2d.activities.PActivity;
34  
35  import org.piccolo2d.util.PBounds;
36  import org.piccolo2d.util.PUtil;
37  
38  import org.dishevelled.multimap.BinaryKeyMap;
39  
40  import static org.dishevelled.multimap.impl.BinaryKeyMaps.createBinaryKeyMap;
41  
42  import traer.physics.Attraction;
43  import traer.physics.Particle;
44  import traer.physics.ParticleSystem;
45  import traer.physics.Spring;
46  
47  /**
48   * Particle system activity.
49   *
50   * @author  Michael Heuer
51   * @version $Revision$ $Date$ 
52  */
53  public class ParticleSystemActivity
54      extends PActivity
55  {
56      /** Particle system for this particle system activity. */
57      private final ParticleSystem particleSystem;
58  
59      /** Map of attractions keyed by source and target nodes. */
60      private final BinaryKeyMap<PNode, PNode, Attraction> attractions;
61  
62      /** Map of particles keyed by node. */
63      private final Map<PNode, Particle> particles;
64  
65      /** Map of springs keyed by source and target nodes. */
66      private final BinaryKeyMap<PNode, PNode, Spring> springs;
67  
68  
69      /**
70       * Create a new particle system activity with the specified duration in milliseconds.
71       *
72       * @param duration duration in milliseconds, or <code>-1</code> ms for an infinite duration
73       */
74      public ParticleSystemActivity(final long duration)
75      {
76          this(duration, PUtil.DEFAULT_ACTIVITY_STEP_RATE, System.currentTimeMillis());
77      }
78  
79      /**
80       * Create a new particle system activity with the specified duration, step rate, and start time in milliseconds.
81       *
82       * @param duration duration in milliseconds, or <code>-1</code> ms for an infinite duration
83       * @param stepRate step rate in milliseconds
84       * @param startTime start time in milliseconds
85       */
86      public ParticleSystemActivity(final long duration, final long stepRate, final long startTime)
87      {
88          super(duration, stepRate, startTime);
89          particleSystem = new ParticleSystem();
90          attractions = createBinaryKeyMap();
91          particles = new HashMap<PNode, Particle>();
92          springs = createBinaryKeyMap();
93      }
94  
95  
96      /**
97       * Set the drag force for the particle system to <code>drag</code>.
98       *
99       * @param drag drag force
100      */
101     public void setDrag(final float drag)
102     {
103         particleSystem.setDrag(drag);
104     }
105 
106     /**
107      * Set the strength of gravity for the particle system to <code>gravity</code>.
108      *
109      * @param gravity strength of gravity
110      */
111     public void setGravity(final float gravity)
112     {
113         particleSystem.setGravity(gravity);
114     }
115 
116     /**
117      * Create a new particle for the specified node with the specified mass.
118      *
119      * @param node node, must not be null
120      * @param mass particle mass
121      */
122     public void createParticle(final PNode node, final float mass)
123     {
124         if (node == null)
125         {
126             throw new IllegalArgumentException("node must not be null");
127         }
128         PBounds fullBounds = node.getFullBoundsReference();
129         float x = (float) fullBounds.getX();
130         float y = (float) fullBounds.getY();
131         Particle particle = particleSystem.makeParticle(mass, x, y, 1.0f);
132         particles.put(node, particle);
133     }
134 
135     /**
136      * Return the velocity for particle associated with the specified node.
137      * A particle must have already been created for the specified node.
138      *
139      * @param node node, must not be null
140      * @return the velocity for particle associated with the specified node
141      */
142     public Point2D getVelocity(final PNode node)
143     {
144         return getVelocity(node, new Point2D.Float(0.0f, 0.0f));
145     }
146 
147     /**
148      * Return the specified velocity, set to the velocity for particle associated with the specified node.
149      * A particle must have already been created for the specified node.
150      *
151      * @param node node, must not be null
152      * @param velocity velocity, must not be null
153      * @return the specified velocity, set to the velocity for particle associated with
154      *    the specified node
155      */
156     public Point2D getVelocity(final PNode node, final Point2D velocity)
157     {
158         checkParticleArgs(node);
159         if (velocity == null)
160         {
161             throw new IllegalArgumentException("velocity must not be null");
162         }
163         Particle particle = particles.get(node);
164         velocity.setLocation(particle.velocity().x(), particle.velocity().y());
165         return velocity;
166     }
167 
168     /**
169      * Set the velocity for the particle associated with the specified node
170      * to <code>[x, y]</code>.  A particle must have already been created
171      * for the specified node.
172      *
173      * @param node node, must not be null
174      * @param x velocity x
175      * @param y velocity y
176      */
177     public void setVelocity(final PNode node, final float x, final float y)
178     {
179         checkParticleArgs(node);
180         particles.get(node).velocity().set(x, y, 0.0f);
181     }
182 
183     /**
184      * Set the velocity for the particle associated with the specified node
185      * to <code>velocity</code>.  A particle must have already been created
186      * for the specified node.
187      *
188      * @param node node, must not be null
189      * @param velocity velocity, must not be null
190      */
191     public void setVelocity(final PNode node, final Point2D velocity)
192     {
193         checkParticleArgs(node);
194         if (velocity == null)
195         {
196             throw new IllegalArgumentException("velocity must not be null");
197         }
198         particles.get(node).velocity().set((float) velocity.getX(), (float) velocity.getY(), 0.0f);
199     }
200 
201     /**
202      * Return the mass of the particle associated with the specified node.  A particle
203      * must have already been created for the specified node.
204      *
205      * @param node node, must not be null
206      * @return the mass of the particle associated with the specified node
207      */
208     public float getParticleMass(final PNode node)
209     {
210         checkParticleArgs(node);
211         return particles.get(node).mass();
212     }
213 
214     /**
215      * Set the mass of the particle associated with the specified node to <code>mass</code>.
216      * A particle must have already been created for the specified node.
217      *
218      * @param node node, must not be null
219      * @param mass particle mass
220      */
221     public void setParticleMass(final PNode node, final float mass)
222     {
223         checkParticleArgs(node);
224         particles.get(node).setMass(mass);
225     }
226 
227     /**
228      * Clamp the velocity for the particle associated with the specified node
229      * to <code>[0.0f, 0.0f]</code>.  A particle must have already been created
230      * for the specified node.  A clamped particle will receive updated position
231      * data from the full bounds of the specified node.
232      *
233      * @param node node, must not be null
234      */
235     public void clamp(final PNode node)
236     {
237         checkParticleArgs(node);
238         particles.get(node).makeFixed();
239     }
240 
241     /**
242      * Release or unclamp the velocity for the particle associated with the specified node.
243      * A particle must have already been created for the specified node.
244      *
245      * @param node node, must not be null
246      */
247     public void release(final PNode node)
248     {
249         checkParticleArgs(node);
250         particles.get(node).makeFree();
251     }
252 
253     /**
254      * Create a new spring between the specified source and target nodes with the
255      * specified strength, damping factor, and rest length.  A particle must have already
256      * been created for both the specified source and target nodes.
257      *
258      * @param source source node, must not be null
259      * @param target target node, must not be null
260      * @param strength spring strength
261      * @param damping damping factor
262      * @param restLength rest length
263      */
264     public void createSpring(final PNode source,
265                              final PNode target,
266                              final float strength,
267                              final float damping,
268                              final float restLength)
269     {
270         if (source == null)
271         {
272             throw new IllegalArgumentException("source node must not be null");
273         }
274         if (target == null)
275         {
276             throw new IllegalArgumentException("target node must not be null");
277         }
278         if (!particles.containsKey(source))
279         {
280             throw new IllegalArgumentException("no particle exists for source node " + source);
281         }
282         if (!particles.containsKey(target))
283         {
284             throw new IllegalArgumentException("no particle exists for target node " + target);
285         }
286         Spring spring = particleSystem.makeSpring(particles.get(source),
287                                                   particles.get(target),
288                                                   strength,
289                                                   damping,
290                                                   restLength);
291         springs.put(source, target, spring);
292     }
293 
294     /**
295      * Enable the spring between the specified source and target nodes.  A spring
296      * must have already been created for the specified source and target nodes.
297      *
298      * @param source source node, must not be null
299      * @param target target node, must not be null
300      */
301     public void enableSpring(final PNode source, final PNode target)
302     {
303         checkSpringArgs(source, target);
304         springs.get(source, target).turnOn();
305     }
306 
307     /**
308      * Disable the spring between the specified source and target nodes.  A spring
309      * must have already been created for the specified source and target nodes.
310      *
311      * @param source source node, must not be null
312      * @param target target node, must not be null
313      */
314     public void disableSpring(final PNode source, final PNode target)
315     {
316         checkSpringArgs(source, target);
317         springs.get(source, target).turnOff();
318     }
319 
320     /**
321      * Return the rest length for the spring between the specified source and target nodes.
322      * A spring must have already been created for the specified source and target nodes.
323      *
324      * @param source source node, must not be null
325      * @param target target node, must not be null
326      * @return the rest length for the spring between the specified source and target nodes
327      */
328     public float getSpringRestLength(final PNode source, final PNode target)
329     {
330         checkSpringArgs(source, target);
331         return springs.get(source, target).restLength();
332     }
333 
334     /**
335      * Set the rest length for the spring between the specified source and target nodes to <code>restLength</code>.
336      * A spring must have already been created for the specified source and target nodes.
337      *
338      * @param source source node, must not be null
339      * @param target target node, must not be null
340      * @param restLength rest length
341      */
342     public void setSpringRestLength(final PNode source, final PNode target, final float restLength)
343     {
344         checkSpringArgs(source, target);
345         springs.get(source, target).setRestLength(restLength);
346     }
347 
348     /**
349      * Return the strength of the spring between the specified source and target nodes.
350      * A spring must have already been created for the specified source and target nodes.
351      *
352      * @param source source node, must not be null
353      * @param target target node, must not be null
354      * @return the strength of the spring between the specified source and target nodes
355      */
356     public float getSpringStrength(final PNode source, final PNode target)
357     {
358         checkSpringArgs(source, target);
359         return springs.get(source, target).strength();
360     }
361 
362     /**
363      * Set the strength of the spring between the specified source and target nodes to <code>strength</code>.
364      * A spring must have already been created for the specified source and target nodes.
365      *
366      * @param source source node, must not be null
367      * @param target target node, must not be null
368      * @param strength spring strength
369      */
370     public void setSpringStrength(final PNode source, final PNode target, final float strength)
371     {
372         checkSpringArgs(source, target);
373         springs.get(source, target).setStrength(strength);
374     }
375 
376     /**
377      * Return the damping factor for the spring between the specified source and target nodes.
378      * A spring must have already been created for the specified source and target nodes.
379      *
380      * @param source source node, must not be null
381      * @param target target node, must not be null
382      * @return the damping factor for the spring between the specified source and target nodes
383      */
384     public float getSpringDamping(final PNode source, final PNode target)
385     {
386         checkSpringArgs(source, target);
387         return springs.get(source, target).damping();
388     }
389 
390     /**
391      * Set the damping factor for the spring between the specified source and target nodes to
392      * <code>dampingFactor</code>.  A spring must have already been created for the specified
393      * source and target nodes.
394      *
395      * @param source source node, must not be null
396      * @param target target node, must not be null
397      * @param damping damping factor
398      */
399     public void setSpringDamping(final PNode source, final PNode target, final float damping)
400     {
401         checkSpringArgs(source, target);
402         springs.get(source, target).setDamping(damping);
403     }
404 
405     /**
406      * Create a new attraction (or repulsion) force between the specified source and target
407      * nodes with the specified strength and minimum distance.  A particle must have already
408      * been created for both the specified source and target nodes.
409      *
410      * @param source source node, must not be null
411      * @param target target node, must not be null
412      * @param strength attraction (or repulsion) force strength
413      * @param minimumDistance minimum distance
414      */
415     public void createAttraction(final PNode source,
416                                  final PNode target,
417                                  final float strength,
418                                  final float minimumDistance)
419     {
420         if (source == null)
421         {
422             throw new IllegalArgumentException("source node must not be null");
423         }
424         if (target == null)
425         {
426             throw new IllegalArgumentException("target node must not be null");
427         }
428         if (!particles.containsKey(source))
429         {
430             throw new IllegalArgumentException("no particle exists for source node " + source);
431         }
432         if (!particles.containsKey(target))
433         {
434             throw new IllegalArgumentException("no particle exists for target node " + target);
435         }
436         Attraction attraction = particleSystem.makeAttraction(particles.get(source),
437                                                               particles.get(target),
438                                                               strength,
439                                                               minimumDistance);
440         attractions.put(source, target, attraction);
441     }
442 
443     /**
444      * Enable the attraction between the specified source and target nodes.  An attraction
445      * must have already been created for the specified source and target nodes.
446      *
447      * @param source source node, must not be null
448      * @param target target node, must not be null
449      */
450     public void enableAttraction(final PNode source, final PNode target)
451     {
452         checkAttractionArgs(source, target);
453         attractions.get(source, target).turnOn();
454     }
455 
456     /**
457      * Disable the attraction between the specified source and target nodes.  An attraction
458      * must have already been created for the specified source and target nodes.
459      *
460      * @param source source node, must not be null
461      * @param target target node, must not be null
462      */
463     public void disableAttraction(final PNode source, final PNode target)
464     {
465         checkAttractionArgs(source, target);
466         attractions.get(source, target).turnOff();
467     }
468 
469     /**
470      * Return the strength of the attraction between the specified source and target nodes.  An attraction
471      * must have already been created for the specified source and target nodes.
472      *
473      * @param source source node, must not be null
474      * @param target target node, must not be null
475      * @return the strength of the attraction between the specified source and target nodes
476      */
477     public float getAttractionStrength(final PNode source, final PNode target)
478     {
479         checkAttractionArgs(source, target);
480         return attractions.get(source, target).getStrength();
481     }
482 
483     /**
484      * Set the strength of the attraction between the specified source and target nodes to <code>strength</code>.
485      * An attraction must have already been created for the specified source and target nodes.
486      *
487      * @param source source node, must not be null
488      * @param target target node, must not be null
489      * @param strength attraction (or repulsion) force strength
490      */
491     public void setAttractionStrength(final PNode source, final PNode target, final float strength)
492     {
493         checkAttractionArgs(source, target);
494         attractions.get(source, target).setStrength(strength);
495     }
496 
497     /**
498      * Return the minimum distance for the attraction between the specified source and target nodes.  An attraction
499      * must have already been created for the specified source and target nodes.
500      *
501      * @param source source node, must not be null
502      * @param target target node, must not be null
503      * @return the minimum distance for the attraction between the specified source and target nodes
504      */
505     public float getAttractionMinimumDistance(final PNode source, final PNode target)
506     {
507         checkAttractionArgs(source, target);
508         return attractions.get(source, target).getMinimumDistance();
509     }
510 
511     /**
512      * Set the minimum distance for the attraction between the specified source and target nodes
513      * to <code>minimumDistance</code>.  An attraction must have already been created for the
514      * specified source and target nodes.
515      *
516      * @param source source node, must not be null
517      * @param target target node, must not be null
518      * @param minimumDistance minimum distance
519      */
520     public void setAttractionMinimumDistance(final PNode source, final PNode target, final float minimumDistance)
521     {
522         checkAttractionArgs(source, target);
523         attractions.get(source, target).setMinimumDistance(minimumDistance);
524     }
525 
526     /**
527      * {@inheritDoc}
528      *
529      * <p>
530      * If subclasses override this method, they must call <code>super.activityFinished()</code>.
531      * </p>
532      */
533     protected void activityStep(final long elapsedTime)
534     {
535         super.activityStep(elapsedTime);
536         particleSystem.tick();
537         for (Map.Entry<PNode, Particle> entry : particles.entrySet())
538         {
539             PNode node = entry.getKey();
540             Particle particle = entry.getValue();
541             // todo:  use local bounds or full bounds?
542             if (particle.isFree())
543             {
544                 node.setOffset(particle.position().x(), particle.position().y());
545             }
546             else
547             {
548                 PBounds fullBounds = node.getFullBoundsReference();
549                 particle.position().set((float) fullBounds.getX(), (float) fullBounds.getY(), 0.0f);
550             }
551         }
552     }
553 
554     /**
555      * {@inheritDoc}
556      *
557      * <p>
558      * If subclasses override this method, they must call <code>super.activityFinished()</code>.
559      * </p>
560      */
561     protected void activityFinished()
562     {
563         super.activityFinished();
564         attractions.clear();
565         particles.clear();
566         springs.clear();
567         particleSystem.clear();
568     }
569 
570 
571     /**
572      * Check the specified particle arguments are valid.  Throws an {@link IllegalArgumentException}
573      * if the specified node is null or if no particle exists for the specified node.
574      *
575      * @param node node, must not be null
576      */
577     private void checkParticleArgs(final PNode node)
578     {
579         if (node == null)
580         {
581             throw new IllegalArgumentException("node must not be null");
582         }
583         Particle particle = particles.get(node);
584         if (particle == null)
585         {
586             throw new IllegalArgumentException("no particle exists for node " + node);
587         }
588     }
589 
590     /**
591      * Check the specified spring arguments are valid.  Throws an {@link IllegalArgumentException}
592      * if either the specified nodes are null or if no spring exists for the specified source and target
593      * nodes.
594      *
595      * @param source source node, must not be null
596      * @param target target node, must not be null
597      */
598     private void checkSpringArgs(final PNode source, final PNode target)
599     {
600         if (source == null)
601         {
602             throw new IllegalArgumentException("source node must not be null");
603         }
604         if (target == null)
605         {
606             throw new IllegalArgumentException("target node must not be null");
607         }
608         if (!springs.containsKey(source, target))
609         {
610             throw new IllegalArgumentException("no spring exists between source node "
611                                                + source + " and target node " + target);
612         }
613     }
614 
615     /**
616      * Check the specified attraction arguments are valid.  Throws an {@link IllegalArgumentException}
617      * if either the specified nodes are null or if no attraction exists for the specified source and target
618      * nodes.
619      *
620      * @param source source node, must not be null
621      * @param target target node, must not be null
622      */
623     private void checkAttractionArgs(final PNode source, final PNode target)
624     {
625         if (source == null)
626         {
627             throw new IllegalArgumentException("source node must not be null");
628         }
629         if (target == null)
630         {
631             throw new IllegalArgumentException("target node must not be null");
632         }
633         if (!attractions.containsKey(source, target))
634         {
635             throw new IllegalArgumentException("no attraction exists between source node "
636                                                + source + " and target node " + target);
637         }
638     }
639 }