Repository navigation
Expand file tree
/
Copy pathagentscript.coffee
More file actions
1756 lines (1542 loc) · 64.5 KB
/
Copy pathagentscript.coffee
File metadata and controls
1756 lines (1542 loc) · 64.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# This documentation uses Jeremy Ashkenas's
# [docco](http://jashkenas.github.com/docco/) which allows
# [markdown](http://daringfireball.net/projects/markdown/syntax).
# Create the namespace **ABM** for our project.
# Note here `this` or `@` == window due to coffeescript wrapper call.
# Thus @ABM is placed in the global scope.
@ABM={}
# Keep copy of global object in ABM
ABM.root = @
# Global shim for not-yet-standard requestAnimationFrame
do ->
@requestAnimFrame = @requestAnimationFrame or null
for vendor in ['ms', 'moz', 'webkit', 'o']
@requestAnimFrame or= @[vendor+'RequestAnimationFrame']
@requestAnimFrame or=
(callback) -> window.setTimeout(callback, 1000 / 60)
# Shim for `Array.indexOf` if not implemented.
# Use [es5-shim](https://github.com/kriskowal/es5-shim) if additional shims needed.
Array::indexOf or= (item) -> # shim for IE8
for x, i in this
return i if x is item
return -1
# **ABM.util** contains the general utilities for the project. Note that within
# **util** `@` referrs to ABM.util, *not* the global name space as above.
ABM.util =
# Shortcut for throwing an error. Good for debugging:
#
# error("wtf? foo=#{foo}") if fooProblem
error: (s) -> throw new Error s
# Good replacements for Javascript's badly broken`typeof` and `instanceof`
# See [underscore.coffee](http://goo.gl/L0umK)
isArray: Array.isArray or # works with agentSets too
(obj) -> !!(obj and obj.concat and obj.unshift and not obj.callee)
isFunction: (obj) ->
!!(obj and obj.constructor and obj.call and obj.apply)
isString: (obj) ->
!!(obj is '' or (obj and obj.charCodeAt and obj.substr))
# ### Numeric Operations
# Return random int in [0,max) or [min,max)
randomInt: (max) -> Math.floor(Math.random() * max)
randomInt2: (min, max) -> min + Math.floor(Math.random() * (max-min))
# Return float in [0,max) or [min,max) or [-r/2,r/2)
randomFloat: (max) -> Math.random() * max
randomFloat2: (min, max) -> min + Math.random() * (max-min)
randomCentered: (r) -> @randomFloat2 -r/2, r/2
# Return log n where base is 10, base, e respectively
log10: (n) -> Math.log(n)/Math.LN10
logN: (n, base) -> Math.log(n)/Math.log(base)
ln: (n) -> Math.log n
# Return true [mod functin](http://goo.gl/spr24), % is remainder, not mod.
mod: (v, n) -> ((v % n) + n) % n
# Return v to be between min, max via mod fcn
wrap: (v, min, max) -> min + @mod(v-min, max-min)
# Return v to be between min, max via clamping with min/max
clamp: (v, min, max) -> Math.max(Math.min(v,max),min)
# Return sign of a number as +/- 1
sign: (v) -> return (if v<0 then -1 else 1)
# Return a string float array for printing using given precision and separator
aToFixed: (a,p=2,s=", ") -> "[#{(i.toFixed p for i in a).join(s)}]"
# ### Color and Angle Operations
# Return a random RGB or gray color. Array passed to minimize garbage collection
randomColor: (color = []) ->
color[i] = @randomInt(256) for i in [0..2]
color
randomGray: (color = [], min = 64, max = 192) ->
r=@randomInt2 min,max
color[i] = r for i in [0..2]
color
# Return new color by scaling each value of an RGB array.
# Note [r,g,b] must be ints
scaleColor: (max, s, color = []) ->
color[i] = @clamp(Math.round(c*s),0,255) for c, i in max
color
# Return HTML color as used by canvas element. Can include Alpha
colorStr: (c) -> if c.length is 3 then "rgb(#{c})" else "rgba(#{c})"
# Compare two colors. Alas, there is no array.Equal operator.
colorsEqual: (c1, c2) -> c1.toString() is c2.toString()
# Return little/big endian-ness of hardware.
# See Mozilla pixel [manipulation article](http://goo.gl/Lxliq)
isLittleEndian: ->
d8 = new Uint8ClampedArray 4
d32 = new Uint32Array d8.buffer
d32[0] = 0x01020304
d8[0] is 4
# Convert between degrees and radians. We/Math package use radians.
degToRad: (degrees) -> degrees * Math.PI / 180
radToDeg: (radians) -> radians * 180 / Math.PI
# Return angle in (-pi,pi] that added to rad2 = rad1
subtractRads: (rad1, rad2) ->
dr = rad1-rad2; PI = Math.PI
dr += 2*PI if dr <= -PI; dr -= 2*PI if dr > PI; dr
# ### Array Operations
# Does the array have any elements? Is the array empty?
any: (array) -> array.length isnt 0
empty: (array) -> array.length is 0
# Make a copy of the array. Needed when you don't want to modify the given
# array with mutator methods like sort, splice or your own functions.
clone: (array) -> array.slice 0
# Return last element of array. Error if empty.
last: (array) ->
@error "last: empty array" if @empty array
array[array.length-1]
# Return random element of array. Error if empty.
oneOf: (array) ->
@error "oneOf: empty array" if @empty array
array[@randomInt array.length]
# Return n random elements of array. Error if n > array size.
nOf: (array, n) -> # REMIND: shuffle then first n may be better
@error "nOf: n > length" if n > array.length
r = []; while r.length < n
o = @oneOf(array)
r.push o unless o in r
r
# Remove an item from an array. Error if item not in array.
removeItem: (array, item) ->
array.splice i, 1 if (i = array.indexOf item) isnt -1
@error "removeItem: item not found" if i < 0
i
# Randomize the elements of array. Clever! See [cookbook](http://goo.gl/TT2SY)
shuffle: (array) -> array.sort -> 0.5 - Math.random()
# Return o when f(o) min/max in array. Error if array empty.
# If f is a string, return element with max value of that property.
# If "valueToo" then return an array of the element and the value.
#
# array = [{x:1,y:2}, {x:3,y:4}]
# # returns {x: 1, y: 2} 5
# [min, dist2] = minOneOf array, ((o)->o.x*o.x+o.y*o.y), true
# # returns {x: 3, y: 4}
# max = maxOneOf array, "x"
minOneOf: (array, f, valueToo=false) ->
@error "minOneOf: empty array" if @empty array
r = Infinity; o = null; (s=f; f = ((o)->o[s])) if @isString f
for a in array
(r = r1; o = a) if (r1=f(a)) < r
if valueToo then [o, r] else o
maxOneOf: (array, f, valueToo=false) ->
@error "maxOneOf: empty array" if @empty array
r = -Infinity; o = null; (s=f; f = ((o)->o[s])) if @isString f
for a in array
(r = r1; o = a) if (r1=f(a)) > r
if valueToo then [o, r] else o
# Return histogram of o when f(o) is a numeric value in array.
# Histogram interval is bin. Error if array empty.
# If f is a string, return histogram of that property.
#
# In examples below, histOf returns [3,1,1,0,0,1]
#
# a = [1,3,4,1,1,10]
# h = histOf a, 2, (i) -> i
#
# b = ({id:i} for i in a)
# h = histOf b, 2, (o) -> o.id
# h = histOf b, 2, "id"
histOf: (array, bin, f) ->
r = []; (s=f; f = ((o)->o[s])) if @isString f
for a in array
i = Math.floor f(a)/bin
r[i] = if (ri=r[i])? then ri+1 else 1
r[i] = 0 for val,i in r when not val?
r
# Mutator. Sorts the array of objects in place by the property. Returns array.
# Clone first if you want to preserve the original array.
#
# array = [{i:1},{i:5},{i:-1},{i:2},{i:2}]
# sortBy array, "i"
# # array now is [{i:-1},{i:1},{i:2},{i:2},{i:5}]
sortBy: (array, prop) -> array.sort (a,b) -> a[prop] - b[prop]
# Mutator. Removes adjacent dups, by reference, in place from sorted array.
# Note "by reference" means litteraly same object, not copy. Returns array.
# Clone first if you want to preserve the original array.
#
# ids = ({id:i} for i in [0..10])
# a = (ids[i] for i in [1,3,4,1,1,10])
# # a is [{id:1},{id:3},{id:4},{id:1},{id:1},{id:10}]
# b = clone a
# sortBy b, "id"
# # b is [{id:1},{id:1},{id:1},{id:3},{id:4},{id:10}]
# uniq b
# # b now is [{id:1},{id:3},{id:4},{id:10}]
uniq: (array) ->
array.splice i,1 for i in [array.length-1..1] by -1 when array[i-1] is array[i]
array
# Return a new array composed of the rows of a matrix. I.e. convert
#
# [[1,2,3],[4,5,6]] to [1,2,3,4,5,6]
flatten: (matrix) -> matrix.reduce( (a,b) -> a.concat b )
# Binary search of a sorted array, adapted from [jaskenas](http://goo.gl/ozAZH).
# Search for index of value with items array, using fcn for item value.
# Return -1 if not found.
binarySearch: (items, value, fcn = (ex) -> ex) ->
start = 0
stop = items.length - 1
pivot = Math.floor (start + stop) / 2
while (pivotVal = fcn(items[pivot])) isnt value and start < stop
stop = pivot - 1 if value < pivotVal # Adjust the search area.
start = pivot + 1 if value > pivotVal
pivot = Math.floor (stop + start) / 2 # Recalculate the pivot.
if fcn(items[pivot]) is value then pivot else -1
# Useful for JS users: max/min of array, push array. Not used in our CS code
aMax: (array) -> Math.max array...
aMin: (array) -> Math.min array...
aPush: (array, a) -> array.push a...
# ### Topology Operations
# Return angle in (-pi,pi] radians from x1,y1 to x2,y2.
radsToward: (x1, y1, x2, y2) ->
PI = Math.PI; dx = x2-x1; dy = y2-y1
if dx is 0 then return 3*PI/2 if dy < 0; return PI/2 if dy > 0; return 0
else return Math.atan(dy/dx) + if dx < 0 then PI else 0
# Return true if x2,y2 is in cone radians around heading radians from x1,x2
# and within distance radius from x1,x2.
# I.e. is p2 in cone/heading/radius from p1
inCone: (heading, cone, radius, x1, y1, x2, y2) ->
if radius < @distance x1, y1, x2, y2 then return false
angle12 = @radsToward x1, y1, x2, y2 # angle from 1 to 2
cone/2 >=Math.abs @subtractRads(heading, angle12)
# Return the Euclidean distance and distance squared between x1,y1, x2,y2.
# The squared distance is used for comparisons to avoid the Math.sqrt fcn.
distance: (x1, y1, x2, y2) -> dx = x1-x2; dy = y1-y2; Math.sqrt dx*dx + dy*dy
sqDistance: (x1, y1, x2, y2) -> dx = x1-x2; dy = y1-y2; dx*dx + dy*dy
# Return the [torus distance](http://goo.gl/PgJ5N) and distance squared
# between two points A(x1,y1) and B(x2,y2):
#
# dx = |x2-x1|; dy = |y2-y1|
# d=sqrt(min(dx, W-dx)^2 + min(dy, H-dy)^2)
#
# Torus note: ABMs often use a Torus topology where the right and left edges
# fold to meet,and similarly for the top/bottom.
# For points, this is easily handled with the mod function .. insuring the
# point is within the rectangle modulo W & H.
#
# The relationship *between* points is more difficult. The relationship between
# A and B must also include the towards-reflections around A, thus 4 points.
#
# | |
# | W |
# -----+---------------+-----
# B1 | B |
# | | H
# | |
# | A |
# -----+---------------+-----
# B3 | B2 |
# | |
torusDistance: (x1, y1, x2, y2, w, h) ->
Math.sqrt @torusSqDistance x1, y1, x2, y2, w, h
torusSqDistance: (x1, y1, x2, y2, w, h) ->
dx = Math.abs x2-x1; dy = Math.abs y2-y1
dxMin = Math.min dx, w-dx; dyMin = Math.min dy, h-dy
dxMin*dxMin + dyMin*dyMin
# Return true if closest path between x1,y1 & x2,y2 wraps around the torus.
torusWraps: (x1, y1, x2, y2, w, h) ->
dx = Math.abs x2-x1; dy = Math.abs y2-y1
dx > w-dx or dy > h-dy
# Return 4 torus point reflections of x2,y2 around x1,y1
torus4Pts: (x1, y1, x2, y2, w, h) ->
x2r = if x2<x1 then x2+w else x2-w
y2r = if y2<y1 then y2+h else y2-h
[ [x2,y2], [x2r,y2], [x2,y2r], [x2r,y2r] ]
# Return closest of 4 torus pts from A to B
torusPt: (x1, y1, x2, y2, w, h) ->
x2r = if x2<x1 then x2+w else x2-w
y2r = if y2<y1 then y2+h else y2-h
x = if Math.abs(x2r-x1) < Math.abs(x2-x1) then x2r else x2
y = if Math.abs(y2r-y1) < Math.abs(y2-y1) then y2r else y2
[x,y]
# Return the angle from x1,y1 to x2.y2 on torus using shortest reflection.
torusRadsToward: (x1, y1, x2, y2, w, h) ->
[x2,y2] = @torusPt x1, y1, x2, y2, w, h
@radsToward x1, y1, x2, y2
# Return true if x2,y2 is in cone radians around heading radians from x1,x2
# and within distance radius from x1,x2 considering all torus reflections.
inTorusCone: (heading, cone, radius, x1, y1, x2, y2, w, h) ->
for p in @torus4Pts x1, y1, x2, y2, w, h
return true if @inCone heading, cone, radius, x1, y1, p[0], p[1]
false
# ### Canvas/Context Operations
# Return a "layer" 2D/3D rendering context within the specified HTML `<div>`,
# with the given width/height positioned absolutely at top/left within the div,
# and with the z-index of z.
#
# The z level gives us the capability of buildng a "stack" of coordinated canvases.
createLayer: (div, width, height, z, ctx = "2d") -> # a canvas ctx object
can = document.createElement 'canvas'
can.setAttribute 'style', "position:absolute;top:0;left:0;z-index:#{z}"
can.width = width; can.height = height
can.ctx = # http://goo.gl/atMRr can't get both 2d/3d contexts, only one allowed
if ctx is "2d" then can.getContext "2d"
else can.getContext("webgl") or can.getContext("experimental-webgl")
document.getElementById(div).appendChild(can)
can.ctx
# Clear the 2D/3D layer to be transparent. Note this [discussion](http://goo.gl/qekXS).
clearCtx: (ctx) ->
if ctx.save? # test for 2D ctx
ctx.save()
ctx.setTransform 1, 0, 0, 1, 0, 0
ctx.clearRect 0, 0, ctx.canvas.width, ctx.canvas.height
ctx.restore()
else # 3D
ctx.clearColor 0, 0, 0, 0 # transparent!
ctx.clear ctx.COLOR_BUFFER_BIT | ctx.DEPTH_BUFFER_BIT
# Fill the 2D/3D layer with the given color
fillCtx: (ctx, color) ->
if ctx.fillStyle? # test for 2D ctx
ctx.save()
ctx.setTransform 1, 0, 0, 1, 0, 0
ctx.fillStyle = @colorStr(color)
ctx.fillRect 0, 0, ctx.canvas.width, ctx.canvas.height
ctx.restore()
console.log "fillCtx: 2d"
else # 3D
console.log "fillCtx: 2d"
ctx.clearColor color..., 1 # alpha = 1 unless color is rgba
ctx.clear ctx.COLOR_BUFFER_BIT | ctx.DEPTH_BUFFER_BIT
# 2D: Draw string of the given color at the xy location.
# Note that this will follow the existing transform.
ctxDrawText: (ctx, string, xy, color = [0,0,0]) ->
ctx.fillStyle = @colorStr color
ctx.fillText(string, xy[0], xy[1])
# 2D: Set the canvas text align and baseline drawing parameters
#
# * font is a HTML/CSS string like: "9px sans-serif"
# * align is left right center start end
# * baseline is top hanging middle alphabetic ideographic bottom
#
# See [reference](http://goo.gl/AvEAq) for details.
ctxTextParams: (ctx, font, align = "center", baseline = "middle") ->
ctx.font = font; ctx.textAlign = align; ctx.textBaseline = baseline
# 2D: Store the default color and xy offset for text labels for agentsets.
# This is simply using the ctx object for convenient storage.
ctxLabelParams: (ctx, color, xy) -> # patches/agents defaults
ctx.labelColor = color; ctx.labelXY = xy
# Import an image, executing function f on completion
importImage: (imageSrc, f=(img)->) ->
img = new Image()
img.onload = -> f(img)
img.src = imageSrc
img
# Convert an image to a context
imageToCtx: (image) ->
canvas = document.createElement "canvas"
canvas.width = image.width
canvas.height = image.height
ctx = canvas.getContext "2d"
ctx.drawImage image, 0, 0
ctx # note: ctx.canvas gives the canvas created above.
# Convert a context to an image
ctxToImage: (ctx) ->
image = new Image()
image.src = ctx.canvas.toDataURL "image/png"
image
# Convert a ctx to an imageData object
ctxToImageData: (ctx) -> ctx.getImageData 0, 0, ctx.canvas.width, ctx.canvas.height
# Canvas versions of above
canvasToImage: (canvas) -> ctxToImage(canvas.getContext "2d")
canvasToImageData: (canvas) -> ctxToImageData(canvas.getContext "2d")
imageToCanvas: (image) -> imageToCtx(image).canvas
# Draw an image centered at x, y w/ image size dx, dy.
# See [this tutorial](http://goo.gl/VUlhY).
drawCenteredImage: (ctx, img, rad, x, y, dx, dy) -> # presume save/restore surrounds this
ctx.translate x, y # translate to center
ctx.rotate rad
ctx.drawImage img, -dx/2, -dy/2
# A *very* simple shapes module for drawing
# [NetLogo-like](http://ccl.northwestern.edu/netlogo/docs/) agents.
u = ABM.util # alias for util module
ABM.shapes = s = # s alias below for ABM.shapes
# Each shape is a named object with two members:
# a boolean "rotate" and a drawing procedure.
# The shape is used in the following context with a color set
# and a transform such that the shape should be drawn in a -.5 to .5 square
#
# ctx.save()
# ctx.fillStyle = u.colorStr @color
# ctx.translate @x, @y; ctx.scale @size, @size;
# ctx.rotate @heading if shape.rotate
# ctx.beginPath()
# shape.draw(ctx)
# ctx.closePath()
# ctx.fill()
# ctx.restore()
#
# The list of current shapes, via `ABM.shapes.names()` below, is:
#
# ["default", "triangle", "arrow", "bug", "pyramid",
# "circle", "square", "pentagon", "ring", "person"]
#
default:
rotate: true
draw: (c) -> s.poly c, [[.5,0],[-.5,-.5],[-.25,0],[-.5,.5]]
triangle:
rotate: true
draw: (c) -> s.poly c, [[.5,0],[-.5,-.4],[-.5,.4]]
arrow:
rotate: true
draw: (c) -> s.poly c, [[.5,0],[0,.5],[0,.2],[-.5,.2],[-.5,-.2],[0,-.2],[0,-.5]]
bug:
rotate: true
draw: (c) ->
PI = Math.PI
c.strokeStyle = c.fillStyle; c.lineWidth = .05
c.moveTo .4,.225; c.lineTo .2,0; c.lineTo .4, -.225
c.stroke()
c.beginPath()
c.arc .12,0,.13,0,2*PI; c.arc -.05,0,.13,0,2*PI; c.arc -.27,0,.2,0,2*PI
pyramid:
rotate: false
draw: (c) -> s.poly c, [[0,.5],[-.433,-.25],[.433,-.25]]
circle:
rotate: false
draw: (c) -> c.arc 0,0,.5,0,2*Math.PI
square:
rotate: false
draw: (c) -> c.fillRect -.5,-.5,1,1
pentagon:
rotate: false
draw: (c) -> s.poly c, [[0,.45],[-.45,.1],[-.3,-.45],[.3,-.45],[.45,.1]]
ring:
rotate: false
draw: (c) ->
c.arc 0,0,.5,0,2*Math.PI,true;c.closePath();c.arc 0,0,.3,0,2*Math.PI,false
person:
rotate: false
draw: (c) ->
s.poly c, [ [.15,.2],[.3,0],[.125,-.1],[.125,.05],
[.1,-.15],[.25,-.5],[.05,-.5],[0,-.25],
[-.05,-.5],[-.25,-.5],[-.1,-.15],[-.125,.05],
[-.125,-.1],[-.3,0],[-.15,.2] ]
c.closePath(); c.arc 0,.35,.15,0,2*Math.PI
# Return a list of the available shapes, see above.
names: ->
(name for own name, val of @ when !ABM.util.isFunction val)
# Add your own shape. Will be included in names list. Usage:
#
# ABM.shapes.add "test", true, (c) -> # bowtie/hourglass
# ABM.shapes.poly c, [[-.5,-.5],[.5,.5],[-.5,.5],[.5,-.5]]
add: (name, rotate, draw) -> @[name] = {rotate,draw}
# A simple polygon utility: c is the 2D context, and a is an array of 2D points.
# c.closePath() and c.fill() will be called by the calling agent, see initial
# discription of drawing context. It is used in adding a new shape above.
poly: (c, a) ->
for p, i in a
if i is 0 then c.moveTo p[0], p[1] else c.lineTo p[0], p[1]
null
# Create an image ctx of a shape by drawing it into a small canvas.
# Used to implement agent sprites.
shapeToCtx: (name, color, scale) ->
shape = @[name]
can = document.createElement 'canvas'
can.width = can.height = scale
ctx = can.getContext "2d"
ctx.scale scale, scale
ctx.translate .5, .5
ctx.fillStyle = u.colorStr color
ctx.beginPath()
shape.draw ctx
ctx.closePath()
ctx.fill()
ctx
# **AgentSet** is a subclass of `Array` and is the base class for
# `Patches`, `Agents`, and `Links`. Its subclasses are also a factory
# for agent classes (`Patch`, `Agent`, `Link`). `AgentSet` keeps track of all
# the created agent instances. It also provides, much like the **ABM.util**
# module, many methods shared by all subclasses of AgentSet.
#
# ABM contains three agentsets created by class Model:
#
# * `ABM.patches`: the model's "world" grid
# * `ABM.agents`: the model's agents living on the patchs
# * `ABM.links`: the network links connecting agent pairs
#
# See NetLogo [documentation](http://ccl.northwestern.edu/netlogo/docs/)
# for explanation on the overall semantics of Agent Based Modeling
# used by AgentSets as well as Patches, Agents, and Links.
#
# Note: subclassing `Array` can be dangerous and we may have to convert
# to a different style. See Trevor Burnham's [comments](http://goo.gl/Lca8g)
# but thus far we've resolved all related problems, mainly by using the
# ABM.util array functions rather than "super".
#
# Because we are an array subset, @[i] below (this[i]), gets the i'th agentset element.
# The usual alias for **ABM.util**. These are equivalent:
#
# ABM.util.clearCtx(ctx)
# u.clearCtx(ctx)
u = ABM.util
class ABM.AgentSet extends Array
# ### Static members
# `asSet` is a static wrapper function converting an array of agents into
# an `AgentSet` .. except for the ID which only impacts the add method.
# It is primarily used to turn a comprehension into an AgentSet instance
# which then gains access to all the methods below. Ex:
#
# evens = (a for a in ABM.agents when a.id % 2 is 0)
# ABM.AgentSet.asSet(evens)
# randomEven = evens.oneOf()
@asSet: (a, setType = ABM.AgentSet) ->
# if a.prototype?
# then a.prototype = setType.prototype
# else a.__proto__ = setType.prototype
a.__proto__ = setType.prototype ? setType.constructor.prototype # setType.__proto__
a
# In the examples below, we'll use an array of primitive agent objects
# with three fields: id, x, y.
#
# AS = for i in [1..5] # long form comprehension
# {id:i, x:u.randomInt(10), y:u.randomInt(10)}
# ABM.AgentSet.asSet AS # Convert AS to AgentSet in place
# # .. produced
# [{id:1,x:0,y:1}, {id:2,x:8,y:0}, {id:3,x:6,y:4},
# {id:4,x:1,y:3}, {id:5,x:1,y:1}]
# ### Constructor and add/remove agents.
# Create an empty `AgentSet` and initialize the `ID` counter for add().
constructor: ->
super()
@ID = 0
# Add an agent to the list. Only used by agentset factory methods. Adds
# the `id` and `hidden` properties to all agents. Increment `ID`.
# Returns the object for chaining. The set will be sorted by `id`.
#
# By "agent" we mean an instance of `Patch`, `Agent` and `Link`.
add: (o) ->
o.id = @ID++
# o.hidden = false
@push o; o
# Remove an agent from the agentset, returning the agentset.
# Note this does not change ID, thus an
# agentset can have gaps in terms of their id's. Assumes set is
# sorted by `id`. If the set is one created by `asSet`, and the original
# array is unsorted, simply call `sortById` first, see `sortById` below.
#
# AS.remove(AS[3]) # [{id:0,x:0,y:1}, {id:1,x:8,y:0},
# {id:2,x:6,y:4}, {id:4,x:1,y:1}]
remove: (o) ->
if o is @last()
@.length--
else
@splice i, 1 if (i = @indexOfID o.id) isnt -1
u.error "remove: indexOfID not in list" if i is -1
@
# Remove adjacent duplicates, by reference, in a sorted agentset.
# Use `sortById` first if agentset not sorted.
#
# as = (AS.oneOf() for i in [1..4]) # 4 random agents w/ dups
# ABM.AgentSet.asSet as # [{id:1,x:8,y:0}, {id:0,x:0,y:1},
# {id:0,x:0,y:1}, {id:2,x:6,y:4}]
# as.sortById().uniq() # [{id:0,x:0,y:1}, {id:1,x:8,y:0},
# {id:2,x:6,y:4}]
uniq: -> u.uniq(@)
# Return the agent with the given `id` within the sorted agentset.
# Uses binary search thus is faster than simple lookup.
#
# AS.withID 4 # {id:4,x:1,y:1}
withID: (id) -> # null if not found
if (i = @indexOfID(id)) isnt -1 then @[i] else null
# Return the array index of the given agent id in the sorted set.
# If agentset is not sorted, call @sortById() first.
#
#
indexOfID: (id, sorted=true) -> # -1 if not found
@sortById() unless sorted
return @length-1 if id is @last().id # no "die" calls yet
u.binarySearch @, id, (o)->o.id
# The static `ABM.AgentSet.asSet` as a method.
# Used by agentset methods creating new agentsets.
asSet: (a) -> ABM.AgentSet.asSet a # , @
# Similar to above but sorted via `id`.
asOrderedSet: (a) -> @asSet(a).sortById()
# Return string representative of agentset.
toString: -> "["+(a.toString() for a in @).join(", ")+"]"
# ### Property Utilities
# Property access, also useful for debugging<br>
# Return an array of a property of the agentset
#
# AS.getProp "x" # [0, 8, 6, 1, 1]
getProp: (prop) -> o[prop] for o in @
# Return an array of arrays of props, given as a string or an array of strings.
#
# AS.getProps "id x y"
# AS.getProps ["id", "x", "y"]
# [[1,0,1],[2,8,0],[3,6,4],[4,1,3],[5,1,1]]
getProps: (props) ->
props = props.split(" ") if u.isString props
(o[p] for p in props) for o in @
# Return an array of agents with the property equal to the given value
#
# AS.getWithProp "x", 1
# [{id:4,x:1,y:3},{id:5,x:1,y:1}]
getWithProp: (prop, value) -> @asSet (o for o in @ when o[prop] is value)
# Set the property of the agents to a given value
#
# # increment x for agents with x=1
# AS1 = ABM.AgentSet.asSet AS.getWithProp("x",1)
# AS1.setProp "x", 2 # {id:4,x:2,y:3},{id:5,x:2,y:1}
#
# Note this changes the last two objects in the original AS above
setProp: (prop, value) -> o[prop] = value for o in @; @
# Get the agent with the min/max prop value in the agentset
#
# min = AS.minProp "y" # 0
# max = AS.maxProp "y" # 4
maxProp: (prop) -> Math.max @getProp(prop)...
minProp: (prop) -> Math.min @getProp(prop)...
# ### Array Utilities, often from ABM.util
# Randomize the agentset
#
# AS.shuffle(); AS.getProp "id" # [3, 2, 1, 4, 5]
shuffle: -> u.shuffle @
# Sort the agentset by the agent's `id`.
#
# AS.shuffle(); AS.getProp "id" # [3, 2, 1, 4, 5]
# AS.sortById(); AS.getProp "id" # [1, 2, 3, 4, 5]
sortById: -> u.sortBy @, "id"
# Make a copy of an agentset, return as new agentset.<br>
# NOTE: does *not* duplicate the objects, simply creates a new agentset
# with references to the same agents. Ex: create a randomized version of AS
# but without mangling AS itself:
#
# as = AS.clone().shuffle()
# AS.getProp "id" # [1, 2, 3, 4, 5]
# as.getProp "id" # [2, 4, 0, 1, 3]
clone: -> @asSet u.clone @
# Return the last agent in the agentset
#
# AS.last().id # l5
# l=AS.last(); p=[l.x,l.y] # [1,1]
last: -> u.last @
# Returns true if the agentset has any agents
#
# AS.any() # true
# AS.getWithProp("x", 99).any() #false
any: -> u.any @
# Return an agentset without given agent a
#
# as = AS.clone().other(AS[0])
# as.getProp "id" # [1, 2, 3, 4]
other: (a) -> @asSet (o for o in @ when o isnt a) # could clone & remove
# Return random agent in agentset
#
# AS.oneOf() # {id:2,x:6,y:4}
oneOf: -> u.oneOf @
# Return agentset made of n distinct agents
#
# AS.nOf(3) # [{id:0,x:0,y:1}, {id:4,x:1,y:1}, {id:1,x:8,y:0}]
nOf: (n) -> @asSet u.nOf @, n
# Return agent when f(o) min/max in agentset. If multiple agents have
# min/max value, return the first. Error if agentset empty.
# If f is a string, return element with min/max value of that property.
# If "valueToo" then return an array of the agent and the value.
#
# AS.minOneOf("x") # {id:0,x:0,y:1}
# AS.maxOneOf((a)->a.x+a.y, true) # {id:2,x:6,y:4},10
minOneOf: (f, valueToo=false) ->
u.minOneOf @, f, valueToo
maxOneOf: (f, valueToo=false) ->
u.maxOneOf @, f, valueToo
# ### Drawing
# For agentsets who's agents have a `draw` method.
# Clears the graphics context (transparent), then
# calls each agent's draw(ctx) method.
draw: (ctx) ->
u.clearCtx(ctx)
o.draw(ctx) for o in @ when not o.hidden; null
# ### Topology
# For ABM.patches & ABM.agents which have x,y. See ABM.util doc.
#
# Return all agents in agentset within d distance from given object.
# By default excludes the given object. Uses linear/torus distance
# depending on patches.isTorus, and patches width/height if needed.
inRadius: (o, d, meToo=false) -> # for any objects w/ x,y
d2 = d*d; x=o.x; y=o.y
if ABM.patches.isTorus
w=ABM.patches.numX; h=ABM.patches.numY
@asSet (a for a in @ when \
u.torusSqDistance(x,y,a.x,a.y,w,h)<=d2 and (meToo or a isnt o))
else
@asSet (a for a in @ when \
u.sqDistance(x,y,a.x,a.y)<=d2 and (meToo or a isnt o))
# As above, but also limited to the angle `cone` around
# a `heading` from object `o`.
inCone: (o, heading, cone, radius, meToo=false) ->
rSet = @inRadius o, radius, meToo
x=o.x; y=o.y
if ABM.patches.isTorus
w=ABM.patches.numX; h=ABM.patches.numY
@asSet (a for a in rSet when \
(a is o and meToo) or u.inTorusCone(heading,cone,radius,x,y,a.x,a.y,w,h))
else
@asSet (a for a in rSet when \
(a is o and meToo) or u.inCone(heading,cone,radius,x,y,a.x,a.y))
# ### Debugging
# Useful in console.
# Also see [CoffeeConsole](http://goo.gl/1i7bd) Chrome extension.
# Similar to NetLogo ask & with operators.
# Allows functions as strings. Use:
#
# AS.getProp("x") # [1, 8, 6, 2, 2]
# AS.with("o.x<5").ask("o.x=o.x+1")
# AS.getProp("x") # [2, 8, 6, 3, 3]
#
# ABM.agents.with("o.id<100").ask("o.color=[255,0,0]")
ask: (f) ->
eval("f=function(o){return "+f+";}") if u.isString f
f(o) for o in @; @
with: (f) ->
eval("f=function(o){return "+f+";}") if u.isString f
@asSet (o for o in @ when f(o))
# The example agentset AS used in the code fragments was made like this,
# slightly more useful than shown above due to the toString method.
class XY
constructor: (@x,@y) ->
toString: -> "{id:#{@id},x:#{@x},y:#{@y}}"
@AS = new ABM.AgentSet # @ => global name space
# The result of
#
# AS.add new XY(u.randomInt(10), u.randomInt(10)) for i in [1..5]
# random run, captured so we can reuse.
AS.add new XY(pt...) for pt in [[0,1],[8,0],[6,4],[1,3],[1,1]]
# There are three agentsets and their corresponding
# agents: Patches/Patch, Agents/Agent, and Links/Link.
# The usual alias for **ABM.util**.
u = ABM.util
# ### Patch and Patches
# Class Patch instances represent a rectangle on a grid with::
#
# * id: installed by Patches agentset
# * x,y: the x,y position within the grid
# * color: the color of the patch as an RGBA array, A optional.
# * label: text for the patch
# * n/n4: adjacent neighbors: n: 8 patches, n4: N,E,S,W patches.
class ABM.Patch
# Class variable defaults, "promoted" to instance variables if needed
# when unique per instance rather than shared.
# This approach yields considerable space effeciency when appropriate.
# Not all patches need their neighbor patches, thus we use a default
# of none. n and n4 are promoted by the ABM.Patches agent set
# constructor if the constructor variable "neighbors" is true,
# the default. This is only important in very large patch sets
# and should be handled with care!
n: null
n4: null
cachedRects: null
# Default color starts as black, can be set to different default value
# by setDefault methods in ABM.Patches
color: [0,0,0]
# Default patch is visible but can be changed to true if
# appropriate by setDefault methods
hidden: false
# new Patch: set x,y. Neighbors set by Patches constructor if needed.
constructor: (@x, @y) ->
# Return a string representation of the patch.
toString: ->
"{id:#{@id} xy:#{u.aToFixed [@x,@y]} c:#{@color}}"
# Set patch color to `c` scaled by `s`. Usage:
#
# p.scaleColor p.color, .8 # reduce patch color by .8
# p.scaleColor @foodColor, p.foodPheromone # ants model
#
# Promotes color if currently using the default.
scaleColor: (c, s) ->
@color = u.clone @color if not @.hasOwnProperty("color")
u.scaleColor c, s, @color
# Draw the patch and its text label if there is one.
draw: (ctx) ->
ctx.fillStyle = u.colorStr @color
ctx.fillRect @x-.5, @y-.5, 1, 1
if @label?
[x,y] = ctx.labelXY
ctx.save()
ctx.translate @x, @y # bug: fonts don't scale for size < 1
ctx.scale 1/ABM.patches.size, -1/ABM.patches.size
u.ctxDrawText ctx, @label, [x,y], ctx.labelColor
ctx.restore()
# Return an array of the agents on this patch.
# See model.setCacheAgentsHere for optimization.
agentsHere: ->
@agents ? (a for a in ABM.agents when a.p is @)
# Returns true if this patch is on the edge of the grid.
isOnEdge: ->
@x is ABM.patches.minX or @x is ABM.patches.maxX or \
@y is ABM.patches.minY or @y is ABM.patches.maxY
# Factory: Create num new agents on this patch.
# The optional init proc is called on each of the newly created agents.<br>
# NOTE: init must be applied after object inserted in agent set
sprout: (num = 1, init = ->) ->
ABM.agents.create num, (a) => # fat arrow so that @ = this patch
a.setXY @x, @y; init(a); a
# Class Patches is a singleton 2D matrix of Patch instances, each patch
# representing a 1x1 square in patch coordinates (via 2D coord transforms).
#
# * size: pixel h/w of each patch.
# * minX/maxX: min/max x coord, each patch being a unit square.
# * minY/maxY: min/max y coord.
# * numX/numY: total patches in x/y direction, width/height of grid.
# * isTorus: topology of patches, see **ABM.util**.
class ABM.Patches extends ABM.AgentSet
# Constructor: set variables, fill patch neighbor variables, n & n4.
constructor: (
@size, @minX, @maxX, @minY, @maxY, @isTorus=true, neighbors=true
) ->
super()
@numX = @maxX-@minX+1
@numY = @maxY-@minY+1
for y in [minY..maxY] by 1
for x in [minX..maxX] by 1
@add new ABM.Patch x, y
@setNeighbors() if neighbors
@setPixels()
@drawWithPixels = @size is 1 #false
# Install neighborhoods in patches
setNeighbors: ->
for p in @
p.n = @patchRect p, 1, 1 # p.n = p.patchRect 1, 1
p.n4 = @asSet (n for n in p.n when n.x is p.x or n.y is p.y)
# Setup pixels used for `drawScaledPixels` and `importColors`
setPixels: ->
if @size is 1
@pixelsCtx = ABM.contexts.patches
else
can = document.createElement 'canvas' # small pixel grid for patch colors
can.width = @numX; can.height = @numY
@pixelsCtx = can.getContext "2d"
@pixelsImageData = @pixelsCtx.getImageData(0, 0, @numX, @numY)
@pixelsData = @pixelsImageData.data
if @pixelsData instanceof Uint8Array # Check for typed arrays
@pixelsData32 = new Uint32Array @pixelsData.buffer
@pixelsAreLittleEndian = u.isLittleEndian()
# If using scaled pixels, use pixel manipulation below, or use default.
draw: (ctx) ->
if @drawWithPixels then @drawScaledPixels ctx else super ctx
# Set the default color for new Patch instances.
# Note coffeescript :: which refers to the Patch prototype.
# This is the usual way to modify class variables.
setDefaultColor: (color) -> ABM.Patch::color = color
# #### Patch grid coord system utilities:
# Return the patch at matrix position x,y where
# x & y are both valid integer patch coordinates.
patchXY: (x,y) -> @[x-@minX + @numX*(y-@minY)]
# Return x,y float values to be between min/max patch coord values
clamp: (x,y) -> [u.clamp(x, @minX-.5, @maxX+.5), u.clamp(y, @minY-.5, @maxY+.5)]
# Return x,y float values to be modulo min/max patch coord values.
wrap: (x,y) -> [u.wrap(x, @minX-.5, @maxX+.5), u.wrap(y, @minY-.5, @maxY+.5)]
# Return x,y float values to be between min/max patch values
# using either clamp/wrap above according to isTorus topology.
coord: (x,y) -> #returns a valid world coord (real, not int)
if @isTorus then @wrap x,y else @clamp x,y
# Return patch at x,y float values according to topology.
patch: (x,y) ->
[x,y]=@coord x,y
x = u.clamp Math.round(x), @minX, @maxX
y = u.clamp Math.round(y), @minY, @maxY
@patchXY x, y
# Return a random valid float x,y point in patch space
randomPt: -> [u.randomFloat2(@minX-.5,@maxX+.5), u.randomFloat2(@minY-.5,@maxY+.5)]
# #### Patch metrics
# Return pixel width/height of patch grid
bitWidth: -> @numX*@size # methods, not constants in case resize
bitHeight: -> @numY*@size
# Convert patch measure to pixels
patches2Bits: (p) -> p*@size
# Convert bit measure to patches
bits2Patches: (b) -> b/@size
# #### Patch utilities
# Return an array of patches in a rectangle centered on the given
# patch `p`, dx, dy units to the right/left and up/down.
# Exclude `p` unless meToo is true, default false.
patchRect: (p, dx, dy, meToo=false) ->
if p.pRect? and p.pRect.radius is dx and p.pRect.radius is dy
return p.pRect
rect = []; # REMIND: could optimize w/ a loop for the all inside patches case
for y in [p.y-dy..p.y+dy] by 1 # by 1: perf: avoid bidir JS for loop
for x in [p.x-dx..p.x+dx] by 1
if @isTorus or (@minX<=x<=@maxX and @minY<=y<=@maxY)
if @isTorus
x+=@numX if x<@minX; x-=@numX if x>@maxX
y+=@numY if y<@minY; y-=@numY if y>@maxY
pnext = @patchXY x, y # much faster than coord()
if not pnext?
u.error "patchRect: x,y out of bounds, see console.log"
console.log " x #{x} y #{y} p.x #{p.x} p.y #{p.y} dx #{dx} dy #{dy} minX #{@minX} minY #{@minY}"
rect.push pnext if (meToo or p isnt pnext)
@asSet rect
# Draws, or "imports" an image URL into the drawing layer.
# The image is scaled to fit the drawing layer.
#
# This is an async load, see this
# [new Image()](http://javascript.mfields.org/2011/creating-an-image-in-javascript/)
# tutorial. We draw the image into the drawing layer as
# soon as the onload callback executes.
# The "fat arrow" insures the callback executes within the importDrawing context.
importDrawing: (imageSrc) ->
u.importImage imageSrc, (img) ->
ctx = ABM.drawing
ctx.save() # revert to native 2D transform
ctx.setTransform 1, 0, 0, 1, 0, 0
ctx.drawImage img, 0, 0, ctx.canvas.width, ctx.canvas.height
ctx.restore() # restore patch transform