From 8c51108a0cab2fedc582b431014e0a1f06dd0cec Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Sun, 24 Apr 2016 11:50:35 -0400 Subject: [PATCH] enable shared private and public maps (#530) * enable shared private and public maps * change the list * yeehaw add collaborators * I believe this fixes the error connor brought up * when topic or synapse is no longer on a map, don't defer * needs to be before? * just do it in the controller * make recommendation they sign in and retry * better email * config for mailer previews * improve wording * shouldn't have included that * switch to green * don't execute if there's no map * wasn't including the right people in some circumstances * Finish breaking out JS files (#551) * metamaps.Realtime refactor * Metamaps.Util * Metamaps.Visualize * Metamaps.SynapseCard * Metamaps.TopicCard * Metamaps.Create.js * Remove erb extension from Metamaps.Map.js * Metmaps.Account and Metamaps.GlobalUI remove extension * Metamaps.JIT no more erb extension * move Backbone.init; standard-format on Metamaps.js.erb * factor out canvas support check function * some llittle template bugs * remove featured from signed in explore maps bar * don't let it overflow off the page --- Gemfile.lock | 3 - app/assets/images/addcollab_sprite.png | Bin 0 -> 322 bytes app/assets/images/exploremaps_sprite.png | Bin 1642 -> 2264 bytes app/assets/images/mm_logo.png | Bin 0 -> 3117 bytes app/assets/images/removecollab_sprite.png | Bin 0 -> 418 bytes app/assets/javascripts/application.js | 7 + ...aps.Account.js.erb => Metamaps.Account.js} | 5 +- .../javascripts/src/Metamaps.Backbone.js | 475 ++- app/assets/javascripts/src/Metamaps.Create.js | 326 ++ ...s.GlobalUI.js.erb => Metamaps.GlobalUI.js} | 6 +- .../{Metamaps.JIT.js.erb => Metamaps.JIT.js} | 4 +- .../{Metamaps.Map.js.erb => Metamaps.Map.js} | 170 +- .../javascripts/src/Metamaps.Realtime.js | 1199 +++++++ app/assets/javascripts/src/Metamaps.Router.js | 2 +- .../javascripts/src/Metamaps.SynapseCard.js | 288 ++ app/assets/javascripts/src/Metamaps.Topic.js | 3 +- .../javascripts/src/Metamaps.TopicCard.js | 451 +++ app/assets/javascripts/src/Metamaps.Util.js | 130 + .../javascripts/src/Metamaps.Visualize.js | 207 ++ app/assets/javascripts/src/Metamaps.js.erb | 3170 +---------------- .../javascripts/src/check-canvas-support.js | 15 + app/assets/stylesheets/application.css.erb | 97 +- app/assets/stylesheets/clean.css.erb | 7 + app/controllers/application_controller.rb | 6 +- app/controllers/main_controller.rb | 6 +- app/controllers/mappings_controller.rb | 7 + app/controllers/maps_controller.rb | 76 +- app/controllers/synapses_controller.rb | 2 +- app/controllers/topics_controller.rb | 4 +- app/mailers/application_mailer.rb | 4 + app/mailers/map_mailer.rb | 10 + app/models/map.rb | 13 +- app/models/synapse.rb | 23 +- app/models/topic.rb | 23 +- app/models/user.rb | 4 +- app/models/user_map.rb | 4 + app/policies/map_policy.rb | 12 +- app/policies/synapse_policy.rb | 21 +- app/policies/topic_policy.rb | 21 +- app/serializers/new_map_serializer.rb | 1 + app/views/layouts/_templates.html.erb | 14 +- app/views/layouts/mailer.html.erb | 5 + app/views/layouts/mailer.text.erb | 1 + .../map_mailer/invite_to_edit_email.html.erb | 22 + .../map_mailer/invite_to_edit_email.text.erb | 7 + app/views/maps/_mapinfobox.html.erb | 39 +- app/views/maps/sharedmaps.html.erb | 15 + app/views/maps/show.html.erb | 1 + config/environments/development.rb | 4 +- config/routes.rb | 3 +- db/migrate/20160331181959_create_user_maps.rb | 10 + ...dd_defers_to_map_to_topics_and_synapses.rb | 6 + db/schema.rb | 18 +- public/famous/main.js | 2 +- public/famous/templates.js | 11 +- spec/mailers/map_mailer_spec.rb | 5 + spec/mailers/previews/map_mailer_preview.rb | 6 + 57 files changed, 3742 insertions(+), 3229 deletions(-) create mode 100644 app/assets/images/addcollab_sprite.png mode change 100755 => 100644 app/assets/images/exploremaps_sprite.png create mode 100644 app/assets/images/mm_logo.png create mode 100644 app/assets/images/removecollab_sprite.png rename app/assets/javascripts/src/{Metamaps.Account.js.erb => Metamaps.Account.js} (96%) create mode 100644 app/assets/javascripts/src/Metamaps.Create.js rename app/assets/javascripts/src/{Metamaps.GlobalUI.js.erb => Metamaps.GlobalUI.js} (98%) rename app/assets/javascripts/src/{Metamaps.JIT.js.erb => Metamaps.JIT.js} (99%) rename app/assets/javascripts/src/{Metamaps.Map.js.erb => Metamaps.Map.js} (78%) create mode 100644 app/assets/javascripts/src/Metamaps.Realtime.js create mode 100644 app/assets/javascripts/src/Metamaps.SynapseCard.js create mode 100644 app/assets/javascripts/src/Metamaps.TopicCard.js create mode 100644 app/assets/javascripts/src/Metamaps.Util.js create mode 100644 app/assets/javascripts/src/Metamaps.Visualize.js create mode 100644 app/assets/javascripts/src/check-canvas-support.js create mode 100644 app/mailers/application_mailer.rb create mode 100644 app/mailers/map_mailer.rb create mode 100644 app/models/user_map.rb create mode 100644 app/views/layouts/mailer.html.erb create mode 100644 app/views/layouts/mailer.text.erb create mode 100644 app/views/map_mailer/invite_to_edit_email.html.erb create mode 100644 app/views/map_mailer/invite_to_edit_email.text.erb create mode 100644 app/views/maps/sharedmaps.html.erb create mode 100644 db/migrate/20160331181959_create_user_maps.rb create mode 100644 db/migrate/20160401133937_add_defers_to_map_to_topics_and_synapses.rb create mode 100644 spec/mailers/map_mailer_spec.rb create mode 100644 spec/mailers/previews/map_mailer_preview.rb diff --git a/Gemfile.lock b/Gemfile.lock index 59a2e295..87e56f79 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -306,6 +306,3 @@ DEPENDENCIES tunemygc uglifier uservoice-ruby - -BUNDLED WITH - 1.11.2 diff --git a/app/assets/images/addcollab_sprite.png b/app/assets/images/addcollab_sprite.png new file mode 100644 index 0000000000000000000000000000000000000000..d3f498727be02e86f7700c0c003182e60819827e GIT binary patch literal 322 zcmeAS@N?(olHy`uVBq!ia0vp^3P3Et!3HGD8EPYe6lZ})WHAE+w=f7ZGR&GI0Th%h zag8Vm&QB{TPb^Aha7@WhN>%X8O-xS>N=;0uEIgTN15|Y0)5S5Q;#Sh1|Nrfo^#lqG zPBD0JGJI_3{dQW6`n$L^qO->;J5(WSg#+Ts_-pK++!q;Uv5 z5N2|DnVuw(VDNyiVL9Ux7ItQZI(wo00v!_{3#cY4sah=h+I)%S24jSf%tnSe1{+m? z0>%?(9=ISZcJmi|sFa4o=T0VpP`1MLo(hAVX{itYDNiBL{Q4GJ0x0000DNk~Le0001h0000$2nGNE0Mgcfe~}?Pe+Xqs zL_t(|+U#82aoac$MlyM~T*1i|oK!)mG!UtR&=thVOr9-2dnS=9FjIk*24cB_=qfN% z!Lfwf^?mfLRsactKb9Tr42NS91eUvt{iFOD6Dd-pNRc8%iWKROqgPcoKY#wbq|-+_ zM!2;4`t|FlC^loheEE{n>6(t|f92)n_PM^tn+oNh(dk@WKBMdA5RHKN`bDS7fJXWr z=MQvjJ2tKn-i0YA1Iq*F8PjQuKesebp~t+gc>aUqdU}A$9}9sE#b2V%T`HfA`4-1L zU7H8YlUU{%H!*;T`A)!Uv~ns=~6 zUG#SYZ)_TR@JZ7uAVH?f7Pz6~3g>Uaop6G3N{QD-JU17Mt?Ayjr2#BOLb{;&*5V#p z`oG$H(U@sZ?tg;Vs_7&&e_6AHmm76C!-h;CSC@g`K_6?#>rCB0B_1N?6vo|HKh8UF#Jj_(g;D@0Hs> zQL?Th)&j9;>5cQtRoUGTiOg>;v1NAF__Z0-}KBfK=~tOe^7J0T%Eh0KoB9N z(|JcOSBA|dv{4?mfcGtTcrMF2Q8tbtkALvYR`9iL)Ccg*`r-Pg%1yhn(q({PwNY`F zpNNnn@2`a0OF|5Q-8cz!3a`>dcF#O4JdJOJbrY2UvIkrcZ)*q7g?QIYgel4YoUY$u zo}!5X`0k{ljUjn7e_ZsnjoNu^;Xi}{1Sf*r*@F>Wx6JTYG*^?R)UCslup)1nCm85R zH6LCAFt5{&CHGOtc zvw`NKSCFdSK`M-;>b)oM5cM9wM}Qb}1{YuF3ajQh1LJ>*f0vjj1s`E8E(<5hRc<)x zqw8hdd#Db$buAJ!j-dsOTQx9*6$xz9AS7&&ZHH7o3wXD&#h~u6(_P;5Ej-SK`2B>x zN0v=`CwY70X_0poN;?+|Wyao$Ej)Ho$?(`A=raDDiQ0RjauK5vAKT6@n-y?Sitvoywh6tbFec+hLT zcIpzlrdi;lW~wzz!isD6?*>p_v_-0W@nOWsZSRS?D?y-Zdr zpU3c67|2=4r%m~;Dj==NB4_NbpFr_tsMuK-DN>|Jks?Kk6e&`qNRc8%iWKQNQsqB}ynFX< zX%s%d&!0adKEVI7Ne%D;NG2~2AAnxaiVr~7%^?~A@%4xgFc5#e;RDdS?BD~?`{aT1 ze~gX52cUUgCO!aO^hMwUK>0iW*H72x0rMo5dB#l);Mw8>*j}__CGFq?bi7__4jmtW za|2lFp-{vJ@SCSwd;laugSdD)c)f@Z;7{%G0T4&L@Bt{n*eDnI_We>`U11^|X0@B!$-SC+)f8y|q4KW!nD9ee;e zmqTDo$F{7ufx%U||HKh8o%jI6%c8;hz2E~758dGdz}jc<61DgMH10P&vus%smF~AU zd;l2sv%m)^4P3}*Y=*`Mfa{+sH|@$wmjQy+M#WivB0`S5-x?o)4V&hF5_|wCf6|E$ zAVZ(xc?Targel4Y9Nv5(J!8ZNunTkxBWR5e(6A|0i4VX$!9cC}0IXPeus;JI0O6;k zJ$!&Rr78;(aXT0vAfO{T2tEKjM7;;_5g^92#s@&_*&81KFCh(JgB`Gg55TQ!qwoP} z+^T^gtZ0snX^0OHSoyTZ2jI|1fBaVB14!Ol;RA^81y7C-Aaa7v@c|GLyzv1-%U;Cz z*7yK0@OJnBD*HZC_fli|H*v%#D?UJ-P;{B3)-THE>3VJg@BuxNakSfHnrFnoWB$e};kV;RDD` zhA}(%0J2%Nm`!i?`I}oUCtq>(*BQIiDTp!Sh_49}DW9zaFe;iz>lI#C+Y@l+yqX0CXl@9&IT>wxPY(F424~K z+5*_WYaaWW>TMjI%&Y&ne}aA{C6ry(-b9iP{ac^8k={7szV*?Nwmit>nJFXiz)xiURuDd^PDGq!GYHh=q z8HP=cB1UM$Wd9iMyQZ(P;o3-|fK?j+U}wvd>Z+mh=hPPOls7)ke}ew*IOV&QW@RiT z35BuqlvMHTNlLYt8bHNly-F)XcfICo^2*)Q<*nR1T@pGY%e z$=^9MLRy$e4!pv>wIP_tn*NIkkR$8j>v!lkmioVu+Lp z`v5HOObMJk$lo|k);Zqe!CML%lI`3j#jUwF$So=8aSu`ze|Rfb0A^mM$yz4T283tz z%%|feTWA2tI(Gh8`$w?vI3L{x}y%6QQ1(Ldg^j2e}X<>>-?bz`hZ?dcESLFbWJ~7 zeL$1`ob&-48>j?*!2i~dKEM~>GS~opzw`k#U>=B-K7cZTOj)8^eL#~eLCVIq`T%Ng zC4(b0`T(jk?r1WU>I10Hyj1|uAv<!gFc|bz8HM~Vpc2q z0BS3$qQWm)AE2y+&-4LJQe}1;l}7pin`0Z(S&(r6sMA;>KN__w?d!(&HA?X8LQ&NZBZgYJA zmUkvB^#Lv$kOq4KtvjQqlOI%^+H}&a!m~M7u9Xr2F ze;@F@(EktAn&zDJ0ZbP?I9}%CJ9RJO`RwOWA3*&sgl~O-hr!xd@A`lurWGdwBIpA; zqo2?80V=;^&%n%aF74Lc^Z{5_JFO4E?3?=tz7iuW5%V?`@5S%ZTpyr0>AXjNn}}eo zn(6m|WAI%TT-5^r!MeBb$E*NA03cY88TLtl0RYb<hY_&}Ddkv%O3NI>C?OBeldk1g!XD zGZ7&^KgL&w@jKBlTNDNW5SRUH0)QtuQ2rz=!rDE;CDcCx=X(PUu(%R>8Let}-8TU3 zjP|`k2=757_^aRASy{N^$G>~u51%+G;dTRM9ty^S(f=A6e2d?L{?GAOV)4L z5p0P0DqS~>zEoQ)A_g!6f^D0L#`E`%)}bhzzF|(27OIYFP|sZ}*|t|rAzn43zgRA=d*achZ&w_B8b7MM zVfJ-XaHnW1LyKnBtVi*?6Oi0TN1=ONa$WOZ-)k9}lPb5^K*6~Lv{K%g-$c_h-KLyc zwY$N|%n~Mr{hf*4^{c%X0m>RFRDY&@lFRa#S>2 zBuTkMJrD$%C0ZP(2Mh%O31JgVnDMXORfL%JG2vioze6&SGY`%YUVplMck4WD_Q&K;#u=_h_zUEJ>0qRq{lHqI5ftO)zZ#=A8 zHM-Y))q}m`3oYs)3Q!7WG~oSu*uh$jF?iF2RCVg7t#V6|03my&ANe=a&&W|-&4Ymq zSV7ZcmDwW=gQOT)RtoOK8r(X4Q*F_(4)IOyh7e%s1Xfye;8j58fVKiP6b_}lTiAEu z53OT$`-QR>?67&@65I~iKi|5rZ9}AXJ^aSsSH*QXRgd z5r%Di#rO*R<4~g?ks07x^oGhFPt6Hv`#x1(uCzU=z@X^qTZBpFTr>3FbB|XcR%HKE z?VDjNA~5jcYT02K=OF4awEg)!$#W{V*as39d9Ov2+NmOoH)iZavvmAJpJ|8fnZHY_ z$B3|;q3xeNPyDg_u9%G|$+OR^W}S1{JV~2y_uJISD89VrZyLcp#{2!zK-YmY{aUZA z>0{myUiV3Og*axgsxMYUP`Y9BLY0u|CCT@G>%Oru8G80|Wlg;rLjl2rt#Rf(RYN~= z2rBrxxXD+QXV*$Emx9zCNe8rHZyI0$leFY1MeF~(JPLg(Sk>1zDNyE5XE8Bh6Drzn} zAZKB>7C`%497(KjdS9@_@-NbOV&Z$*475-udlF+AAdbx{L4ER2UzHj&aVe?Qk?fwy zz1HuYCpz!@Do2rq+diHLC<6_gIV&UZ>yVrUzQsU{6?e3yd%j;LLE0nc0Av6= zXk3vjY~yHzi((&0d043|t$@~m_{2Va`A8B>d~roKS3^UUYcJbkWdD>l$GV%)EAO~u zwZKe&VR8chocWCTX}#pOpM2k@ZejSR5dh51evLx-FH~ottEeNh3YvwY7iP)sFC#tG)r}WHVam8nZoO z4(%pR7=v5(AUjkL!1GXvJkc%ncmU7U9cO!#G?tn`ZIpr1G|~s9Qu<-|bMxxUWqK6a|nXWMhQ&MT&d=)q43TK zE2++pgsZLJiY2DNB+|lZcU%tUNByz*wMk-XBVRtGz|%@ock4x5gkb1u$@$pYUh5L& zcxIlp`{{EHX)is%tbNB#0khD5<^g2)*qY#fJeta->FKXN@>5y-8#y;mjz6$#cJqa- z!o=c!eBzz6mR&IE!}K&8WT-9fJvL_qd~Ipg-Uc0Bt)&ww#>#qk%)O&y@@JO0N`AveWswGw#7fq(YS*Xv(UZtL2V>fD=j8tZ)uy#nPj*eYTZ1Fgu> zzj^mFtYqqrB$vJ1Q_^Er3qhvfJC+x3jNw)gU+15=UPIiksV?Ux`zoOmcI+c&a@D+A zi3DJ$7w)R zr{FFNC-b|FL!!=ZtiLmtom7wDOf=-DoLp~u=azSGKCLqvVLsrt;B7EwG}=z!fPSz{ zX3o$a%1R5TSZtDyysy(ntRoAZCw(5&n7T03?(sBXv;ngWWz%s}6KR4D<2CC`-}DWa zfrGNtodP2WZ425PtN;BhR(L;s`Q3FOfPu=>nx|DS#|pFy#k4m6Sn)*atyv*#80hJI z`D{7FKn#Po`_(_m`Dy0;yXuHO*3D8+JCF}wQUIzi{$9vM_5dVOTJkFjr&4vqnQKv) z@8||C{996ApHag?ljsn5{a1BLHg9Bj%cLv7}C zQE$j^FG7oqBr&q6ioV`6d}@OgWN;O`uk9nS1Y`(!{xLwC&{?-Pn{pn-7+O$5|=7>sD_m=nBE5UOL$~f3f3v zeH89?X&T2r|JDe{@r?ZEd92P@6b*NYh;scP&%RQy>>|cjmO#!NDLYtpY4@Nw+|#Y9 z+qbsswS09XwR(zNFnT^g@l?fMv4|Z7my&gFdypt%ZW2?6mXyxWFk?Py%auwJr83wI zw&_5O*00qDC5w6>H5D8B$)wfmXI25iQr&esuJS0`aj+*WAGD~s*UCO8i(9)gx2i?Z zUCaPTr^k(e_ED04p3@qRU)S=Ey+3U*;E%t#-RG)eArO93B?~iH*>L^mN257teMRfp zwjojJh6=JGRx`H;Sy;tk&7qD8If*sjd0b$MNk=ZoI@C80J32b9)j{4IY?hw{ z=sjC)02eW8i^{C%B05XR|0G9IR=Q!Ot_GH+ie7Iqn7aRoW?D^ZhsQ@~vZx!F#O7 zBaP0z*N=Dh|B~O*rB_GFMX6`f*;1hyF(PgiXy`4$ZFt~3E`Fy{6m2@~#S!6Mf11Kc z$Vs`Rqg*WE_v5xe(cc>Rj!AQ(NIV~4>4g8t71MI9FtST%o G#r+R$Lgv{3 literal 0 HcmV?d00001 diff --git a/app/assets/images/removecollab_sprite.png b/app/assets/images/removecollab_sprite.png new file mode 100644 index 0000000000000000000000000000000000000000..2036da5e480b6a28603cb4fd6a2658f3f7f8f348 GIT binary patch literal 418 zcmV;T0bTxyP)WXXB1#m`9M318Ae5G`;TLSEb3)^dArJcxXp zYgX66(;G0h3AI2VH&GNx14wv@+$2fzJph`%lqQ_Kl#*yV3Xi%{Re>F1uQVt$ayCS+ z;W@b^!+PN^N`pXC0NeJ1hUW^9WLU5_=N1mgFdO*pgkxpcyxB&0(=jsa#Zwu2Oon{n z3SqG{8J>iN8^lX8>})$zhblvm)lYDQ-|4m!bzDiV#k+t1H~a`N0RI!LXwINArT_o{ M07*qoM6N<$g4WEcE&u=k literal 0 HcmV?d00001 diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 3e19bbc2..8dcb1df3 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -24,7 +24,14 @@ //= require ./src/views/videoView //= require ./src/views/room //= require ./src/JIT +//= require ./src/check-canvas-support //= require ./src/Metamaps +//= require ./src/Metamaps.Create +//= require ./src/Metamaps.TopicCard +//= require ./src/Metamaps.SynapseCard +//= require ./src/Metamaps.Visualize +//= require ./src/Metamaps.Util +//= require ./src/Metamaps.Realtime //= require ./src/Metamaps.Control //= require ./src/Metamaps.Filter //= require ./src/Metamaps.Listeners diff --git a/app/assets/javascripts/src/Metamaps.Account.js.erb b/app/assets/javascripts/src/Metamaps.Account.js similarity index 96% rename from app/assets/javascripts/src/Metamaps.Account.js.erb rename to app/assets/javascripts/src/Metamaps.Account.js index 89f8cfca..a2286ad8 100644 --- a/app/assets/javascripts/src/Metamaps.Account.js.erb +++ b/app/assets/javascripts/src/Metamaps.Account.js @@ -3,7 +3,8 @@ /* * Metamaps.Account.js.erb * - * Dependencies: none! + * Dependencies: + * - Metamaps.Erb */ Metamaps.Account = { @@ -95,7 +96,7 @@ Metamaps.Account = { var self = Metamaps.Account $('.userImageDiv canvas').remove() - $('.userImageDiv img').attr('src', '<%= asset_path('user.png') %>').show() + $('.userImageDiv img').attr('src', Metamaps.Erb['user.png']).show() $('.userImageMenu').hide() var input = $('#user_image') diff --git a/app/assets/javascripts/src/Metamaps.Backbone.js b/app/assets/javascripts/src/Metamaps.Backbone.js index 7b40c3cc..63025c54 100644 --- a/app/assets/javascripts/src/Metamaps.Backbone.js +++ b/app/assets/javascripts/src/Metamaps.Backbone.js @@ -5,10 +5,24 @@ * * Dependencies: * - Metamaps.Active + * - Metamaps.Collaborators + * - Metamaps.Creators + * - Metamaps.Filter + * - Metamaps.JIT * - Metamaps.Loading * - Metamaps.Map * - Metamaps.Mapper + * - Metamaps.Mappers + * - Metamaps.Mappings + * - Metamaps.Metacodes * - Metamaps.Realtime + * - Metamaps.Synapse + * - Metamaps.SynapseCard + * - Metamaps.Synapses + * - Metamaps.Topic + * - Metamaps.TopicCard + * - Metamaps.Topics + * - Metamaps.Visualize */ Metamaps.Backbone = {} @@ -47,12 +61,21 @@ Metamaps.Backbone.Map = Backbone.Model.extend({ Metamaps.Realtime.sendMapChange(this) }, authorizeToEdit: function (mapper) { - if (mapper && (this.get('permission') === 'commons' || this.get('user_id') === mapper.get('id'))) return true - else return false + if (mapper && ( + this.get('permission') === 'commons' || + this.get('collaborator_ids').includes(mapper.get('id')) || + this.get('user_id') === mapper.get('id'))) { + return true + } else { + return false + } }, authorizePermissionChange: function (mapper) { - if (mapper && this.get('user_id') === mapper.get('id')) return true - else return false + if (mapper && this.get('user_id') === mapper.get('id')) { + return true + } else { + return false + } }, getUser: function () { return Metamaps.Mapper.get(this.get('user_id')) @@ -70,6 +93,7 @@ Metamaps.Backbone.Map = Backbone.Model.extend({ $.ajax({ url: '/maps/' + this.id + '/contains.json', success: start, + error: errorFunc, async: false }) }, @@ -254,3 +278,446 @@ Metamaps.Backbone.MapperCollection = Backbone.Collection.extend({ model: Metamaps.Backbone.Mapper, url: '/users' }) + +Metamaps.Backbone.init = function () { + var self = Metamaps.Backbone + + self.Metacode = Backbone.Model.extend({ + initialize: function () { + var image = new Image() + image.crossOrigin = 'Anonymous' + image.src = this.get('icon') + this.set('image', image) + }, + prepareLiForFilter: function () { + var li = '' + li += '
  • ' + li += '' + li += '

    ' + this.get('name').toLowerCase() + '

  • ' + return li + } + + }) + self.MetacodeCollection = Backbone.Collection.extend({ + model: this.Metacode, + url: '/metacodes', + comparator: function (a, b) { + a = a.get('name').toLowerCase() + b = b.get('name').toLowerCase() + return a > b ? 1 : a < b ? -1 : 0 + } + }) + + self.Topic = Backbone.Model.extend({ + urlRoot: '/topics', + blacklist: ['node', 'created_at', 'updated_at', 'user_name', 'user_image', 'map_count', 'synapse_count'], + toJSON: function (options) { + return _.omit(this.attributes, this.blacklist) + }, + save: function (key, val, options) { + var attrs + + // Handle both `"key", value` and `{key: value}` -style arguments. + if (key == null || typeof key === 'object') { + attrs = key + options = val + } else { + (attrs = {})[key] = val + } + + var newOptions = options || {} + var s = newOptions.success + + var permBefore = this.get('permission') + + newOptions.success = function (model, response, opt) { + if (s) s(model, response, opt) + model.trigger('saved') + + if (permBefore === 'private' && model.get('permission') !== 'private') { + model.trigger('noLongerPrivate') + } + else if (permBefore !== 'private' && model.get('permission') === 'private') { + model.trigger('nowPrivate') + } + } + return Backbone.Model.prototype.save.call(this, attrs, newOptions) + }, + initialize: function () { + if (this.isNew()) { + this.set({ + 'user_id': Metamaps.Active.Mapper.id, + 'desc': '', + 'link': '', + 'permission': Metamaps.Active.Map ? Metamaps.Active.Map.get('permission') : 'commons' + }) + } + + this.on('changeByOther', this.updateCardView) + this.on('change', this.updateNodeView) + this.on('saved', this.savedEvent) + this.on('nowPrivate', function () { + var removeTopicData = { + mappableid: this.id + } + + $(document).trigger(Metamaps.JIT.events.removeTopic, [removeTopicData]) + }) + this.on('noLongerPrivate', function () { + var newTopicData = { + mappingid: this.getMapping().id, + mappableid: this.id + } + + $(document).trigger(Metamaps.JIT.events.newTopic, [newTopicData]) + }) + + this.on('change:metacode_id', Metamaps.Filter.checkMetacodes, this) + }, + authorizeToEdit: function (mapper) { + if (mapper && + (this.get('calculated_permission') === 'commons' || + this.get('collaborator_ids').includes(mapper.get('id')) || + this.get('user_id') === mapper.get('id'))) { + return true + } else { + return false + } + }, + authorizePermissionChange: function (mapper) { + if (mapper && this.get('user_id') === mapper.get('id')) return true + else return false + }, + getDate: function () {}, + getMetacode: function () { + return Metamaps.Metacodes.get(this.get('metacode_id')) + }, + getMapping: function () { + if (!Metamaps.Active.Map) return false + + return Metamaps.Mappings.findWhere({ + map_id: Metamaps.Active.Map.id, + mappable_type: 'Topic', + mappable_id: this.isNew() ? this.cid : this.id + }) + }, + createNode: function () { + var mapping + var node = { + adjacencies: [], + id: this.isNew() ? this.cid : this.id, + name: this.get('name') + } + + if (Metamaps.Active.Map) { + mapping = this.getMapping() + node.data = { + $mapping: null, + $mappingID: mapping.id + } + } + + return node + }, + updateNode: function () { + var mapping + var node = this.get('node') + node.setData('topic', this) + + if (Metamaps.Active.Map) { + mapping = this.getMapping() + node.setData('mapping', mapping) + } + + return node + }, + savedEvent: function () { + Metamaps.Realtime.sendTopicChange(this) + }, + updateViews: function () { + var onPageWithTopicCard = Metamaps.Active.Map || Metamaps.Active.Topic + var node = this.get('node') + // update topic card, if this topic is the one open there + if (onPageWithTopicCard && this == Metamaps.TopicCard.openTopicCard) { + Metamaps.TopicCard.showCard(node) + } + + // update the node on the map + if (onPageWithTopicCard && node) { + node.name = this.get('name') + Metamaps.Visualize.mGraph.plot() + } + }, + updateCardView: function () { + var onPageWithTopicCard = Metamaps.Active.Map || Metamaps.Active.Topic + var node = this.get('node') + // update topic card, if this topic is the one open there + if (onPageWithTopicCard && this == Metamaps.TopicCard.openTopicCard) { + Metamaps.TopicCard.showCard(node) + } + }, + updateNodeView: function () { + var onPageWithTopicCard = Metamaps.Active.Map || Metamaps.Active.Topic + var node = this.get('node') + + // update the node on the map + if (onPageWithTopicCard && node) { + node.name = this.get('name') + Metamaps.Visualize.mGraph.plot() + } + } + }) + + self.TopicCollection = Backbone.Collection.extend({ + model: self.Topic, + url: '/topics' + }) + + self.Synapse = Backbone.Model.extend({ + urlRoot: '/synapses', + blacklist: ['edge', 'created_at', 'updated_at'], + toJSON: function (options) { + return _.omit(this.attributes, this.blacklist) + }, + save: function (key, val, options) { + var attrs + + // Handle both `"key", value` and `{key: value}` -style arguments. + if (key == null || typeof key === 'object') { + attrs = key + options = val + } else { + (attrs = {})[key] = val + } + + var newOptions = options || {} + var s = newOptions.success + + var permBefore = this.get('permission') + + newOptions.success = function (model, response, opt) { + if (s) s(model, response, opt) + model.trigger('saved') + + if (permBefore === 'private' && model.get('permission') !== 'private') { + model.trigger('noLongerPrivate') + } + else if (permBefore !== 'private' && model.get('permission') === 'private') { + model.trigger('nowPrivate') + } + } + return Backbone.Model.prototype.save.call(this, attrs, newOptions) + }, + initialize: function () { + if (this.isNew()) { + this.set({ + 'user_id': Metamaps.Active.Mapper.id, + 'permission': Metamaps.Active.Map ? Metamaps.Active.Map.get('permission') : 'commons', + 'category': 'from-to' + }) + } + + this.on('changeByOther', this.updateCardView) + this.on('change', this.updateEdgeView) + this.on('saved', this.savedEvent) + this.on('noLongerPrivate', function () { + var newSynapseData = { + mappingid: this.getMapping().id, + mappableid: this.id + } + + $(document).trigger(Metamaps.JIT.events.newSynapse, [newSynapseData]) + }) + this.on('nowPrivate', function () { + $(document).trigger(Metamaps.JIT.events.removeSynapse, [{ + mappableid: this.id + }]) + }) + + this.on('change:desc', Metamaps.Filter.checkSynapses, this) + }, + prepareLiForFilter: function () { + var li = '' + li += '
  • ' + li += '
  • ' + return li + }, + authorizeToEdit: function (mapper) { + if (mapper && (this.get('calculated_permission') === 'commons' || this.get('collaborator_ids').includes(mapper.get('id')) || this.get('user_id') === mapper.get('id'))) return true + else return false + }, + authorizePermissionChange: function (mapper) { + if (mapper && this.get('user_id') === mapper.get('id')) return true + else return false + }, + getTopic1: function () { + return Metamaps.Topics.get(this.get('node1_id')) + }, + getTopic2: function () { + return Metamaps.Topics.get(this.get('node2_id')) + }, + getDirection: function () { + var t1 = this.getTopic1(), + t2 = this.getTopic2() + + return t1 && t2 ? [ + t1.get('node').id, + t2.get('node').id + ] : false + }, + getMapping: function () { + if (!Metamaps.Active.Map) return false + + return Metamaps.Mappings.findWhere({ + map_id: Metamaps.Active.Map.id, + mappable_type: 'Synapse', + mappable_id: this.isNew() ? this.cid : this.id + }) + }, + createEdge: function (providedMapping) { + var mapping, mappingID + var synapseID = this.isNew() ? this.cid : this.id + + var edge = { + nodeFrom: this.get('node1_id'), + nodeTo: this.get('node2_id'), + data: { + $synapses: [], + $synapseIDs: [synapseID], + } + } + + if (Metamaps.Active.Map) { + mapping = providedMapping || this.getMapping() + mappingID = mapping.isNew() ? mapping.cid : mapping.id + edge.data.$mappings = [] + edge.data.$mappingIDs = [mappingID] + } + + return edge + }, + updateEdge: function () { + var mapping + var edge = this.get('edge') + edge.getData('synapses').push(this) + + if (Metamaps.Active.Map) { + mapping = this.getMapping() + edge.getData('mappings').push(mapping) + } + + return edge + }, + savedEvent: function () { + Metamaps.Realtime.sendSynapseChange(this) + }, + updateViews: function () { + this.updateCardView() + this.updateEdgeView() + }, + updateCardView: function () { + var onPageWithSynapseCard = Metamaps.Active.Map || Metamaps.Active.Topic + var edge = this.get('edge') + + // update synapse card, if this synapse is the one open there + if (onPageWithSynapseCard && edge == Metamaps.SynapseCard.openSynapseCard) { + Metamaps.SynapseCard.showCard(edge) + } + }, + updateEdgeView: function () { + var onPageWithSynapseCard = Metamaps.Active.Map || Metamaps.Active.Topic + var edge = this.get('edge') + + // update the edge on the map + if (onPageWithSynapseCard && edge) { + Metamaps.Visualize.mGraph.plot() + } + } + }) + + self.SynapseCollection = Backbone.Collection.extend({ + model: self.Synapse, + url: '/synapses' + }) + + self.Mapping = Backbone.Model.extend({ + urlRoot: '/mappings', + blacklist: ['created_at', 'updated_at'], + toJSON: function (options) { + return _.omit(this.attributes, this.blacklist) + }, + initialize: function () { + if (this.isNew()) { + this.set({ + 'user_id': Metamaps.Active.Mapper.id, + 'map_id': Metamaps.Active.Map ? Metamaps.Active.Map.id : null + }) + } + }, + getMap: function () { + return Metamaps.Map.get(this.get('map_id')) + }, + getTopic: function () { + if (this.get('mappable_type') === 'Topic') return Metamaps.Topic.get(this.get('mappable_id')) + else return false + }, + getSynapse: function () { + if (this.get('mappable_type') === 'Synapse') return Metamaps.Synapse.get(this.get('mappable_id')) + else return false + } + }) + + self.MappingCollection = Backbone.Collection.extend({ + model: self.Mapping, + url: '/mappings' + }) + + Metamaps.Metacodes = Metamaps.Metacodes ? new self.MetacodeCollection(Metamaps.Metacodes) : new self.MetacodeCollection() + + Metamaps.Topics = Metamaps.Topics ? new self.TopicCollection(Metamaps.Topics) : new self.TopicCollection() + + Metamaps.Synapses = Metamaps.Synapses ? new self.SynapseCollection(Metamaps.Synapses) : new self.SynapseCollection() + + Metamaps.Mappers = Metamaps.Mappers ? new self.MapperCollection(Metamaps.Mappers) : new self.MapperCollection() + + Metamaps.Collaborators = Metamaps.Collaborators ? new self.MapperCollection(Metamaps.Collaborators) : new self.MapperCollection() + + // this is for topic view + Metamaps.Creators = Metamaps.Creators ? new self.MapperCollection(Metamaps.Creators) : new self.MapperCollection() + + if (Metamaps.Active.Map) { + Metamaps.Mappings = Metamaps.Mappings ? new self.MappingCollection(Metamaps.Mappings) : new self.MappingCollection() + + Metamaps.Active.Map = new self.Map(Metamaps.Active.Map) + } + + if (Metamaps.Active.Topic) Metamaps.Active.Topic = new self.Topic(Metamaps.Active.Topic) + + // attach collection event listeners + self.attachCollectionEvents = function () { + Metamaps.Topics.on('add remove', function (topic) { + Metamaps.Map.InfoBox.updateNumbers() + Metamaps.Filter.checkMetacodes() + Metamaps.Filter.checkMappers() + }) + + Metamaps.Synapses.on('add remove', function (synapse) { + Metamaps.Map.InfoBox.updateNumbers() + Metamaps.Filter.checkSynapses() + Metamaps.Filter.checkMappers() + }) + + if (Metamaps.Active.Map) { + Metamaps.Mappings.on('add remove', function (mapping) { + Metamaps.Map.InfoBox.updateNumbers() + Metamaps.Filter.checkSynapses() + Metamaps.Filter.checkMetacodes() + Metamaps.Filter.checkMappers() + }) + } + } + self.attachCollectionEvents() +}; // end Metamaps.Backbone.init diff --git a/app/assets/javascripts/src/Metamaps.Create.js b/app/assets/javascripts/src/Metamaps.Create.js new file mode 100644 index 00000000..bb01d129 --- /dev/null +++ b/app/assets/javascripts/src/Metamaps.Create.js @@ -0,0 +1,326 @@ +/* global Metamaps, $ */ + +/* + * Metamaps.Create.js + * + * Dependencies: + * - Metamaps.Backbone + * - Metamaps.GlobalUI + * - Metamaps.Metacodes + * - Metamaps.Mouse + * - Metamaps.Selected + * - Metamaps.Synapse + * - Metamaps.Topic + * - Metamaps.Visualize + */ + +Metamaps.Create = { + isSwitchingSet: false, // indicates whether the metacode set switch lightbox is open + selectedMetacodeSet: null, + selectedMetacodeSetIndex: null, + selectedMetacodeNames: [], + newSelectedMetacodeNames: [], + selectedMetacodes: [], + newSelectedMetacodes: [], + init: function () { + var self = Metamaps.Create + self.newTopic.init() + self.newSynapse.init() + + // // SWITCHING METACODE SETS + + $('#metacodeSwitchTabs').tabs({ + selected: self.selectedMetacodeSetIndex + }).addClass('ui-tabs-vertical ui-helper-clearfix') + $('#metacodeSwitchTabs .ui-tabs-nav li').removeClass('ui-corner-top').addClass('ui-corner-left') + $('.customMetacodeList li').click(self.toggleMetacodeSelected) // within the custom metacode set tab + }, + toggleMetacodeSelected: function () { + var self = Metamaps.Create + + if ($(this).attr('class') != 'toggledOff') { + $(this).addClass('toggledOff') + var value_to_remove = $(this).attr('id') + var name_to_remove = $(this).attr('data-name') + self.newSelectedMetacodes.splice(self.newSelectedMetacodes.indexOf(value_to_remove), 1) + self.newSelectedMetacodeNames.splice(self.newSelectedMetacodeNames.indexOf(name_to_remove), 1) + } else if ($(this).attr('class') == 'toggledOff') { + $(this).removeClass('toggledOff') + self.newSelectedMetacodes.push($(this).attr('id')) + self.newSelectedMetacodeNames.push($(this).attr('data-name')) + } + }, + updateMetacodeSet: function (set, index, custom) { + if (custom && Metamaps.Create.newSelectedMetacodes.length == 0) { + alert('Please select at least one metacode to use!') + return false + } + + var codesToSwitchToIds + var metacodeModels = new Metamaps.Backbone.MetacodeCollection() + Metamaps.Create.selectedMetacodeSetIndex = index + Metamaps.Create.selectedMetacodeSet = 'metacodeset-' + set + + if (!custom) { + codesToSwitchToIds = $('#metacodeSwitchTabs' + set).attr('data-metacodes').split(',') + $('.customMetacodeList li').addClass('toggledOff') + Metamaps.Create.selectedMetacodes = [] + Metamaps.Create.selectedMetacodeNames = [] + Metamaps.Create.newSelectedMetacodes = [] + Metamaps.Create.newSelectedMetacodeNames = [] + } + else if (custom) { + // uses .slice to avoid setting the two arrays to the same actual array + Metamaps.Create.selectedMetacodes = Metamaps.Create.newSelectedMetacodes.slice(0) + Metamaps.Create.selectedMetacodeNames = Metamaps.Create.newSelectedMetacodeNames.slice(0) + codesToSwitchToIds = Metamaps.Create.selectedMetacodes.slice(0) + } + + // sort by name + for (var i = 0; i < codesToSwitchToIds.length; i++) { + metacodeModels.add(Metamaps.Metacodes.get(codesToSwitchToIds[i])) + } + metacodeModels.sort() + + $('#metacodeImg, #metacodeImgTitle').empty() + $('#metacodeImg').removeData('cloudcarousel') + var newMetacodes = '' + metacodeModels.each(function (metacode) { + newMetacodes += '' + metacode.get('name') + '' + }) + + $('#metacodeImg').empty().append(newMetacodes).CloudCarousel({ + titleBox: $('#metacodeImgTitle'), + yRadius: 40, + xRadius: 190, + xPos: 170, + yPos: 40, + speed: 0.3, + mouseWheel: true, + bringToFront: true + }) + + Metamaps.GlobalUI.closeLightbox() + $('#topic_name').focus() + + var mdata = { + 'metacodes': { + 'value': custom ? Metamaps.Create.selectedMetacodes.toString() : Metamaps.Create.selectedMetacodeSet + } + } + $.ajax({ + type: 'POST', + dataType: 'json', + url: '/user/updatemetacodes', + data: mdata, + success: function (data) { + console.log('selected metacodes saved') + }, + error: function () { + console.log('failed to save selected metacodes') + } + }) + }, + + cancelMetacodeSetSwitch: function () { + var self = Metamaps.Create + self.isSwitchingSet = false + + if (self.selectedMetacodeSet != 'metacodeset-custom') { + $('.customMetacodeList li').addClass('toggledOff') + self.selectedMetacodes = [] + self.selectedMetacodeNames = [] + self.newSelectedMetacodes = [] + self.newSelectedMetacodeNames = [] + } else { // custom set is selected + // reset it to the current actual selection + $('.customMetacodeList li').addClass('toggledOff') + for (var i = 0; i < self.selectedMetacodes.length; i++) { + $('#' + self.selectedMetacodes[i]).removeClass('toggledOff') + } + // uses .slice to avoid setting the two arrays to the same actual array + self.newSelectedMetacodeNames = self.selectedMetacodeNames.slice(0) + self.newSelectedMetacodes = self.selectedMetacodes.slice(0) + } + $('#metacodeSwitchTabs').tabs('option', 'active', self.selectedMetacodeSetIndex) + $('#topic_name').focus() + }, + newTopic: { + init: function () { + $('#topic_name').keyup(function () { + Metamaps.Create.newTopic.name = $(this).val() + }) + + var topicBloodhound = new Bloodhound({ + datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), + queryTokenizer: Bloodhound.tokenizers.whitespace, + remote: { + url: '/topics/autocomplete_topic?term=%QUERY', + wildcard: '%QUERY', + }, + }) + + // initialize the autocomplete results for the metacode spinner + $('#topic_name').typeahead( + { + highlight: true, + minLength: 2, + }, + [{ + name: 'topic_autocomplete', + limit: 8, + display: function (s) { return s.label; }, + templates: { + suggestion: function (s) { + return Hogan.compile($('#topicAutocompleteTemplate').html()).render(s) + }, + }, + source: topicBloodhound, + }] + ) + + // tell the autocomplete to submit the form with the topic you clicked on if you pick from the autocomplete + $('#topic_name').bind('typeahead:select', function (event, datum, dataset) { + Metamaps.Topic.getTopicFromAutocomplete(datum.id) + }) + + // initialize metacode spinner and then hide it + $('#metacodeImg').CloudCarousel({ + titleBox: $('#metacodeImgTitle'), + yRadius: 40, + xRadius: 190, + xPos: 170, + yPos: 40, + speed: 0.3, + mouseWheel: true, + bringToFront: true + }) + $('.new_topic').hide() + }, + name: null, + newId: 1, + beingCreated: false, + metacode: null, + x: null, + y: null, + addSynapse: false, + open: function () { + $('#new_topic').fadeIn('fast', function () { + $('#topic_name').focus() + }) + Metamaps.Create.newTopic.beingCreated = true + Metamaps.Create.newTopic.name = '' + }, + hide: function () { + $('#new_topic').fadeOut('fast') + $('#topic_name').typeahead('val', '') + Metamaps.Create.newTopic.beingCreated = false + } + }, + newSynapse: { + init: function () { + var self = Metamaps.Create.newSynapse + + var synapseBloodhound = new Bloodhound({ + datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), + queryTokenizer: Bloodhound.tokenizers.whitespace, + remote: { + url: '/search/synapses?term=%QUERY', + wildcard: '%QUERY', + }, + }) + var existingSynapseBloodhound = new Bloodhound({ + datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), + queryTokenizer: Bloodhound.tokenizers.whitespace, + remote: { + url: '/search/synapses?topic1id=%TOPIC1&topic2id=%TOPIC2', + prepare: function (query, settings) { + var self = Metamaps.Create.newSynapse + if (Metamaps.Selected.Nodes.length < 2) { + settings.url = settings.url.replace('%TOPIC1', self.topic1id).replace('%TOPIC2', self.topic2id) + return settings + } else { + return null + } + }, + }, + }) + + // initialize the autocomplete results for synapse creation + $('#synapse_desc').typeahead( + { + highlight: true, + minLength: 2, + }, + [{ + name: 'synapse_autocomplete', + display: function (s) { return s.label; }, + templates: { + suggestion: function (s) { + return Hogan.compile("
    {{label}}
    ").render(s) + }, + }, + source: synapseBloodhound, + }, + { + name: 'existing_synapses', + limit: 50, + display: function (s) { return s.label; }, + templates: { + suggestion: function (s) { + return Hogan.compile($('#synapseAutocompleteTemplate').html()).render(s) + }, + header: '

    Existing synapses

    ' + }, + source: existingSynapseBloodhound, + }] + ) + + $('#synapse_desc').keyup(function (e) { + var ESC = 27, BACKSPACE = 8, DELETE = 46 + if (e.keyCode === BACKSPACE && $(this).val() === '' || + e.keyCode === DELETE && $(this).val() === '' || + e.keyCode === ESC) { + Metamaps.Create.newSynapse.hide() + } // if + Metamaps.Create.newSynapse.description = $(this).val() + }) + + $('#synapse_desc').focusout(function () { + if (Metamaps.Create.newSynapse.beingCreated) { + Metamaps.Synapse.createSynapseLocally() + } + }) + + $('#synapse_desc').bind('typeahead:select', function (event, datum, dataset) { + if (datum.id) { // if they clicked on an existing synapse get it + Metamaps.Synapse.getSynapseFromAutocomplete(datum.id) + } else { + Metamaps.Create.newSynapse.description = datum.value + Metamaps.Synapse.createSynapseLocally() + } + }) + }, + beingCreated: false, + description: null, + topic1id: null, + topic2id: null, + newSynapseId: null, + open: function () { + $('#new_synapse').fadeIn(100, function () { + $('#synapse_desc').focus() + }) + Metamaps.Create.newSynapse.beingCreated = true + }, + hide: function () { + $('#new_synapse').fadeOut('fast') + $('#synapse_desc').typeahead('val', '') + Metamaps.Create.newSynapse.beingCreated = false + Metamaps.Create.newTopic.addSynapse = false + Metamaps.Create.newSynapse.topic1id = 0 + Metamaps.Create.newSynapse.topic2id = 0 + Metamaps.Mouse.synapseStartCoordinates = [] + Metamaps.Visualize.mGraph.plot() + }, + } +}; // end Metamaps.Create diff --git a/app/assets/javascripts/src/Metamaps.GlobalUI.js.erb b/app/assets/javascripts/src/Metamaps.GlobalUI.js similarity index 98% rename from app/assets/javascripts/src/Metamaps.GlobalUI.js.erb rename to app/assets/javascripts/src/Metamaps.GlobalUI.js index 690bba1f..d8b91a31 100644 --- a/app/assets/javascripts/src/Metamaps.GlobalUI.js.erb +++ b/app/assets/javascripts/src/Metamaps.GlobalUI.js @@ -93,6 +93,7 @@ Metamaps.GlobalUI = { if (Metamaps.Active.Mapper) Metamaps.Active.Mapper = new Metamaps.Backbone.Mapper(Metamaps.Active.Mapper); var myCollection = Metamaps.Maps.Mine ? Metamaps.Maps.Mine : []; + var sharedCollection = Metamaps.Maps.Shared ? Metamaps.Maps.Shared : []; var mapperCollection = []; var mapperOptionsObj = {id: 'mapper', sortBy: 'updated_at' }; if (Metamaps.Maps.Mapper) { @@ -102,6 +103,7 @@ Metamaps.GlobalUI = { var featuredCollection = Metamaps.Maps.Featured ? Metamaps.Maps.Featured : []; var activeCollection = Metamaps.Maps.Active ? Metamaps.Maps.Active : []; Metamaps.Maps.Mine = new Metamaps.Backbone.MapsCollection(myCollection, {id: 'mine', sortBy: 'updated_at' }); + Metamaps.Maps.Shared = new Metamaps.Backbone.MapsCollection(sharedCollection, {id: 'shared', sortBy: 'updated_at' }); // 'Mapper' refers to another mapper Metamaps.Maps.Mapper = new Metamaps.Backbone.MapsCollection(mapperCollection, mapperOptionsObj); Metamaps.Maps.Featured = new Metamaps.Backbone.MapsCollection(featuredCollection, {id: 'featured', sortBy: 'updated_at' }); @@ -501,7 +503,7 @@ Metamaps.GlobalUI.Search = { return Hogan.compile(topicheader + $('#topicSearchTemplate').html()).render({ value: "No results", label: "No results", - typeImageURL: "<%= asset_path('icons/wildcard.png') %>", + typeImageURL: Metamaps.Erb['icons/wildcard.png'], rtype: "noresult" }); }, @@ -569,7 +571,7 @@ Metamaps.GlobalUI.Search = { value: "No results", label: "No results", rtype: "noresult", - profile: "<%= asset_path('user.png') %>", + profile: Metamaps.Erb['user.png'] }); }, header: mapperheader, diff --git a/app/assets/javascripts/src/Metamaps.JIT.js.erb b/app/assets/javascripts/src/Metamaps.JIT.js similarity index 99% rename from app/assets/javascripts/src/Metamaps.JIT.js.erb rename to app/assets/javascripts/src/Metamaps.JIT.js index 4b72b714..01b321f7 100644 --- a/app/assets/javascripts/src/Metamaps.JIT.js.erb +++ b/app/assets/javascripts/src/Metamaps.JIT.js @@ -29,10 +29,10 @@ Metamaps.JIT = { $('.takeScreenshot').click(Metamaps.Map.exportImage) self.topicDescImage = new Image() - self.topicDescImage.src = '<%= asset_path('topic_description_signifier.png') %>' + self.topicDescImage.src = Metamaps.Erb['topic_description_signifier.png'] self.topicLinkImage = new Image() - self.topicLinkImage.src = '<%= asset_path('topic_link_signifier.png') %>' + self.topicLinkImage.src = Metamaps.Erb['topic_link_signifier.png'] }, /** * convert our topic JSON into something JIT can use diff --git a/app/assets/javascripts/src/Metamaps.Map.js.erb b/app/assets/javascripts/src/Metamaps.Map.js similarity index 78% rename from app/assets/javascripts/src/Metamaps.Map.js.erb rename to app/assets/javascripts/src/Metamaps.Map.js index 0207d081..34374614 100644 --- a/app/assets/javascripts/src/Metamaps.Map.js.erb +++ b/app/assets/javascripts/src/Metamaps.Map.js @@ -4,17 +4,18 @@ * Metamaps.Map.js.erb * * Dependencies: - * Metamaps.Create - * Metamaps.Filter - * Metamaps.JIT - * Metamaps.Loading - * Metamaps.Maps - * Metamaps.Realtime - * Metamaps.Router - * Metamaps.Selected - * Metamaps.SynapseCard - * Metamaps.TopicCard - * Metamaps.Visualize + * - Metamaps.Create + * - Metamaps.Erb + * - Metamaps.Filter + * - Metamaps.JIT + * - Metamaps.Loading + * - Metamaps.Maps + * - Metamaps.Realtime + * - Metamaps.Router + * - Metamaps.Selected + * - Metamaps.SynapseCard + * - Metamaps.TopicCard + * - Metamaps.Visualize * - Metamaps.Active * - Metamaps.Backbone * - Metamaps.GlobalUI @@ -64,6 +65,7 @@ Metamaps.Map = { var start = function (data) { Metamaps.Active.Map = new bb.Map(data.map) Metamaps.Mappers = new bb.MapperCollection(data.mappers) + Metamaps.Collaborators = new bb.MapperCollection(data.collaborators) Metamaps.Topics = new bb.TopicCollection(data.topics) Metamaps.Synapses = new bb.SynapseCollection(data.synapses) Metamaps.Mappings = new bb.MappingCollection(data.mappings) @@ -180,13 +182,13 @@ Metamaps.Map = { Metamaps.Router.home() Metamaps.GlobalUI.notifyUser('Sorry! That map has been changed to Private.') }, - commonsToPublic: function () { + cantEditNow: function () { Metamaps.Realtime.turnOff(true); // true is for 'silence' Metamaps.GlobalUI.notifyUser('Map was changed to Public. Editing is disabled.') Metamaps.Active.Map.trigger('changeByOther') }, - publicToCommons: function () { - var confirmString = 'This map permission has been changed to Commons! ' + canEditNow: function () { + var confirmString = "You've been granted permission to edit this map. " confirmString += 'Do you want to reload and enable realtime collaboration?' var c = confirm(confirmString) if (c) { @@ -419,7 +421,7 @@ Metamaps.Map.InfoBox = { isOpen: false, changing: false, selectingPermission: false, - changePermissionText: "
    As the creator, you can change the permission of this map, but the permissions of the topics and synapses on it must be changed independently.
    ", + changePermissionText: "
    As the creator, you can change the permission of this map, and the permission of all the topics and synapses you have authority to change will change as well.
    ", nameHTML: '{{name}}', descHTML: '{{desc}}', init: function () { @@ -433,6 +435,8 @@ Metamaps.Map.InfoBox = { self.attachEventListeners() + + self.generateBoxHTML = Hogan.compile($('#mapInfoBoxTemplate').html()) }, toggleBox: function (event) { @@ -473,19 +477,23 @@ Metamaps.Map.InfoBox = { var map = Metamaps.Active.Map - var obj = map.pick('permission', 'contributor_count', 'topic_count', 'synapse_count') + var obj = map.pick('permission', 'topic_count', 'synapse_count') var isCreator = map.authorizePermissionChange(Metamaps.Active.Mapper) var canEdit = map.authorizeToEdit(Metamaps.Active.Mapper) + var relevantPeople = map.get('permission') === 'commons' ? Metamaps.Mappers : Metamaps.Collaborators var shareable = map.get('permission') !== 'private' obj['name'] = canEdit ? Hogan.compile(self.nameHTML).render({id: map.id, name: map.get('name')}) : map.get('name') obj['desc'] = canEdit ? Hogan.compile(self.descHTML).render({id: map.id, desc: map.get('desc')}) : map.get('desc') obj['map_creator_tip'] = isCreator ? self.changePermissionText : '' - obj['contributors_class'] = Metamaps.Mappers.length > 1 ? 'multiple' : '' - obj['contributors_class'] += Metamaps.Mappers.length === 2 ? ' mTwo' : '' - obj['contributor_image'] = Metamaps.Mappers.length > 0 ? Metamaps.Mappers.models[0].get('image') : "<%= asset_path('user.png') %>" + + obj['contributor_count'] = relevantPeople.length + obj['contributors_class'] = relevantPeople.length > 1 ? 'multiple' : '' + obj['contributors_class'] += relevantPeople.length === 2 ? ' mTwo' : '' + obj['contributor_image'] = relevantPeople.length > 0 ? relevantPeople.models[0].get('image') : Metamaps.Erb['user.png'] obj['contributor_list'] = self.createContributorList() + obj['user_name'] = isCreator ? 'You' : map.get('user_name') obj['created_at'] = map.get('created_at_clean') obj['updated_at'] = map.get('updated_at_clean') @@ -554,6 +562,87 @@ Metamaps.Map.InfoBox = { $('.mapInfoBox').unbind('.hideTip').bind('click.hideTip', function () { $('.mapContributors .tip').hide() }) + + self.addTypeahead() + }, + addTypeahead: function () { + var self = Metamaps.Map.InfoBox + + if (!Metamaps.Active.Map) return + + // for autocomplete + var collaborators = { + name: 'collaborators', + limit: 9999, + display: function(s) { return s.label; }, + templates: { + notFound: function(s) { + return Hogan.compile($('#collaboratorSearchTemplate').html()).render({ + value: "No results", + label: "No results", + rtype: "noresult", + profile: Metamaps.Erb['user.png'], + }); + }, + suggestion: function(s) { + return Hogan.compile($('#collaboratorSearchTemplate').html()).render(s); + }, + }, + source: new Bloodhound({ + datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), + queryTokenizer: Bloodhound.tokenizers.whitespace, + remote: { + url: '/search/mappers?term=%QUERY', + wildcard: '%QUERY', + }, + }) + } + + // for adding map collaborators, who will have edit rights + if (Metamaps.Active.Mapper && Metamaps.Active.Mapper.id === Metamaps.Active.Map.get('user_id')) { + $('.collaboratorSearchField').typeahead( + { + highlight: false, + }, + [collaborators] + ) + $('.collaboratorSearchField').bind('typeahead:select', self.handleResultClick) + $('.mapContributors .removeCollaborator').click(function () { + self.removeCollaborator(parseInt($(this).data('id'))) + }) + } + }, + removeCollaborator: function (collaboratorId) { + var self = Metamaps.Map.InfoBox + Metamaps.Collaborators.remove(Metamaps.Collaborators.get(collaboratorId)) + var mapperIds = Metamaps.Collaborators.models.map(function (mapper) { return mapper.id }) + $.post('/maps/' + Metamaps.Active.Map.id + '/access', { access: mapperIds }) + self.updateNumbers() + }, + addCollaborator: function (newCollaboratorId) { + var self = Metamaps.Map.InfoBox + + if (Metamaps.Collaborators.get(newCollaboratorId)) { + Metamaps.GlobalUI.notifyUser('That user already has access') + return + } + + function callback(mapper) { + Metamaps.Collaborators.add(mapper) + var mapperIds = Metamaps.Collaborators.models.map(function (mapper) { return mapper.id }) + $.post('/maps/' + Metamaps.Active.Map.id + '/access', { access: mapperIds }) + var name = Metamaps.Collaborators.get(newCollaboratorId).get('name') + Metamaps.GlobalUI.notifyUser(name + ' will be notified by email') + self.updateNumbers() + } + + $.getJSON('/users/' + newCollaboratorId + '.json', callback) + }, + handleResultClick: function (event, item) { + var self = Metamaps.Map.InfoBox + + self.addCollaborator(item.id) + $('.collaboratorSearchField').typeahead('val', '') }, updateNameDescPerm: function (name, desc, perm) { $('.mapInfoName .best_in_place_name').html(name) @@ -562,33 +651,48 @@ Metamaps.Map.InfoBox = { }, createContributorList: function () { var self = Metamaps.Map.InfoBox - + var relevantPeople = Metamaps.Active.Map.get('permission') === 'commons' ? Metamaps.Mappers : Metamaps.Collaborators + var activeMapperIsCreator = Metamaps.Active.Mapper && Metamaps.Active.Mapper.id === Metamaps.Active.Map.get('user_id') var string = '' string += '' + + if (activeMapperIsCreator) { + string += '
    ' + } return string }, updateNumbers: function () { var self = Metamaps.Map.InfoBox var mapper = Metamaps.Active.Mapper + var relevantPeople = Metamaps.Active.Map.get('permission') === 'commons' ? Metamaps.Mappers : Metamaps.Collaborators var contributors_class = '' - if (Metamaps.Mappers.length === 2) contributors_class = 'multiple mTwo' - else if (Metamaps.Mappers.length > 2) contributors_class = 'multiple' + if (relevantPeople.length === 2) contributors_class = 'multiple mTwo' + else if (relevantPeople.length > 2) contributors_class = 'multiple' - var contributors_image = "<%= asset_path('user.png') %>" - if (Metamaps.Mappers.length > 0) { + var contributors_image = Metamaps.Erb['user.png'] + if (relevantPeople.length > 0) { // get the first contributor and use their image - contributors_image = Metamaps.Mappers.models[0].get('image') + contributors_image = relevantPeople.models[0].get('image') } $('.mapContributors img').attr('src', contributors_image).removeClass('multiple mTwo').addClass(contributors_class) - $('.mapContributors span').text(Metamaps.Mappers.length) + $('.mapContributors span').text(relevantPeople.length) $('.mapContributors .tip').html(self.createContributorList()) + self.addTypeahead() + $('.mapContributors .tip').unbind().click(function (event) { + event.stopPropagation() + }) $('.mapTopics').text(Metamaps.Topics.length) $('.mapSynapses').text(Metamaps.Synapses.length) @@ -623,19 +727,10 @@ Metamaps.Map.InfoBox = { self.selectingPermission = false var permission = $(this).attr('class') - var permBefore = Metamaps.Active.Map.get('permission') Metamaps.Active.Map.save({ permission: permission }) Metamaps.Active.Map.updateMapWrapper() - if (permBefore !== 'commons' && permission === 'commons') { - Metamaps.Realtime.setupSocket() - Metamaps.Realtime.turnOn() - } - else if (permBefore === 'commons' && permission === 'public') { - Metamaps.Realtime.turnOff(true); // true is to 'silence' - // the notification that would otherwise be sent - } shareable = permission === 'private' ? '' : 'shareable' $('.mapPermission').removeClass('commons public private minimize').addClass(permission) $('.mapPermission .permissionSelect').remove() @@ -656,6 +751,7 @@ Metamaps.Map.InfoBox = { Metamaps.Maps.Active.remove(map) Metamaps.Maps.Featured.remove(map) Metamaps.Maps.Mine.remove(map) + Metamaps.Maps.Shared.remove(map) map.destroy() Metamaps.Router.home() Metamaps.GlobalUI.notifyUser('Map eliminated!') diff --git a/app/assets/javascripts/src/Metamaps.Realtime.js b/app/assets/javascripts/src/Metamaps.Realtime.js new file mode 100644 index 00000000..b43a9b96 --- /dev/null +++ b/app/assets/javascripts/src/Metamaps.Realtime.js @@ -0,0 +1,1199 @@ +/* global Metamaps, $ */ + +/* + * Metamaps.Realtime.js + * + * Dependencies: + * - Metamaps.Active + * - Metamaps.Backbone + * - Metamaps.Backbone + * - Metamaps.Control + * - Metamaps.Erb + * - Metamaps.GlobalUI + * - Metamaps.JIT + * - Metamaps.Map + * - Metamaps.Mapper + * - Metamaps.Mappers + * - Metamaps.Mappings + * - Metamaps.Messages + * - Metamaps.Synapses + * - Metamaps.Topic + * - Metamaps.Topics + * - Metamaps.Util + * - Metamaps.Views + * - Metamaps.Visualize + */ + +Metamaps.Realtime = { + videoId: 'video-wrapper', + socket: null, + webrtc: null, + readyToCall: false, + mappersOnMap: {}, + disconnected: false, + chatOpen: false, + status: true, // stores whether realtime is True/On or False/Off, + broadcastingStatus: false, + inConversation: false, + localVideo: null, + init: function () { + var self = Metamaps.Realtime + + self.addJuntoListeners() + + self.socket = new SocketIoConnection({ url: Metamaps.Erb['REALTIME_SERVER']}) + self.socket.on('connect', function () { + console.log('connected') + if (!self.disconnected) { + self.startActiveMap() + } else self.disconnected = false + }) + self.socket.on('disconnect', function () { + self.disconnected = true + }) + + if (Metamaps.Active.Mapper) { + self.webrtc = new SimpleWebRTC({ + connection: self.socket, + localVideoEl: self.videoId, + remoteVideosEl: '', + detectSpeakingEvents: true, + autoAdjustMic: false, // true, + autoRequestMedia: false, + localVideo: { + autoplay: true, + mirror: true, + muted: true + }, + media: { + video: true, + audio: true + }, + nick: Metamaps.Active.Mapper.id + }) + + var $video = $('').attr('id', self.videoId) + self.localVideo = { + $video: $video, + view: new Metamaps.Views.videoView($video[0], $('body'), 'me', true, { + DOUBLE_CLICK_TOLERANCE: 200, + avatar: Metamaps.Active.Mapper ? Metamaps.Active.Mapper.get('image') : '' + }) + } + + self.room = new Metamaps.Views.room({ + webrtc: self.webrtc, + socket: self.socket, + username: Metamaps.Active.Mapper ? Metamaps.Active.Mapper.get('name') : '', + image: Metamaps.Active.Mapper ? Metamaps.Active.Mapper.get('image') : '', + room: 'global', + $video: self.localVideo.$video, + myVideoView: self.localVideo.view, + config: { DOUBLE_CLICK_TOLERANCE: 200 } + }) + self.room.videoAdded(self.handleVideoAdded) + + self.room.chat.$container.hide() + $('body').prepend(self.room.chat.$container) + } // if Metamaps.Active.Mapper + }, + addJuntoListeners: function () { + var self = Metamaps.Realtime + + $(document).on(Metamaps.Views.chatView.events.openTray, function () { + $('.main').addClass('compressed') + self.chatOpen = true + self.positionPeerIcons() + }) + $(document).on(Metamaps.Views.chatView.events.closeTray, function () { + $('.main').removeClass('compressed') + self.chatOpen = false + self.positionPeerIcons() + }) + $(document).on(Metamaps.Views.chatView.events.videosOn, function () { + $('#wrapper').removeClass('hideVideos') + }) + $(document).on(Metamaps.Views.chatView.events.videosOff, function () { + $('#wrapper').addClass('hideVideos') + }) + $(document).on(Metamaps.Views.chatView.events.cursorsOn, function () { + $('#wrapper').removeClass('hideCursors') + }) + $(document).on(Metamaps.Views.chatView.events.cursorsOff, function () { + $('#wrapper').addClass('hideCursors') + }) + }, + handleVideoAdded: function (v, id) { + var self = Metamaps.Realtime + self.positionVideos() + v.setParent($('#wrapper')) + v.$container.find('.video-cutoff').css({ + border: '4px solid ' + self.mappersOnMap[id].color + }) + $('#wrapper').append(v.$container) + }, + positionVideos: function () { + var self = Metamaps.Realtime + var videoIds = Object.keys(self.room.videos) + var numOfVideos = videoIds.length + var numOfVideosToPosition = _.filter(videoIds, function (id) { + return !self.room.videos[id].manuallyPositioned + }).length + + var screenHeight = $(document).height() + var screenWidth = $(document).width() + var topExtraPadding = 20 + var topPadding = 30 + var leftPadding = 30 + var videoHeight = 150 + var videoWidth = 180 + var column = 0 + var row = 0 + var yFormula = function () { + var y = topExtraPadding + (topPadding + videoHeight) * row + topPadding + if (y + videoHeight > screenHeight) { + row = 0 + column += 1 + y = yFormula() + } + row++ + return y + } + var xFormula = function () { + var x = (leftPadding + videoWidth) * column + leftPadding + return x + } + + // do self first + var myVideo = Metamaps.Realtime.localVideo.view + if (!myVideo.manuallyPositioned) { + myVideo.$container.css({ + top: yFormula() + 'px', + left: xFormula() + 'px' + }) + } + videoIds.forEach(function (id) { + var video = self.room.videos[id] + if (!video.manuallyPositioned) { + video.$container.css({ + top: yFormula() + 'px', + left: xFormula() + 'px' + }) + } + }) + }, + startActiveMap: function () { + var self = Metamaps.Realtime + + if (Metamaps.Active.Map && Metamaps.Active.Mapper) { + if (Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper)) { + self.turnOn() + self.setupSocket() + } else { + self.attachMapListener() + } + self.room.addMessages(new Metamaps.Backbone.MessageCollection(Metamaps.Messages), true) + } + }, + endActiveMap: function () { + var self = Metamaps.Realtime + + $(document).off('mousemove') + self.socket.removeAllListeners() + if (self.inConversation) self.leaveCall() + self.socket.emit('endMapperNotify') + $('.collabCompass').remove() + self.status = false + self.room.leave() + self.room.chat.$container.hide() + self.room.chat.close() + }, + turnOn: function (notify) { + var self = Metamaps.Realtime + + if (notify) self.sendRealtimeOn() + self.status = true + $('.collabCompass').show() + self.room.chat.$container.show() + self.room.room = 'map-' + Metamaps.Active.Map.id + self.checkForACallToJoin() + + self.activeMapper = { + id: Metamaps.Active.Mapper.id, + name: Metamaps.Active.Mapper.get('name'), + username: Metamaps.Active.Mapper.get('name'), + image: Metamaps.Active.Mapper.get('image'), + color: Metamaps.Util.getPastelColor(), + self: true + } + self.localVideo.view.$container.find('.video-cutoff').css({ + border: '4px solid ' + self.activeMapper.color + }) + self.room.chat.addParticipant(self.activeMapper) + }, + checkForACallToJoin: function () { + var self = Metamaps.Realtime + self.socket.emit('checkForCall', { room: self.room.room, mapid: Metamaps.Active.Map.id }) + }, + promptToJoin: function () { + var self = Metamaps.Realtime + + var notifyText = "There's a conversation happening, want to join?" + notifyText += ' ' + notifyText += ' ' + Metamaps.GlobalUI.notifyUser(notifyText, true) + self.room.conversationInProgress() + }, + conversationHasBegun: function () { + var self = Metamaps.Realtime + + if (self.inConversation) return + var notifyText = "There's a conversation starting, want to join?" + notifyText += ' ' + notifyText += ' ' + Metamaps.GlobalUI.notifyUser(notifyText, true) + self.room.conversationInProgress() + }, + countOthersInConversation: function () { + var self = Metamaps.Realtime + var count = 0 + + for (var key in self.mappersOnMap) { + if (self.mappersOnMap[key].inConversation) count++ + } + return count + }, + mapperJoinedCall: function (id) { + var self = Metamaps.Realtime + var mapper = self.mappersOnMap[id] + + if (mapper) { + if (self.inConversation) { + var username = mapper.name + var notifyText = username + ' joined the call' + Metamaps.GlobalUI.notifyUser(notifyText) + } + + mapper.inConversation = true + self.room.chat.mapperJoinedCall(id) + } + }, + mapperLeftCall: function (id) { + var self = Metamaps.Realtime + var mapper = self.mappersOnMap[id] + + if (mapper) { + if (self.inConversation) { + var username = mapper.name + var notifyText = username + ' left the call' + Metamaps.GlobalUI.notifyUser(notifyText) + } + + mapper.inConversation = false + self.room.chat.mapperLeftCall(id) + + if ((self.inConversation && self.countOthersInConversation() === 0) || + (!self.inConversation && self.countOthersInConversation() === 1)) { + self.callEnded() + } + } + }, + callEnded: function () { + var self = Metamaps.Realtime + + self.room.conversationEnding() + self.room.leaveVideoOnly() + self.inConversation = false + self.localVideo.view.$container.hide().css({ + top: '72px', + left: '30px' + }) + self.localVideo.view.audioOn() + self.localVideo.view.videoOn() + self.webrtc.webrtc.localStreams.forEach(function (stream) { + stream.getTracks().forEach(function (track) { + track.stop() + }) + }) + self.webrtc.webrtc.localStreams = [] + }, + invitedToCall: function (inviter) { + var self = Metamaps.Realtime + + self.room.chat.sound.stop('sessioninvite') + self.room.chat.sound.play('sessioninvite') + + var username = self.mappersOnMap[inviter].name + var notifyText = '' + notifyText += username + ' is inviting you to a conversation. Join live?' + notifyText += ' ' + notifyText += ' ' + Metamaps.GlobalUI.notifyUser(notifyText, true) + }, + invitedToJoin: function (inviter) { + var self = Metamaps.Realtime + + self.room.chat.sound.stop('sessioninvite') + self.room.chat.sound.play('sessioninvite') + + var username = self.mappersOnMap[inviter].name + var notifyText = username + ' is inviting you to the conversation. Join?' + notifyText += ' ' + notifyText += ' ' + Metamaps.GlobalUI.notifyUser(notifyText, true) + }, + acceptCall: function (userid) { + var self = Metamaps.Realtime + self.room.chat.sound.stop('sessioninvite') + self.socket.emit('callAccepted', { + mapid: Metamaps.Active.Map.id, + invited: Metamaps.Active.Mapper.id, + inviter: userid + }) + $.post('/maps/' + Metamaps.Active.Map.id + '/events/conversation') + self.joinCall() + Metamaps.GlobalUI.clearNotify() + }, + denyCall: function (userid) { + var self = Metamaps.Realtime + self.room.chat.sound.stop('sessioninvite') + self.socket.emit('callDenied', { + mapid: Metamaps.Active.Map.id, + invited: Metamaps.Active.Mapper.id, + inviter: userid + }) + Metamaps.GlobalUI.clearNotify() + }, + denyInvite: function (userid) { + var self = Metamaps.Realtime + self.room.chat.sound.stop('sessioninvite') + self.socket.emit('inviteDenied', { + mapid: Metamaps.Active.Map.id, + invited: Metamaps.Active.Mapper.id, + inviter: userid + }) + Metamaps.GlobalUI.clearNotify() + }, + inviteACall: function (userid) { + var self = Metamaps.Realtime + self.socket.emit('inviteACall', { + mapid: Metamaps.Active.Map.id, + inviter: Metamaps.Active.Mapper.id, + invited: userid + }) + self.room.chat.invitationPending(userid) + Metamaps.GlobalUI.clearNotify() + }, + inviteToJoin: function (userid) { + var self = Metamaps.Realtime + self.socket.emit('inviteToJoin', { + mapid: Metamaps.Active.Map.id, + inviter: Metamaps.Active.Mapper.id, + invited: userid + }) + self.room.chat.invitationPending(userid) + }, + callAccepted: function (userid) { + var self = Metamaps.Realtime + + var username = self.mappersOnMap[userid].name + Metamaps.GlobalUI.notifyUser('Conversation starting...') + self.joinCall() + self.room.chat.invitationAnswered(userid) + }, + callDenied: function (userid) { + var self = Metamaps.Realtime + + var username = self.mappersOnMap[userid].name + Metamaps.GlobalUI.notifyUser(username + " didn't accept your invitation") + self.room.chat.invitationAnswered(userid) + }, + inviteDenied: function (userid) { + var self = Metamaps.Realtime + + var username = self.mappersOnMap[userid].name + Metamaps.GlobalUI.notifyUser(username + " didn't accept your invitation") + self.room.chat.invitationAnswered(userid) + }, + joinCall: function () { + var self = Metamaps.Realtime + + self.webrtc.off('readyToCall') + self.webrtc.once('readyToCall', function () { + self.videoInitialized = true + self.readyToCall = true + self.localVideo.view.manuallyPositioned = false + self.positionVideos() + self.localVideo.view.$container.show() + if (self.localVideo && self.status) { + $('#wrapper').append(self.localVideo.view.$container) + } + self.room.join() + }) + self.inConversation = true + self.socket.emit('mapperJoinedCall', { + mapid: Metamaps.Active.Map.id, + id: Metamaps.Active.Mapper.id + }) + self.webrtc.startLocalVideo() + Metamaps.GlobalUI.clearNotify() + self.room.chat.mapperJoinedCall(Metamaps.Active.Mapper.id) + }, + leaveCall: function () { + var self = Metamaps.Realtime + + self.socket.emit('mapperLeftCall', { + mapid: Metamaps.Active.Map.id, + id: Metamaps.Active.Mapper.id + }) + + self.room.chat.mapperLeftCall(Metamaps.Active.Mapper.id) + self.room.leaveVideoOnly() + self.inConversation = false + self.localVideo.view.$container.hide() + + // if there's only two people in the room, and we're leaving + // we should shut down the call locally + if (self.countOthersInConversation() === 1) { + self.callEnded() + } + }, + turnOff: function (silent) { + var self = Metamaps.Realtime + + if (self.status) { + if (!silent) self.sendRealtimeOff() + // $(".rtMapperSelf").removeClass('littleRtOn').addClass('littleRtOff') + // $('.rtOn').removeClass('active') + // $('.rtOff').addClass('active') + self.status = false + // $(".sidebarCollaborateIcon").removeClass("blue") + $('.collabCompass').hide() + $('#' + self.videoId).remove() + } + }, + setupSocket: function () { + var self = Metamaps.Realtime + var socket = Metamaps.Realtime.socket + var myId = Metamaps.Active.Mapper.id + + socket.emit('newMapperNotify', { + userid: myId, + username: Metamaps.Active.Mapper.get('name'), + userimage: Metamaps.Active.Mapper.get('image'), + mapid: Metamaps.Active.Map.id + }) + + socket.on(myId + '-' + Metamaps.Active.Map.id + '-invitedToCall', self.invitedToCall) // new call + socket.on(myId + '-' + Metamaps.Active.Map.id + '-invitedToJoin', self.invitedToJoin) // call already in progress + socket.on(myId + '-' + Metamaps.Active.Map.id + '-callAccepted', self.callAccepted) + socket.on(myId + '-' + Metamaps.Active.Map.id + '-callDenied', self.callDenied) + socket.on(myId + '-' + Metamaps.Active.Map.id + '-inviteDenied', self.inviteDenied) + + // receive word that there's a conversation in progress + socket.on('maps-' + Metamaps.Active.Map.id + '-callInProgress', self.promptToJoin) + socket.on('maps-' + Metamaps.Active.Map.id + '-callStarting', self.conversationHasBegun) + + socket.on('maps-' + Metamaps.Active.Map.id + '-mapperJoinedCall', self.mapperJoinedCall) + socket.on('maps-' + Metamaps.Active.Map.id + '-mapperLeftCall', self.mapperLeftCall) + + // if you're the 'new guy' update your list with who's already online + socket.on(myId + '-' + Metamaps.Active.Map.id + '-UpdateMapperList', self.updateMapperList) + + // receive word that there's a new mapper on the map + socket.on('maps-' + Metamaps.Active.Map.id + '-newmapper', self.newPeerOnMap) + + // receive word that a mapper left the map + socket.on('maps-' + Metamaps.Active.Map.id + '-lostmapper', self.lostPeerOnMap) + + // receive word that there's a mapper turned on realtime + socket.on('maps-' + Metamaps.Active.Map.id + '-newrealtime', self.newCollaborator) + + // receive word that there's a mapper turned on realtime + socket.on('maps-' + Metamaps.Active.Map.id + '-lostrealtime', self.lostCollaborator) + + // + socket.on('maps-' + Metamaps.Active.Map.id + '-topicDrag', self.topicDrag) + + // + socket.on('maps-' + Metamaps.Active.Map.id + '-newTopic', self.newTopic) + + // + socket.on('maps-' + Metamaps.Active.Map.id + '-newMessage', self.newMessage) + + // + socket.on('maps-' + Metamaps.Active.Map.id + '-removeTopic', self.removeTopic) + + // + socket.on('maps-' + Metamaps.Active.Map.id + '-newSynapse', self.newSynapse) + + // + socket.on('maps-' + Metamaps.Active.Map.id + '-removeSynapse', self.removeSynapse) + + // update mapper compass position + socket.on('maps-' + Metamaps.Active.Map.id + '-updatePeerCoords', self.updatePeerCoords) + + // deletions + socket.on('deleteTopicFromServer', self.removeTopic) + socket.on('deleteSynapseFromServer', self.removeSynapse) + + socket.on('topicChangeFromServer', self.topicChange) + socket.on('synapseChangeFromServer', self.synapseChange) + self.attachMapListener() + + // local event listeners that trigger events + var sendCoords = function (event) { + var pixels = { + x: event.pageX, + y: event.pageY + } + var coords = Metamaps.Util.pixelsToCoords(pixels) + self.sendCoords(coords) + } + $(document).mousemove(sendCoords) + + var zoom = function (event, e) { + if (e) { + var pixels = { + x: e.pageX, + y: e.pageY + } + var coords = Metamaps.Util.pixelsToCoords(pixels) + self.sendCoords(coords) + } + self.positionPeerIcons() + } + $(document).on(Metamaps.JIT.events.zoom, zoom) + + $(document).on(Metamaps.JIT.events.pan, self.positionPeerIcons) + + var sendTopicDrag = function (event, positions) { + self.sendTopicDrag(positions) + } + $(document).on(Metamaps.JIT.events.topicDrag, sendTopicDrag) + + var sendNewTopic = function (event, data) { + self.sendNewTopic(data) + } + $(document).on(Metamaps.JIT.events.newTopic, sendNewTopic) + + var sendDeleteTopic = function (event, data) { + self.sendDeleteTopic(data) + } + $(document).on(Metamaps.JIT.events.deleteTopic, sendDeleteTopic) + + var sendRemoveTopic = function (event, data) { + self.sendRemoveTopic(data) + } + $(document).on(Metamaps.JIT.events.removeTopic, sendRemoveTopic) + + var sendNewSynapse = function (event, data) { + self.sendNewSynapse(data) + } + $(document).on(Metamaps.JIT.events.newSynapse, sendNewSynapse) + + var sendDeleteSynapse = function (event, data) { + self.sendDeleteSynapse(data) + } + $(document).on(Metamaps.JIT.events.deleteSynapse, sendDeleteSynapse) + + var sendRemoveSynapse = function (event, data) { + self.sendRemoveSynapse(data) + } + $(document).on(Metamaps.JIT.events.removeSynapse, sendRemoveSynapse) + + var sendNewMessage = function (event, data) { + self.sendNewMessage(data) + } + $(document).on(Metamaps.Views.room.events.newMessage, sendNewMessage) + }, + attachMapListener: function () { + var self = Metamaps.Realtime + var socket = Metamaps.Realtime.socket + + socket.on('mapChangeFromServer', self.mapChange) + }, + sendRealtimeOn: function () { + var self = Metamaps.Realtime + var socket = Metamaps.Realtime.socket + + // send this new mapper back your details, and the awareness that you're online + var update = { + username: Metamaps.Active.Mapper.get('name'), + userid: Metamaps.Active.Mapper.id, + mapid: Metamaps.Active.Map.id + } + socket.emit('notifyStartRealtime', update) + }, + sendRealtimeOff: function () { + var self = Metamaps.Realtime + var socket = Metamaps.Realtime.socket + + // send this new mapper back your details, and the awareness that you're online + var update = { + username: Metamaps.Active.Mapper.get('name'), + userid: Metamaps.Active.Mapper.id, + mapid: Metamaps.Active.Map.id + } + socket.emit('notifyStopRealtime', update) + }, + updateMapperList: function (data) { + var self = Metamaps.Realtime + var socket = Metamaps.Realtime.socket + + // data.userid + // data.username + // data.userimage + // data.userrealtime + + self.mappersOnMap[data.userid] = { + id: data.userid, + name: data.username, + username: data.username, + image: data.userimage, + color: Metamaps.Util.getPastelColor(), + realtime: data.userrealtime, + inConversation: data.userinconversation, + coords: { + x: 0, + y: 0 + } + } + + if (data.userid !== Metamaps.Active.Mapper.id) { + self.room.chat.addParticipant(self.mappersOnMap[data.userid]) + if (data.userinconversation) self.room.chat.mapperJoinedCall(data.userid) + + // create a div for the collaborators compass + self.createCompass(data.username, data.userid, data.userimage, self.mappersOnMap[data.userid].color, !self.status) + } + }, + newPeerOnMap: function (data) { + var self = Metamaps.Realtime + var socket = Metamaps.Realtime.socket + + // data.userid + // data.username + // data.userimage + // data.coords + var firstOtherPerson = Object.keys(self.mappersOnMap).length === 0 + + self.mappersOnMap[data.userid] = { + id: data.userid, + name: data.username, + username: data.username, + image: data.userimage, + color: Metamaps.Util.getPastelColor(), + realtime: true, + coords: { + x: 0, + y: 0 + }, + } + + // create an item for them in the realtime box + if (data.userid !== Metamaps.Active.Mapper.id && self.status) { + self.room.chat.sound.play('joinmap') + self.room.chat.addParticipant(self.mappersOnMap[data.userid]) + + // create a div for the collaborators compass + self.createCompass(data.username, data.userid, data.userimage, self.mappersOnMap[data.userid].color, !self.status) + + var notifyMessage = data.username + ' just joined the map' + if (firstOtherPerson) { + notifyMessage += ' ' + } + Metamaps.GlobalUI.notifyUser(notifyMessage) + + // send this new mapper back your details, and the awareness that you've loaded the map + var update = { + userToNotify: data.userid, + username: Metamaps.Active.Mapper.get('name'), + userimage: Metamaps.Active.Mapper.get('image'), + userid: Metamaps.Active.Mapper.id, + userrealtime: self.status, + userinconversation: self.inConversation, + mapid: Metamaps.Active.Map.id + } + socket.emit('updateNewMapperList', update) + } + }, + createCompass: function (name, id, image, color, hide) { + var str = '

    ' + name + '

    ' + str += '
    ' + $('#compass' + id).remove() + $('
    ', { + id: 'compass' + id, + class: 'collabCompass' + }).html(str).appendTo('#wrapper') + if (hide) { + $('#compass' + id).hide() + } + $('#compass' + id + ' img').css({ + 'border': '2px solid ' + color + }) + $('#compass' + id + ' p').css({ + 'background-color': color + }) + }, + lostPeerOnMap: function (data) { + var self = Metamaps.Realtime + var socket = Metamaps.Realtime.socket + + // data.userid + // data.username + + delete self.mappersOnMap[data.userid] + self.room.chat.sound.play('leavemap') + // $('#mapper' + data.userid).remove() + $('#compass' + data.userid).remove() + self.room.chat.removeParticipant(data.username) + + Metamaps.GlobalUI.notifyUser(data.username + ' just left the map') + + if ((self.inConversation && self.countOthersInConversation() === 0) || + (!self.inConversation && self.countOthersInConversation() === 1)) { + self.callEnded() + } + }, + newCollaborator: function (data) { + var self = Metamaps.Realtime + var socket = Metamaps.Realtime.socket + + // data.userid + // data.username + + self.mappersOnMap[data.userid].realtime = true + + // $('#mapper' + data.userid).removeClass('littleRtOff').addClass('littleRtOn') + $('#compass' + data.userid).show() + + Metamaps.GlobalUI.notifyUser(data.username + ' just turned on realtime') + }, + lostCollaborator: function (data) { + var self = Metamaps.Realtime + var socket = Metamaps.Realtime.socket + + // data.userid + // data.username + + self.mappersOnMap[data.userid].realtime = false + + // $('#mapper' + data.userid).removeClass('littleRtOn').addClass('littleRtOff') + $('#compass' + data.userid).hide() + + Metamaps.GlobalUI.notifyUser(data.username + ' just turned off realtime') + }, + updatePeerCoords: function (data) { + var self = Metamaps.Realtime + var socket = Metamaps.Realtime.socket + + self.mappersOnMap[data.userid].coords = {x: data.usercoords.x,y: data.usercoords.y} + self.positionPeerIcon(data.userid) + }, + positionPeerIcons: function () { + var self = Metamaps.Realtime + var socket = Metamaps.Realtime.socket + + if (self.status) { // if i have realtime turned on + for (var key in self.mappersOnMap) { + var mapper = self.mappersOnMap[key] + if (mapper.realtime) { + self.positionPeerIcon(key) + } + } + } + }, + positionPeerIcon: function (id) { + var self = Metamaps.Realtime + var socket = Metamaps.Realtime.socket + + var boundary = self.chatOpen ? '#wrapper' : document + var mapper = self.mappersOnMap[id] + var xMax = $(boundary).width() + var yMax = $(boundary).height() + var compassDiameter = 56 + var compassArrowSize = 24 + + var origPixels = Metamaps.Util.coordsToPixels(mapper.coords) + var pixels = self.limitPixelsToScreen(origPixels) + $('#compass' + id).css({ + left: pixels.x + 'px', + top: pixels.y + 'px' + }) + /* showing the arrow if the collaborator is off of the viewport screen */ + if (origPixels.x !== pixels.x || origPixels.y !== pixels.y) { + var dy = origPixels.y - pixels.y // opposite + var dx = origPixels.x - pixels.x // adjacent + var ratio = dy / dx + var angle = Math.atan2(dy, dx) + + $('#compassArrow' + id).show().css({ + transform: 'rotate(' + angle + 'rad)', + '-webkit-transform': 'rotate(' + angle + 'rad)', + }) + + if (dx > 0) { + $('#compass' + id).addClass('labelLeft') + } + } else { + $('#compassArrow' + id).hide() + $('#compass' + id).removeClass('labelLeft') + } + }, + limitPixelsToScreen: function (pixels) { + var self = Metamaps.Realtime + var socket = Metamaps.Realtime.socket + + var boundary = self.chatOpen ? '#wrapper' : document + var xLimit, yLimit + var xMax = $(boundary).width() + var yMax = $(boundary).height() + var compassDiameter = 56 + var compassArrowSize = 24 + + xLimit = Math.max(0 + compassArrowSize, pixels.x) + xLimit = Math.min(xLimit, xMax - compassDiameter) + yLimit = Math.max(0 + compassArrowSize, pixels.y) + yLimit = Math.min(yLimit, yMax - compassDiameter) + + return {x: xLimit,y: yLimit} + }, + sendCoords: function (coords) { + var self = Metamaps.Realtime + var socket = Metamaps.Realtime.socket + + var map = Metamaps.Active.Map + var mapper = Metamaps.Active.Mapper + + if (self.status && map.authorizeToEdit(mapper) && socket) { + var update = { + usercoords: coords, + userid: Metamaps.Active.Mapper.id, + mapid: Metamaps.Active.Map.id + } + socket.emit('updateMapperCoords', update) + } + }, + sendTopicDrag: function (positions) { + var self = Metamaps.Realtime + var socket = self.socket + + if (Metamaps.Active.Map && self.status) { + positions.mapid = Metamaps.Active.Map.id + socket.emit('topicDrag', positions) + } + }, + topicDrag: function (positions) { + var self = Metamaps.Realtime + var socket = self.socket + + var topic + var node + + if (Metamaps.Active.Map && self.status) { + for (var key in positions) { + topic = Metamaps.Topics.get(key) + if (topic) node = topic.get('node') + if (node) node.pos.setc(positions[key].x, positions[key].y) + } // for + Metamaps.Visualize.mGraph.plot() + } + }, + sendTopicChange: function (topic) { + var self = Metamaps.Realtime + var socket = self.socket + + var data = { + topicId: topic.id + } + + socket.emit('topicChangeFromClient', data) + }, + topicChange: function (data) { + var topic = Metamaps.Topics.get(data.topicId) + if (topic) { + var node = topic.get('node') + topic.fetch({ + success: function (model) { + model.set({ node: node }) + model.trigger('changeByOther') + } + }) + } + }, + sendSynapseChange: function (synapse) { + var self = Metamaps.Realtime + var socket = self.socket + + var data = { + synapseId: synapse.id + } + + socket.emit('synapseChangeFromClient', data) + }, + synapseChange: function (data) { + var synapse = Metamaps.Synapses.get(data.synapseId) + if (synapse) { + // edge reset necessary because fetch causes model reset + var edge = synapse.get('edge') + synapse.fetch({ + success: function (model) { + model.set({ edge: edge }) + model.trigger('changeByOther') + } + }) + } + }, + sendMapChange: function (map) { + var self = Metamaps.Realtime + var socket = self.socket + + var data = { + mapId: map.id + } + + socket.emit('mapChangeFromClient', data) + }, + mapChange: function (data) { + var map = Metamaps.Active.Map + var isActiveMap = map && data.mapId === map.id + if (isActiveMap) { + var couldEditBefore = map.authorizeToEdit(Metamaps.Active.Mapper) + var idBefore = map.id + map.fetch({ + success: function (model, response) { + var idNow = model.id + var canEditNow = model.authorizeToEdit(Metamaps.Active.Mapper) + if (idNow !== idBefore) { + Metamaps.Map.leavePrivateMap() // this means the map has been changed to private + } + else if (couldEditBefore && !canEditNow) { + Metamaps.Map.cantEditNow() + } + else if (!couldEditBefore && canEditNow) { + Metamaps.Map.canEditNow() + } else { + model.fetchContained() + model.trigger('changeByOther') + } + } + }) + } + }, + // newMessage + sendNewMessage: function (data) { + var self = Metamaps.Realtime + var socket = self.socket + + var message = data.attributes + message.mapid = Metamaps.Active.Map.id + socket.emit('newMessage', message) + }, + newMessage: function (data) { + var self = Metamaps.Realtime + var socket = self.socket + + self.room.addMessages(new Metamaps.Backbone.MessageCollection(data)) + }, + // newTopic + sendNewTopic: function (data) { + var self = Metamaps.Realtime + var socket = self.socket + + if (Metamaps.Active.Map && self.status) { + data.mapperid = Metamaps.Active.Mapper.id + data.mapid = Metamaps.Active.Map.id + socket.emit('newTopic', data) + } + }, + newTopic: function (data) { + var topic, mapping, mapper, mapperCallback, cancel + + var self = Metamaps.Realtime + var socket = self.socket + + if (!self.status) return + + function waitThenRenderTopic () { + if (topic && mapping && mapper) { + Metamaps.Topic.renderTopic(mapping, topic, false, false) + } + else if (!cancel) { + setTimeout(waitThenRenderTopic, 10) + } + } + + mapper = Metamaps.Mappers.get(data.mapperid) + if (mapper === undefined) { + mapperCallback = function (m) { + Metamaps.Mappers.add(m) + mapper = m + } + Metamaps.Mapper.get(data.mapperid, mapperCallback) + } + $.ajax({ + url: '/topics/' + data.mappableid + '.json', + success: function (response) { + Metamaps.Topics.add(response) + topic = Metamaps.Topics.get(response.id) + }, + error: function () { + cancel = true + } + }) + $.ajax({ + url: '/mappings/' + data.mappingid + '.json', + success: function (response) { + Metamaps.Mappings.add(response) + mapping = Metamaps.Mappings.get(response.id) + }, + error: function () { + cancel = true + } + }) + + waitThenRenderTopic() + }, + // removeTopic + sendDeleteTopic: function (data) { + var self = Metamaps.Realtime + var socket = self.socket + + if (Metamaps.Active.Map) { + socket.emit('deleteTopicFromClient', data) + } + }, + // removeTopic + sendRemoveTopic: function (data) { + var self = Metamaps.Realtime + var socket = self.socket + + if (Metamaps.Active.Map) { + data.mapid = Metamaps.Active.Map.id + socket.emit('removeTopic', data) + } + }, + removeTopic: function (data) { + var self = Metamaps.Realtime + var socket = self.socket + + if (!self.status) return + + var topic = Metamaps.Topics.get(data.mappableid) + if (topic) { + var node = topic.get('node') + var mapping = topic.getMapping() + Metamaps.Control.hideNode(node.id) + Metamaps.Topics.remove(topic) + Metamaps.Mappings.remove(mapping) + } + }, + // newSynapse + sendNewSynapse: function (data) { + var self = Metamaps.Realtime + var socket = self.socket + + if (Metamaps.Active.Map) { + data.mapperid = Metamaps.Active.Mapper.id + data.mapid = Metamaps.Active.Map.id + socket.emit('newSynapse', data) + } + }, + newSynapse: function (data) { + var topic1, topic2, node1, node2, synapse, mapping, cancel + + var self = Metamaps.Realtime + var socket = self.socket + + if (!self.status) return + + function waitThenRenderSynapse () { + if (synapse && mapping && mapper) { + topic1 = synapse.getTopic1() + node1 = topic1.get('node') + topic2 = synapse.getTopic2() + node2 = topic2.get('node') + + Metamaps.Synapse.renderSynapse(mapping, synapse, node1, node2, false) + } + else if (!cancel) { + setTimeout(waitThenRenderSynapse, 10) + } + } + + mapper = Metamaps.Mappers.get(data.mapperid) + if (mapper === undefined) { + mapperCallback = function (m) { + Metamaps.Mappers.add(m) + mapper = m + } + Metamaps.Mapper.get(data.mapperid, mapperCallback) + } + $.ajax({ + url: '/synapses/' + data.mappableid + '.json', + success: function (response) { + Metamaps.Synapses.add(response) + synapse = Metamaps.Synapses.get(response.id) + }, + error: function () { + cancel = true + } + }) + $.ajax({ + url: '/mappings/' + data.mappingid + '.json', + success: function (response) { + Metamaps.Mappings.add(response) + mapping = Metamaps.Mappings.get(response.id) + }, + error: function () { + cancel = true + } + }) + waitThenRenderSynapse() + }, + // deleteSynapse + sendDeleteSynapse: function (data) { + var self = Metamaps.Realtime + var socket = self.socket + + if (Metamaps.Active.Map) { + data.mapid = Metamaps.Active.Map.id + socket.emit('deleteSynapseFromClient', data) + } + }, + // removeSynapse + sendRemoveSynapse: function (data) { + var self = Metamaps.Realtime + var socket = self.socket + + if (Metamaps.Active.Map) { + data.mapid = Metamaps.Active.Map.id + socket.emit('removeSynapse', data) + } + }, + removeSynapse: function (data) { + var self = Metamaps.Realtime + var socket = self.socket + + if (!self.status) return + + var synapse = Metamaps.Synapses.get(data.mappableid) + if (synapse) { + var edge = synapse.get('edge') + var mapping = synapse.getMapping() + if (edge.getData('mappings').length - 1 === 0) { + Metamaps.Control.hideEdge(edge) + } + + var index = _.indexOf(edge.getData('synapses'), synapse) + edge.getData('mappings').splice(index, 1) + edge.getData('synapses').splice(index, 1) + if (edge.getData('displayIndex')) { + delete edge.data.$displayIndex + } + Metamaps.Synapses.remove(synapse) + Metamaps.Mappings.remove(mapping) + } + }, +}; // end Metamaps.Realtime diff --git a/app/assets/javascripts/src/Metamaps.Router.js b/app/assets/javascripts/src/Metamaps.Router.js index 6f673b61..5e574637 100644 --- a/app/assets/javascripts/src/Metamaps.Router.js +++ b/app/assets/javascripts/src/Metamaps.Router.js @@ -87,7 +87,7 @@ // either 'featured', 'mapper', or 'active' var capitalize = section.charAt(0).toUpperCase() + section.slice(1) - if (section === 'featured' || section === 'active') { + if (section === 'shared' || section === 'featured' || section === 'active') { document.title = 'Explore ' + capitalize + ' Maps | Metamaps' } else if (section === 'mapper') { $.ajax({ diff --git a/app/assets/javascripts/src/Metamaps.SynapseCard.js b/app/assets/javascripts/src/Metamaps.SynapseCard.js new file mode 100644 index 00000000..f71601e5 --- /dev/null +++ b/app/assets/javascripts/src/Metamaps.SynapseCard.js @@ -0,0 +1,288 @@ +/* global Metamaps, $ */ + +/* + * Metamaps.SynapseCard.js + * + * Dependencies: + * - Metamaps.Active + * - Metamaps.Control + * - Metamaps.Mapper + * - Metamaps.Visualize + */ +Metamaps.SynapseCard = { + openSynapseCard: null, + showCard: function (edge, e) { + var self = Metamaps.SynapseCard + + // reset so we don't interfere with other edges, but first, save its x and y + var myX = $('#edit_synapse').css('left') + var myY = $('#edit_synapse').css('top') + $('#edit_synapse').remove() + + // so label is missing while editing + Metamaps.Control.deselectEdge(edge) + + var index = edge.getData('displayIndex') ? edge.getData('displayIndex') : 0 + var synapse = edge.getData('synapses')[index]; // for now, just get the first synapse + + // create the wrapper around the form elements, including permissions + // classes to make best_in_place happy + var edit_div = document.createElement('div') + edit_div.innerHTML = '
    ' + edit_div.setAttribute('id', 'edit_synapse') + if (synapse.authorizeToEdit(Metamaps.Active.Mapper)) { + edit_div.className = 'permission canEdit' + edit_div.className += synapse.authorizePermissionChange(Metamaps.Active.Mapper) ? ' yourEdge' : '' + } else { + edit_div.className = 'permission cannotEdit' + } + $('#wrapper').append(edit_div) + + self.populateShowCard(edge, synapse) + + // drop it in the right spot, activate it + $('#edit_synapse').css('position', 'absolute') + if (e) { + $('#edit_synapse').css('left', e.clientX) + $('#edit_synapse').css('top', e.clientY) + } else { + $('#edit_synapse').css('left', myX) + $('#edit_synapse').css('top', myY) + } + // $('#edit_synapse_name').click() //required in case name is empty + // $('#edit_synapse_name input').focus() + $('#edit_synapse').show() + + self.openSynapseCard = edge + }, + + hideCard: function () { + $('#edit_synapse').remove() + Metamaps.SynapseCard.openSynapseCard = null + }, + + populateShowCard: function (edge, synapse) { + var self = Metamaps.SynapseCard + + self.add_synapse_count(edge) + self.add_desc_form(synapse) + self.add_drop_down(edge, synapse) + self.add_user_info(synapse) + self.add_perms_form(synapse) + self.add_direction_form(synapse) + }, + add_synapse_count: function (edge) { + var count = edge.getData('synapses').length + + $('#editSynUpperBar').append('
    ' + count + '
    ') + }, + add_desc_form: function (synapse) { + var data_nil = 'Click to add description.' + + // TODO make it so that this would work even in sandbox mode, + // currently with Best_in_place it won't + + // desc editing form + $('#editSynUpperBar').append('
    ') + $('#edit_synapse_desc').attr('class', 'best_in_place best_in_place_desc') + $('#edit_synapse_desc').attr('data-object', 'synapse') + $('#edit_synapse_desc').attr('data-attribute', 'desc') + $('#edit_synapse_desc').attr('data-type', 'textarea') + $('#edit_synapse_desc').attr('data-nil', data_nil) + $('#edit_synapse_desc').attr('data-url', '/synapses/' + synapse.id) + $('#edit_synapse_desc').html(synapse.get('desc')) + + // if edge data is blank or just whitespace, populate it with data_nil + if ($('#edit_synapse_desc').html().trim() == '') { + if (synapse.authorizeToEdit(Metamaps.Active.Mapper)) { + $('#edit_synapse_desc').html(data_nil) + } else { + $('#edit_synapse_desc').html('(no description)') + } + } + + $('#edit_synapse_desc').bind('ajax:success', function () { + var desc = $(this).html() + if (desc == data_nil) { + synapse.set('desc', '') + } else { + synapse.set('desc', desc) + } + synapse.trigger('saved') + Metamaps.Control.selectEdge(synapse.get('edge')) + Metamaps.Visualize.mGraph.plot() + }) + }, + add_drop_down: function (edge, synapse) { + var list, i, synapses, l, desc + + synapses = edge.getData('synapses') + l = synapses.length + + if (l > 1) { + // append the element that you click to show dropdown select + $('#editSynUpperBar').append('') + $('#dropdownSynapses').click(function (e) { + e.preventDefault() + e.stopPropagation() // stop it from immediately closing it again + $('#switchSynapseList').toggle() + }) + // hide the dropdown again if you click anywhere else on the synapse card + $('#edit_synapse').click(function () { + $('#switchSynapseList').hide() + }) + + // generate the list of other synapses + list = '
      ' + for (i = 0; i < l; i++) { + if (synapses[i] !== synapse) { // don't add the current one to the list + desc = synapses[i].get('desc') + desc = desc === '' || desc === null ? '(no description)' : desc + list += '
    • ' + desc + '
    • ' + } + } + list += '
    ' + // add the list of the other synapses + $('#editSynLowerBar').append(list) + + // attach click listeners to list items that + // will cause it to switch the displayed synapse + // when you click it + $('#switchSynapseList li').click(function (e) { + e.stopPropagation() + var index = parseInt($(this).attr('data-synapse-index')) + edge.setData('displayIndex', index) + Metamaps.Visualize.mGraph.plot() + Metamaps.SynapseCard.showCard(edge, false) + }) + } + }, + add_user_info: function (synapse) { + var u = '
    ' + u += ' ' + u += '
    ' + synapse.get('user_name') + '
    ' + $('#editSynLowerBar').append(u) + + // get mapper image + var setMapperImage = function (mapper) { + $('#edgeUser img').attr('src', mapper.get('image')) + } + Metamaps.Mapper.get(synapse.get('user_id'), setMapperImage) + }, + + add_perms_form: function (synapse) { + // permissions - if owner, also allow permission editing + $('#editSynLowerBar').append('
    ') + + // ability to change permission + var selectingPermission = false + var permissionLiClick = function (event) { + selectingPermission = false + var permission = $(this).attr('class') + synapse.save({ + permission: permission, + defer_to_map_id: null + }) + $('#edit_synapse .mapPerm').removeClass('co pu pr minimize').addClass(permission.substring(0, 2)) + $('#edit_synapse .permissionSelect').remove() + event.stopPropagation() + } + + var openPermissionSelect = function (event) { + if (!selectingPermission) { + selectingPermission = true + $(this).addClass('minimize') // this line flips the drop down arrow to a pull up arrow + if ($(this).hasClass('co')) { + $(this).append('
    ') + } else if ($(this).hasClass('pu')) { + $(this).append('
    ') + } else if ($(this).hasClass('pr')) { + $(this).append('
    ') + } + $('#edit_synapse .permissionSelect li').click(permissionLiClick) + event.stopPropagation() + } + } + + var hidePermissionSelect = function () { + selectingPermission = false + $('#edit_synapse.yourEdge .mapPerm').removeClass('minimize') // this line flips the pull up arrow to a drop down arrow + $('#edit_synapse .permissionSelect').remove() + } + + if (synapse.authorizePermissionChange(Metamaps.Active.Mapper)) { + $('#edit_synapse.yourEdge .mapPerm').click(openPermissionSelect) + $('#edit_synapse').click(hidePermissionSelect) + } + }, // add_perms_form + + add_direction_form: function (synapse) { + // directionality checkboxes + $('#editSynLowerBar').append('
    ') + $('#editSynLowerBar').append('
    ') + + var edge = synapse.get('edge') + + // determine which node is to the left and the right + // if directly in a line, top is left + if (edge.nodeFrom.pos.x < edge.nodeTo.pos.x || + edge.nodeFrom.pos.x == edge.nodeTo.pos.x && + edge.nodeFrom.pos.y < edge.nodeTo.pos.y) { + var left = edge.nodeTo.getData('topic') + var right = edge.nodeFrom.getData('topic') + } else { + var left = edge.nodeFrom.getData('topic') + var right = edge.nodeTo.getData('topic') + } + + /* + * One node is actually on the left onscreen. Call it left, & the other right. + * If category is from-to, and that node is first, check the 'right' checkbox. + * Else check the 'left' checkbox since the arrow is incoming. + */ + + var directionCat = synapse.get('category'); // both, none, from-to + if (directionCat == 'from-to') { + var from_to = [synapse.get('node1_id'), synapse.get('node2_id')] + if (from_to[0] == left.id) { + // check left checkbox + $('#edit_synapse_left').addClass('checked') + } else { + // check right checkbox + $('#edit_synapse_right').addClass('checked') + } + } else if (directionCat == 'both') { + // check both checkboxes + $('#edit_synapse_left').addClass('checked') + $('#edit_synapse_right').addClass('checked') + } + + if (synapse.authorizeToEdit(Metamaps.Active.Mapper)) { + $('#edit_synapse_left, #edit_synapse_right').click(function () { + $(this).toggleClass('checked') + + var leftChecked = $('#edit_synapse_left').is('.checked') + var rightChecked = $('#edit_synapse_right').is('.checked') + + var dir = synapse.getDirection() + var dirCat = 'none' + if (leftChecked && rightChecked) { + dirCat = 'both' + } else if (!leftChecked && rightChecked) { + dirCat = 'from-to' + dir = [right.id, left.id] + } else if (leftChecked && !rightChecked) { + dirCat = 'from-to' + dir = [left.id, right.id] + } + + synapse.save({ + category: dirCat, + node1_id: dir[0], + node2_id: dir[1] + }) + Metamaps.Visualize.mGraph.plot() + }) + } // if + } // add_direction_form +}; // end Metamaps.SynapseCard diff --git a/app/assets/javascripts/src/Metamaps.Topic.js b/app/assets/javascripts/src/Metamaps.Topic.js index dc242f7d..da1bfc8c 100644 --- a/app/assets/javascripts/src/Metamaps.Topic.js +++ b/app/assets/javascripts/src/Metamaps.Topic.js @@ -299,7 +299,8 @@ Metamaps.Topic = { var topic = new Metamaps.Backbone.Topic({ name: Metamaps.Create.newTopic.name, - metacode_id: metacode.id + metacode_id: metacode.id, + defer_to_map_id: Metamaps.Active.Map.id }) Metamaps.Topics.add(topic) diff --git a/app/assets/javascripts/src/Metamaps.TopicCard.js b/app/assets/javascripts/src/Metamaps.TopicCard.js new file mode 100644 index 00000000..194433db --- /dev/null +++ b/app/assets/javascripts/src/Metamaps.TopicCard.js @@ -0,0 +1,451 @@ +/* global Metamaps, $ */ + +/* + * Metamaps.TopicCard.js + * + * Dependencies: + * - Metamaps.Active + * - Metamaps.GlobalUI + * - Metamaps.Mapper + * - Metamaps.Metacodes + * - Metamaps.Router + * - Metamaps.Util + * - Metamaps.Visualize + */ +Metamaps.TopicCard = { + openTopicCard: null, // stores the topic that's currently open + authorizedToEdit: false, // stores boolean for edit permission for open topic card + init: function () { + var self = Metamaps.TopicCard + + // initialize best_in_place editing + $('.authenticated div.permission.canEdit .best_in_place').best_in_place() + + Metamaps.TopicCard.generateShowcardHTML = Hogan.compile($('#topicCardTemplate').html()) + + // initialize topic card draggability and resizability + $('.showcard').draggable({ + handle: '.metacodeImage' + }) + + embedly('on', 'card.rendered', self.embedlyCardRendered) + }, + /** + * Will open the Topic Card for the node that it's passed + * @param {$jit.Graph.Node} node + */ + showCard: function (node) { + var self = Metamaps.TopicCard + + var topic = node.getData('topic') + + self.openTopicCard = topic + self.authorizedToEdit = topic.authorizeToEdit(Metamaps.Active.Mapper) + // populate the card that's about to show with the right topics data + self.populateShowCard(topic) + $('.showcard').fadeIn('fast') + }, + hideCard: function () { + var self = Metamaps.TopicCard + + $('.showcard').fadeOut('fast') + self.openTopicCard = null + self.authorizedToEdit = false + }, + embedlyCardRendered: function (iframe) { + var self = Metamaps.TopicCard + + $('#embedlyLinkLoader').hide() + + // means that the embedly call returned 404 not found + if ($('#embedlyLink')[0]) { + $('#embedlyLink').css('display', 'block').fadeIn('fast') + $('.embeds').addClass('nonEmbedlyLink') + } + + $('.CardOnGraph').addClass('hasAttachment') + if (self.authorizedToEdit) { + $('.embeds').append('
    ') + $('#linkremove').click(self.removeLink) + } + }, + removeLink: function () { + var self = Metamaps.TopicCard + self.openTopicCard.save({ + link: null + }) + $('.embeds').empty().removeClass('nonEmbedlyLink') + $('#addLinkInput input').val('') + $('.attachments').removeClass('hidden') + $('.CardOnGraph').removeClass('hasAttachment') + }, + bindShowCardListeners: function (topic) { + var self = Metamaps.TopicCard + var showCard = document.getElementById('showcard') + + var authorized = self.authorizedToEdit + + // get mapper image + var setMapperImage = function (mapper) { + $('.contributorIcon').attr('src', mapper.get('image')) + } + Metamaps.Mapper.get(topic.get('user_id'), setMapperImage) + + // starting embed.ly + var resetFunc = function () { + $('#addLinkInput input').val('') + $('#addLinkInput input').focus() + } + var inputEmbedFunc = function (event) { + var element = this + setTimeout(function () { + var text = $(element).val() + if (event.type == 'paste' || (event.type == 'keyup' && event.which == 13)) { + // TODO evaluate converting this to '//' no matter what (infer protocol) + if (text.slice(0, 7) !== 'http://' && + text.slice(0, 8) !== 'https://' && + text.slice(0, 2) !== '//') { + text = '//' + text + } + topic.save({ + link: text + }) + var embedlyEl = $('', { + id: 'embedlyLink', + 'data-card-description': '0', + href: text + }).html(text) + $('.attachments').addClass('hidden') + $('.embeds').append(embedlyEl) + $('.embeds').append('
    ') + var loader = new CanvasLoader('embedlyLinkLoader') + loader.setColor('#4fb5c0'); // default is '#000000' + loader.setDiameter(28) // default is 40 + loader.setDensity(41) // default is 40 + loader.setRange(0.9); // default is 1.3 + loader.show() // Hidden by default + var e = embedly('card', document.getElementById('embedlyLink')) + if (!e) { + self.handleInvalidLink() + } + } + }, 100) + } + $('#addLinkReset').click(resetFunc) + $('#addLinkInput input').bind('paste keyup', inputEmbedFunc) + + // initialize the link card, if there is a link + if (topic.get('link') && topic.get('link') !== '') { + var loader = new CanvasLoader('embedlyLinkLoader') + loader.setColor('#4fb5c0'); // default is '#000000' + loader.setDiameter(28) // default is 40 + loader.setDensity(41) // default is 40 + loader.setRange(0.9); // default is 1.3 + loader.show() // Hidden by default + var e = embedly('card', document.getElementById('embedlyLink')) + if (!e) { + self.handleInvalidLink() + } + } + + var selectingMetacode = false + // attach the listener that shows the metacode title when you hover over the image + $('.showcard .metacodeImage').mouseenter(function () { + $('.showcard .icon').css('z-index', '4') + $('.showcard .metacodeTitle').show() + }) + $('.showcard .linkItem.icon').mouseleave(function () { + if (!selectingMetacode) { + $('.showcard .metacodeTitle').hide() + $('.showcard .icon').css('z-index', '1') + } + }) + + var metacodeLiClick = function () { + selectingMetacode = false + var metacodeId = parseInt($(this).attr('data-id')) + var metacode = Metamaps.Metacodes.get(metacodeId) + $('.CardOnGraph').find('.metacodeTitle').html(metacode.get('name')) + .append('
    ') + .attr('class', 'metacodeTitle mbg' + metacode.id) + $('.CardOnGraph').find('.metacodeImage').css('background-image', 'url(' + metacode.get('icon') + ')') + topic.save({ + metacode_id: metacode.id + }) + Metamaps.Visualize.mGraph.plot() + $('.metacodeSelect').hide().removeClass('onRightEdge onBottomEdge') + $('.metacodeTitle').hide() + $('.showcard .icon').css('z-index', '1') + } + + var openMetacodeSelect = function (event) { + var windowWidth + var showcardLeft + var TOPICCARD_WIDTH = 300 + var METACODESELECT_WIDTH = 404 + var distanceFromEdge + + var MAX_METACODELIST_HEIGHT = 270 + var windowHeight + var showcardTop + var topicTitleHeight + var distanceFromBottom + + if (!selectingMetacode) { + selectingMetacode = true + + // this is to make sure the metacode + // select is accessible onscreen, when opened + // while topic card is close to the right + // edge of the screen + windowWidth = $(window).width() + showcardLeft = parseInt($('.showcard').css('left')) + distanceFromEdge = windowWidth - (showcardLeft + TOPICCARD_WIDTH) + if (distanceFromEdge < METACODESELECT_WIDTH) { + $('.metacodeSelect').addClass('onRightEdge') + } + + // this is to make sure the metacode + // select is accessible onscreen, when opened + // while topic card is close to the bottom + // edge of the screen + windowHeight = $(window).height() + showcardTop = parseInt($('.showcard').css('top')) + topicTitleHeight = $('.showcard .title').height() + parseInt($('.showcard .title').css('padding-top')) + parseInt($('.showcard .title').css('padding-bottom')) + heightOfSetList = $('.showcard .metacodeSelect').height() + distanceFromBottom = windowHeight - (showcardTop + topicTitleHeight) + if (distanceFromBottom < MAX_METACODELIST_HEIGHT) { + $('.metacodeSelect').addClass('onBottomEdge') + } + + $('.metacodeSelect').show() + event.stopPropagation() + } + } + + var hideMetacodeSelect = function () { + selectingMetacode = false + $('.metacodeSelect').hide().removeClass('onRightEdge onBottomEdge') + $('.metacodeTitle').hide() + $('.showcard .icon').css('z-index', '1') + } + + if (authorized) { + $('.showcard .metacodeTitle').click(openMetacodeSelect) + $('.showcard').click(hideMetacodeSelect) + $('.metacodeSelect > ul > li').click(function (event) { + event.stopPropagation() + }) + $('.metacodeSelect li li').click(metacodeLiClick) + + var bipName = $(showCard).find('.best_in_place_name') + bipName.bind('best_in_place:activate', function () { + var $el = bipName.find('textarea') + var el = $el[0] + + $el.attr('maxlength', '140') + + $('.showcard .title').append('
    ') + + var callback = function (data) { + $('.nameCounter.forTopic').html(data.all + '/140') + } + Countable.live(el, callback) + }) + bipName.bind('best_in_place:deactivate', function () { + $('.nameCounter.forTopic').remove() + }) + + // bind best_in_place ajax callbacks + bipName.bind('ajax:success', function () { + var name = Metamaps.Util.decodeEntities($(this).html()) + topic.set('name', name) + topic.trigger('saved') + }) + + $(showCard).find('.best_in_place_desc').bind('ajax:success', function () { + this.innerHTML = this.innerHTML.replace(/\r/g, '') + var desc = $(this).html() === $(this).data('nil') ? '' : $(this).html() + topic.set('desc', desc) + topic.trigger('saved') + }) + } + + var permissionLiClick = function (event) { + selectingPermission = false + var permission = $(this).attr('class') + topic.save({ + permission: permission, + defer_to_map_id: null + }) + $('.showcard .mapPerm').removeClass('co pu pr minimize').addClass(permission.substring(0, 2)) + $('.showcard .permissionSelect').remove() + event.stopPropagation() + } + + var openPermissionSelect = function (event) { + if (!selectingPermission) { + selectingPermission = true + $(this).addClass('minimize') // this line flips the drop down arrow to a pull up arrow + if ($(this).hasClass('co')) { + $(this).append('
    ') + } else if ($(this).hasClass('pu')) { + $(this).append('
    ') + } else if ($(this).hasClass('pr')) { + $(this).append('
    ') + } + $('.showcard .permissionSelect li').click(permissionLiClick) + event.stopPropagation() + } + } + + var hidePermissionSelect = function () { + selectingPermission = false + $('.showcard .yourTopic .mapPerm').removeClass('minimize') // this line flips the pull up arrow to a drop down arrow + $('.showcard .permissionSelect').remove() + } + // ability to change permission + var selectingPermission = false + if (topic.authorizePermissionChange(Metamaps.Active.Mapper)) { + $('.showcard .yourTopic .mapPerm').click(openPermissionSelect) + $('.showcard').click(hidePermissionSelect) + } + + $('.links .mapCount').unbind().click(function (event) { + $('.mapCount .tip').toggle() + $('.showcard .hoverTip').toggleClass('hide') + event.stopPropagation() + }) + $('.mapCount .tip').unbind().click(function (event) { + event.stopPropagation() + }) + $('.showcard').unbind('.hideTip').bind('click.hideTip', function () { + $('.mapCount .tip').hide() + $('.showcard .hoverTip').removeClass('hide') + }) + + $('.mapCount .tip li a').click(Metamaps.Router.intercept) + + var originalText = $('.showMore').html() + $('.mapCount .tip .showMore').unbind().toggle( + function (event) { + $('.extraText').toggleClass('hideExtra') + $('.showMore').html('Show less...') + }, + function (event) { + $('.extraText').toggleClass('hideExtra') + $('.showMore').html(originalText) + }) + + $('.mapCount .tip showMore').unbind().click(function (event) { + event.stopPropagation() + }) + }, + handleInvalidLink: function () { + var self = Metamaps.TopicCard + + self.removeLink() + Metamaps.GlobalUI.notifyUser('Invalid link') + }, + populateShowCard: function (topic) { + var self = Metamaps.TopicCard + + var showCard = document.getElementById('showcard') + + $(showCard).find('.permission').remove() + + var topicForTemplate = self.buildObject(topic) + var html = self.generateShowcardHTML.render(topicForTemplate) + + if (topic.authorizeToEdit(Metamaps.Active.Mapper)) { + var perm = document.createElement('div') + + var string = 'permission canEdit' + if (topic.authorizePermissionChange(Metamaps.Active.Mapper)) string += ' yourTopic' + perm.className = string + perm.innerHTML = html + showCard.appendChild(perm) + } else { + var perm = document.createElement('div') + perm.className = 'permission cannotEdit' + perm.innerHTML = html + showCard.appendChild(perm) + } + + Metamaps.TopicCard.bindShowCardListeners(topic) + }, + generateShowcardHTML: null, // will be initialized into a Hogan template within init function + // generateShowcardHTML + buildObject: function (topic) { + var self = Metamaps.TopicCard + + var nodeValues = {} + + var authorized = topic.authorizeToEdit(Metamaps.Active.Mapper) + + if (!authorized) { + } else { + } + + var desc_nil = 'Click to add description...' + + nodeValues.attachmentsHidden = '' + if (topic.get('link') && topic.get('link') !== '') { + nodeValues.embeds = '
    ' + nodeValues.embeds += topic.get('link') + nodeValues.embeds += '
    ' + nodeValues.attachmentsHidden = 'hidden' + nodeValues.hasAttachment = 'hasAttachment' + } else { + nodeValues.embeds = '' + nodeValues.hasAttachment = '' + } + + if (authorized) { + nodeValues.attachments = '' + } else { + nodeValues.attachmentsHidden = 'hidden' + nodeValues.attachments = '' + } + + var inmapsAr = topic.get('inmaps') + var inmapsLinks = topic.get('inmapsLinks') + nodeValues.inmaps = '' + if (inmapsAr.length < 6) { + for (i = 0; i < inmapsAr.length; i++) { + var url = '/maps/' + inmapsLinks[i] + nodeValues.inmaps += '
  • ' + inmapsAr[i] + '
  • ' + } + } else { + for (i = 0; i < 5; i++) { + var url = '/maps/' + inmapsLinks[i] + nodeValues.inmaps += '
  • ' + inmapsAr[i] + '
  • ' + } + extra = inmapsAr.length - 5 + nodeValues.inmaps += '
  • See ' + extra + ' more...
  • ' + for (i = 5; i < inmapsAr.length; i++) { + var url = '/maps/' + inmapsLinks[i] + nodeValues.inmaps += '
  • ' + inmapsAr[i] + '
  • ' + } + } + nodeValues.permission = topic.get('calculated_permission') + nodeValues.mk_permission = topic.get('calculated_permission').substring(0, 2) + nodeValues.map_count = topic.get('map_count').toString() + nodeValues.synapse_count = topic.get('synapse_count').toString() + nodeValues.id = topic.isNew() ? topic.cid : topic.id + nodeValues.metacode = topic.getMetacode().get('name') + nodeValues.metacode_class = 'mbg' + topic.get('metacode_id') + nodeValues.imgsrc = topic.getMetacode().get('icon') + nodeValues.name = topic.get('name') + nodeValues.userid = topic.get('user_id') + nodeValues.username = topic.get('user_name') + nodeValues.date = topic.getDate() + // the code for this is stored in /views/main/_metacodeOptions.html.erb + nodeValues.metacode_select = $('#metacodeOptions').html() + nodeValues.desc_nil = desc_nil + nodeValues.desc = (topic.get('desc') == '' && authorized) ? desc_nil : topic.get('desc') + return nodeValues + } +}; // end Metamaps.TopicCard diff --git a/app/assets/javascripts/src/Metamaps.Util.js b/app/assets/javascripts/src/Metamaps.Util.js new file mode 100644 index 00000000..e150d3bb --- /dev/null +++ b/app/assets/javascripts/src/Metamaps.Util.js @@ -0,0 +1,130 @@ +/* global Metamaps */ + +/* + * Metamaps.Util.js + * + * Dependencies: + * - Metamaps.Visualize + */ + +Metamaps.Util = { + // helper function to determine how many lines are needed + // Line Splitter Function + // copyright Stephen Chapman, 19th April 2006 + // you may copy this code but please keep the copyright notice as well + splitLine: function (st, n) { + var b = '' + var s = st ? st : '' + while (s.length > n) { + var c = s.substring(0, n) + var d = c.lastIndexOf(' ') + var e = c.lastIndexOf('\n') + if (e != -1) d = e + if (d == -1) d = n + b += c.substring(0, d) + '\n' + s = s.substring(d + 1) + } + return b + s + }, + nowDateFormatted: function () { + var date = new Date(Date.now()) + var month = (date.getMonth() + 1) < 10 ? '0' + (date.getMonth() + 1) : (date.getMonth() + 1) + var day = date.getDate() < 10 ? '0' + date.getDate() : date.getDate() + var year = date.getFullYear() + + return month + '/' + day + '/' + year + }, + decodeEntities: function (desc) { + var str, temp = document.createElement('p') + temp.innerHTML = desc // browser handles the topics + str = temp.textContent || temp.innerText + temp = null // delete the element + return str + }, // decodeEntities + getDistance: function (p1, p2) { + return Math.sqrt(Math.pow((p2.x - p1.x), 2) + Math.pow((p2.y - p1.y), 2)) + }, + coordsToPixels: function (coords) { + if (Metamaps.Visualize.mGraph) { + var canvas = Metamaps.Visualize.mGraph.canvas, + s = canvas.getSize(), + p = canvas.getPos(), + ox = canvas.translateOffsetX, + oy = canvas.translateOffsetY, + sx = canvas.scaleOffsetX, + sy = canvas.scaleOffsetY + var pixels = { + x: (coords.x / (1 / sx)) + p.x + s.width / 2 + ox, + y: (coords.y / (1 / sy)) + p.y + s.height / 2 + oy + } + return pixels + } else { + return { + x: 0, + y: 0 + } + } + }, + pixelsToCoords: function (pixels) { + var coords + if (Metamaps.Visualize.mGraph) { + var canvas = Metamaps.Visualize.mGraph.canvas, + s = canvas.getSize(), + p = canvas.getPos(), + ox = canvas.translateOffsetX, + oy = canvas.translateOffsetY, + sx = canvas.scaleOffsetX, + sy = canvas.scaleOffsetY + coords = { + x: (pixels.x - p.x - s.width / 2 - ox) * (1 / sx), + y: (pixels.y - p.y - s.height / 2 - oy) * (1 / sy), + } + } else { + coords = { + x: 0, + y: 0 + } + } + return coords + }, + getPastelColor: function () { + var r = (Math.round(Math.random() * 127) + 127).toString(16) + var g = (Math.round(Math.random() * 127) + 127).toString(16) + var b = (Math.round(Math.random() * 127) + 127).toString(16) + return Metamaps.Util.colorLuminance('#' + r + g + b, -0.4) + }, + // darkens a hex value by 'lum' percentage + colorLuminance: function (hex, lum) { + // validate hex string + hex = String(hex).replace(/[^0-9a-f]/gi, '') + if (hex.length < 6) { + hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2] + } + lum = lum || 0 + + // convert to decimal and change luminosity + var rgb = '#', c, i + for (i = 0; i < 3; i++) { + c = parseInt(hex.substr(i * 2, 2), 16) + c = Math.round(Math.min(Math.max(0, c + (c * lum)), 255)).toString(16) + rgb += ('00' + c).substr(c.length) + } + + return rgb + }, + generateOptionsList: function (data) { + var newlist = '' + for (var i = 0; i < data.length; i++) { + newlist = newlist + '' + } + return newlist + }, + checkURLisImage: function (url) { + // when the page reloads the following regular expression will be screwed up + // please replace it with this one before you save: /*backslashhere*.(jpeg|jpg|gif|png)$/ + return (url.match(/\.(jpeg|jpg|gif|png)$/) != null) + }, + checkURLisYoutubeVideo: function (url) { + return (url.match(/^https?:\/\/(?:www\.)?youtube.com\/watch\?(?=[^?]*v=\w+)(?:[^\s?]+)?$/) != null) + } +}; // end Metamaps.Util diff --git a/app/assets/javascripts/src/Metamaps.Visualize.js b/app/assets/javascripts/src/Metamaps.Visualize.js new file mode 100644 index 00000000..2b8231a8 --- /dev/null +++ b/app/assets/javascripts/src/Metamaps.Visualize.js @@ -0,0 +1,207 @@ +/* global Metamaps, $ */ +/* + * Metamaps.Visualize + * + * Dependencies: + * - Metamaps.Active + * - Metamaps.JIT + * - Metamaps.Loading + * - Metamaps.Metacodes + * - Metamaps.Router + * - Metamaps.Synapses + * - Metamaps.TopicCard + * - Metamaps.Topics + * - Metamaps.Touch + * - Metamaps.Visualize + */ + +Metamaps.Visualize = { + mGraph: null, // a reference to the graph object. + cameraPosition: null, // stores the camera position when using a 3D visualization + type: 'ForceDirected', // the type of graph we're building, could be "RGraph", "ForceDirected", or "ForceDirected3D" + loadLater: false, // indicates whether there is JSON that should be loaded right in the offset, or whether to wait till the first topic is created + init: function () { + var self = Metamaps.Visualize + // disable awkward dragging of the canvas element that would sometimes happen + $('#infovis-canvas').on('dragstart', function (event) { + event.preventDefault() + }) + + // prevent touch events on the canvas from default behaviour + $('#infovis-canvas').bind('touchstart', function (event) { + event.preventDefault() + self.mGraph.events.touched = true + }) + + // prevent touch events on the canvas from default behaviour + $('#infovis-canvas').bind('touchmove', function (event) { + // Metamaps.JIT.touchPanZoomHandler(event) + }) + + // prevent touch events on the canvas from default behaviour + $('#infovis-canvas').bind('touchend touchcancel', function (event) { + lastDist = 0 + if (!self.mGraph.events.touchMoved && !Metamaps.Touch.touchDragNode) Metamaps.TopicCard.hideCurrentCard() + self.mGraph.events.touched = self.mGraph.events.touchMoved = false + Metamaps.Touch.touchDragNode = false + }) + }, + computePositions: function () { + var self = Metamaps.Visualize, + mapping + + if (self.type == 'RGraph') { + var i, l, startPos, endPos, topic, synapse + + self.mGraph.graph.eachNode(function (n) { + topic = Metamaps.Topics.get(n.id) + topic.set({ node: n }, { silent: true }) + topic.updateNode() + + n.eachAdjacency(function (edge) { + if (!edge.getData('init')) { + edge.setData('init', true) + + l = edge.getData('synapseIDs').length + for (i = 0; i < l; i++) { + synapse = Metamaps.Synapses.get(edge.getData('synapseIDs')[i]) + synapse.set({ edge: edge }, { silent: true }) + synapse.updateEdge() + } + } + }) + + var pos = n.getPos() + pos.setc(-200, -200) + }) + self.mGraph.compute('end') + } else if (self.type == 'ForceDirected') { + var i, l, startPos, endPos, topic, synapse + + self.mGraph.graph.eachNode(function (n) { + topic = Metamaps.Topics.get(n.id) + topic.set({ node: n }, { silent: true }) + topic.updateNode() + mapping = topic.getMapping() + + n.eachAdjacency(function (edge) { + if (!edge.getData('init')) { + edge.setData('init', true) + + l = edge.getData('synapseIDs').length + for (i = 0; i < l; i++) { + synapse = Metamaps.Synapses.get(edge.getData('synapseIDs')[i]) + synapse.set({ edge: edge }, { silent: true }) + synapse.updateEdge() + } + } + }) + + startPos = new $jit.Complex(0, 0) + endPos = new $jit.Complex(mapping.get('xloc'), mapping.get('yloc')) + n.setPos(startPos, 'start') + n.setPos(endPos, 'end') + }) + } else if (self.type == 'ForceDirected3D') { + self.mGraph.compute() + } + }, + /** + * render does the heavy lifting of creating the engine that renders the graph with the properties we desire + * + */ + render: function () { + var self = Metamaps.Visualize, RGraphSettings, FDSettings + + if (self.type == 'RGraph' && (!self.mGraph || self.mGraph instanceof $jit.ForceDirected)) { + RGraphSettings = $.extend(true, {}, Metamaps.JIT.ForceDirected.graphSettings) + + $jit.RGraph.Plot.NodeTypes.implement(Metamaps.JIT.ForceDirected.nodeSettings) + $jit.RGraph.Plot.EdgeTypes.implement(Metamaps.JIT.ForceDirected.edgeSettings) + + RGraphSettings.width = $(document).width() + RGraphSettings.height = $(document).height() + RGraphSettings.background = Metamaps.JIT.RGraph.background + RGraphSettings.levelDistance = Metamaps.JIT.RGraph.levelDistance + + self.mGraph = new $jit.RGraph(RGraphSettings) + } else if (self.type == 'ForceDirected' && (!self.mGraph || self.mGraph instanceof $jit.RGraph)) { + FDSettings = $.extend(true, {}, Metamaps.JIT.ForceDirected.graphSettings) + + $jit.ForceDirected.Plot.NodeTypes.implement(Metamaps.JIT.ForceDirected.nodeSettings) + $jit.ForceDirected.Plot.EdgeTypes.implement(Metamaps.JIT.ForceDirected.edgeSettings) + + FDSettings.width = $('body').width() + FDSettings.height = $('body').height() + + self.mGraph = new $jit.ForceDirected(FDSettings) + } else if (self.type == 'ForceDirected3D' && !self.mGraph) { + // init ForceDirected3D + self.mGraph = new $jit.ForceDirected3D(Metamaps.JIT.ForceDirected3D.graphSettings) + self.cameraPosition = self.mGraph.canvas.canvases[0].camera.position + } else { + self.mGraph.graph.empty() + } + + function runAnimation () { + Metamaps.Loading.hide() + // load JSON data, if it's not empty + if (!self.loadLater) { + // load JSON data. + var rootIndex = 0 + if (Metamaps.Active.Topic) { + var node = _.find(Metamaps.JIT.vizData, function (node) { + return node.id === Metamaps.Active.Topic.id + }) + rootIndex = _.indexOf(Metamaps.JIT.vizData, node) + } + self.mGraph.loadJSON(Metamaps.JIT.vizData, rootIndex) + // compute positions and plot. + self.computePositions() + self.mGraph.busy = true + if (self.type == 'RGraph') { + self.mGraph.fx.animate(Metamaps.JIT.RGraph.animate) + } else if (self.type == 'ForceDirected') { + self.mGraph.animate(Metamaps.JIT.ForceDirected.animateSavedLayout) + } else if (self.type == 'ForceDirected3D') { + self.mGraph.animate(Metamaps.JIT.ForceDirected.animateFDLayout) + } + } + } + // hold until all the needed metacode images are loaded + // hold for a maximum of 80 passes, or 4 seconds of waiting time + var tries = 0 + function hold () { + var unique = _.uniq(Metamaps.Topics.models, function (metacode) { return metacode.get('metacode_id'); }), + requiredMetacodes = _.map(unique, function (metacode) { return metacode.get('metacode_id'); }), + loadedCount = 0 + + _.each(requiredMetacodes, function (metacode_id) { + var metacode = Metamaps.Metacodes.get(metacode_id), + img = metacode ? metacode.get('image') : false + + if (img && (img.complete || (typeof img.naturalWidth !== 'undefined' && img.naturalWidth !== 0))) { + loadedCount += 1 + } + }) + + if (loadedCount === requiredMetacodes.length || tries > 80) runAnimation() + else setTimeout(function () { tries++; hold() }, 50) + } + hold() + + // update the url now that the map is ready + clearTimeout(Metamaps.Router.timeoutId) + Metamaps.Router.timeoutId = setTimeout(function () { + var m = Metamaps.Active.Map + var t = Metamaps.Active.Topic + + if (m && window.location.pathname !== '/maps/' + m.id) { + Metamaps.Router.navigate('/maps/' + m.id) + } + else if (t && window.location.pathname !== '/topics/' + t.id) { + Metamaps.Router.navigate('/topics/' + t.id) + } + }, 800) + } +}; // end Metamaps.Visualize diff --git a/app/assets/javascripts/src/Metamaps.js.erb b/app/assets/javascripts/src/Metamaps.js.erb index 592c7990..a2fd8a21 100644 --- a/app/assets/javascripts/src/Metamaps.js.erb +++ b/app/assets/javascripts/src/Metamaps.js.erb @@ -1,3123 +1,73 @@ -// TODO document this user agent function +/* global Metamaps */ -var labelType, useGradients, nativeTextSupport, animate; - -(function () { - var ua = navigator.userAgent, - iStuff = ua.match(/iPhone/i) || ua.match(/iPad/i), - typeOfCanvas = typeof HTMLCanvasElement, - nativeCanvasSupport = (typeOfCanvas == 'object' || typeOfCanvas == 'function'), - textSupport = nativeCanvasSupport && (typeof document.createElement('canvas').getContext('2d').fillText == 'function'); - //I'm setting this based on the fact that ExCanvas provides text support for IE - //and that as of today iPhone/iPad current text support is lame - labelType = (!nativeCanvasSupport || (textSupport && !iStuff)) ? 'Native' : 'HTML'; - nativeTextSupport = labelType == 'Native'; - useGradients = nativeCanvasSupport; - animate = !(iStuff || !nativeCanvasSupport); -})(); +/* + * Metamaps.js.erb + */ // TODO eliminate these 5 top-level variables -Metamaps.panningInt = null; -Metamaps.tempNode = null; -Metamaps.tempInit = false; -Metamaps.tempNode2 = null; +Metamaps.panningInt = null +Metamaps.tempNode = null +Metamaps.tempInit = false +Metamaps.tempNode2 = null Metamaps.VERSION = '<%= METAMAPS_VERSION %>' +/* erb variables from rails */ +Metamaps.Erb = {} +Metamaps.Erb['REALTIME_SERVER'] = '<%= ENV['REALTIME_SERVER'] %>' +Metamaps.Erb['junto_spinner_darkgrey.gif'] = '<%= asset_path('junto_spinner_darkgrey.gif') %>' +Metamaps.Erb['user.png'] = '<%= asset_path('user.png') %>' +Metamaps.Erb['icons/wildcard.png'] = '<%= asset_path('icons/wildcard.png') %>' +Metamaps.Erb['topic_description_signifier.png'] = '<%= asset_path('topic_description_signifier.png') %>' +Metamaps.Erb['topic_link_signifier.png'] = '<%= asset_path('topic_link_signifier.png') %>' +Metamaps.Erb['synapse16.png'] = '<%= asset_path('synapse16.png') %>' + Metamaps.Settings = { - embed: false, // indicates that the app is on a page that is optimized for embedding in iFrames on other web pages - sandbox: false, // puts the app into a mode (when true) where it only creates data locally, and isn't writing it to the database - colors: { - background: '#344A58', - synapses: { - normal: '#888888', - hover: '#888888', - selected: '#FFFFFF' - }, - topics: { - selected: '#FFFFFF' - }, - labels: { - background: '#18202E', - text: '#DDD' - } + embed: false, // indicates that the app is on a page that is optimized for embedding in iFrames on other web pages + sandbox: false, // puts the app into a mode (when true) where it only creates data locally, and isn't writing it to the database + colors: { + background: '#344A58', + synapses: { + normal: '#888888', + hover: '#888888', + selected: '#FFFFFF' }, -}; + topics: { + selected: '#FFFFFF' + }, + labels: { + background: '#18202E', + text: '#DDD' + } + }, +} Metamaps.Touch = { - touchPos: null, // this stores the x and y values of a current touch event - touchDragNode: null // this stores a reference to a JIT node that is being dragged -}; + touchPos: null, // this stores the x and y values of a current touch event + touchDragNode: null // this stores a reference to a JIT node that is being dragged +} Metamaps.Mouse = { - didPan: false, - didBoxZoom: false, - changeInX: 0, - changeInY: 0, - edgeHoveringOver: false, - boxStartCoordinates: false, - boxEndCoordinates: false, - synapseStartCoordinates: [], - synapseEndCoordinates: null, - lastNodeClick: 0, - lastCanvasClick: 0, - DOUBLE_CLICK_TOLERANCE: 300 -}; + didPan: false, + didBoxZoom: false, + changeInX: 0, + changeInY: 0, + edgeHoveringOver: false, + boxStartCoordinates: false, + boxEndCoordinates: false, + synapseStartCoordinates: [], + synapseEndCoordinates: null, + lastNodeClick: 0, + lastCanvasClick: 0, + DOUBLE_CLICK_TOLERANCE: 300 +} Metamaps.Selected = { - reset: function () { - var self = Metamaps.Selected; - - self.Nodes = []; - self.Edges = []; - }, - Nodes: [], - Edges: [] -}; - -/* - * - * BACKBONE - * - */ -Metamaps.Backbone.init = function () { - var self = Metamaps.Backbone; - - self.Metacode = Backbone.Model.extend({ - initialize: function () { - var image = new Image(); - image.crossOrigin = "Anonymous"; - image.src = this.get('icon'); - this.set('image',image); - }, - prepareLiForFilter: function () { - var li = ''; - li += '
  • ';       - li += '';       - li += '

    ' + this.get('name').toLowerCase() + '

  • '; - return li; - } - - }); - self.MetacodeCollection = Backbone.Collection.extend({ - model: this.Metacode, - url: '/metacodes', - comparator: function (a, b) { - a = a.get('name').toLowerCase(); - b = b.get('name').toLowerCase(); - return a > b ? 1 : a < b ? -1 : 0; - } - }); - - self.Topic = Backbone.Model.extend({ - urlRoot: '/topics', - blacklist: ['node', 'created_at', 'updated_at', 'user_name', 'user_image', 'map_count', 'synapse_count'], - toJSON: function (options) { - return _.omit(this.attributes, this.blacklist); - }, - save: function (key, val, options) { - - var attrs; - - // Handle both `"key", value` and `{key: value}` -style arguments. - if (key == null || typeof key === 'object') { - attrs = key; - options = val; - } else { - (attrs = {})[key] = val; - } - - var newOptions = options || {}; - var s = newOptions.success; - - var permBefore = this.get('permission'); - - newOptions.success = function (model, response, opt) { - if (s) s(model, response, opt); - model.trigger('saved'); - - if (permBefore === 'private' && model.get('permission') !== 'private') { - model.trigger('noLongerPrivate'); - } - else if (permBefore !== 'private' && model.get('permission') === 'private') { - model.trigger('nowPrivate'); - } - }; - return Backbone.Model.prototype.save.call(this, attrs, newOptions); - }, - initialize: function () { - if (this.isNew()) { - this.set({ - "user_id": Metamaps.Active.Mapper.id, - "desc": '', - "link": '', - "permission": Metamaps.Active.Map ? Metamaps.Active.Map.get('permission') : 'commons' - }); - } - - this.on('changeByOther', this.updateCardView); - this.on('change', this.updateNodeView); - this.on('saved', this.savedEvent); - this.on('nowPrivate', function(){ - var removeTopicData = { - mappableid: this.id - }; - - $(document).trigger(Metamaps.JIT.events.removeTopic, [removeTopicData]); - }); - this.on('noLongerPrivate', function(){ - var newTopicData = { - mappingid: this.getMapping().id, - mappableid: this.id - }; - - $(document).trigger(Metamaps.JIT.events.newTopic, [newTopicData]); - }); - - this.on('change:metacode_id', Metamaps.Filter.checkMetacodes, this); - - }, - authorizeToEdit: function (mapper) { - if (mapper && (this.get('permission') === "commons" || this.get('user_id') === mapper.get('id'))) return true; - else return false; - }, - authorizePermissionChange: function (mapper) { - if (mapper && this.get('user_id') === mapper.get('id')) return true; - else return false; - }, - getDate: function () { - - }, - getMetacode: function () { - return Metamaps.Metacodes.get(this.get('metacode_id')); - }, - getMapping: function () { - - if (!Metamaps.Active.Map) return false; - - return Metamaps.Mappings.findWhere({ - map_id: Metamaps.Active.Map.id, - mappable_type: "Topic", - mappable_id: this.isNew() ? this.cid : this.id - }); - }, - createNode: function () { - var mapping; - var node = { - adjacencies: [], - id: this.isNew() ? this.cid : this.id, - name: this.get('name') - }; - - if (Metamaps.Active.Map) { - mapping = this.getMapping(); - node.data = { - $mapping: null, - $mappingID: mapping.id - }; - } - - return node; - }, - updateNode: function () { - var mapping; - var node = this.get('node'); - node.setData('topic', this); - - if (Metamaps.Active.Map) { - mapping = this.getMapping(); - node.setData('mapping', mapping); - } - - return node; - }, - savedEvent: function() { - Metamaps.Realtime.sendTopicChange(this); - }, - updateViews: function() { - var onPageWithTopicCard = Metamaps.Active.Map || Metamaps.Active.Topic; - var node = this.get('node'); - // update topic card, if this topic is the one open there - if (onPageWithTopicCard && this == Metamaps.TopicCard.openTopicCard) { - Metamaps.TopicCard.showCard(node); - } - - // update the node on the map - if (onPageWithTopicCard && node) { - node.name = this.get('name'); - Metamaps.Visualize.mGraph.plot(); - } - }, - updateCardView: function() { - var onPageWithTopicCard = Metamaps.Active.Map || Metamaps.Active.Topic; - var node = this.get('node'); - // update topic card, if this topic is the one open there - if (onPageWithTopicCard && this == Metamaps.TopicCard.openTopicCard) { - Metamaps.TopicCard.showCard(node); - } - }, - updateNodeView: function() { - var onPageWithTopicCard = Metamaps.Active.Map || Metamaps.Active.Topic; - var node = this.get('node'); - - // update the node on the map - if (onPageWithTopicCard && node) { - node.name = this.get('name'); - Metamaps.Visualize.mGraph.plot(); - } - } - }); - - self.TopicCollection = Backbone.Collection.extend({ - model: self.Topic, - url: '/topics' - }); - - self.Synapse = Backbone.Model.extend({ - urlRoot: '/synapses', - blacklist: ['edge', 'created_at', 'updated_at'], - toJSON: function (options) { - return _.omit(this.attributes, this.blacklist); - }, - save: function (key, val, options) { - - var attrs; - - // Handle both `"key", value` and `{key: value}` -style arguments. - if (key == null || typeof key === 'object') { - attrs = key; - options = val; - } else { - (attrs = {})[key] = val; - } - - var newOptions = options || {}; - var s = newOptions.success; - - var permBefore = this.get('permission'); - - newOptions.success = function (model, response, opt) { - if (s) s(model, response, opt); - model.trigger('saved'); - - if (permBefore === 'private' && model.get('permission') !== 'private') { - model.trigger('noLongerPrivate'); - } - else if (permBefore !== 'private' && model.get('permission') === 'private') { - model.trigger('nowPrivate'); - } - }; - return Backbone.Model.prototype.save.call(this, attrs, newOptions); - }, - initialize: function () { - if (this.isNew()) { - this.set({ - "user_id": Metamaps.Active.Mapper.id, - "permission": Metamaps.Active.Map ? Metamaps.Active.Map.get('permission') : 'commons', - "category": "from-to" - }); - } - - this.on('changeByOther', this.updateCardView); - this.on('change', this.updateEdgeView); - this.on('saved', this.savedEvent); - this.on('noLongerPrivate', function(){ - var newSynapseData = { - mappingid: this.getMapping().id, - mappableid: this.id - }; - - $(document).trigger(Metamaps.JIT.events.newSynapse, [newSynapseData]); - }); - this.on('nowPrivate', function(){ - $(document).trigger(Metamaps.JIT.events.removeSynapse, [{ - mappableid: this.id - }]); - }); - - this.on('change:desc', Metamaps.Filter.checkSynapses, this); - }, - prepareLiForFilter: function () { - var li = ''; - li += '
  • ';       - li += '
  • '; - return li; - }, - authorizeToEdit: function (mapper) { - if (mapper && (this.get('permission') === "commons" || this.get('user_id') === mapper.get('id'))) return true; - else return false; - }, - authorizePermissionChange: function (mapper) { - if (mapper && this.get('user_id') === mapper.get('id')) return true; - else return false; - }, - getTopic1: function () { - return Metamaps.Topics.get(this.get('node1_id')); - }, - getTopic2: function () { - return Metamaps.Topics.get(this.get('node2_id')); - }, - getDirection: function () { - var t1 = this.getTopic1(), - t2 = this.getTopic2(); - - return t1 && t2 ? [ - t1.get('node').id, - t2.get('node').id - ] : false; - }, - getMapping: function () { - - if (!Metamaps.Active.Map) return false; - - return Metamaps.Mappings.findWhere({ - map_id: Metamaps.Active.Map.id, - mappable_type: "Synapse", - mappable_id: this.isNew() ? this.cid : this.id - }); - }, - createEdge: function (providedMapping) { - var mapping, mappingID; - var synapseID = this.isNew() ? this.cid : this.id; - - var edge = { - nodeFrom: this.get('node1_id'), - nodeTo: this.get('node2_id'), - data: { - $synapses: [], - $synapseIDs: [synapseID], - } - }; - - if (Metamaps.Active.Map) { - mapping = providedMapping || this.getMapping(); - mappingID = mapping.isNew() ? mapping.cid : mapping.id; - edge.data.$mappings = []; - edge.data.$mappingIDs = [mappingID]; - } - - return edge; - }, - updateEdge: function () { - var mapping; - var edge = this.get('edge'); - edge.getData('synapses').push(this); - - if (Metamaps.Active.Map) { - mapping = this.getMapping(); - edge.getData('mappings').push(mapping); - } - - return edge; - }, - savedEvent: function() { - Metamaps.Realtime.sendSynapseChange(this); - }, - updateViews: function() { - this.updateCardView(); - this.updateEdgeView(); - }, - updateCardView: function() { - var onPageWithSynapseCard = Metamaps.Active.Map || Metamaps.Active.Topic; - var edge = this.get('edge'); - - // update synapse card, if this synapse is the one open there - if (onPageWithSynapseCard && edge == Metamaps.SynapseCard.openSynapseCard) { - Metamaps.SynapseCard.showCard(edge); - } - }, - updateEdgeView: function() { - var onPageWithSynapseCard = Metamaps.Active.Map || Metamaps.Active.Topic; - var edge = this.get('edge'); - - // update the edge on the map - if (onPageWithSynapseCard && edge) { - Metamaps.Visualize.mGraph.plot(); - } - } - }); - - self.SynapseCollection = Backbone.Collection.extend({ - model: self.Synapse, - url: '/synapses' - }); - - self.Mapping = Backbone.Model.extend({ - urlRoot: '/mappings', - blacklist: ['created_at', 'updated_at'], - toJSON: function (options) { - return _.omit(this.attributes, this.blacklist); - }, - initialize: function () { - if (this.isNew()) { - this.set({ - "user_id": Metamaps.Active.Mapper.id, - "map_id": Metamaps.Active.Map ? Metamaps.Active.Map.id : null - }); - } - }, - getMap: function () { - return Metamaps.Map.get(this.get('map_id')); - }, - getTopic: function () { - if (this.get('mappable_type') === 'Topic') return Metamaps.Topic.get(this.get('mappable_id')); - else return false; - }, - getSynapse: function () { - if (this.get('mappable_type') === 'Synapse') return Metamaps.Synapse.get(this.get('mappable_id')); - else return false; - } - }); - - self.MappingCollection = Backbone.Collection.extend({ - model: self.Mapping, - url: '/mappings' - }); - - Metamaps.Metacodes = Metamaps.Metacodes ? new self.MetacodeCollection(Metamaps.Metacodes) : new self.MetacodeCollection(); - - Metamaps.Topics = Metamaps.Topics ? new self.TopicCollection(Metamaps.Topics) : new self.TopicCollection(); - - Metamaps.Synapses = Metamaps.Synapses ? new self.SynapseCollection(Metamaps.Synapses) : new self.SynapseCollection(); - - Metamaps.Mappers = Metamaps.Mappers ? new self.MapperCollection(Metamaps.Mappers) : new self.MapperCollection(); - - // this is for topic view - Metamaps.Creators = Metamaps.Creators ? new self.MapperCollection(Metamaps.Creators) : new self.MapperCollection(); - - if (Metamaps.Active.Map) { - Metamaps.Mappings = Metamaps.Mappings ? new self.MappingCollection(Metamaps.Mappings) : new self.MappingCollection(); - - Metamaps.Active.Map = new self.Map(Metamaps.Active.Map); - } - - if (Metamaps.Active.Topic) Metamaps.Active.Topic = new self.Topic(Metamaps.Active.Topic); - - //attach collection event listeners - self.attachCollectionEvents = function () { - - Metamaps.Topics.on("add remove", function(topic){ - Metamaps.Map.InfoBox.updateNumbers(); - Metamaps.Filter.checkMetacodes(); - Metamaps.Filter.checkMappers(); - }); - - Metamaps.Synapses.on("add remove", function(synapse){ - Metamaps.Map.InfoBox.updateNumbers(); - Metamaps.Filter.checkSynapses(); - Metamaps.Filter.checkMappers(); - }); - - if (Metamaps.Active.Map) { - Metamaps.Mappings.on("add remove", function(mapping){ - Metamaps.Map.InfoBox.updateNumbers(); - Metamaps.Filter.checkSynapses(); - Metamaps.Filter.checkMetacodes(); - Metamaps.Filter.checkMappers(); - }); - } - } - self.attachCollectionEvents(); -}; // end Metamaps.Backbone.init - - -/* - * - * CREATE - * - */ -Metamaps.Create = { - isSwitchingSet: false, // indicates whether the metacode set switch lightbox is open - selectedMetacodeSet: null, - selectedMetacodeSetIndex: null, - selectedMetacodeNames: [], - newSelectedMetacodeNames: [], - selectedMetacodes: [], - newSelectedMetacodes: [], - init: function () { - var self = Metamaps.Create; - self.newTopic.init(); - self.newSynapse.init(); - - ////// - ////// - //// SWITCHING METACODE SETS - - $('#metacodeSwitchTabs').tabs({ - selected: self.selectedMetacodeSetIndex - }).addClass("ui-tabs-vertical ui-helper-clearfix"); - $("#metacodeSwitchTabs .ui-tabs-nav li").removeClass("ui-corner-top").addClass("ui-corner-left"); - $('.customMetacodeList li').click(self.toggleMetacodeSelected); // within the custom metacode set tab - }, - toggleMetacodeSelected: function () { - var self = Metamaps.Create; - - if ($(this).attr('class') != 'toggledOff') { - $(this).addClass('toggledOff'); - var value_to_remove = $(this).attr('id'); - var name_to_remove = $(this).attr('data-name'); - self.newSelectedMetacodes.splice(self.newSelectedMetacodes.indexOf(value_to_remove), 1); - self.newSelectedMetacodeNames.splice(self.newSelectedMetacodeNames.indexOf(name_to_remove), 1); - } else if ($(this).attr('class') == 'toggledOff') { - $(this).removeClass('toggledOff'); - self.newSelectedMetacodes.push($(this).attr('id')); - self.newSelectedMetacodeNames.push($(this).attr('data-name')); - } - }, - updateMetacodeSet: function (set, index, custom) { - - if (custom && Metamaps.Create.newSelectedMetacodes.length == 0) { - alert('Please select at least one metacode to use!'); - return false; - } - - var codesToSwitchToIds; - var metacodeModels = new Metamaps.Backbone.MetacodeCollection(); - Metamaps.Create.selectedMetacodeSetIndex = index; - Metamaps.Create.selectedMetacodeSet = "metacodeset-" + set; - - if (!custom) { - codesToSwitchToIds = $('#metacodeSwitchTabs' + set).attr('data-metacodes').split(','); - $('.customMetacodeList li').addClass('toggledOff'); - Metamaps.Create.selectedMetacodes = []; - Metamaps.Create.selectedMetacodeNames = []; - Metamaps.Create.newSelectedMetacodes = []; - Metamaps.Create.newSelectedMetacodeNames = []; - } - else if (custom) { - // uses .slice to avoid setting the two arrays to the same actual array - Metamaps.Create.selectedMetacodes = Metamaps.Create.newSelectedMetacodes.slice(0); - Metamaps.Create.selectedMetacodeNames = Metamaps.Create.newSelectedMetacodeNames.slice(0); - codesToSwitchToIds = Metamaps.Create.selectedMetacodes.slice(0); - } - - // sort by name - for (var i = 0; i < codesToSwitchToIds.length; i++) { - metacodeModels.add( Metamaps.Metacodes.get(codesToSwitchToIds[i]) ); - }; - metacodeModels.sort(); - - $('#metacodeImg, #metacodeImgTitle').empty(); - $('#metacodeImg').removeData('cloudcarousel'); - var newMetacodes = ""; - metacodeModels.each(function(metacode){ - newMetacodes += '' + metacode.get('name') + ''; - }); - - $('#metacodeImg').empty().append(newMetacodes).CloudCarousel({ - titleBox: $('#metacodeImgTitle'), - yRadius: 40, - xRadius: 190, - xPos: 170, - yPos: 40, - speed: 0.3, - mouseWheel: true, - bringToFront: true - }); - - Metamaps.GlobalUI.closeLightbox(); - $('#topic_name').focus(); - - var mdata = { - "metacodes": { - "value": custom ? Metamaps.Create.selectedMetacodes.toString() : Metamaps.Create.selectedMetacodeSet - } - }; - $.ajax({ - type: "POST", - dataType: 'json', - url: "/user/updatemetacodes", - data: mdata, - success: function (data) { - console.log('selected metacodes saved'); - }, - error: function () { - console.log('failed to save selected metacodes'); - } - }); - }, - - cancelMetacodeSetSwitch: function () { - var self = Metamaps.Create; - self.isSwitchingSet = false; - - if (self.selectedMetacodeSet != "metacodeset-custom") { - $('.customMetacodeList li').addClass('toggledOff'); - self.selectedMetacodes = []; - self.selectedMetacodeNames = []; - self.newSelectedMetacodes = []; - self.newSelectedMetacodeNames = []; - } else { // custom set is selected - // reset it to the current actual selection - $('.customMetacodeList li').addClass('toggledOff'); - for (var i = 0; i < self.selectedMetacodes.length; i++) { - $('#' + self.selectedMetacodes[i]).removeClass('toggledOff'); - }; - // uses .slice to avoid setting the two arrays to the same actual array - self.newSelectedMetacodeNames = self.selectedMetacodeNames.slice(0); - self.newSelectedMetacodes = self.selectedMetacodes.slice(0); - } - $('#metacodeSwitchTabs').tabs("option", "active", self.selectedMetacodeSetIndex); - $('#topic_name').focus(); - }, - newTopic: { - init: function () { - - $('#topic_name').keyup(function () { - Metamaps.Create.newTopic.name = $(this).val(); - }); - - var topicBloodhound = new Bloodhound({ - datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), - queryTokenizer: Bloodhound.tokenizers.whitespace, - remote: { - url: '/topics/autocomplete_topic?term=%QUERY', - wildcard: '%QUERY', - }, - }); - - // initialize the autocomplete results for the metacode spinner - $('#topic_name').typeahead( - { - highlight: true, - minLength: 2, - }, - [{ - name: 'topic_autocomplete', - limit: 8, - display: function (s) { return s.label; }, - templates: { - suggestion: function(s) { - return Hogan.compile($('#topicAutocompleteTemplate').html()).render(s); - }, - }, - source: topicBloodhound, - }] - ); - - // tell the autocomplete to submit the form with the topic you clicked on if you pick from the autocomplete - $('#topic_name').bind('typeahead:select', function (event, datum, dataset) { - Metamaps.Topic.getTopicFromAutocomplete(datum.id); - }); - - // initialize metacode spinner and then hide it - $("#metacodeImg").CloudCarousel({ - titleBox: $('#metacodeImgTitle'), - yRadius: 40, - xRadius: 190, - xPos: 170, - yPos: 40, - speed: 0.3, - mouseWheel: true, - bringToFront: true - }); - $('.new_topic').hide(); - }, - name: null, - newId: 1, - beingCreated: false, - metacode: null, - x: null, - y: null, - addSynapse: false, - open: function () { - $('#new_topic').fadeIn('fast', function () { - $('#topic_name').focus(); - }); - Metamaps.Create.newTopic.beingCreated = true; - Metamaps.Create.newTopic.name = ""; - }, - hide: function () { - $('#new_topic').fadeOut('fast'); - $("#topic_name").typeahead('val', ''); - Metamaps.Create.newTopic.beingCreated = false; - } - }, - newSynapse: { - init: function () { - var self = Metamaps.Create.newSynapse; - - var synapseBloodhound = new Bloodhound({ - datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), - queryTokenizer: Bloodhound.tokenizers.whitespace, - remote: { - url: '/search/synapses?term=%QUERY', - wildcard: '%QUERY', - }, - }); - var existingSynapseBloodhound = new Bloodhound({ - datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), - queryTokenizer: Bloodhound.tokenizers.whitespace, - remote: { - url: '/search/synapses?topic1id=%TOPIC1&topic2id=%TOPIC2', - prepare: function(query, settings) { - var self = Metamaps.Create.newSynapse; - if (Metamaps.Selected.Nodes.length < 2) { - settings.url = settings.url.replace("%TOPIC1", self.topic1id).replace("%TOPIC2", self.topic2id); - return settings; - } else { - return null; - } - }, - }, - }); - - // initialize the autocomplete results for synapse creation - $('#synapse_desc').typeahead( - { - highlight: true, - minLength: 2, - }, - [{ - name: 'synapse_autocomplete', - display: function(s) { return s.label; }, - templates: { - suggestion: function(s) { - return Hogan.compile("
    {{label}}
    ").render(s); - }, - }, - source: synapseBloodhound, - }, - { - name: 'existing_synapses', - limit: 50, - display: function(s) { return s.label; }, - templates: { - suggestion: function(s) { - return Hogan.compile($('#synapseAutocompleteTemplate').html()).render(s); - }, - header: "

    Existing synapses

    " - }, - source: existingSynapseBloodhound, - }] - ); - - $('#synapse_desc').keyup(function (e) { - var ESC = 27, BACKSPACE = 8, DELETE = 46; - if (e.keyCode === BACKSPACE && $(this).val() === "" || - e.keyCode === DELETE && $(this).val() === "" || - e.keyCode === ESC) { - Metamaps.Create.newSynapse.hide(); - }//if - Metamaps.Create.newSynapse.description = $(this).val(); - }); - - $('#synapse_desc').focusout(function() { - if (Metamaps.Create.newSynapse.beingCreated) { - Metamaps.Synapse.createSynapseLocally(); - } - }); - - $('#synapse_desc').bind('typeahead:select', function (event, datum, dataset) { - if (datum.id) { // if they clicked on an existing synapse get it - Metamaps.Synapse.getSynapseFromAutocomplete(datum.id); - } - else { - Metamaps.Create.newSynapse.description = datum.value; - Metamaps.Synapse.createSynapseLocally(); - } - }); - }, - beingCreated: false, - description: null, - topic1id: null, - topic2id: null, - newSynapseId: null, - open: function () { - $('#new_synapse').fadeIn(100, function () { - $('#synapse_desc').focus(); - }); - Metamaps.Create.newSynapse.beingCreated = true; - }, - hide: function () { - $('#new_synapse').fadeOut('fast'); - $("#synapse_desc").typeahead('val', ''); - Metamaps.Create.newSynapse.beingCreated = false; - Metamaps.Create.newTopic.addSynapse = false; - Metamaps.Create.newSynapse.topic1id = 0; - Metamaps.Create.newSynapse.topic2id = 0; - Metamaps.Mouse.synapseStartCoordinates = []; - Metamaps.Visualize.mGraph.plot(); - }, - } -}; // end Metamaps.Create - - -////////////////// TOPIC AND SYNAPSE CARDS ////////////////////////// - - -/* - * - * TOPICCARD - * - */ -Metamaps.TopicCard = { - openTopicCard: null, //stores the topic that's currently open - authorizedToEdit: false, // stores boolean for edit permission for open topic card - init: function () { - var self = Metamaps.TopicCard; - - // initialize best_in_place editing - $('.authenticated div.permission.canEdit .best_in_place').best_in_place(); - - Metamaps.TopicCard.generateShowcardHTML = Hogan.compile($('#topicCardTemplate').html()); - - // initialize topic card draggability and resizability - $('.showcard').draggable({ - handle: ".metacodeImage" - }); - - embedly('on', 'card.rendered', self.embedlyCardRendered); - }, - /** - * Will open the Topic Card for the node that it's passed - * @param {$jit.Graph.Node} node - */ - showCard: function (node) { - var self = Metamaps.TopicCard; - - var topic = node.getData('topic'); - - self.openTopicCard = topic; - self.authorizedToEdit = topic.authorizeToEdit(Metamaps.Active.Mapper); - //populate the card that's about to show with the right topics data - self.populateShowCard(topic); - $('.showcard').fadeIn('fast'); - }, - hideCard: function () { - var self = Metamaps.TopicCard; - - $('.showcard').fadeOut('fast'); - self.openTopicCard = null; - self.authorizedToEdit = false; - }, - embedlyCardRendered: function (iframe) { - var self = Metamaps.TopicCard; - - $('#embedlyLinkLoader').hide(); - - // means that the embedly call returned 404 not found - if ($('#embedlyLink')[0]) { - $('#embedlyLink').css('display', 'block').fadeIn('fast'); - $('.embeds').addClass('nonEmbedlyLink'); - } - - $('.CardOnGraph').addClass('hasAttachment'); - if (self.authorizedToEdit) { - $('.embeds').append('
    '); - $('#linkremove').click(self.removeLink); - } - }, - removeLink: function () { - var self = Metamaps.TopicCard; - self.openTopicCard.save({ - link: null - }); - $('.embeds').empty().removeClass('nonEmbedlyLink'); - $('#addLinkInput input').val(""); - $('.attachments').removeClass('hidden'); - $('.CardOnGraph').removeClass('hasAttachment'); - }, - bindShowCardListeners: function (topic) { - var self = Metamaps.TopicCard; - var showCard = document.getElementById('showcard'); - - var authorized = self.authorizedToEdit; - - // get mapper image - var setMapperImage = function (mapper) { - $('.contributorIcon').attr('src', mapper.get('image')); - }; - Metamaps.Mapper.get(topic.get('user_id'), setMapperImage); - - // starting embed.ly - var resetFunc = function () { - $('#addLinkInput input').val(""); - $('#addLinkInput input').focus(); - }; - var inputEmbedFunc = function (event) { - - var element = this; - setTimeout(function () { - var text = $(element).val(); - if (event.type=="paste" || (event.type=="keyup" && event.which==13)){ - // TODO evaluate converting this to '//' no matter what (infer protocol) - if (text.slice(0, 7) !== 'http://' && - text.slice(0, 8) !== 'https://' && - text.slice(0, 2) !== '//') { - text='//'+text; - } - topic.save({ - link: text - }); - var embedlyEl = $('', { - id: 'embedlyLink', - 'data-card-description': '0', - href: text - }).html(text); - $('.attachments').addClass('hidden'); - $('.embeds').append(embedlyEl); - $('.embeds').append('
    '); - var loader = new CanvasLoader('embedlyLinkLoader'); - loader.setColor('#4fb5c0'); // default is '#000000' - loader.setDiameter(28); // default is 40 - loader.setDensity(41); // default is 40 - loader.setRange(0.9); // default is 1.3 - loader.show(); // Hidden by default - var e = embedly('card', document.getElementById('embedlyLink')); - if (!e) { - self.handleInvalidLink(); - } - } - }, 100); - }; - $('#addLinkReset').click(resetFunc); - $('#addLinkInput input').bind("paste keyup",inputEmbedFunc); - - // initialize the link card, if there is a link - if (topic.get('link') && topic.get('link') !== '') { - var loader = new CanvasLoader('embedlyLinkLoader'); - loader.setColor('#4fb5c0'); // default is '#000000' - loader.setDiameter(28); // default is 40 - loader.setDensity(41); // default is 40 - loader.setRange(0.9); // default is 1.3 - loader.show(); // Hidden by default - var e = embedly('card', document.getElementById('embedlyLink')); - if (!e) { - self.handleInvalidLink(); - } - } - - - var selectingMetacode = false; - // attach the listener that shows the metacode title when you hover over the image - $('.showcard .metacodeImage').mouseenter(function () { - $('.showcard .icon').css('z-index', '4'); - $('.showcard .metacodeTitle').show(); - }); - $('.showcard .linkItem.icon').mouseleave(function () { - if (!selectingMetacode) { - $('.showcard .metacodeTitle').hide(); - $('.showcard .icon').css('z-index', '1'); - } - }); - - var metacodeLiClick = function () { - selectingMetacode = false; - var metacodeId = parseInt($(this).attr('data-id')); - var metacode = Metamaps.Metacodes.get(metacodeId); - $('.CardOnGraph').find('.metacodeTitle').html(metacode.get('name')) - .append('
    ') - .attr('class', 'metacodeTitle mbg' + metacode.id); - $('.CardOnGraph').find('.metacodeImage').css('background-image', 'url(' + metacode.get('icon') + ')'); - topic.save({ - metacode_id: metacode.id - }); - Metamaps.Visualize.mGraph.plot(); - $('.metacodeSelect').hide().removeClass('onRightEdge onBottomEdge'); - $('.metacodeTitle').hide(); - $('.showcard .icon').css('z-index', '1'); - }; - - var openMetacodeSelect = function (event) { - var windowWidth; - var showcardLeft; - var TOPICCARD_WIDTH = 300; - var METACODESELECT_WIDTH = 404; - var distanceFromEdge; - - var MAX_METACODELIST_HEIGHT = 270; - var windowHeight; - var showcardTop; - var topicTitleHeight; - var distanceFromBottom; - - if (!selectingMetacode) { - selectingMetacode = true; - - // this is to make sure the metacode - // select is accessible onscreen, when opened - // while topic card is close to the right - // edge of the screen - windowWidth = $(window).width(); - showcardLeft = parseInt($('.showcard').css('left')); - distanceFromEdge = windowWidth - (showcardLeft + TOPICCARD_WIDTH); - if (distanceFromEdge < METACODESELECT_WIDTH) { - $('.metacodeSelect').addClass('onRightEdge'); - } - - // this is to make sure the metacode - // select is accessible onscreen, when opened - // while topic card is close to the bottom - // edge of the screen - windowHeight = $(window).height(); - showcardTop = parseInt($('.showcard').css('top')); - topicTitleHeight = $('.showcard .title').height() + parseInt($('.showcard .title').css('padding-top')) + parseInt($('.showcard .title').css('padding-bottom')); - heightOfSetList = $('.showcard .metacodeSelect').height(); - distanceFromBottom = windowHeight - (showcardTop + topicTitleHeight); - if (distanceFromBottom < MAX_METACODELIST_HEIGHT) { - $('.metacodeSelect').addClass('onBottomEdge'); - } - - $('.metacodeSelect').show(); - event.stopPropagation(); - } - }; - - var hideMetacodeSelect = function () { - selectingMetacode = false; - $('.metacodeSelect').hide().removeClass('onRightEdge onBottomEdge'); - $('.metacodeTitle').hide(); - $('.showcard .icon').css('z-index', '1'); - }; - - if (authorized) { - $('.showcard .metacodeTitle').click(openMetacodeSelect); - $('.showcard').click(hideMetacodeSelect); - $('.metacodeSelect > ul > li').click(function (event){ - event.stopPropagation(); - }); - $('.metacodeSelect li li').click(metacodeLiClick); - - var bipName = $(showCard).find('.best_in_place_name'); - bipName.bind("best_in_place:activate", function () { - var $el = bipName.find('textarea'); - var el = $el[0]; - - $el.attr('maxlength', '140'); - - $('.showcard .title').append('
    '); - - var callback = function (data) { - $('.nameCounter.forTopic').html(data.all + '/140'); - }; - Countable.live(el, callback); - }); - bipName.bind("best_in_place:deactivate", function () { - $('.nameCounter.forTopic').remove(); - }); - - //bind best_in_place ajax callbacks - bipName.bind("ajax:success", function () { - var name = Metamaps.Util.decodeEntities($(this).html()); - topic.set("name", name); - topic.trigger('saved'); - }); - - $(showCard).find('.best_in_place_desc').bind("ajax:success", function () { - this.innerHTML = this.innerHTML.replace(/\r/g, ''); - var desc = $(this).html() === $(this).data('nil') ? "" : $(this).html(); - topic.set("desc", desc); - topic.trigger('saved'); - }); - } - - - var permissionLiClick = function (event) { - selectingPermission = false; - var permission = $(this).attr('class'); - topic.save({ - permission: permission - }); - $('.showcard .mapPerm').removeClass('co pu pr minimize').addClass(permission.substring(0, 2)); - $('.showcard .permissionSelect').remove(); - event.stopPropagation(); - }; - - var openPermissionSelect = function (event) { - if (!selectingPermission) { - selectingPermission = true; - $(this).addClass('minimize'); // this line flips the drop down arrow to a pull up arrow - if ($(this).hasClass('co')) { - $(this).append('
    '); - } else if ($(this).hasClass('pu')) { - $(this).append('
    '); - } else if ($(this).hasClass('pr')) { - $(this).append('
    '); - } - $('.showcard .permissionSelect li').click(permissionLiClick); - event.stopPropagation(); - } - }; - - var hidePermissionSelect = function () { - selectingPermission = false; - $('.showcard .yourTopic .mapPerm').removeClass('minimize'); // this line flips the pull up arrow to a drop down arrow - $('.showcard .permissionSelect').remove(); - }; - // ability to change permission - var selectingPermission = false; - if (topic.authorizePermissionChange(Metamaps.Active.Mapper)) { - $('.showcard .yourTopic .mapPerm').click(openPermissionSelect); - $('.showcard').click(hidePermissionSelect); - } - - $('.links .mapCount').unbind().click(function(event){ - $('.mapCount .tip').toggle(); - $('.showcard .hoverTip').toggleClass('hide'); - event.stopPropagation(); - }); - $('.mapCount .tip').unbind().click(function(event){ - event.stopPropagation(); - }); - $('.showcard').unbind('.hideTip').bind('click.hideTip', function(){ - $('.mapCount .tip').hide(); - $('.showcard .hoverTip').removeClass('hide'); - }); - - $('.mapCount .tip li a').click(Metamaps.Router.intercept); - - var originalText = $('.showMore').html(); - $('.mapCount .tip .showMore').unbind().toggle( - function(event){ - $('.extraText').toggleClass("hideExtra"); - $('.showMore').html('Show less...'); - }, - function(event){ - $('.extraText').toggleClass("hideExtra"); - $('.showMore').html(originalText); - }); - - $('.mapCount .tip showMore').unbind().click(function(event){ - event.stopPropagation(); - }); - }, - handleInvalidLink: function() { - var self = Metamaps.TopicCard; - - self.removeLink(); - Metamaps.GlobalUI.notifyUser("Invalid link"); - }, - populateShowCard: function (topic) { - var self = Metamaps.TopicCard; - - var showCard = document.getElementById('showcard'); - - $(showCard).find('.permission').remove(); - - var topicForTemplate = self.buildObject(topic); - var html = self.generateShowcardHTML.render(topicForTemplate); - - if (topic.authorizeToEdit(Metamaps.Active.Mapper)) { - var perm = document.createElement('div'); - - var string = 'permission canEdit'; - if (topic.authorizePermissionChange(Metamaps.Active.Mapper)) string += ' yourTopic'; - perm.className = string; - perm.innerHTML = html; - showCard.appendChild(perm); - } else { - var perm = document.createElement('div'); - perm.className = 'permission cannotEdit'; - perm.innerHTML = html; - showCard.appendChild(perm); - } - - Metamaps.TopicCard.bindShowCardListeners(topic); - }, - generateShowcardHTML: null, // will be initialized into a Hogan template within init function - //generateShowcardHTML - buildObject: function (topic) { - var self=Metamaps.TopicCard; - - var nodeValues = {}; - - var authorized = topic.authorizeToEdit(Metamaps.Active.Mapper); - - if (!authorized) { - - } else { - - } - - var desc_nil = "Click to add description..."; - - nodeValues.attachmentsHidden = ''; - if (topic.get('link') && topic.get('link')!== '') { - nodeValues.embeds = '
    '; - nodeValues.embeds += topic.get('link'); - nodeValues.embeds += '
    '; - nodeValues.attachmentsHidden = 'hidden'; - nodeValues.hasAttachment = "hasAttachment"; - } - else { - nodeValues.embeds = ''; - nodeValues.hasAttachment = ''; - } - - if (authorized) { - nodeValues.attachments = ''; - } else { - nodeValues.attachmentsHidden = 'hidden'; - nodeValues.attachments = ''; - } - - var inmapsAr = topic.get("inmaps"); - var inmapsLinks = topic.get("inmapsLinks"); - nodeValues.inmaps =''; - if (inmapsAr.length < 6) { - for (i = 0; i < inmapsAr.length; i++) { - var url = "/maps/" + inmapsLinks[i]; - nodeValues.inmaps += '
  • ' + inmapsAr[i]+ '
  • '; - } - } - else { - for (i = 0; i < 5; i++){ - var url = "/maps/" + inmapsLinks[i]; - nodeValues.inmaps += '
  • ' + inmapsAr[i] + '
  • '; - } - extra = inmapsAr.length - 5; - nodeValues.inmaps += '
  • See ' + extra + ' more...
  • ' - for (i = 5; i < inmapsAr.length; i++){ - var url = "/maps/" + inmapsLinks[i]; - nodeValues.inmaps += '
  • ' + inmapsAr[i]+ '
  • '; - } - } - nodeValues.permission = topic.get("permission"); - nodeValues.mk_permission = topic.get("permission").substring(0, 2); - nodeValues.map_count = topic.get("map_count").toString(); - nodeValues.synapse_count = topic.get("synapse_count").toString(); - nodeValues.id = topic.isNew() ? topic.cid : topic.id; - nodeValues.metacode = topic.getMetacode().get("name"); - nodeValues.metacode_class = 'mbg' + topic.get('metacode_id'); - nodeValues.imgsrc = topic.getMetacode().get("icon"); - nodeValues.name = topic.get("name"); - nodeValues.userid = topic.get("user_id"); - nodeValues.username = topic.get("user_name"); - nodeValues.date = topic.getDate(); - // the code for this is stored in /views/main/_metacodeOptions.html.erb - nodeValues.metacode_select = $('#metacodeOptions').html(); - nodeValues.desc_nil = desc_nil; - nodeValues.desc = (topic.get("desc") == "" && authorized) ? desc_nil : topic.get("desc"); - return nodeValues; - } -}; // end Metamaps.TopicCard - - -/* - * - * SYNAPSECARD - * - */ -Metamaps.SynapseCard = { - openSynapseCard: null, - showCard: function (edge, e) { - var self = Metamaps.SynapseCard; - - //reset so we don't interfere with other edges, but first, save its x and y - var myX = $('#edit_synapse').css('left'); - var myY = $('#edit_synapse').css('top'); - $('#edit_synapse').remove(); - - //so label is missing while editing - Metamaps.Control.deselectEdge(edge); - - var index = edge.getData("displayIndex") ? edge.getData("displayIndex") : 0; - var synapse = edge.getData('synapses')[index]; // for now, just get the first synapse - - //create the wrapper around the form elements, including permissions - //classes to make best_in_place happy - var edit_div = document.createElement('div'); - edit_div.innerHTML = '
    '; - edit_div.setAttribute('id', 'edit_synapse'); - if (synapse.authorizeToEdit(Metamaps.Active.Mapper)) { - edit_div.className = 'permission canEdit'; - edit_div.className += synapse.authorizePermissionChange(Metamaps.Active.Mapper) ? ' yourEdge' : ''; - } else { - edit_div.className = 'permission cannotEdit'; - } - $('#wrapper').append(edit_div); - - self.populateShowCard(edge, synapse); - - //drop it in the right spot, activate it - $('#edit_synapse').css('position', 'absolute'); - if (e) { - $('#edit_synapse').css('left', e.clientX); - $('#edit_synapse').css('top', e.clientY); - } else { - $('#edit_synapse').css('left', myX); - $('#edit_synapse').css('top', myY); - } - //$('#edit_synapse_name').click(); //required in case name is empty - //$('#edit_synapse_name input').focus(); - $('#edit_synapse').show(); - - self.openSynapseCard = edge; - }, - - hideCard: function () { - $('#edit_synapse').remove(); - Metamaps.SynapseCard.openSynapseCard = null; - }, - - populateShowCard: function (edge, synapse) { - var self = Metamaps.SynapseCard; - - self.add_synapse_count(edge); - self.add_desc_form(synapse); - self.add_drop_down(edge, synapse); - self.add_user_info(synapse); - self.add_perms_form(synapse); - self.add_direction_form(synapse); - }, - add_synapse_count: function (edge) { - var count = edge.getData("synapses").length; - - $('#editSynUpperBar').append('
    ' + count + '
    ') - }, - add_desc_form: function (synapse) { - var data_nil = 'Click to add description.'; - - // TODO make it so that this would work even in sandbox mode, - // currently with Best_in_place it won't - - //desc editing form - $('#editSynUpperBar').append('
    '); - $('#edit_synapse_desc').attr('class', 'best_in_place best_in_place_desc'); - $('#edit_synapse_desc').attr('data-object', 'synapse'); - $('#edit_synapse_desc').attr('data-attribute', 'desc'); - $('#edit_synapse_desc').attr('data-type', 'textarea'); - $('#edit_synapse_desc').attr('data-nil', data_nil); - $('#edit_synapse_desc').attr('data-url', '/synapses/' + synapse.id); - $('#edit_synapse_desc').html(synapse.get("desc")); - - //if edge data is blank or just whitespace, populate it with data_nil - if ($('#edit_synapse_desc').html().trim() == '') { - if (synapse.authorizeToEdit(Metamaps.Active.Mapper)) { - $('#edit_synapse_desc').html(data_nil); - } - else { - $('#edit_synapse_desc').html("(no description)"); - } - } - - $('#edit_synapse_desc').bind("ajax:success", function () { - var desc = $(this).html(); - if (desc == data_nil) { - synapse.set("desc", ''); - } else { - synapse.set("desc", desc); - } - synapse.trigger('saved'); - Metamaps.Control.selectEdge(synapse.get('edge')); - Metamaps.Visualize.mGraph.plot(); - }); - }, - add_drop_down: function (edge, synapse) { - var list, i, synapses, l, desc; - - synapses = edge.getData("synapses"); - l = synapses.length; - - if (l > 1) { - // append the element that you click to show dropdown select - $('#editSynUpperBar').append(''); - $('#dropdownSynapses').click(function(e){ - e.preventDefault(); - e.stopPropagation(); // stop it from immediately closing it again - $('#switchSynapseList').toggle(); - }); - // hide the dropdown again if you click anywhere else on the synapse card - $('#edit_synapse').click(function(){ - $('#switchSynapseList').hide(); - }); - - // generate the list of other synapses - list = '
      '; - for (i = 0; i < l; i++) { - if (synapses[i] !== synapse) { // don't add the current one to the list - desc = synapses[i].get('desc'); - desc = desc === "" || desc === null ? "(no description)" : desc; - list += '
    • ' + desc + '
    • '; - } - } - list += '
    ' - // add the list of the other synapses - $('#editSynLowerBar').append(list); - - // attach click listeners to list items that - // will cause it to switch the displayed synapse - // when you click it - $('#switchSynapseList li').click(function(e){ - e.stopPropagation(); - var index = parseInt($(this).attr('data-synapse-index')); - edge.setData('displayIndex', index); - Metamaps.Visualize.mGraph.plot(); - Metamaps.SynapseCard.showCard(edge, false); - }); - } - }, - add_user_info: function (synapse) { - var u = '
    '; - u += ' ' - u += '
    ' + synapse.get("user_name") + '
    '; - $('#editSynLowerBar').append(u); - - // get mapper image - var setMapperImage = function (mapper) { - $('#edgeUser img').attr('src', mapper.get('image')); - }; - Metamaps.Mapper.get(synapse.get('user_id'), setMapperImage); - }, - - add_perms_form: function (synapse) { - //permissions - if owner, also allow permission editing - $('#editSynLowerBar').append('
    '); - - // ability to change permission - var selectingPermission = false; - var permissionLiClick = function (event) { - selectingPermission = false; - var permission = $(this).attr('class'); - synapse.save({ - permission: permission - }); - $('#edit_synapse .mapPerm').removeClass('co pu pr minimize').addClass(permission.substring(0, 2)); - $('#edit_synapse .permissionSelect').remove(); - event.stopPropagation(); - }; - - var openPermissionSelect = function (event) { - if (!selectingPermission) { - selectingPermission = true; - $(this).addClass('minimize'); // this line flips the drop down arrow to a pull up arrow - if ($(this).hasClass('co')) { - $(this).append('
    '); - } else if ($(this).hasClass('pu')) { - $(this).append('
    '); - } else if ($(this).hasClass('pr')) { - $(this).append('
    '); - } - $('#edit_synapse .permissionSelect li').click(permissionLiClick); - event.stopPropagation(); - } - }; - - var hidePermissionSelect = function () { - selectingPermission = false; - $('#edit_synapse.yourEdge .mapPerm').removeClass('minimize'); // this line flips the pull up arrow to a drop down arrow - $('#edit_synapse .permissionSelect').remove(); - }; - - if (synapse.authorizePermissionChange(Metamaps.Active.Mapper)) { - $('#edit_synapse.yourEdge .mapPerm').click(openPermissionSelect); - $('#edit_synapse').click(hidePermissionSelect); - } - }, //add_perms_form - - add_direction_form: function (synapse) { - //directionality checkboxes - $('#editSynLowerBar').append('
    '); - $('#editSynLowerBar').append('
    '); - - var edge = synapse.get('edge'); - - //determine which node is to the left and the right - //if directly in a line, top is left - if (edge.nodeFrom.pos.x < edge.nodeTo.pos.x || - edge.nodeFrom.pos.x == edge.nodeTo.pos.x && - edge.nodeFrom.pos.y < edge.nodeTo.pos.y) { - var left = edge.nodeTo.getData("topic"); - var right = edge.nodeFrom.getData("topic"); - } else { - var left = edge.nodeFrom.getData("topic"); - var right = edge.nodeTo.getData("topic"); - } - - /* - * One node is actually on the left onscreen. Call it left, & the other right. - * If category is from-to, and that node is first, check the 'right' checkbox. - * Else check the 'left' checkbox since the arrow is incoming. - */ - - var directionCat = synapse.get('category'); //both, none, from-to - if (directionCat == 'from-to') { - var from_to = [synapse.get("node1_id"), synapse.get("node2_id")]; - if (from_to[0] == left.id) { - //check left checkbox - $('#edit_synapse_left').addClass('checked'); - } else { - //check right checkbox - $('#edit_synapse_right').addClass('checked'); - } - } else if (directionCat == 'both') { - //check both checkboxes - $('#edit_synapse_left').addClass('checked'); - $('#edit_synapse_right').addClass('checked'); - } - - if (synapse.authorizeToEdit(Metamaps.Active.Mapper)) { - $('#edit_synapse_left, #edit_synapse_right').click(function () { - - $(this).toggleClass('checked'); - - var leftChecked = $('#edit_synapse_left').is('.checked'); - var rightChecked = $('#edit_synapse_right').is('.checked'); - - var dir = synapse.getDirection(); - var dirCat = 'none'; - if (leftChecked && rightChecked) { - dirCat = 'both'; - } else if (!leftChecked && rightChecked) { - dirCat = 'from-to'; - dir = [right.id, left.id]; - } else if (leftChecked && !rightChecked) { - dirCat = 'from-to'; - dir = [left.id, right.id]; - } - - synapse.save({ - category: dirCat, - node1_id: dir[0], - node2_id: dir[1] - }); - Metamaps.Visualize.mGraph.plot(); - }); - } // if - } //add_direction_form -}; // end Metamaps.SynapseCard - - -////////////////////// END TOPIC AND SYNAPSE CARDS ////////////////////////////////// - - -/* - * - * VISUALIZE - * - */ -Metamaps.Visualize = { - mGraph: null, // a reference to the graph object. - cameraPosition: null, // stores the camera position when using a 3D visualization - type: "ForceDirected", // the type of graph we're building, could be "RGraph", "ForceDirected", or "ForceDirected3D" - loadLater: false, // indicates whether there is JSON that should be loaded right in the offset, or whether to wait till the first topic is created - init: function () { - var self = Metamaps.Visualize; - // disable awkward dragging of the canvas element that would sometimes happen - $('#infovis-canvas').on('dragstart', function (event) { - event.preventDefault(); - }); - - // prevent touch events on the canvas from default behaviour - $("#infovis-canvas").bind('touchstart', function (event) { - event.preventDefault(); - self.mGraph.events.touched = true; - }); - - // prevent touch events on the canvas from default behaviour - $("#infovis-canvas").bind('touchmove', function (event) { - //Metamaps.JIT.touchPanZoomHandler(event); - }); - - // prevent touch events on the canvas from default behaviour - $("#infovis-canvas").bind('touchend touchcancel', function (event) { - lastDist = 0; - if (!self.mGraph.events.touchMoved && !Metamaps.Touch.touchDragNode) Metamaps.TopicCard.hideCurrentCard(); - self.mGraph.events.touched = self.mGraph.events.touchMoved = false; - Metamaps.Touch.touchDragNode = false; - }); - }, - computePositions: function () { - var self = Metamaps.Visualize, - mapping; - - if (self.type == "RGraph") { - var i, l, startPos, endPos, topic, synapse; - - self.mGraph.graph.eachNode(function (n) { - topic = Metamaps.Topics.get(n.id); - topic.set({ node: n }, { silent: true }); - topic.updateNode(); - - n.eachAdjacency(function (edge) { - if(!edge.getData('init')) { - edge.setData('init', true); - - l = edge.getData('synapseIDs').length; - for (i = 0; i < l; i++) { - synapse = Metamaps.Synapses.get(edge.getData('synapseIDs')[i]); - synapse.set({ edge: edge }, { silent: true }); - synapse.updateEdge(); - } - } - }); - - var pos = n.getPos(); - pos.setc(-200, -200); - }); - self.mGraph.compute('end'); - } else if (self.type == "ForceDirected") { - var i, l, startPos, endPos, topic, synapse; - - self.mGraph.graph.eachNode(function (n) { - topic = Metamaps.Topics.get(n.id); - topic.set({ node: n }, { silent: true }); - topic.updateNode(); - mapping = topic.getMapping(); - - n.eachAdjacency(function (edge) { - if(!edge.getData('init')) { - edge.setData('init', true); - - l = edge.getData('synapseIDs').length; - for (i = 0; i < l; i++) { - synapse = Metamaps.Synapses.get(edge.getData('synapseIDs')[i]); - synapse.set({ edge: edge }, { silent: true }); - synapse.updateEdge(); - } - } - }); - - startPos = new $jit.Complex(0, 0); - endPos = new $jit.Complex(mapping.get('xloc'), mapping.get('yloc')); - n.setPos(startPos, 'start'); - n.setPos(endPos, 'end'); - }); - } else if (self.type == "ForceDirected3D") { - self.mGraph.compute(); - } - }, - /** - * render does the heavy lifting of creating the engine that renders the graph with the properties we desire - * - */ - render: function () { - var self = Metamaps.Visualize, RGraphSettings, FDSettings; - - if (self.type == "RGraph" && (!self.mGraph || self.mGraph instanceof $jit.ForceDirected)) { - - RGraphSettings = $.extend(true, {}, Metamaps.JIT.ForceDirected.graphSettings); - - $jit.RGraph.Plot.NodeTypes.implement(Metamaps.JIT.ForceDirected.nodeSettings); - $jit.RGraph.Plot.EdgeTypes.implement(Metamaps.JIT.ForceDirected.edgeSettings); - - RGraphSettings.width = $(document).width(); - RGraphSettings.height = $(document).height(); - RGraphSettings.background = Metamaps.JIT.RGraph.background; - RGraphSettings.levelDistance = Metamaps.JIT.RGraph.levelDistance; - - self.mGraph = new $jit.RGraph(RGraphSettings); - - } else if (self.type == "ForceDirected" && (!self.mGraph || self.mGraph instanceof $jit.RGraph)) { - - FDSettings = $.extend(true, {}, Metamaps.JIT.ForceDirected.graphSettings); - - $jit.ForceDirected.Plot.NodeTypes.implement(Metamaps.JIT.ForceDirected.nodeSettings); - $jit.ForceDirected.Plot.EdgeTypes.implement(Metamaps.JIT.ForceDirected.edgeSettings); - - FDSettings.width = $('body').width(); - FDSettings.height = $('body').height(); - - self.mGraph = new $jit.ForceDirected(FDSettings); - - } else if (self.type == "ForceDirected3D" && !self.mGraph) { - // init ForceDirected3D - self.mGraph = new $jit.ForceDirected3D(Metamaps.JIT.ForceDirected3D.graphSettings); - self.cameraPosition = self.mGraph.canvas.canvases[0].camera.position; - } - else { - self.mGraph.graph.empty(); - } - - function runAnimation() { - Metamaps.Loading.hide(); - // load JSON data, if it's not empty - if (!self.loadLater) { - //load JSON data. - var rootIndex = 0; - if (Metamaps.Active.Topic) { - var node = _.find(Metamaps.JIT.vizData, function(node){ - return node.id === Metamaps.Active.Topic.id; - }); - rootIndex = _.indexOf(Metamaps.JIT.vizData, node); - } - self.mGraph.loadJSON(Metamaps.JIT.vizData, rootIndex); - //compute positions and plot. - self.computePositions(); - self.mGraph.busy = true; - if (self.type == "RGraph") { - self.mGraph.fx.animate(Metamaps.JIT.RGraph.animate); - } else if (self.type == "ForceDirected") { - self.mGraph.animate(Metamaps.JIT.ForceDirected.animateSavedLayout); - } else if (self.type == "ForceDirected3D") { - self.mGraph.animate(Metamaps.JIT.ForceDirected.animateFDLayout); - } - } - } - // hold until all the needed metacode images are loaded - // hold for a maximum of 80 passes, or 4 seconds of waiting time - var tries = 0; - function hold() { - var unique = _.uniq(Metamaps.Topics.models, function (metacode) { return metacode.get('metacode_id'); }), - requiredMetacodes = _.map(unique, function (metacode) { return metacode.get('metacode_id'); }), - loadedCount = 0; - - _.each(requiredMetacodes, function (metacode_id) { - var metacode = Metamaps.Metacodes.get(metacode_id), - img = metacode ? metacode.get('image') : false; - - if (img && (img.complete || (typeof img.naturalWidth !== "undefined" && img.naturalWidth !== 0))) { - loadedCount += 1; - } - }); - - if (loadedCount === requiredMetacodes.length || tries > 80) runAnimation(); - else setTimeout(function(){ tries++; hold() }, 50); - } - hold(); - - // update the url now that the map is ready - clearTimeout(Metamaps.Router.timeoutId); - Metamaps.Router.timeoutId = setTimeout(function(){ - var m = Metamaps.Active.Map; - var t = Metamaps.Active.Topic; - - if (m && window.location.pathname !== "/maps/" + m.id) { - Metamaps.Router.navigate("/maps/" + m.id); - } - else if (t && window.location.pathname !== "/topics/" + t.id) { - Metamaps.Router.navigate("/topics/" + t.id); - } - }, 800); - - } -}; // end Metamaps.Visualize - - -/* - * - * UTIL - * - */ -Metamaps.Util = { - // helper function to determine how many lines are needed - // Line Splitter Function - // copyright Stephen Chapman, 19th April 2006 - // you may copy this code but please keep the copyright notice as well - splitLine: function (st, n) { - var b = ''; - var s = st ? st : ''; - while (s.length > n) { - var c = s.substring(0, n); - var d = c.lastIndexOf(' '); - var e = c.lastIndexOf('\n'); - if (e != -1) d = e; - if (d == -1) d = n; - b += c.substring(0, d) + '\n'; - s = s.substring(d + 1); - } - return b + s; - }, - nowDateFormatted: function () { - var date = new Date(Date.now()); - var month = (date.getMonth() + 1) < 10 ? '0' + (date.getMonth() + 1) : (date.getMonth() + 1); - var day = date.getDate() < 10 ? '0' + date.getDate() : date.getDate(); - var year = date.getFullYear(); - - return month + '/' + day + '/' + year; - }, - decodeEntities: function (desc) { - var str, temp = document.createElement('p'); - temp.innerHTML = desc; //browser handles the topics - str = temp.textContent || temp.innerText; - temp = null; //delete the element; - return str; - }, //decodeEntities - getDistance: function (p1, p2) { - return Math.sqrt(Math.pow((p2.x - p1.x), 2) + Math.pow((p2.y - p1.y), 2)); - }, - coordsToPixels: function (coords) { - if (Metamaps.Visualize.mGraph) { - var canvas = Metamaps.Visualize.mGraph.canvas, - s = canvas.getSize(), - p = canvas.getPos(), - ox = canvas.translateOffsetX, - oy = canvas.translateOffsetY, - sx = canvas.scaleOffsetX, - sy = canvas.scaleOffsetY; - var pixels = { - x: (coords.x / (1/sx)) + p.x + s.width/2 + ox, - y: (coords.y / (1/sy)) + p.y + s.height/2 + oy - }; - return pixels; - } - else { - return { - x: 0, - y: 0 - }; - } - }, - pixelsToCoords: function (pixels) { - var coords; - if (Metamaps.Visualize.mGraph) { - var canvas = Metamaps.Visualize.mGraph.canvas, - s = canvas.getSize(), - p = canvas.getPos(), - ox = canvas.translateOffsetX, - oy = canvas.translateOffsetY, - sx = canvas.scaleOffsetX, - sy = canvas.scaleOffsetY; - coords = { - x: (pixels.x - p.x - s.width/2 - ox) * (1/sx), - y: (pixels.y - p.y - s.height/2 - oy) * (1/sy), - }; - } - else { - coords = { - x: 0, - y: 0 - }; - } - return coords; - }, - getPastelColor: function () { - var r = (Math.round(Math.random()* 127) + 127).toString(16); - var g = (Math.round(Math.random()* 127) + 127).toString(16); - var b = (Math.round(Math.random()* 127) + 127).toString(16); - return Metamaps.Util.colorLuminance('#' + r + g + b, -0.4); - }, - // darkens a hex value by 'lum' percentage - colorLuminance: function (hex, lum) { - - // validate hex string - hex = String(hex).replace(/[^0-9a-f]/gi, ''); - if (hex.length < 6) { - hex = hex[0]+hex[0]+hex[1]+hex[1]+hex[2]+hex[2]; - } - lum = lum || 0; - - // convert to decimal and change luminosity - var rgb = "#", c, i; - for (i = 0; i < 3; i++) { - c = parseInt(hex.substr(i*2,2), 16); - c = Math.round(Math.min(Math.max(0, c + (c * lum)), 255)).toString(16); - rgb += ("00"+c).substr(c.length); - } - - return rgb; - }, - generateOptionsList: function (data) { - var newlist = ""; - for (var i = 0; i < data.length; i++) { - newlist = newlist + ''; - } - return newlist; - }, - checkURLisImage: function (url) { - // when the page reloads the following regular expression will be screwed up - // please replace it with this one before you save: /*backslashhere*.(jpeg|jpg|gif|png)$/ - return (url.match(/\.(jpeg|jpg|gif|png)$/) != null); - }, - checkURLisYoutubeVideo: function (url) { - return (url.match(/^https?:\/\/(?:www\.)?youtube.com\/watch\?(?=[^?]*v=\w+)(?:[^\s?]+)?$/) != null); - } -}; // end Metamaps.Util - -/* - * - * REALTIME - * - */ -Metamaps.Realtime = { - videoId: 'video-wrapper', - socket: null, - webrtc: null, - readyToCall: false, - mappersOnMap: {}, - disconnected: false, - chatOpen: false, - status: true, // stores whether realtime is True/On or False/Off, - broadcastingStatus: false, - inConversation: false, - localVideo: null, - init: function () { - var self = Metamaps.Realtime; - - self.addJuntoListeners(); - - self.socket = new SocketIoConnection({ url: '<%= ENV['REALTIME_SERVER'] %>' }); - self.socket.on('connect', function () { - console.log('connected'); - if (!self.disconnected) { - self.startActiveMap(); - } else self.disconnected = false; - }); - self.socket.on('disconnect', function () { - self.disconnected = true; - }); - - if (Metamaps.Active.Mapper) { - - self.webrtc = new SimpleWebRTC({ - connection: self.socket, - localVideoEl: self.videoId, - remoteVideosEl: '', - detectSpeakingEvents: true, - autoAdjustMic: false, //true, - autoRequestMedia: false, - localVideo: { - autoplay: true, - mirror: true, - muted: true - }, - media: { - video: true, - audio: true - }, - nick: Metamaps.Active.Mapper.id - }); - - var - $video = $('').attr('id', self.videoId); - self.localVideo = { - $video: $video, - view: new Metamaps.Views.videoView($video[0], $('body'), 'me', true, { - DOUBLE_CLICK_TOLERANCE: 200, - avatar: Metamaps.Active.Mapper ? Metamaps.Active.Mapper.get('image') : '' - }) - }; - - self.room = new Metamaps.Views.room({ - webrtc: self.webrtc, - socket: self.socket, - username: Metamaps.Active.Mapper ? Metamaps.Active.Mapper.get('name') : '', - image: Metamaps.Active.Mapper ? Metamaps.Active.Mapper.get('image') : '', - room: 'global', - $video: self.localVideo.$video, - myVideoView: self.localVideo.view, - config: { DOUBLE_CLICK_TOLERANCE: 200 } - }); - self.room.videoAdded(self.handleVideoAdded); - - self.room.chat.$container.hide(); - $('body').prepend(self.room.chat.$container); - } // if Metamaps.Active.Mapper - }, - addJuntoListeners: function () { - var self = Metamaps.Realtime; - - $(document).on(Metamaps.Views.chatView.events.openTray, function () { - $('.main').addClass('compressed'); - self.chatOpen = true; - self.positionPeerIcons(); - }); - $(document).on(Metamaps.Views.chatView.events.closeTray, function () { - $('.main').removeClass('compressed'); - self.chatOpen = false; - self.positionPeerIcons(); - }); - $(document).on(Metamaps.Views.chatView.events.videosOn, function () { - $('#wrapper').removeClass('hideVideos'); - }); - $(document).on(Metamaps.Views.chatView.events.videosOff, function () { - $('#wrapper').addClass('hideVideos'); - }); - $(document).on(Metamaps.Views.chatView.events.cursorsOn, function () { - $('#wrapper').removeClass('hideCursors'); - }); - $(document).on(Metamaps.Views.chatView.events.cursorsOff, function () { - $('#wrapper').addClass('hideCursors'); - }); - }, - handleVideoAdded: function (v, id) { - var self = Metamaps.Realtime; - self.positionVideos(); - v.setParent($('#wrapper')); - v.$container.find('.video-cutoff').css({ - border: '4px solid ' + self.mappersOnMap[id].color - }); - $('#wrapper').append(v.$container); - }, - positionVideos: function () { - var self = Metamaps.Realtime; - var videoIds = Object.keys(self.room.videos); - var numOfVideos = videoIds.length; - var numOfVideosToPosition = _.filter(videoIds, function (id) { - return !self.room.videos[id].manuallyPositioned; - }).length; - - var screenHeight = $(document).height(); - var screenWidth = $(document).width(); - var topExtraPadding = 20; - var topPadding = 30; - var leftPadding = 30; - var videoHeight = 150; - var videoWidth = 180; - var column = 0; - var row = 0; - var yFormula = function () { - var y = topExtraPadding + (topPadding + videoHeight)*row + topPadding; - if (y + videoHeight > screenHeight) { - row = 0; - column += 1; - y = yFormula(); - } - row++; - return y; - }; - var xFormula = function () { - var x = (leftPadding + videoWidth)*column + leftPadding; - return x; - }; - - // do self first - var myVideo = Metamaps.Realtime.localVideo.view; - if (!myVideo.manuallyPositioned) { - myVideo.$container.css({ - top: yFormula() + 'px', - left: xFormula() + 'px' - }); - } - videoIds.forEach(function (id) { - var video = self.room.videos[id]; - if (!video.manuallyPositioned) { - video.$container.css({ - top: yFormula() + 'px', - left: xFormula() + 'px' - }); - } - }); - }, - startActiveMap: function () { - var self = Metamaps.Realtime; - - if (Metamaps.Active.Map && Metamaps.Active.Mapper) { - var commonsMap = Metamaps.Active.Map.get('permission') === 'commons'; - var publicMap = Metamaps.Active.Map.get('permission') === 'public'; - - if (commonsMap) { - self.turnOn(); - self.setupSocket(); - } - else if (publicMap) { - self.attachMapListener(); - } - self.room.addMessages(new Metamaps.Backbone.MessageCollection(Metamaps.Messages), true); - } - }, - endActiveMap: function () { - var self = Metamaps.Realtime; - - $(document).off('mousemove'); - self.socket.removeAllListeners(); - if (self.inConversation) self.leaveCall(); - self.socket.emit('endMapperNotify'); - $(".collabCompass").remove(); - self.status = false; - self.room.leave(); - self.room.chat.$container.hide(); - self.room.chat.close(); - }, - reenableRealtime: function() { - var confirmString = "The layout of your map has fallen out of sync with the saved copy. "; - confirmString += "To save your changes without overwriting the map, hit 'Cancel' and "; - confirmString += "then use 'Save to new map'. "; - confirmString += "Do you want to discard your changes and enable realtime?"; - var c = confirm(confirmString); - if (c) { - Metamaps.Router.maps(Metamaps.Active.Map.id); - } - }, - turnOn: function (notify) { - var self = Metamaps.Realtime; - - if (notify) self.sendRealtimeOn(); - //$(".rtMapperSelf").removeClass('littleRtOff').addClass('littleRtOn'); - //$('.rtOn').addClass('active'); - //$('.rtOff').removeClass('active'); - self.status = true; - //$(".sidebarCollaborateIcon").addClass("blue"); - $(".collabCompass").show(); - self.room.chat.$container.show(); - self.room.room = 'map-' + Metamaps.Active.Map.id; - self.checkForACallToJoin(); - - self.activeMapper = { - id: Metamaps.Active.Mapper.id, - name: Metamaps.Active.Mapper.get('name'), - username: Metamaps.Active.Mapper.get('name'), - image: Metamaps.Active.Mapper.get('image'), - color: Metamaps.Util.getPastelColor(), - self: true - }; - self.localVideo.view.$container.find('.video-cutoff').css({ - border: '4px solid ' + self.activeMapper.color - }); - self.room.chat.addParticipant(self.activeMapper); - }, - checkForACallToJoin: function () { - var self = Metamaps.Realtime; - self.socket.emit('checkForCall', { room: self.room.room, mapid: Metamaps.Active.Map.id }); - }, - promptToJoin: function () { - var self = Metamaps.Realtime; - - var notifyText = 'There\'s a conversation happening, want to join?'; - notifyText += ' '; - notifyText += ' '; - Metamaps.GlobalUI.notifyUser(notifyText, true); - self.room.conversationInProgress(); - }, - conversationHasBegun: function () { - var self = Metamaps.Realtime; - - if (self.inConversation) return; - var notifyText = 'There\'s a conversation starting, want to join?'; - notifyText += ' '; - notifyText += ' '; - Metamaps.GlobalUI.notifyUser(notifyText, true); - self.room.conversationInProgress(); - }, - countOthersInConversation: function () { - var self = Metamaps.Realtime; - var count = 0; - - for (var key in self.mappersOnMap) { - if (self.mappersOnMap[key].inConversation) count++; - } - return count; - }, - mapperJoinedCall: function (id) { - var self = Metamaps.Realtime; - var mapper = self.mappersOnMap[id]; - - if (mapper) { - if (self.inConversation) { - var username = mapper.name; - var notifyText = username + ' joined the call'; - Metamaps.GlobalUI.notifyUser(notifyText); - } - - mapper.inConversation = true; - self.room.chat.mapperJoinedCall(id); - } - }, - mapperLeftCall: function (id) { - var self = Metamaps.Realtime; - var mapper = self.mappersOnMap[id]; - - if (mapper) { - if (self.inConversation) { - var username = mapper.name; - var notifyText = username + ' left the call'; - Metamaps.GlobalUI.notifyUser(notifyText); - } - - mapper.inConversation = false; - self.room.chat.mapperLeftCall(id); - - if ((self.inConversation && self.countOthersInConversation() === 0) || - (!self.inConversation && self.countOthersInConversation() === 1)) { - self.callEnded(); - } - } - }, - callEnded: function () { - var self = Metamaps.Realtime; - - self.room.conversationEnding(); - self.room.leaveVideoOnly(); - self.inConversation = false; - self.localVideo.view.$container.hide().css({ - top: '72px', - left: '30px' - }); - self.localVideo.view.audioOn(); - self.localVideo.view.videoOn(); - self.webrtc.webrtc.localStreams.forEach(function (stream) { - stream.getTracks().forEach(function (track) { - track.stop(); - }); - }); - self.webrtc.webrtc.localStreams = []; - }, - invitedToCall: function (inviter) { - var self = Metamaps.Realtime; - - self.room.chat.sound.stop('sessioninvite'); - self.room.chat.sound.play('sessioninvite'); - - var username = self.mappersOnMap[inviter].name; - var notifyText = "' style='display: inline-block; margin-top: -12px; vertical-align: top;' />"; - notifyText += username + ' is inviting you to a conversation. Join live?'; - notifyText += ' '; - notifyText += ' '; - Metamaps.GlobalUI.notifyUser(notifyText, true); - }, - invitedToJoin: function (inviter) { - var self = Metamaps.Realtime; - - self.room.chat.sound.stop('sessioninvite'); - self.room.chat.sound.play('sessioninvite'); - - var username = self.mappersOnMap[inviter].name; - var notifyText = username + ' is inviting you to the conversation. Join?'; - notifyText += ' '; - notifyText += ' '; - Metamaps.GlobalUI.notifyUser(notifyText, true); - }, - acceptCall: function (userid) { - var self = Metamaps.Realtime; - self.room.chat.sound.stop('sessioninvite'); - self.socket.emit('callAccepted', { - mapid: Metamaps.Active.Map.id, - invited: Metamaps.Active.Mapper.id, - inviter: userid - }); - $.post('/maps/' + Metamaps.Active.Map.id + '/events/conversation'); - self.joinCall(); - Metamaps.GlobalUI.clearNotify(); - }, - denyCall: function (userid) { - var self = Metamaps.Realtime; - self.room.chat.sound.stop('sessioninvite'); - self.socket.emit('callDenied', { - mapid: Metamaps.Active.Map.id, - invited: Metamaps.Active.Mapper.id, - inviter: userid - }); - Metamaps.GlobalUI.clearNotify(); - }, - denyInvite: function (userid) { - var self = Metamaps.Realtime; - self.room.chat.sound.stop('sessioninvite'); - self.socket.emit('inviteDenied', { - mapid: Metamaps.Active.Map.id, - invited: Metamaps.Active.Mapper.id, - inviter: userid - }); - Metamaps.GlobalUI.clearNotify(); - }, - inviteACall: function (userid) { - var self = Metamaps.Realtime; - self.socket.emit('inviteACall', { - mapid: Metamaps.Active.Map.id, - inviter: Metamaps.Active.Mapper.id, - invited: userid - }); - self.room.chat.invitationPending(userid); - Metamaps.GlobalUI.clearNotify(); - }, - inviteToJoin: function (userid) { - var self = Metamaps.Realtime; - self.socket.emit('inviteToJoin', { - mapid: Metamaps.Active.Map.id, - inviter: Metamaps.Active.Mapper.id, - invited: userid - }); - self.room.chat.invitationPending(userid); - }, - callAccepted: function (userid) { - var self = Metamaps.Realtime; - - var username = self.mappersOnMap[userid].name; - Metamaps.GlobalUI.notifyUser('Conversation starting...'); - self.joinCall(); - self.room.chat.invitationAnswered(userid); - }, - callDenied: function (userid) { - var self = Metamaps.Realtime; - - var username = self.mappersOnMap[userid].name; - Metamaps.GlobalUI.notifyUser(username + ' didn\'t accept your invitation'); - self.room.chat.invitationAnswered(userid); - }, - inviteDenied: function (userid) { - var self = Metamaps.Realtime; - - var username = self.mappersOnMap[userid].name; - Metamaps.GlobalUI.notifyUser(username + ' didn\'t accept your invitation'); - self.room.chat.invitationAnswered(userid); - }, - joinCall: function () { - var self = Metamaps.Realtime; - - self.webrtc.off('readyToCall'); - self.webrtc.once('readyToCall', function () { - self.videoInitialized = true; - self.readyToCall = true; - self.localVideo.view.manuallyPositioned = false; - self.positionVideos(); - self.localVideo.view.$container.show(); - if (self.localVideo && self.status) { - $('#wrapper').append(self.localVideo.view.$container); - } - self.room.join(); - }); - self.inConversation = true; - self.socket.emit('mapperJoinedCall', { - mapid: Metamaps.Active.Map.id, - id: Metamaps.Active.Mapper.id - }); - self.webrtc.startLocalVideo(); - Metamaps.GlobalUI.clearNotify(); - self.room.chat.mapperJoinedCall(Metamaps.Active.Mapper.id); - }, - leaveCall: function () { - var self = Metamaps.Realtime; - - self.socket.emit('mapperLeftCall', { - mapid: Metamaps.Active.Map.id, - id: Metamaps.Active.Mapper.id - }); - - self.room.chat.mapperLeftCall(Metamaps.Active.Mapper.id); - self.room.leaveVideoOnly(); - self.inConversation = false; - self.localVideo.view.$container.hide(); - - // if there's only two people in the room, and we're leaving - // we should shut down the call locally - if (self.countOthersInConversation() === 1) { - self.callEnded(); - } - }, - turnOff: function (silent) { - var self = Metamaps.Realtime; - - if (self.status) { - if (!silent) self.sendRealtimeOff(); - //$(".rtMapperSelf").removeClass('littleRtOn').addClass('littleRtOff'); - //$('.rtOn').removeClass('active'); - //$('.rtOff').addClass('active'); - self.status = false; - //$(".sidebarCollaborateIcon").removeClass("blue"); - $(".collabCompass").hide(); - $('#' + self.videoId).remove(); - } - }, - setupSocket: function () { - var self = Metamaps.Realtime; - var socket = Metamaps.Realtime.socket; - var myId = Metamaps.Active.Mapper.id; - - socket.emit('newMapperNotify', { - userid: myId, - username: Metamaps.Active.Mapper.get("name"), - userimage: Metamaps.Active.Mapper.get("image"), - mapid: Metamaps.Active.Map.id - }); - - socket.on(myId + '-' + Metamaps.Active.Map.id + '-invitedToCall', self.invitedToCall); // new call - socket.on(myId + '-' + Metamaps.Active.Map.id + '-invitedToJoin', self.invitedToJoin); // call already in progress - socket.on(myId + '-' + Metamaps.Active.Map.id + '-callAccepted', self.callAccepted); - socket.on(myId + '-' + Metamaps.Active.Map.id + '-callDenied', self.callDenied); - socket.on(myId + '-' + Metamaps.Active.Map.id + '-inviteDenied', self.inviteDenied); - - // receive word that there's a conversation in progress - socket.on('maps-' + Metamaps.Active.Map.id + '-callInProgress', self.promptToJoin); - socket.on('maps-' + Metamaps.Active.Map.id + '-callStarting', self.conversationHasBegun); - - socket.on('maps-' + Metamaps.Active.Map.id + '-mapperJoinedCall', self.mapperJoinedCall); - socket.on('maps-' + Metamaps.Active.Map.id + '-mapperLeftCall', self.mapperLeftCall); - - // if you're the 'new guy' update your list with who's already online - socket.on(myId + '-' + Metamaps.Active.Map.id + '-UpdateMapperList', self.updateMapperList); - - // receive word that there's a new mapper on the map - socket.on('maps-' + Metamaps.Active.Map.id + '-newmapper', self.newPeerOnMap); - - // receive word that a mapper left the map - socket.on('maps-' + Metamaps.Active.Map.id + '-lostmapper', self.lostPeerOnMap); - - // receive word that there's a mapper turned on realtime - socket.on('maps-' + Metamaps.Active.Map.id + '-newrealtime', self.newCollaborator); - - // receive word that there's a mapper turned on realtime - socket.on('maps-' + Metamaps.Active.Map.id + '-lostrealtime', self.lostCollaborator); - - // - socket.on('maps-' + Metamaps.Active.Map.id + '-topicDrag', self.topicDrag); - - // - socket.on('maps-' + Metamaps.Active.Map.id + '-newTopic', self.newTopic); - - // - socket.on('maps-' + Metamaps.Active.Map.id + '-newMessage', self.newMessage); - - // - socket.on('maps-' + Metamaps.Active.Map.id + '-removeTopic', self.removeTopic); - - // - socket.on('maps-' + Metamaps.Active.Map.id + '-newSynapse', self.newSynapse); - - // - socket.on('maps-' + Metamaps.Active.Map.id + '-removeSynapse', self.removeSynapse); - - // update mapper compass position - socket.on('maps-' + Metamaps.Active.Map.id + '-updatePeerCoords', self.updatePeerCoords); - - // deletions - socket.on('deleteTopicFromServer', self.removeTopic); - socket.on('deleteSynapseFromServer', self.removeSynapse); - - socket.on('topicChangeFromServer', self.topicChange); - socket.on('synapseChangeFromServer', self.synapseChange); - self.attachMapListener(); - - // local event listeners that trigger events - var sendCoords = function (event) { - var pixels = { - x: event.pageX, - y: event.pageY - }; - var coords = Metamaps.Util.pixelsToCoords(pixels); - self.sendCoords(coords); - }; - $(document).mousemove(sendCoords); - - var zoom = function (event, e) { - if (e) { - var pixels = { - x: e.pageX, - y: e.pageY - }; - var coords = Metamaps.Util.pixelsToCoords(pixels); - self.sendCoords(coords); - } - self.positionPeerIcons(); - }; - $(document).on(Metamaps.JIT.events.zoom, zoom); - - $(document).on(Metamaps.JIT.events.pan, self.positionPeerIcons); - - var sendTopicDrag = function (event, positions) { - self.sendTopicDrag(positions); - }; - $(document).on(Metamaps.JIT.events.topicDrag, sendTopicDrag); - - var sendNewTopic = function (event, data) { - self.sendNewTopic(data); - }; - $(document).on(Metamaps.JIT.events.newTopic, sendNewTopic); - - var sendDeleteTopic = function (event, data) { - self.sendDeleteTopic(data); - }; - $(document).on(Metamaps.JIT.events.deleteTopic, sendDeleteTopic); - - var sendRemoveTopic = function (event, data) { - self.sendRemoveTopic(data); - }; - $(document).on(Metamaps.JIT.events.removeTopic, sendRemoveTopic); - - var sendNewSynapse = function (event, data) { - self.sendNewSynapse(data); - }; - $(document).on(Metamaps.JIT.events.newSynapse, sendNewSynapse); - - var sendDeleteSynapse = function (event, data) { - self.sendDeleteSynapse(data); - }; - $(document).on(Metamaps.JIT.events.deleteSynapse, sendDeleteSynapse); - - var sendRemoveSynapse = function (event, data) { - self.sendRemoveSynapse(data); - }; - $(document).on(Metamaps.JIT.events.removeSynapse, sendRemoveSynapse); - - var sendNewMessage = function (event, data) { - self.sendNewMessage(data); - }; - $(document).on(Metamaps.Views.room.events.newMessage, sendNewMessage); - - }, - attachMapListener: function(){ - var self = Metamaps.Realtime; - var socket = Metamaps.Realtime.socket; - - socket.on('mapChangeFromServer', self.mapChange); - }, - sendRealtimeOn: function () { - var self = Metamaps.Realtime; - var socket = Metamaps.Realtime.socket; - - // send this new mapper back your details, and the awareness that you're online - var update = { - username: Metamaps.Active.Mapper.get("name"), - userid: Metamaps.Active.Mapper.id, - mapid: Metamaps.Active.Map.id - }; - socket.emit('notifyStartRealtime', update); - }, - sendRealtimeOff: function () { - var self = Metamaps.Realtime; - var socket = Metamaps.Realtime.socket; - - // send this new mapper back your details, and the awareness that you're online - var update = { - username: Metamaps.Active.Mapper.get("name"), - userid: Metamaps.Active.Mapper.id, - mapid: Metamaps.Active.Map.id - }; - socket.emit('notifyStopRealtime', update); - }, - updateMapperList: function (data) { - var self = Metamaps.Realtime; - var socket = Metamaps.Realtime.socket; - - // data.userid - // data.username - // data.userimage - // data.userrealtime - - self.mappersOnMap[data.userid] = { - id: data.userid, - name: data.username, - username: data.username, - image: data.userimage, - color: Metamaps.Util.getPastelColor(), - realtime: data.userrealtime, - inConversation: data.userinconversation, - coords: { - x: 0, - y: 0 - } - }; - - if (data.userid !== Metamaps.Active.Mapper.id) { - self.room.chat.addParticipant(self.mappersOnMap[data.userid]); - if (data.userinconversation) self.room.chat.mapperJoinedCall(data.userid); - - // create a div for the collaborators compass - self.createCompass(data.username, data.userid, data.userimage, self.mappersOnMap[data.userid].color, !self.status); - } - }, - newPeerOnMap: function (data) { - var self = Metamaps.Realtime; - var socket = Metamaps.Realtime.socket; - - // data.userid - // data.username - // data.userimage - // data.coords - var firstOtherPerson = Object.keys(self.mappersOnMap).length === 0; - - self.mappersOnMap[data.userid] = { - id: data.userid, - name: data.username, - username: data.username, - image: data.userimage, - color: Metamaps.Util.getPastelColor(), - realtime: true, - coords: { - x: 0, - y: 0 - }, - }; - - // create an item for them in the realtime box - if (data.userid !== Metamaps.Active.Mapper.id && self.status) { - self.room.chat.sound.play('joinmap'); - self.room.chat.addParticipant(self.mappersOnMap[data.userid]); - - // create a div for the collaborators compass - self.createCompass(data.username, data.userid, data.userimage, self.mappersOnMap[data.userid].color, !self.status); - - var notifyMessage = data.username + ' just joined the map'; - if (firstOtherPerson) { - notifyMessage += ' '; - } - Metamaps.GlobalUI.notifyUser(notifyMessage); - - // send this new mapper back your details, and the awareness that you've loaded the map - var update = { - userToNotify: data.userid, - username: Metamaps.Active.Mapper.get("name"), - userimage: Metamaps.Active.Mapper.get("image"), - userid: Metamaps.Active.Mapper.id, - userrealtime: self.status, - userinconversation: self.inConversation, - mapid: Metamaps.Active.Map.id - }; - socket.emit('updateNewMapperList', update); - } - }, - createCompass: function(name, id, image, color, hide) { - var str = '

    '+name+'

    '; - str += '
    '; - $('#compass' + id).remove(); - $('
    ', { - id: 'compass' + id, - class: 'collabCompass' - }).html(str).appendTo('#wrapper'); - if (hide) { - $('#compass' + id).hide(); - } - $('#compass' + id + ' img').css({ - 'border': '2px solid ' + color - }); - $('#compass' + id + ' p').css({ - 'background-color': color - }); - }, - lostPeerOnMap: function (data) { - var self = Metamaps.Realtime; - var socket = Metamaps.Realtime.socket; - - // data.userid - // data.username - - delete self.mappersOnMap[data.userid]; - self.room.chat.sound.play('leavemap'); - //$('#mapper' + data.userid).remove(); - $('#compass' + data.userid).remove(); - self.room.chat.removeParticipant(data.username); - - Metamaps.GlobalUI.notifyUser(data.username + ' just left the map'); - - if ((self.inConversation && self.countOthersInConversation() === 0) || - (!self.inConversation && self.countOthersInConversation() === 1)) { - self.callEnded(); - } - }, - newCollaborator: function (data) { - var self = Metamaps.Realtime; - var socket = Metamaps.Realtime.socket; - - // data.userid - // data.username - - self.mappersOnMap[data.userid].realtime = true; - - //$('#mapper' + data.userid).removeClass('littleRtOff').addClass('littleRtOn'); - $('#compass' + data.userid).show(); - - Metamaps.GlobalUI.notifyUser(data.username + ' just turned on realtime'); - }, - lostCollaborator: function (data) { - var self = Metamaps.Realtime; - var socket = Metamaps.Realtime.socket; - - // data.userid - // data.username - - self.mappersOnMap[data.userid].realtime = false; - - //$('#mapper' + data.userid).removeClass('littleRtOn').addClass('littleRtOff'); - $('#compass' + data.userid).hide(); - - Metamaps.GlobalUI.notifyUser(data.username + ' just turned off realtime'); - }, - updatePeerCoords: function (data) { - var self = Metamaps.Realtime; - var socket = Metamaps.Realtime.socket; - - self.mappersOnMap[data.userid].coords={x: data.usercoords.x,y:data.usercoords.y}; - self.positionPeerIcon(data.userid); - }, - positionPeerIcons: function () { - var self = Metamaps.Realtime; - var socket = Metamaps.Realtime.socket; - - if (self.status) { // if i have realtime turned on - for (var key in self.mappersOnMap) { - var mapper = self.mappersOnMap[key]; - if (mapper.realtime) { - self.positionPeerIcon(key); - } - } - } - }, - positionPeerIcon: function (id) { - var self = Metamaps.Realtime; - var socket = Metamaps.Realtime.socket; - - var boundary = self.chatOpen ? '#wrapper' : document; - var mapper = self.mappersOnMap[id]; - var xMax=$(boundary).width(); - var yMax=$(boundary).height(); - var compassDiameter=56; - var compassArrowSize=24; - - var origPixels = Metamaps.Util.coordsToPixels(mapper.coords); - var pixels = self.limitPixelsToScreen(origPixels); - $('#compass' + id).css({ - left: pixels.x + 'px', - top: pixels.y + 'px' - }); - /* showing the arrow if the collaborator is off of the viewport screen */ - if (origPixels.x !== pixels.x || origPixels.y !== pixels.y) { - - var dy = origPixels.y - pixels.y; //opposite - var dx = origPixels.x - pixels.x; // adjacent - var ratio = dy / dx; - var angle = Math.atan2(dy, dx); - - $('#compassArrow' + id).show().css({ - transform: 'rotate(' + angle + 'rad)', - "-webkit-transform": 'rotate(' + angle + 'rad)', - }); - - if (dx > 0) { - $('#compass' + id).addClass('labelLeft'); - } - } else { - $('#compassArrow' + id).hide(); - $('#compass' + id).removeClass('labelLeft'); - } - }, - limitPixelsToScreen: function (pixels) { - var self = Metamaps.Realtime; - var socket = Metamaps.Realtime.socket; - - var boundary = self.chatOpen ? '#wrapper' : document; - var xLimit, yLimit; - var xMax=$(boundary).width(); - var yMax=$(boundary).height(); - var compassDiameter=56; - var compassArrowSize=24; - - xLimit = Math.max(0 + compassArrowSize, pixels.x); - xLimit = Math.min(xLimit, xMax - compassDiameter); - yLimit = Math.max(0 + compassArrowSize, pixels.y); - yLimit = Math.min(yLimit, yMax - compassDiameter); - - return {x:xLimit,y:yLimit}; - }, - sendCoords: function (coords) { - var self = Metamaps.Realtime; - var socket = Metamaps.Realtime.socket; - - var map = Metamaps.Active.Map; - var mapper = Metamaps.Active.Mapper; - - if (self.status && map.authorizeToEdit(mapper) && socket) { - var update = { - usercoords: coords, - userid: Metamaps.Active.Mapper.id, - mapid: Metamaps.Active.Map.id - }; - socket.emit('updateMapperCoords', update); - } - }, - sendTopicDrag: function (positions) { - var self = Metamaps.Realtime; - var socket = self.socket; - - if (Metamaps.Active.Map && self.status) { - positions.mapid = Metamaps.Active.Map.id; - socket.emit('topicDrag', positions); - } - }, - topicDrag: function (positions) { - var self = Metamaps.Realtime; - var socket = self.socket; - - var topic; - var node; - - if (Metamaps.Active.Map && self.status) { - for (var key in positions) { - topic = Metamaps.Topics.get(key); - if (topic) node = topic.get('node'); - if (node) node.pos.setc(positions[key].x, positions[key].y); - } //for - Metamaps.Visualize.mGraph.plot(); - } - }, - sendTopicChange: function (topic) { - var self = Metamaps.Realtime; - var socket = self.socket; - - var data = { - topicId: topic.id - } - - socket.emit('topicChangeFromClient', data); - }, - topicChange: function (data) { - var topic = Metamaps.Topics.get(data.topicId); - if (topic) { - var node = topic.get('node'); - topic.fetch({ - success: function (model) { - model.set({ node: node }); - model.trigger('changeByOther'); - } - }); - } - }, - sendSynapseChange: function (synapse) { - var self = Metamaps.Realtime; - var socket = self.socket; - - var data = { - synapseId: synapse.id - } - - socket.emit('synapseChangeFromClient', data); - }, - synapseChange: function (data) { - var synapse = Metamaps.Synapses.get(data.synapseId); - if (synapse) { - // edge reset necessary because fetch causes model reset - var edge = synapse.get('edge'); - synapse.fetch({ - success: function (model) { - model.set({ edge: edge }); - model.trigger('changeByOther'); - } - }); - } - }, - sendMapChange: function (map) { - var self = Metamaps.Realtime; - var socket = self.socket; - - var data = { - mapId: map.id - } - - socket.emit('mapChangeFromClient', data); - }, - mapChange: function (data) { - var map = Metamaps.Active.Map; - var isActiveMap = map && data.mapId === map.id; - if (isActiveMap) { - var permBefore = map.get('permission'); - var idBefore = map.id; - map.fetch({ - success: function (model, response) { - - var idNow = model.id; - var permNow = model.get('permission'); - if (idNow !== idBefore) { - Metamaps.Map.leavePrivateMap(); // this means the map has been changed to private - } - else if (permNow === 'public' && permBefore === 'commons') { - Metamaps.Map.commonsToPublic(); - } - else if (permNow === 'commons' && permBefore === 'public') { - Metamaps.Map.publicToCommons(); - } - else { - model.fetchContained(); - model.trigger('changeByOther'); - } - } - }); - } - }, - // newMessage - sendNewMessage: function (data) { - var self = Metamaps.Realtime; - var socket = self.socket; - - var message = data.attributes; - message.mapid = Metamaps.Active.Map.id; - socket.emit('newMessage', message); - }, - newMessage: function (data) { - var self = Metamaps.Realtime; - var socket = self.socket; - - self.room.addMessages(new Metamaps.Backbone.MessageCollection(data)); - }, - // newTopic - sendNewTopic: function (data) { - var self = Metamaps.Realtime; - var socket = self.socket; - - if (Metamaps.Active.Map && self.status) { - data.mapperid = Metamaps.Active.Mapper.id; - data.mapid = Metamaps.Active.Map.id; - socket.emit('newTopic', data); - } - }, - newTopic: function (data) { - var topic, mapping, mapper, mapperCallback, cancel; - - var self = Metamaps.Realtime; - var socket = self.socket; - - if (!self.status) return; - - function waitThenRenderTopic() { - if (topic && mapping && mapper) { - Metamaps.Topic.renderTopic(mapping, topic, false, false); - } - else if (!cancel) { - setTimeout(waitThenRenderTopic, 10); - } - } - - mapper = Metamaps.Mappers.get(data.mapperid); - if (mapper === undefined) { - mapperCallback = function (m) { - Metamaps.Mappers.add(m); - mapper = m; - }; - Metamaps.Mapper.get(data.mapperid, mapperCallback); - } - $.ajax({ - url: "/topics/" + data.mappableid + ".json", - success: function (response) { - Metamaps.Topics.add(response); - topic = Metamaps.Topics.get(response.id); - }, - error: function () { - cancel = true; - } - }); - $.ajax({ - url: "/mappings/" + data.mappingid + ".json", - success: function (response) { - Metamaps.Mappings.add(response); - mapping = Metamaps.Mappings.get(response.id); - }, - error: function () { - cancel = true; - } - }); - - waitThenRenderTopic(); - }, - // removeTopic - sendDeleteTopic: function (data) { - var self = Metamaps.Realtime; - var socket = self.socket; - - if (Metamaps.Active.Map) { - socket.emit('deleteTopicFromClient', data); - } - }, - // removeTopic - sendRemoveTopic: function (data) { - var self = Metamaps.Realtime; - var socket = self.socket; - - if (Metamaps.Active.Map) { - data.mapid = Metamaps.Active.Map.id; - socket.emit('removeTopic', data); - } - }, - removeTopic: function (data) { - var self = Metamaps.Realtime; - var socket = self.socket; - - if (!self.status) return; - - var topic = Metamaps.Topics.get(data.mappableid); - if (topic) { - var node = topic.get('node'); - var mapping = topic.getMapping(); - Metamaps.Control.hideNode(node.id); - Metamaps.Topics.remove(topic); - Metamaps.Mappings.remove(mapping); - } - }, - // newSynapse - sendNewSynapse: function (data) { - var self = Metamaps.Realtime; - var socket = self.socket; - - if (Metamaps.Active.Map) { - data.mapperid = Metamaps.Active.Mapper.id; - data.mapid = Metamaps.Active.Map.id; - socket.emit('newSynapse', data); - } - }, - newSynapse: function (data) { - var topic1, topic2, node1, node2, synapse, mapping, cancel; - - var self = Metamaps.Realtime; - var socket = self.socket; - - if (!self.status) return; - - function waitThenRenderSynapse() { - if (synapse && mapping && mapper) { - topic1 = synapse.getTopic1(); - node1 = topic1.get('node'); - topic2 = synapse.getTopic2(); - node2 = topic2.get('node'); - - Metamaps.Synapse.renderSynapse(mapping, synapse, node1, node2, false); - } - else if (!cancel) { - setTimeout(waitThenRenderSynapse, 10); - } - } - - mapper = Metamaps.Mappers.get(data.mapperid); - if (mapper === undefined) { - mapperCallback = function (m) { - Metamaps.Mappers.add(m); - mapper = m; - }; - Metamaps.Mapper.get(data.mapperid, mapperCallback); - } - $.ajax({ - url: "/synapses/" + data.mappableid + ".json", - success: function (response) { - Metamaps.Synapses.add(response); - synapse = Metamaps.Synapses.get(response.id); - }, - error: function () { - cancel = true; - } - }); - $.ajax({ - url: "/mappings/" + data.mappingid + ".json", - success: function (response) { - Metamaps.Mappings.add(response); - mapping = Metamaps.Mappings.get(response.id); - }, - error: function () { - cancel = true; - } - }); - waitThenRenderSynapse(); - }, - // deleteSynapse - sendDeleteSynapse: function (data) { - var self = Metamaps.Realtime; - var socket = self.socket; - - if (Metamaps.Active.Map) { - data.mapid = Metamaps.Active.Map.id; - socket.emit('deleteSynapseFromClient', data); - } - }, - // removeSynapse - sendRemoveSynapse: function (data) { - var self = Metamaps.Realtime; - var socket = self.socket; - - if (Metamaps.Active.Map) { - data.mapid = Metamaps.Active.Map.id; - socket.emit('removeSynapse', data); - } - }, - removeSynapse: function (data) { - var self = Metamaps.Realtime; - var socket = self.socket; - - if (!self.status) return; - - var synapse = Metamaps.Synapses.get(data.mappableid); - if (synapse) { - var edge = synapse.get('edge'); - var mapping = synapse.getMapping(); - if (edge.getData("mappings").length - 1 === 0) { - Metamaps.Control.hideEdge(edge); - } - - var index = _.indexOf(edge.getData("synapses"), synapse); - edge.getData("mappings").splice(index, 1); - edge.getData("synapses").splice(index, 1); - if (edge.getData("displayIndex")) { - delete edge.data.$displayIndex; - } - Metamaps.Synapses.remove(synapse); - Metamaps.Mappings.remove(mapping); - } - }, -}; // end Metamaps.Realtime + reset: function () { + var self = Metamaps.Selected + + self.Nodes = [] + self.Edges = [] + }, + Nodes: [], + Edges: [] +} diff --git a/app/assets/javascripts/src/check-canvas-support.js b/app/assets/javascripts/src/check-canvas-support.js new file mode 100644 index 00000000..90afdde1 --- /dev/null +++ b/app/assets/javascripts/src/check-canvas-support.js @@ -0,0 +1,15 @@ +// TODO document this user agent function +var labelType, useGradients, nativeTextSupport, animate +;(function () { + var ua = navigator.userAgent, + iStuff = ua.match(/iPhone/i) || ua.match(/iPad/i), + typeOfCanvas = typeof HTMLCanvasElement, + nativeCanvasSupport = (typeOfCanvas == 'object' || typeOfCanvas == 'function'), + textSupport = nativeCanvasSupport && (typeof document.createElement('canvas').getContext('2d').fillText == 'function') + // I'm setting this based on the fact that ExCanvas provides text support for IE + // and that as of today iPhone/iPad current text support is lame + labelType = (!nativeCanvasSupport || (textSupport && !iStuff)) ? 'Native' : 'HTML' + nativeTextSupport = labelType == 'Native' + useGradients = nativeCanvasSupport + animate = !(iStuff || !nativeCanvasSupport) +})() diff --git a/app/assets/stylesheets/application.css.erb b/app/assets/stylesheets/application.css.erb index 5970c18b..6c76dd9e 100644 --- a/app/assets/stylesheets/application.css.erb +++ b/app/assets/stylesheets/application.css.erb @@ -1569,6 +1569,11 @@ h3.filterBox { background-repeat: no-repeat; text-align: left; } + +.commonsMap .mapContributors { + visibility: hidden; +} + .mapContributors { position: relative; height: 30px; @@ -1576,6 +1581,7 @@ h3.filterBox { padding: 0; width: 64px; } + #mapContribs { float: left; border: 2px solid #424242; @@ -1591,7 +1597,7 @@ h3.filterBox { #mapContribs.multiple { box-shadow: 1px 1px 0 0 #B5B5B5,3px 2px 0 0 #424242,4px 3px 0 0 #B5B5B5,5px 4px 0 0 #424242; } -.mapContributors span { +.mapContributors span.count { height: 20px; padding-top: 5px; padding-left: 8px; @@ -1626,10 +1632,11 @@ h3.filterBox { .mapContributors .tip { top: 45px; left: -10px; + min-width: 200px; } .mapContributors .tip ul { - max-height: 188px; + max-height: 144px; overflow-y: auto; } @@ -1652,7 +1659,7 @@ h3.filterBox { .mapContributors .tip li a { color: white; } -.mapContributors div:after { +.mapContributors div.tip:after { content: ''; position: absolute; top: -4px; @@ -1672,6 +1679,90 @@ h3.filterBox { border-radius: 14px; } +.mapContributors span.twitter-typeahead { + padding: 0; +} + +.collabSearchField { + text-align: left; +} + +.collabNameWrapper { + float: left; +} + +.collabIconWrapper img.icon { + position: relative; + top: 0; + left: 0; +} + +.collabIconWrapper { + position: relative; + float: left; + padding: 0 4px; +} + +.mapContributors .collabName { + font-weight: normal; + font-size: 14px; + line-height: 28px; + color: #424242; + padding: 0 4px; +} + +span.removeCollaborator { + position: absolute; + top: 11px; + right: 8px; + height: 16px; + width: 16px; + background-image: url(<%= asset_data_uri('removecollab_sprite.png') %>); + cursor: pointer; +} + +span.removeCollaborator:hover { + background-position: -16px 0; +} + +span.addCollab { + width: 16px; + height: 16px; + background-image: url(<%= asset_data_uri('addcollab_sprite.png') %>); + display: inline-block; + vertical-align: middle; + margin: 0 12px 0 10px; +} + +input.collaboratorSearchField { + background: #FFFFFF; + height: 14px; + margin: 0; + padding: 10px 6px; + border: none; + border-radius: 2px; + outline: none; + font-size: 14px; + line-height: 14px; + color: #424242; + font-family: 'din-medium', helvetica, sans-serif; +} + +.tt-dataset.tt-dataset-collaborators { + padding: 2px; + background: #E0E0E0; + min-width: 156px; + border-radius: 2px; +} + +.tt-dataset.tt-dataset .collabResult { + padding: 4px; +} + +.collabResult.tt-suggestion.tt-cursor, .collabResult.tt-suggestion:hover { + background-color: #CCCCCC; +} + .mapInfoBox .mapPermission .tooltips { top: -20px; right: 36px; diff --git a/app/assets/stylesheets/clean.css.erb b/app/assets/stylesheets/clean.css.erb index 5665716a..47e18a94 100644 --- a/app/assets/stylesheets/clean.css.erb +++ b/app/assets/stylesheets/clean.css.erb @@ -681,6 +681,10 @@ background-image: url(<%= asset_data_uri 'exploremaps_sprite.png' %>); background-position: 0 0; } +.exploreMapsCenter .sharedMaps .exploreMapsIcon { + background-image: url(<%= asset_data_uri 'exploremaps_sprite.png' %>); + background-position: -96px 0; +} .exploreMapsCenter .activeMaps .exploreMapsIcon { background-image: url(<%= asset_data_uri 'exploremaps_sprite.png' %>); background-position: -32px 0; @@ -698,6 +702,9 @@ .featuredMaps:hover .exploreMapsIcon, .featuredMaps.active .exploreMapsIcon { background-position: -64px -32px; } +.sharedMaps:hover .exploreMapsIcon, .sharedMaps.active .exploreMapsIcon { + background-position: -96px -32px; +} .mapsWrapper { /*overflow-y: auto; */ diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index c1e9e26d..5db7d62a 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -32,7 +32,11 @@ class ApplicationController < ActionController::Base end def handle_unauthorized - head :forbidden # TODO make this better + if authenticated? + head :forbidden # TODO make this better + else + redirect_to new_user_session_path, notice: "Try signing in to do that." + end end private diff --git a/app/controllers/main_controller.rb b/app/controllers/main_controller.rb index f7d9a84a..87264c4c 100644 --- a/app/controllers/main_controller.rb +++ b/app/controllers/main_controller.rb @@ -4,7 +4,7 @@ class MainController < ApplicationController include UsersHelper include SynapsesHelper - after_action :verify_policy_scoped, except: :requestinvite + after_action :verify_policy_scoped, except: [:requestinvite, :searchmappers] respond_to :html, :json @@ -133,9 +133,9 @@ class MainController < ApplicationController #remove "mapper:" if appended at beginning term = term[7..-1] if term.downcase[0..6] == "mapper:" search = term.downcase + '%' + skip_policy_scope # TODO builder = policy_scope(User) - builder = User - builder = builder.where('LOWER("name") like ?', search) + builder = User.where('LOWER("name") like ?', search) @mappers = builder.order(:name) else @mappers = [] diff --git a/app/controllers/mappings_controller.rb b/app/controllers/mappings_controller.rb index 936ffcc2..9a5f1f1f 100644 --- a/app/controllers/mappings_controller.rb +++ b/app/controllers/mappings_controller.rb @@ -45,6 +45,13 @@ class MappingsController < ApplicationController @mapping = Mapping.find(params[:id]) authorize @mapping + mappable = @mapping.mappable + if mappable.defer_to_map + mappable.permission = mappable.defer_to_map.permission + mappable.defer_to_map_id = nil + mappable.save + end + @mapping.destroy head :no_content diff --git a/app/controllers/maps_controller.rb b/app/controllers/maps_controller.rb index 91eb97e1..b605b5ad 100644 --- a/app/controllers/maps_controller.rb +++ b/app/controllers/maps_controller.rb @@ -1,8 +1,7 @@ class MapsController < ApplicationController - - before_action :require_user, only: [:create, :update, :screenshot, :events, :destroy] - after_action :verify_authorized, except: [:activemaps, :featuredmaps, :mymaps, :usermaps, :events] - after_action :verify_policy_scoped, only: [:activemaps, :featuredmaps, :mymaps, :usermaps] + before_action :require_user, only: [:create, :update, :access, :screenshot, :events, :destroy] + after_action :verify_authorized, except: [:activemaps, :featuredmaps, :mymaps, :sharedmaps, :usermaps, :events] + after_action :verify_policy_scoped, only: [:activemaps, :featuredmaps, :mymaps, :sharedmaps, :usermaps] respond_to :html, :json, :csv @@ -53,6 +52,21 @@ class MapsController < ApplicationController end end + # GET /explore/shared + def sharedmaps + return redirect_to activemaps_url if !authenticated? + + page = params[:page].present? ? params[:page] : 1 + @maps = policy_scope( + Map.where("maps.id IN (?)", current_user.shared_maps.map(&:id)) + ).order("updated_at DESC").page(page).per(20) + + respond_to do |format| + format.html { respond_with(@maps, @user) } + format.json { render json: @maps } + end + end + # GET /explore/mapper/:id def usermaps page = params[:page].present? ? params[:page] : 1 @@ -74,15 +88,13 @@ class MapsController < ApplicationController respond_to do |format| format.html { @allmappers = @map.contributors - @alltopics = @map.topics.to_a.delete_if {|t| t.permission == "private" && (!authenticated? || (authenticated? && current_user.id != t.user_id)) } - @allsynapses = @map.synapses.to_a.delete_if {|s| s.permission == "private" && (!authenticated? || (authenticated? && current_user.id != s.user_id)) } - @allmappings = @map.mappings.to_a.delete_if {|m| - object = m.mappable - !object || (object.permission == "private" && (!authenticated? || (authenticated? && current_user.id != object.user_id))) - } + @allcollaborators = @map.editors + @alltopics = @map.topics.to_a.delete_if {|t| not policy(t).show? } + @allsynapses = @map.synapses.to_a.delete_if {|s| not policy(s).show? } + @allmappings = @map.mappings.to_a.delete_if {|m| not policy(m).show? } @allmessages = @map.messages.sort_by(&:created_at) - respond_with(@allmappers, @allmappings, @allsynapses, @alltopics, @allmessages, @map) + respond_with(@allmappers, @allcollaborators, @allmappings, @allsynapses, @alltopics, @allmessages, @map) } format.json { render json: @map } format.csv { redirect_to action: :export, format: :csv } @@ -127,12 +139,11 @@ class MapsController < ApplicationController authorize @map @allmappers = @map.contributors - @alltopics = @map.topics.to_a.delete_if {|t| t.permission == "private" && (!authenticated? || (authenticated? && current_user.id != t.user_id)) } - @allsynapses = @map.synapses.to_a.delete_if {|s| s.permission == "private" && (!authenticated? || (authenticated? && current_user.id != s.user_id)) } - @allmappings = @map.mappings.to_a.delete_if {|m| - object = m.mappable - !object || (object.permission == "private" && (!authenticated? || (authenticated? && current_user.id != object.user_id))) - } + @allcollaborators = @map.editors + @alltopics = @map.topics.to_a.delete_if {|t| not policy(t).show? } + @allsynapses = @map.synapses.to_a.delete_if {|s| not policy(s).show? } + @allmappings = @map.mappings.to_a.delete_if {|m| not policy(m).show? } + @json = Hash.new() @json['map'] = @map @@ -140,6 +151,7 @@ class MapsController < ApplicationController @json['synapses'] = @allsynapses @json['mappings'] = @allmappings @json['mappers'] = @allmappers + @json['collaborators'] = @allcollaborators @json['messages'] = @map.messages.sort_by(&:created_at) respond_to do |format| @@ -215,6 +227,36 @@ class MapsController < ApplicationController end end + # POST maps/:id/access + def access + @map = Map.find(params[:id]) + authorize @map + userIds = params[:access] || [] + added = userIds.select { |uid| + user = User.find(uid) + if user.nil? || (current_user && user == current_user) + false + else + not @map.collaborators.include?(user) + end + } + removed = @map.collaborators.select { |user| not userIds.include?(user.id.to_s) }.map(&:id) + added.each { |uid| + um = UserMap.create({ user_id: uid.to_i, map_id: @map.id }) + user = User.find(uid.to_i) + MapMailer.invite_to_edit_email(@map, current_user, user).deliver_later + } + removed.each { |uid| + @map.user_maps.select{ |um| um.user_id == uid }.each{ |um| um.destroy } + } + + respond_to do |format| + format.json do + render :json => { :message => "Successfully altered edit permissions" } + end + end + end + # POST maps/:id/upload_screenshot def screenshot @map = Map.find(params[:id]) diff --git a/app/controllers/synapses_controller.rb b/app/controllers/synapses_controller.rb index 4440872f..b8ccfc5f 100644 --- a/app/controllers/synapses_controller.rb +++ b/app/controllers/synapses_controller.rb @@ -51,7 +51,7 @@ class SynapsesController < ApplicationController def destroy @synapse = Synapse.find(params[:id]) authorize @synapse - @synapse.delete + @synapse.destroy respond_to do |format| format.json { head :no_content } diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb index 9911a7fe..88e9cef4 100644 --- a/app/controllers/topics_controller.rb +++ b/app/controllers/topics_controller.rb @@ -150,7 +150,7 @@ puts @allsynapses.length @topic = Topic.find(params[:id]) authorize @topic - @topic.delete + @topic.destroy respond_to do |format| format.json { head :no_content } end @@ -159,6 +159,6 @@ puts @allsynapses.length private def topic_params - params.require(:topic).permit(:id, :name, :desc, :link, :permission, :user_id, :metacode_id) + params.require(:topic).permit(:id, :name, :desc, :link, :permission, :user_id, :metacode_id, :defer_to_map_id) end end diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb new file mode 100644 index 00000000..0d9431a3 --- /dev/null +++ b/app/mailers/application_mailer.rb @@ -0,0 +1,4 @@ +class ApplicationMailer < ActionMailer::Base + default from: "team@metamaps.cc" + layout 'mailer' +end diff --git a/app/mailers/map_mailer.rb b/app/mailers/map_mailer.rb new file mode 100644 index 00000000..dd164cf3 --- /dev/null +++ b/app/mailers/map_mailer.rb @@ -0,0 +1,10 @@ +class MapMailer < ApplicationMailer + default from: "team@metamaps.cc" + + def invite_to_edit_email(map, inviter, invitee) + @inviter = inviter + @map = map + subject = @map.name + ' - Invitation to edit' + mail(to: invitee.email, subject: subject) + end +end diff --git a/app/models/map.rb b/app/models/map.rb index d9eb6a18..8ffe14d6 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -8,6 +8,9 @@ class Map < ActiveRecord::Base has_many :synapses, through: :synapsemappings, source: :mappable, source_type: "Synapse" has_many :messages, as: :resource, dependent: :destroy + has_many :user_maps, dependent: :destroy + has_many :collaborators, through: :user_maps, source: :user + has_many :webhooks, as: :hookable has_many :events, -> { includes :user }, as: :eventable, dependent: :destroy @@ -45,6 +48,10 @@ class Map < ActiveRecord::Base return contributors end + def editors + collaborators + [self.user] + end + def topic_count topics.length end @@ -65,6 +72,10 @@ class Map < ActiveRecord::Base contributors.length end + def collaborator_ids + collaborators.map(&:id) + end + def screenshot_url screenshot.url(:thumb) end @@ -78,7 +89,7 @@ class Map < ActiveRecord::Base end def as_json(options={}) - json = super(:methods =>[:user_name, :user_image, :topic_count, :synapse_count, :contributor_count, :screenshot_url], :except => [:screenshot_content_type, :screenshot_file_size, :screenshot_file_name, :screenshot_updated_at]) + json = super(:methods =>[:user_name, :user_image, :topic_count, :synapse_count, :contributor_count, :collaborator_ids, :screenshot_url], :except => [:screenshot_content_type, :screenshot_file_size, :screenshot_file_name, :screenshot_updated_at]) json[:created_at_clean] = created_at_str json[:updated_at_clean] = updated_at_str json diff --git a/app/models/synapse.rb b/app/models/synapse.rb index 540376bb..7e361af3 100644 --- a/app/models/synapse.rb +++ b/app/models/synapse.rb @@ -1,5 +1,6 @@ class Synapse < ActiveRecord::Base belongs_to :user + belongs_to :defer_to_map, :class_name => 'Map', :foreign_key => 'defer_to_map_id' belongs_to :topic1, :class_name => "Topic", :foreign_key => "node1_id" belongs_to :topic2, :class_name => "Topic", :foreign_key => "node2_id" @@ -32,9 +33,29 @@ class Synapse < ActiveRecord::Base end # :nocov: + # :nocov: + def collaborator_ids + if defer_to_map + defer_to_map.editors.select{|mapper| not mapper == self.user }.map(&:id) + else + [] + end + end + # :nocov: + + # :nocov: + def calculated_permission + if defer_to_map + defer_to_map.permission + else + permission + end + end + # :nocov: + # :nocov: def as_json(options={}) - super(:methods =>[:user_name, :user_image]) + super(:methods =>[:user_name, :user_image, :calculated_permission, :collaborator_ids]) end # :nocov: diff --git a/app/models/topic.rb b/app/models/topic.rb index f1a73c1b..61f405de 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -2,6 +2,7 @@ class Topic < ActiveRecord::Base include TopicsHelper belongs_to :user + belongs_to :defer_to_map, :class_name => 'Map', :foreign_key => 'defer_to_map_id' has_many :synapses1, :class_name => 'Synapse', :foreign_key => 'node1_id', dependent: :destroy has_many :synapses2, :class_name => 'Synapse', :foreign_key => 'node2_id', dependent: :destroy @@ -11,6 +12,8 @@ class Topic < ActiveRecord::Base has_many :mappings, as: :mappable, dependent: :destroy has_many :maps, :through => :mappings + belongs_to :metacode + validates :permission, presence: true validates :permission, inclusion: { in: Perm::ISSIONS.map(&:to_s) } @@ -39,8 +42,6 @@ class Topic < ActiveRecord::Base topics1 + topics2 end - belongs_to :metacode - scope :relatives1, ->(topic_id = nil) { includes(:topics1) .where('synapses.node1_id = ?', topic_id) @@ -77,8 +78,24 @@ class Topic < ActiveRecord::Base maps.map(&:id) end + def calculated_permission + if defer_to_map + defer_to_map.permission + else + permission + end + end + def as_json(options={}) - super(:methods =>[:user_name, :user_image, :map_count, :synapse_count, :inmaps, :inmapsLinks]) + super(:methods =>[:user_name, :user_image, :map_count, :synapse_count, :inmaps, :inmapsLinks, :calculated_permission, :collaborator_ids]) + end + + def collaborator_ids + if defer_to_map + defer_to_map.editors.select{|mapper| not mapper == self.user }.map(&:id) + else + [] + end end # TODO move to a decorator? diff --git a/app/models/user.rb b/app/models/user.rb index 10a0a71c..a38d7177 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -7,7 +7,9 @@ class User < ActiveRecord::Base has_many :maps has_many :mappings has_many :tokens - + has_many :user_maps, dependent: :destroy + has_many :shared_maps, through: :user_maps, source: :map + after_create :generate_code devise :database_authenticatable, :recoverable, :rememberable, :trackable, :registerable diff --git a/app/models/user_map.rb b/app/models/user_map.rb new file mode 100644 index 00000000..5e91ecc2 --- /dev/null +++ b/app/models/user_map.rb @@ -0,0 +1,4 @@ +class UserMap < ActiveRecord::Base + belongs_to :map + belongs_to :user +end diff --git a/app/policies/map_policy.rb b/app/policies/map_policy.rb index b1ece0e3..17943086 100644 --- a/app/policies/map_policy.rb +++ b/app/policies/map_policy.rb @@ -4,7 +4,8 @@ class MapPolicy < ApplicationPolicy visible = ['public', 'commons'] permission = 'maps.permission IN (?)' if user - scope.where(permission + ' OR maps.user_id = ?', visible, user.id) + shared_maps = user.shared_maps.map(&:id) + scope.where(permission + ' OR maps.id IN (?) OR maps.user_id = ?', visible, shared_maps, user.id) else scope.where(permission, visible) end @@ -28,7 +29,7 @@ class MapPolicy < ApplicationPolicy end def show? - record.permission == 'commons' || record.permission == 'public' || record.user == user + record.permission == 'commons' || record.permission == 'public' || record.collaborators.include?(user) || record.user == user end def export? @@ -48,7 +49,12 @@ class MapPolicy < ApplicationPolicy end def update? - user.present? && (record.permission == 'commons' || record.user == user) + user.present? && (record.permission == 'commons' || record.collaborators.include?(user) || record.user == user) + end + + def access? + # note that this is to edit access + user.present? && record.user == user end def screenshot? diff --git a/app/policies/synapse_policy.rb b/app/policies/synapse_policy.rb index 042c9a75..9b1a8524 100644 --- a/app/policies/synapse_policy.rb +++ b/app/policies/synapse_policy.rb @@ -4,7 +4,7 @@ class SynapsePolicy < ApplicationPolicy visible = ['public', 'commons'] permission = 'synapses.permission IN (?)' if user - scope.where(permission + ' OR synapses.user_id = ?', visible, user.id) + scope.where(permission + ' OR synapses.defer_to_map_id IN (?) OR synapses.user_id = ?', visible, user.shared_maps.map(&:id), user.id) else scope.where(permission, visible) end @@ -17,14 +17,29 @@ class SynapsePolicy < ApplicationPolicy end def show? - record.permission == 'commons' || record.permission == 'public' || record.user == user + if record.defer_to_map.present? + map_policy.show? + else + record.permission == 'commons' || record.permission == 'public' || record.user == user + end end def update? - user.present? && (record.permission == 'commons' || record.user == user) + if not user.present? + false + elsif record.defer_to_map.present? + map_policy.update? + else + record.permission == 'commons' || record.user == user + end end def destroy? record.user == user || admin_override end + + # Helpers + def map_policy + @map_policy ||= Pundit.policy(user, record.defer_to_map) + end end diff --git a/app/policies/topic_policy.rb b/app/policies/topic_policy.rb index 335a2ed2..2eb2abb6 100644 --- a/app/policies/topic_policy.rb +++ b/app/policies/topic_policy.rb @@ -4,7 +4,7 @@ class TopicPolicy < ApplicationPolicy visible = ['public', 'commons'] permission = 'topics.permission IN (?)' if user - scope.where(permission + ' OR topics.user_id = ?', visible, user.id) + scope.where(permission + ' OR topics.defer_to_map_id IN (?) OR topics.user_id = ?', visible, user.shared_maps.map(&:id), user.id) else scope.where(permission, visible) end @@ -16,11 +16,21 @@ class TopicPolicy < ApplicationPolicy end def show? - record.permission == 'commons' || record.permission == 'public' || record.user == user + if record.defer_to_map.present? + map_policy.show? + else + record.permission == 'commons' || record.permission == 'public' || record.user == user + end end def update? - user.present? && (record.permission == 'commons' || record.user == user) + if not user.present? + false + elsif record.defer_to_map.present? + map_policy.update? + else + record.permission == 'commons' || record.user == user + end end def destroy? @@ -42,4 +52,9 @@ class TopicPolicy < ApplicationPolicy def relatives? show? end + + # Helpers + def map_policy + @map_policy ||= Pundit.policy(user, record.defer_to_map) + end end diff --git a/app/serializers/new_map_serializer.rb b/app/serializers/new_map_serializer.rb index 9b2ff400..3c56f82f 100644 --- a/app/serializers/new_map_serializer.rb +++ b/app/serializers/new_map_serializer.rb @@ -12,5 +12,6 @@ class NewMapSerializer < ActiveModel::Serializer has_many :synapses, serializer: NewSynapseSerializer has_many :mappings, serializer: NewMappingSerializer has_many :contributors, root: :users, serializer: NewUserSerializer + has_many :collaborators, root: :users, serializer: NewUserSerializer end diff --git a/app/views/layouts/_templates.html.erb b/app/views/layouts/_templates.html.erb index b3099ad9..7e5a6293 100644 --- a/app/views/layouts/_templates.html.erb +++ b/app/views/layouts/_templates.html.erb @@ -12,7 +12,7 @@
    - {{contributor_count}} + {{contributor_count}}
    {{{contributor_list}}}
    @@ -181,6 +181,18 @@
    + + diff --git a/app/views/maps/show.html.erb b/app/views/maps/show.html.erb index 5d6859b7..9a74b337 100644 --- a/app/views/maps/show.html.erb +++ b/app/views/maps/show.html.erb @@ -10,6 +10,7 @@ Metamaps.currentPage = <%= @map.id.to_s %>; Metamaps.Active.Map = <%= @map.to_json.html_safe %>; Metamaps.Mappers = <%= @allmappers.to_json.html_safe %>; + Metamaps.Collaborators = <%= @allcollaborators.to_json.html_safe %>; Metamaps.Topics = <%= @alltopics.to_json.html_safe %>; Metamaps.Synapses = <%= @allsynapses.to_json.html_safe %>; Metamaps.Mappings = <%= @allmappings.to_json.html_safe %>; diff --git a/config/environments/development.rb b/config/environments/development.rb index 593fbd3c..bad33ab9 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -30,7 +30,7 @@ Metamaps::Application.configure do port: ENV['SMTP_PORT'], user_name: ENV['SMTP_USERNAME'], password: ENV['SMTP_PASSWORD'], - #domain: ENV['SMTP_DOMAIN'] + domain: ENV['SMTP_DOMAIN'], authentication: 'plain', enable_starttls_auto: true, openssl_verify_mode: 'none' } @@ -41,6 +41,8 @@ Metamaps::Application.configure do # Print deprecation notices to the Rails logger config.active_support.deprecation = :log + config.action_mailer.preview_path = '/vagrant/spec/mailers/previews' + # Expands the lines which load the assets config.assets.debug = true end diff --git a/config/routes.rb b/config/routes.rb index 345547f4..a60749b7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -40,12 +40,13 @@ Metamaps::Application.routes.draw do post 'maps/:id/events/:event', to: 'maps#events' get 'maps/:id/contains', to: 'maps#contains', as: :contains post 'maps/:id/upload_screenshot', to: 'maps#screenshot', as: :screenshot + post 'maps/:id/access', to: 'maps#access', as: :access, defaults: {format: :json} get 'explore/active', to: 'maps#activemaps' get 'explore/featured', to: 'maps#featuredmaps' get 'explore/mine', to: 'maps#mymaps' + get 'explore/shared', to: 'maps#sharedmaps' get 'explore/mapper/:id', to: 'maps#usermaps' - devise_for :users, controllers: { registrations: 'users/registrations', passwords: 'users/passwords', sessions: 'devise/sessions' }, :skip => :sessions diff --git a/db/migrate/20160331181959_create_user_maps.rb b/db/migrate/20160331181959_create_user_maps.rb new file mode 100644 index 00000000..2af6e87a --- /dev/null +++ b/db/migrate/20160331181959_create_user_maps.rb @@ -0,0 +1,10 @@ +class CreateUserMaps < ActiveRecord::Migration + def change + create_table :user_maps do |t| + t.references :user, index: true + t.references :map, index: true + + t.timestamps + end + end +end diff --git a/db/migrate/20160401133937_add_defers_to_map_to_topics_and_synapses.rb b/db/migrate/20160401133937_add_defers_to_map_to_topics_and_synapses.rb new file mode 100644 index 00000000..afcd6df2 --- /dev/null +++ b/db/migrate/20160401133937_add_defers_to_map_to_topics_and_synapses.rb @@ -0,0 +1,6 @@ +class AddDefersToMapToTopicsAndSynapses < ActiveRecord::Migration + def change + add_column :topics, :defer_to_map_id, :integer + add_column :synapses, :defer_to_map_id, :integer + end +end diff --git a/db/schema.rb b/db/schema.rb index 1ad745a2..d3dc9634 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160318141618) do +ActiveRecord::Schema.define(version: 20160401133937) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -180,8 +180,9 @@ ActiveRecord::Schema.define(version: 20160318141618) do t.integer "node1_id" t.integer "node2_id" t.integer "user_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "defer_to_map_id" end add_index "synapses", ["node1_id", "node1_id"], name: "index_synapses_on_node1_id_and_node1_id", using: :btree @@ -217,11 +218,22 @@ ActiveRecord::Schema.define(version: 20160318141618) do t.string "audio_content_type" t.integer "audio_file_size" t.datetime "audio_updated_at" + t.integer "defer_to_map_id" end add_index "topics", ["metacode_id"], name: "index_topics_on_metacode_id", using: :btree add_index "topics", ["user_id"], name: "index_topics_on_user_id", using: :btree + create_table "user_maps", force: :cascade do |t| + t.integer "user_id" + t.integer "map_id" + t.datetime "created_at" + t.datetime "updated_at" + end + + add_index "user_maps", ["map_id"], name: "index_user_maps_on_map_id", using: :btree + add_index "user_maps", ["user_id"], name: "index_user_maps_on_user_id", using: :btree + create_table "users", force: :cascade do |t| t.string "name" t.string "email" diff --git a/public/famous/main.js b/public/famous/main.js index 2a147e83..7ec38c5c 100644 --- a/public/famous/main.js +++ b/public/famous/main.js @@ -303,7 +303,7 @@ Metamaps.Famous.build = function () { var loggedIn = Metamaps.Active.Mapper ? 'Auth' : ''; - if (section === "mine" || section === "active" || section === "featured") { + if (section === "mine" || section === "shared" || section === "active" || section === "featured") { f.explore.surf.setContent(templates[section + loggedIn + 'Content']); } else if (section === "mapper") { diff --git a/public/famous/templates.js b/public/famous/templates.js index 66e6b9ac..fa1024bb 100644 --- a/public/famous/templates.js +++ b/public/famous/templates.js @@ -19,16 +19,17 @@ t.logoContent += ''; /* logged in explore maps bars */ t.mineAuthContent = '
    My Maps
    '; + t.mineAuthContent += '
    Shared With Me
    '; t.mineAuthContent += '
    Recently Active
    '; - t.mineAuthContent += '
    Featured
    '; + + t.sharedAuthContent = '
    My Maps
    '; + t.sharedAuthContent += '
    Shared With Me
    '; + t.sharedAuthContent += '
    Recently Active
    '; t.activeAuthContent = '
    My Maps
    '; + t.activeAuthContent += '
    Shared With Me
    '; t.activeAuthContent += '
    Recently Active
    '; - t.activeAuthContent += '
    Featured
    '; - t.featuredAuthContent = '
    My Maps
    '; - t.featuredAuthContent += '
    Recently Active
    '; - t.featuredAuthContent += '
    Featured
    '; /* apps bars */ t.registeredAppsContent = '
    Registered Apps
    '; diff --git a/spec/mailers/map_mailer_spec.rb b/spec/mailers/map_mailer_spec.rb new file mode 100644 index 00000000..8572c8f5 --- /dev/null +++ b/spec/mailers/map_mailer_spec.rb @@ -0,0 +1,5 @@ +require "rails_helper" + +RSpec.describe MapMailer, type: :mailer do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/mailers/previews/map_mailer_preview.rb b/spec/mailers/previews/map_mailer_preview.rb new file mode 100644 index 00000000..60310bf4 --- /dev/null +++ b/spec/mailers/previews/map_mailer_preview.rb @@ -0,0 +1,6 @@ +# Preview all emails at http://localhost:3000/rails/mailers/map_mailer +class MapMailerPreview < ActionMailer::Preview + def invite_to_edit_email + MapMailer.invite_to_edit_email(Map.first, User.first, User.second) + end +end