From 3a51c5c978648c69e786a6f7e4953c0a34c29487 Mon Sep 17 00:00:00 2001
From: Sander Roosendaal
| Date/Time: | {{ workout.startdatetime }} | -||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Distance: | {{ workout.distance }}m | -||||||||||||||
| Duration: | {{ workout.duration |durationprint:"%H:%M:%S.%f" }} | -||||||||||||||
| Public link to this workout | -- https://rowsandall.com/rowers/workout/{{ workout.id }} - | -||||||||||||||
| Comments | -- Comment - | - -||||||||||||||
| Public link to interactive chart | -
+
+ {% localtime on %}
+
| + |
- Add Time Plot -
-- Add Pie Chart -
-- Power Pie Chart -
-Generating images takes roughly 1 second per minute - of your workout's duration. Please reload after a minute or so.
-+ Add Time Plot +
++ Add Pie Chart +
++ Power Pie Chart +
Generating images takes roughly 1 second per minute + of your workout's duration. Please reload after a minute or so.
+No graphs found
- {% endif %} -
u!PQ_NR7?o(NylLXVTfDO zxs5=@|GsYEsNo4M#nT%N!UE(?dnS)t2+{ELYAFp*3=iF=|EQnTp`#vlSXuGVraYo? z+RCzXo6h3qA8{KG?S4nE(lM+;Eb4nT3XV;7gcAxUi5m)`k5tv}cPy()8ZR3TLW3I- zAS^}cq-IJvL7a4RgR!yk@~RT%$lA7{L5ES*hyx)M4(yxI$Ub(4f)K|^v1>zvwQY!_ zIrWw8q9GS^!Dp~}+?mbnB6jDF8mVlbQ!jFKDY;w=7;XO{9bq7>LXGK24WA`;rL)_Z z)&j}pbV (;6gY;VMhbxgvn`X;6x}VUEE-7 z%)7j-%t8S=ZL3yc)HbXDAqJZvBTPoiW_A-+a8m3_Z?v{DN7Tnr#O_VUMT0UBt$;p` zDh6JbGHN8JJ*JN%y2%msb97@_S>9!%Egwk;?PEkU9ntz&3uR}%Fj5d$JHQbQb3}a{ zSzFT^#n =VInPpcAS}CNxj?_ zVscANk5Cfz(51EI1pz};AWWb|kgbYNb4wCEGUn3+eMUMV?1-{=I4TlmLJMot@rd07 zZuo2hk1ccu{YmGkcYdWAVdk{Z4Nm?^cTD&}jGm+Q1SYIXMwmG*oO*83>l%nbR`G zhh=lZ%xIb7kU3#;TBbfECrnC9P=-XpL|TG2BoZdj61*XiFbW8?1Z_wp%#;>${SUIy V$8qr;L*)Pf002ovPDHLkV1hYLS~36t literal 0 HcmV?d00001 diff --git a/static/leaflet/draw/images/layers.png b/static/leaflet/draw/images/layers.png new file mode 100644 index 0000000000000000000000000000000000000000..1a72e5784b2b456eac5d7670738db80697af3377 GIT binary patch literal 696 zcmV;p0!RIcP) *@&l2<6p=!C&s@#ZL+%BQvF&b?w6S%wp=I>1QHj7AP5C)IWy#b znXXB;g;j=$a-tW89K%FbDceHVq&unY*Wx3L#=EGWH=rjqnp|4c_Ulec!ql3#G-5ZF zVlbBA@XP=)C8U&+Lrc)S4O5%1$&{(;7R^K(CSnvSr$v;+B$8q&7Bf|h$#PARo1^%M zf1H^nG-EiXVXr07OH(*8R)xa|F D;lXUlg_-%)~ZGsL2cX0NXaAzN2q%jqLRR6ruVk8`Jb7n#{`T;o@`F= z#3YcynIR^s83UNF3D!f5m#Mg)NJ24&Qfrqb&_z=yF;=B)#9Iq7u-@^O!(mW{D;qvr zPc)gVb%aowtS8m@ElL4A9G>w#ffQ~q{i&_i)*6f^)Sz|C?C>zb4Uo?H<-&Hz@a?J; z$ml@zGygWofb9$ZBj6aLjpLhsT2AzjOu=-*u_gSCU Luqp7Vds@BKcX=lOk~^Pb;%&wG9p3o}FL;Zuhp5D3)R z2yY2yCGfH2=LJ mOkQw^ 9n>daF6?Fz>oOD64$CM+_T`j0x%{zb|G zWolt{H|diO#S`|$6RM $ zYQuE4RW{2yZ`>fAt>jzyYyOB?)~HrQBlbbLT5yF%Xq8FEuzo80dd{%Q!{_)^mTE`^ z2$xe>TH$qiDJ+}(ajTp$Y*4vgGRrt^_?JwUO3+hm&{M b<8aRtf7%F@*!JJv* zmcB*cag=-t4U&g79u1krRAKHHm?ZXHP8z-#KdAM9?vU7sxldD%A5;r0Rk~kblro}5 z9YhoJP18m~=v^kMBWPltYTV$TD;r4n^eZVWmDs^6;ZN_RG+a#^(N18a+%xd;JvScL zu54_hiMdFR4767cmcp!KOryQBQG{$|3e)h(z_sY-NRM>A$84n-CdxAt6V242bQmV| z86*uGCJtVTXCvLyz=eM@jE-Vz#IeA4DY~VhqL`R_>D;YIh9amQX~+l$Sfbohb*X)d zKiDG!?8t|64T_+_Jzbv6K)P|KG-6qDVGPYUwpPqb#c;-juz~ZW0bFF4JlB>cOB#?3 z9XJ~@0J1u{T_(66oVpmpLOkqOk6}qY=vN7820OS|_L-o5(4!i~Ivv=j{IKzS2m>C_ zhm9Npo09&0s*wy#K%InNpSW)yCZOhAFhe UQtcXnn!x)WSjonNUm7@fguKPg0C3ESs~`Bd3Pyd$@XU8m z0JZWv0l=fZ{{jH?{!9Nt!mEGL|9_Oug?i>9H?4E!|Krk+(hy9WRiM;!>w8@J9&fq& z${#rK1z4j2$*KVGO=b{ivL6FFEPprv0No7|9RPB_H>dzW{;{(>P`XWmKn^Y#<8`e9 zc*;k@X>z(^khkvlh3UB1ICnF@RRHbZaQhkI;sl{txVGnBEzaFKZpw96Fm8qu^5@!a z+db!omc48o>}VvJr!j9Mpo^ZMPs2FKikZu-3edWhZ~5&Mp15G60gsVYic)|~eH4Q6 zF8d5^efqo~DD}CwR pRO|j91O-zygw(bv;<>V5MDzeC#nk zosJI@GCU;ylx)tp87H~!5Gl8^4UxdZ-ZLrRy7g=zwjIe|v>O(6W-QBuv-7h4HT Lcz&ce9H!^9o^4XLD_t08@f%uD+tdxMAHzHi z6>y1>XBw|wNRu9u6j`13s*X9iz%Z1zep^?+<}$-U*uzd9$?LD0QWc+GSyhyvx< ?!6YcvM{vC6CN2-dD>XyCsuOMe zdjA0H)tFMHvR%5Uqd_swkzDP0t5)bhy5xwusp(WsD}~`13N0NuN78MHcc03G_@3v- zZOvStb!W8+G+$o+mNh5)?USue0<9~5nql|l&C!mcb^cmUZGk2gF&p9IOMcs@2-WZX z+M_WESiwx34!IyuOY(`!=Sit;If5uuYqSJm`D>ogL1P7x5=v2W{zicaAxUs>WGzTn zQv?x3HR!VK$IB{-D-)cU&hLE;M2}umynSZBHRVLCW#WkaY>!>~#*V;;^Ck!H4Swwp zDHCGo7gMu}4-?)ga$s&da$6}|l&eSgpl~CnG5l
bg z7&|&nHy^@(l0;d(4qw!>Pc+03BPqwvhV@DjJr)KAb74dUY>mzPErgW+cGhAfAE(Hx zg7S551PZuugrt1qVHk*xE*1`NeDO|ZnOO1ye(Ps{N=r+Q=S*|(%4dYb+TIr5*H@Ka z&IFce5q4snQ7O4sQm?Pxu??B#U>#Bu+HC!Ti{Sl150Y#4pk06Ac+lU@`2YRqk-uHH zZoIWi#kr-H+gi|P?w*2JMQ7U)c>*fCAPTksemc#0N4+Zgz+o*bN1@=(#&Q(RLz+r2 zQx|up>q>^w^^^t*`_3bp*JBDwCvP3iT>oMu+dLrW{Yd*GhC1Kx;_L$zF%*j;?iDxZ zrao$m-Bw;}qtlD8Ts>}{*(A|it9iEx_ZRY$yVv3y#q}J<;l}p;3_y0NqKJBW%sac- z#s<-=rSr4%CNFQcuf<8$A3ba|hx+!=-B0jwr*} bFG1p0OLTqz#DYd z16dVY=E5n{UkaA*7{FAF7c$=SE0gV@(AxW_6rfOFvBFyfQpO=ChwyqQo?nZOT`6__ zP3(sCcoy|xktOO{hUoSFKDM)^*yWXvlS$9yTyC~k^q#t~$$O;oU_E7XGiY~S^b+mS zVh=RZHn+0(T-ooM5xx%AW =ZUqv zgKQURIr-z7x5ejdVPYlT>F)dyou|#!MM#5qXK_BVQyz*bJ!*A&^rr((=SaeGlUNwV z01+e{DcnsPPIth+gTfMc34NrqGRM-T5f0=)<0vZ6?K`I0Z1Y3GdqxI|$iyh%qoeNX UQO-*oc+)|Q_08}VdXD6O0C*xx%>V!Z literal 0 HcmV?d00001 diff --git a/static/leaflet/draw/images/marker-icon.png b/static/leaflet/draw/images/marker-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..950edf24677ded147df13b26f91baa2b0fa70513 GIT binary patch literal 1466 zcmV;r1x5OaP) P001cn1^@s6z>|W`000GnNkl GNuHDcIX17Zdjl&3`L?0sTjIws<{((Dh&g-s0<@jYQyl?D*X^?%13;ml^gy> ziMrY_^1WI=(g@LMizu=zCoA>C`6|QEq1eV92k*7m>G65*&@&6) aC&e}G zI)pf-Za|N`DT&Cn1J|o`19mumxW~hiKiKyc-P`S@q)rdTo84@QI@;0yXrG%9uhI>A zG5QHb6s4=<6xy{1 z@NMxEkryp{LS44%z$3lP^cX!9+2-;CTt3wM4(k*#C{aiIiLuB>jJj;KPhPzIC00bL zU3a#;aJld94lCW=`4&aAy8M7PY=HQ>O%$YEP4c4UY#CRxfgbE~(|uiI=YS8q;O9y6 zmIkXzR`}p7ti|PrM3a}WMnR=3NVnWdAAR>b9X@)DKL6=YsvmH%?I24wdq?Gh54_;# z$?_LvgjEdspdQlf t#4CQ z`2Zyvy?*)N1Ftw|{_hakhG9WjS?Az@I@+IZ8JbWewR !XUK4&6346+d#~gsE0SY(LX8&JfY>Aj)RxGy96nwhs2rv zzW6pTn MpFkDSkT*a*6Dx|u@ds6ISVn0@^RmIsKZ5Y ;bazbc;tTSq(kg(=481ODrPyNB6n z-$+U}(w$m6U6H$w17Bw+wDaFIe~GvNMYvnw31MpY0eQKT9l>SU``8k7w4)z!GZKMI z#_cEKq7k~i%nlK@6c-K?+R;B#5$?T#YpKD`t_4bAs^#E+@5QW$@OX3* `;(#{U^d-vY)&xEE>n5lYl&T?Amke9$Lam@{1K@O ze*LXqlKQHiv=gx+V^Cbb2?z@ISBQ*3amF;9UJ3SBg(N|710TLamQmYZ&Qjn2LuO<* zCZlB4n%@pc&7NNnY1}x+NWpHlq`OJEo|`aYN9<`RBUB+79g;>dgb6YlfN#kGL?lO_ z!6~M^7s OnbsUkKk<@Ysie&`G>ruxH&Mgy&8;i=A zB9OO!xR{AyODw>DS-q5YM {0ExFEAzt zm>RdS+ssW(-8|?xr0(?$vBVB*%(xDLtq3Hf0I5yFm<_g=W2`QWAax{1rWVH=I!VrP zs(rTFX@W#t$hXNvbgX`gK&^w_YD;CQ!B@e0QbLIWaKAXQe2-kkloo;{iF#6}z!4=W zi$giRj1{ zt;2w`VSCF#WE&*ev7jpsC=6175@(~nTE2;7M-L((0bH@yG}-TB$R~WXd?tA$s3|%y zA`9$sA(>F%J3ioz<-LJl*^o1|w84l>HBR`>3l9c8$5Xr@xCiIQ7{x$fMCzOk_-M=% z+{a_Q#;42`#KfUte@$NT77uaTz?b-fBe)1s5XE$yA79fm?KqM^VgLXD07*qoM6N<$ Ef<_J(9smFU literal 0 HcmV?d00001 diff --git a/static/leaflet/draw/images/spritesheet-2x.png b/static/leaflet/draw/images/spritesheet-2x.png new file mode 100644 index 0000000000000000000000000000000000000000..1525c9f698ed277b39e7bb19a99309cf4f536a46 GIT binary patch literal 2078 zcmb7F`#%$k1D$BfH!~^k=PfCugrO!Mkq|<-^O!XY*ZVOu%siS+uT@?v3AxG0V+f0R z>}$2%Z!3?eEhE!fVa+2n&$`{u_b<5ThjY&7e9q@{emvKFyw52sX)6H$0A+VK7e4@C zhwd*A-39!`M_;2*zeFqbEG+e6Vq_{7nGykTx||puVdx%@jEeA!Kwi#B{$B(X08nJQ zyEyr$kAIyU@fCRNV<#sU$jU(<{lR`tujG4xrpKEBp>jTQH9I_auGvFf{K3>KhxOLd zl4N%dxT+T!zc*`(TK)Mt{U*5yX&)@5mYSRF{ *@ z9~C=Baz!GPWJ6z1Z#8%d|56U{0k|NK#R`F+yQ&>SfRDHonqHPLNq!AYo)ki {90zlJDo3HY5-+U2Bs@KK%VhB$Uem6^uO?2h2S%GISVu8+_ z;!PaQbUf{>5T>G&K_(M#naJp20qv64a}143oZdIgLTW1%5C0tll-H^JNP3^OIFr;v z$IL+>Xk$!o68##34C%4Ho~iL}ww%;`UOB3{>HMSk5<_F@7HkCD;CaL7Jl5R8^j)4x z^;nMc)N6XH7mkVPw;d^%WR7>S6LJTY7HCbaxJlF8@2i0b#Fw=EED#4h-dWOYn@@JB zDmKK-hec%=YMV%JD|UkPNOPORVC$EwmCs}q+WJ?AE?;OCXD+dNvty!ZeJC$87Afg= zl(y@TlKcqr627N?0V4H`+rYQrVNr)p@}6lz@^Mf!H#0y49w3!&%q#NOhgPRG%8JHT zA{^qLYeIpKS =(b}U&r?L=AIvG_6^(d=Pp?9mRM)%XF^4O zd{{&B>GJ7pThpOjrkk)%t7RD_Ja)wFxhP+MfK)HEY-d)FesU~I7;VLz(7Q^g8cC5- zFqxrr>{R$6mkkM$DaF~hGKOW5@i7-izwpkS6L#pCWP>g$;^#?~PnVvs<3V|2uWmLH zZ{-T4O?`bdZmR+FB|-xNQvPbRzDMT_WTJoa=#0aUyspK4kiGE1voou2FFY!-h27|8 z9sJgV8@l_(lVi8&uubp-PZ-C2>cCS4&BJ7edFvWE`f>mnyGw@>qZ^0+X%wfR1_9Ti zhq&dMf1uwQ6>XAa6ZBB;tCUkDa#ne%RtP=fc$dW !MKHRq?j^w9$B39|-9@h_bI1@JEf}d{F1}SSfs;sB8VG z4H~RBO#aVrjE`Nl&Q8UeKUMWuY^?_G6(m7BWVfzrzOv;)vifhRQWwCJcMEFU)*b=< zryJf&ZcHmR#1F7{%KQG)#I^jqdy3Z4Vq^t2>%GK_P`P1}WT+7RdKtSBT>{}v^V@aJ zyzdW?K0aUC6&u=Jr(yG5=oM)fzFTJ}-9g%$Y`4=We@;wVEJp~0j1N2_*?fRSpwNS6 zd_>G565nm*Kq}9{e8Vx&T@ANUZNZ~T`nR$NfV*@QZXpZEXJ(#xZ5ixv4>Be^ zNgj75TOF9X#c=DadJ!_a*;LN$^hm7QrQ;jiV2ON$=&fL>qWOoVFD5fW^$o5m-+n+# zn$5^iAD}#Fuok3jSH4&z=4TqwZd!nk_1@92=~TBfZ63 M1%`hDxoN7nMu2hli=DbI* 4X3j-oWPj!`o=PN*bMpKv~p( zlK?>bLve1yjn9!PJt^r S6X2Q7N0bo$yHWMC}3ggipD8vsX|xW!=U#I}LWz z^QnfSrM*$QQ{ryc$jHb5k=UWl;GE*YDBK*|2h61)7W<2wvTJ?p)5iRGT5JEC+@%`% zqS6Z%SFIPtt0zaN=eCZlFYjOW`hszTixox$Zr%@j-VXa>c1Sz2W4PM({(;ce?St=s z;7(-eXX4Dn@faRw%x-a`JGUdS|JLnZ!E%QoTvY;b{k^pS#LL#o%6_}2uB 3_$$L!~B0eY#os+2dKcS$ZPmw!~e pF literal 0 HcmV?d00001 diff --git a/static/leaflet/draw/images/spritesheet.png b/static/leaflet/draw/images/spritesheet.png new file mode 100644 index 0000000000000000000000000000000000000000..f7035a1cb0294a91eaedd98d2cbc0de6264eb4f9 GIT binary patch literal 1056 zcmV+*1mF9KP) ^cqr000000000000000000000001}0u3|)s5-;puuZqwcDub(csIZEHt~3&VXFw9 z=S}Dq3 )De xi9xvEV 5Dhi)kbe9jqQL|8Ub3b#khy(tY gUY?(RiCn z90SpaZP ZKS$*92+NJaP-|e z*+P3q)ZU8&?Y+?KZ}|`G;oSX4g2Bs{iwx>FbBYFUO+J?;8V0MJqEL$%hwPHfa}=E` zonPVhPJ3_Y*bT&*e94n9J5II0^Sn`xyiS$t&?+#f8zLZOiAMYt%)SM=$Dr4)IvY zE=K7Gz@)v$;#ry}R>w+x?$gzi^Bif*&Cl|&@@sNZ*q5>VKmx&adr9P98S#y8by=#s z#)GTmiNry=ug>qSKwm`SNz#GV|Is)RT2MTADN_28bJ>37$o``|vVLOJVC?)oIodlg z*~UutGtV_xUY7VI7_NL2W(Pime9pta^!fq-00000000000000000000000000H`3p a1Q-A)p~XwR_AF)q0000 literal 0 HcmV?d00001 diff --git a/static/leaflet/draw/images/spritesheet.svg b/static/leaflet/draw/images/spritesheet.svg new file mode 100644 index 00000000..54237932 --- /dev/null +++ b/static/leaflet/draw/images/spritesheet.svg @@ -0,0 +1,27 @@ + + diff --git a/static/leaflet/draw/leaflet.draw-src.css b/static/leaflet/draw/leaflet.draw-src.css new file mode 100644 index 00000000..5de40902 --- /dev/null +++ b/static/leaflet/draw/leaflet.draw-src.css @@ -0,0 +1,317 @@ +/* ================================================================== */ +/* Toolbars +/* ================================================================== */ + +.leaflet-draw-section { + position: relative; +} + +.leaflet-draw-toolbar { + margin-top: 12px; +} + +.leaflet-draw-toolbar-top { + margin-top: 0; +} + +.leaflet-draw-toolbar-notop a:first-child { + border-top-right-radius: 0; +} + +.leaflet-draw-toolbar-nobottom a:last-child { + border-bottom-right-radius: 0; +} + +.leaflet-draw-toolbar a { + background-image: url('images/spritesheet.png'); + background-image: linear-gradient(transparent, transparent), url('images/spritesheet.svg'); + background-repeat: no-repeat; + background-size: 270px 30px; + background-clip: padding-box; +} + +.leaflet-retina .leaflet-draw-toolbar a { + background-image: url('images/spritesheet-2x.png'); + background-image: linear-gradient(transparent, transparent), url('images/spritesheet.svg'); +} + +.leaflet-draw a { + display: block; + text-align: center; + text-decoration: none; +} + +.leaflet-draw a .sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0,0,0,0); + border: 0; +} + +/* ================================================================== */ +/* Toolbar actions menu +/* ================================================================== */ + +.leaflet-draw-actions { + display: none; + list-style: none; + margin: 0; + padding: 0; + position: absolute; + left: 26px; /* leaflet-draw-toolbar.left + leaflet-draw-toolbar.width */ + top: 0; + white-space: nowrap; +} + +.leaflet-touch .leaflet-draw-actions { + left: 32px; +} + +.leaflet-right .leaflet-draw-actions { + right: 26px; + left: auto; +} + +.leaflet-touch .leaflet-right .leaflet-draw-actions { + right: 32px; + left: auto; +} + +.leaflet-draw-actions li { + display: inline-block; +} + +.leaflet-draw-actions li:first-child a { + border-left: none; +} + +.leaflet-draw-actions li:last-child a { + -webkit-border-radius: 0 4px 4px 0; + border-radius: 0 4px 4px 0; +} + +.leaflet-right .leaflet-draw-actions li:last-child a { + -webkit-border-radius: 0; + border-radius: 0; +} + +.leaflet-right .leaflet-draw-actions li:first-child a { + -webkit-border-radius: 4px 0 0 4px; + border-radius: 4px 0 0 4px; +} + +.leaflet-draw-actions a { + background-color: #919187; + border-left: 1px solid #AAA; + color: #FFF; + font: 11px/19px "Helvetica Neue", Arial, Helvetica, sans-serif; + line-height: 28px; + text-decoration: none; + padding-left: 10px; + padding-right: 10px; + height: 28px; +} + +.leaflet-touch .leaflet-draw-actions a { + font-size: 12px; + line-height: 30px; + height: 30px; +} + +.leaflet-draw-actions-bottom { + margin-top: 0; +} + +.leaflet-draw-actions-top { + margin-top: 1px; +} + +.leaflet-draw-actions-top a, +.leaflet-draw-actions-bottom a { + height: 27px; + line-height: 27px; +} + +.leaflet-draw-actions a:hover { + background-color: #A0A098; +} + +.leaflet-draw-actions-top.leaflet-draw-actions-bottom a { + height: 26px; + line-height: 26px; +} + +/* ================================================================== */ +/* Draw toolbar +/* ================================================================== */ + +.leaflet-draw-toolbar .leaflet-draw-draw-polyline { + background-position: -2px -2px; +} + +.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-draw-polyline { + background-position: 0 -1px; +} + +.leaflet-draw-toolbar .leaflet-draw-draw-polygon { + background-position: -31px -2px; +} + +.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-draw-polygon { + background-position: -29px -1px; +} + +.leaflet-draw-toolbar .leaflet-draw-draw-rectangle { + background-position: -62px -2px; +} + +.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-draw-rectangle { + background-position: -60px -1px; +} + +.leaflet-draw-toolbar .leaflet-draw-draw-circle { + background-position: -92px -2px; +} + +.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-draw-circle { + background-position: -90px -1px; +} + +.leaflet-draw-toolbar .leaflet-draw-draw-marker { + background-position: -122px -2px; +} + +.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-draw-marker { + background-position: -120px -1px; +} + +/* ================================================================== */ +/* Edit toolbar +/* ================================================================== */ + +.leaflet-draw-toolbar .leaflet-draw-edit-edit { + background-position: -152px -2px; +} + +.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-edit-edit { + background-position: -150px -1px; +} + +.leaflet-draw-toolbar .leaflet-draw-edit-remove { + background-position: -182px -2px; +} + +.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-edit-remove { + background-position: -180px -1px; +} + +.leaflet-draw-toolbar .leaflet-draw-edit-edit.leaflet-disabled { + background-position: -212px -2px; +} + +.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-edit-edit.leaflet-disabled { + background-position: -210px -1px; +} + +.leaflet-draw-toolbar .leaflet-draw-edit-remove.leaflet-disabled { + background-position: -242px -2px; +} + +.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-edit-remove.leaflet-disabled { + background-position: -240px -2px; +} + +/* ================================================================== */ +/* Drawing styles +/* ================================================================== */ + +.leaflet-mouse-marker { + background-color: #fff; + cursor: crosshair; +} + +.leaflet-draw-tooltip { + background: rgb(54, 54, 54); + background: rgba(0, 0, 0, 0.5); + border: 1px solid transparent; + -webkit-border-radius: 4px; + border-radius: 4px; + color: #fff; + font: 12px/18px "Helvetica Neue", Arial, Helvetica, sans-serif; + margin-left: 20px; + margin-top: -21px; + padding: 4px 8px; + position: absolute; + visibility: hidden; + white-space: nowrap; + z-index: 6; +} + +.leaflet-draw-tooltip:before { + border-right: 6px solid black; + border-right-color: rgba(0, 0, 0, 0.5); + border-top: 6px solid transparent; + border-bottom: 6px solid transparent; + content: ""; + position: absolute; + top: 7px; + left: -7px; +} + +.leaflet-error-draw-tooltip { + background-color: #F2DEDE; + border: 1px solid #E6B6BD; + color: #B94A48; +} + +.leaflet-error-draw-tooltip:before { + border-right-color: #E6B6BD; +} + +.leaflet-draw-tooltip-single { + margin-top: -12px +} + +.leaflet-draw-tooltip-subtext { + color: #f8d5e4; +} + +.leaflet-draw-guide-dash { + font-size: 1%; + opacity: 0.6; + position: absolute; + width: 5px; + height: 5px; +} + +/* ================================================================== */ +/* Edit styles +/* ================================================================== */ + +.leaflet-edit-marker-selected { + background-color: rgba(254, 87, 161, 0.1); + border: 4px dashed rgba(254, 87, 161, 0.6); + -webkit-border-radius: 4px; + border-radius: 4px; + box-sizing: content-box; +} + +.leaflet-edit-move { + cursor: move; +} + +.leaflet-edit-resize { + cursor: pointer; +} + +/* ================================================================== */ +/* Old IE styles +/* ================================================================== */ + +.leaflet-oldie .leaflet-draw-toolbar { + border: 1px solid #999; +} \ No newline at end of file diff --git a/static/leaflet/draw/leaflet.draw-src.js b/static/leaflet/draw/leaflet.draw-src.js new file mode 100644 index 00000000..2b49356f --- /dev/null +++ b/static/leaflet/draw/leaflet.draw-src.js @@ -0,0 +1,4387 @@ +/* + Leaflet.draw 0.4.9, a plugin that adds drawing and editing tools to Leaflet powered maps. + (c) 2012-2017, Jacob Toye, Jon West, Smartrak, Leaflet + + https://github.com/Leaflet/Leaflet.draw + http://leafletjs.com + */ +(function (window, document, undefined) {/** + * Leaflet.draw assumes that you have already included the Leaflet library. + */ +L.drawVersion = "0.4.9"; +/** + * @class L.Draw + * @aka Draw + * + * + * To add the draw toolbar set the option drawControl: true in the map options. + * + * @example + * ```js + * var map = L.map('map', {drawControl: true}).setView([51.505, -0.09], 13); + * + * L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', { + * attribution: '© OpenStreetMap contributors' + * }).addTo(map); + * ``` + * + * ### Adding the edit toolbar + * To use the edit toolbar you must initialise the Leaflet.draw control and manually add it to the map. + * + * ```js + * var map = L.map('map').setView([51.505, -0.09], 13); + * + * L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', { + * attribution: '© OpenStreetMap contributors' + * }).addTo(map); + * + * // FeatureGroup is to store editable layers + * var drawnItems = new L.FeatureGroup(); + * map.addLayer(drawnItems); + * + * var drawControl = new L.Control.Draw({ + * edit: { + * featureGroup: drawnItems + * } + * }); + * map.addControl(drawControl); + * ``` + * + * The key here is the featureGroup option. This tells the plugin which FeatureGroup contains the layers that + * should be editable. The featureGroup can contain 0 or more features with geometry types Point, LineString, and Polygon. + * Leaflet.draw does not work with multigeometry features such as MultiPoint, MultiLineString, MultiPolygon, + * or GeometryCollection. If you need to add multigeometry features to the draw plugin, convert them to a + * FeatureCollection of non-multigeometries (Points, LineStrings, or Polygons). + */ +L.Draw = {}; + +/** + * @class L.drawLocal + * @aka L.drawLocal + * + * The core toolbar class of the API — it is used to create the toolbar ui + * + * @example + * ```js + * var modifiedDraw = L.drawLocal.extend({ + * draw: { + * toolbar: { + * buttons: { + * polygon: 'Draw an awesome polygon' + * } + * } + * } + * }); + * ``` + * + * The default state for the control is the draw toolbar just below the zoom control. + * This will allow map users to draw vectors and markers. + * **Please note the edit toolbar is not enabled by default.** + */ +L.drawLocal = { + draw: { + toolbar: { + // #TODO: this should be reorganized where actions are nested in actions + // ex: actions.undo or actions.cancel + actions: { + title: 'Cancel drawing', + text: 'Cancel' + }, + finish: { + title: 'Finish drawing', + text: 'Finish' + }, + undo: { + title: 'Delete last point drawn', + text: 'Delete last point' + }, + buttons: { + polyline: 'Draw a polyline', + polygon: 'Draw a polygon', + rectangle: 'Draw a rectangle', + circle: 'Draw a circle', + marker: 'Draw a marker' + } + }, + handlers: { + circle: { + tooltip: { + start: 'Click and drag to draw circle.' + }, + radius: 'Radius' + }, + marker: { + tooltip: { + start: 'Click map to place marker.' + } + }, + polygon: { + tooltip: { + start: 'Click to start drawing shape.', + cont: 'Click to continue drawing shape.', + end: 'Click first point to close this shape.' + } + }, + polyline: { + error: 'Error: shape edges cannot cross!', + tooltip: { + start: 'Click to start drawing line.', + cont: 'Click to continue drawing line.', + end: 'Click last point to finish line.' + } + }, + rectangle: { + tooltip: { + start: 'Click and drag to draw rectangle.' + } + }, + simpleshape: { + tooltip: { + end: 'Release mouse to finish drawing.' + } + } + } + }, + edit: { + toolbar: { + actions: { + save: { + title: 'Save changes.', + text: 'Save' + }, + cancel: { + title: 'Cancel editing, discards all changes.', + text: 'Cancel' + } + }, + buttons: { + edit: 'Edit layers.', + editDisabled: 'No layers to edit.', + remove: 'Delete layers.', + removeDisabled: 'No layers to delete.' + } + }, + handlers: { + edit: { + tooltip: { + text: 'Drag handles, or marker to edit feature.', + subtext: 'Click cancel to undo changes.' + } + }, + remove: { + tooltip: { + text: 'Click on a feature to remove' + } + } + } + } +}; + + + +/** + * ### Events + * Once you have successfully added the Leaflet.draw plugin to your map you will want to respond to the different + * actions users can initiate. The following events will be triggered on the map: + * + * @class L.Draw.Event + * @aka Draw.Event + * + * Use `L.Draw.Event.EVENTNAME` constants to ensure events are correct. + * + * @example + * ```js + * map.on(L.Draw.Event.CREATED; function (e) { + * var type = e.layerType; + * layer = e.layer; + * + * if (type === 'marker') { + * // Do marker specific actions + * } + * + * // Do whatever else you need to. (save to db; add to map etc) + * map.addLayer(layer); + *}); + * ``` + */ +L.Draw.Event = {}; +/** + * @event draw:created: PolyLine; Polygon; Rectangle; Circle; Marker | String + * + * Layer that was just created. + * The type of layer this is. One of: `polyline`; `polygon`; `rectangle`; `circle`; `marker` + * Triggered when a new vector or marker has been created. + * + */ +L.Draw.Event.CREATED = 'draw:created'; + +/** + * @event draw:edited: LayerGroup + * + * List of all layers just edited on the map. + * + * + * Triggered when layers in the FeatureGroup; initialised with the plugin; have been edited and saved. + * + * @example + * ```js + * map.on('draw:edited'; function (e) { + * var layers = e.layers; + * layers.eachLayer(function (layer) { + * //do whatever you want; most likely save back to db + * }); + * }); + * ``` + */ +L.Draw.Event.EDITED = 'draw:edited'; + +/** + * @event draw:deleted: LayerGroup + * + * List of all layers just removed from the map. + * + * Triggered when layers have been removed (and saved) from the FeatureGroup. + */ +L.Draw.Event.DELETED = 'draw:deleted'; + +/** + * @event draw:drawstart: String + * + * The type of layer this is. One of:`polyline`; `polygon`; `rectangle`; `circle`; `marker` + * + * Triggered when the user has chosen to draw a particular vector or marker. + */ +L.Draw.Event.DRAWSTART = 'draw:drawstart'; + +/** + * @event draw:drawstop: String + * + * The type of layer this is. One of: `polyline`; `polygon`; `rectangle`; `circle`; `marker` + * + * Triggered when the user has finished a particular vector or marker. + */ + +L.Draw.Event.DRAWSTOP = 'draw:drawstop'; + +/** + * @event draw:drawvertex: LayerGroup + * + * List of all layers just being added from the map. + * + * Triggered when a vertex is created on a polyline or polygon. + */ +L.Draw.Event.DRAWVERTEX = 'draw:drawvertex'; + +/** + * @event draw:editstart: String + * + * The type of edit this is. One of: `edit` + * + * Triggered when the user starts edit mode by clicking the edit tool button. + */ + +L.Draw.Event.EDITSTART = 'draw:editstart'; + +/** + * @event draw:editmove: ILayer + * + * Layer that was just moved. + * + * Triggered as the user moves a rectangle; circle or marker. + */ +L.Draw.Event.EDITMOVE = 'draw:editmove'; + +/** + * @event draw:editresize: ILayer + * + * Layer that was just moved. + * + * Triggered as the user resizes a rectangle or circle. + */ +L.Draw.Event.EDITRESIZE = 'draw:editresize'; + +/** + * @event draw:editvertex: LayerGroup + * + * List of all layers just being edited from the map. + * + * Triggered when a vertex is edited on a polyline or polygon. + */ +L.Draw.Event.EDITVERTEX = 'draw:editvertex'; + +/** + * @event draw:editstop: String + * + * The type of edit this is. One of: `edit` + * + * Triggered when the user has finshed editing (edit mode) and saves edits. + */ +L.Draw.Event.EDITSTOP = 'draw:editstop'; + +/** + * @event draw:deletestart: String + * + * The type of edit this is. One of: `remove` + * + * Triggered when the user starts remove mode by clicking the remove tool button. + */ +L.Draw.Event.DELETESTART = 'draw:deletestart'; + +/** + * @event draw:deletestop: String + * + * The type of edit this is. One of: `remove` + * + * Triggered when the user has finished removing shapes (remove mode) and saves. + */ +L.Draw.Event.DELETESTOP = 'draw:deletestop'; + + +L.Draw = L.Draw || {}; + +/** + * @class L.Draw.Feature + * @aka Draw.Feature + */ +L.Draw.Feature = L.Handler.extend({ + includes: L.Mixin.Events, + + // @method initialize(): void + initialize: function (map, options) { + this._map = map; + this._container = map._container; + this._overlayPane = map._panes.overlayPane; + this._popupPane = map._panes.popupPane; + + // Merge default shapeOptions options with custom shapeOptions + if (options && options.shapeOptions) { + options.shapeOptions = L.Util.extend({}, this.options.shapeOptions, options.shapeOptions); + } + L.setOptions(this, options); + }, + + // @method enable(): void + // Enables this handler + enable: function () { + if (this._enabled) { + return; + } + + L.Handler.prototype.enable.call(this); + + this.fire('enabled', { handler: this.type }); + + this._map.fire(L.Draw.Event.DRAWSTART, { layerType: this.type }); + }, + + // @method initialize(): void + disable: function () { + if (!this._enabled) { + return; + } + + L.Handler.prototype.disable.call(this); + + this._map.fire(L.Draw.Event.DRAWSTOP, { layerType: this.type }); + + this.fire('disabled', { handler: this.type }); + }, + + // @method addHooks(): void + // Add's event listeners to this handler + addHooks: function () { + var map = this._map; + + if (map) { + L.DomUtil.disableTextSelection(); + + map.getContainer().focus(); + + this._tooltip = new L.Draw.Tooltip(this._map); + + L.DomEvent.on(this._container, 'keyup', this._cancelDrawing, this); + } + }, + + // @method removeHooks(): void + // Removes event listeners from this handler + removeHooks: function () { + if (this._map) { + L.DomUtil.enableTextSelection(); + + this._tooltip.dispose(); + this._tooltip = null; + + L.DomEvent.off(this._container, 'keyup', this._cancelDrawing, this); + } + }, + + // @method setOptions(object): void + // Sets new options to this handler + setOptions: function (options) { + L.setOptions(this, options); + }, + + _fireCreatedEvent: function (layer) { + this._map.fire(L.Draw.Event.CREATED, { layer: layer, layerType: this.type }); + }, + + // Cancel drawing when the escape key is pressed + _cancelDrawing: function (e) { + this._map.fire('draw:canceled', { layerType: this.type }); + if (e.keyCode === 27) { + this.disable(); + } + } +}); + + + +/** + * @class L.Draw.Polyline + * @aka Draw.Polyline + * @inherits L.Draw.Feature + */ +L.Draw.Polyline = L.Draw.Feature.extend({ + statics: { + TYPE: 'polyline' + }, + + Poly: L.Polyline, + + options: { + allowIntersection: true, + repeatMode: false, + drawError: { + color: '#b00b00', + timeout: 2500 + }, + icon: new L.DivIcon({ + iconSize: new L.Point(8, 8), + className: 'leaflet-div-icon leaflet-editing-icon' + }), + touchIcon: new L.DivIcon({ + iconSize: new L.Point(20, 20), + className: 'leaflet-div-icon leaflet-editing-icon leaflet-touch-icon' + }), + guidelineDistance: 20, + maxGuideLineLength: 4000, + shapeOptions: { + stroke: true, + color: '#3388ff', + weight: 4, + opacity: 0.5, + fill: false, + clickable: true + }, + metric: true, // Whether to use the metric measurement system or imperial + feet: true, // When not metric, to use feet instead of yards for display. + nautic: false, // When not metric, not feet use nautic mile for display + showLength: true, // Whether to display distance in the tooltip + zIndexOffset: 2000 // This should be > than the highest z-index any map layers + }, + + // @method initialize(): void + initialize: function (map, options) { + // if touch, switch to touch icon + if (L.Browser.touch) { + this.options.icon = this.options.touchIcon; + } + + // Need to set this here to ensure the correct message is used. + this.options.drawError.message = L.drawLocal.draw.handlers.polyline.error; + + // Merge default drawError options with custom options + if (options && options.drawError) { + options.drawError = L.Util.extend({}, this.options.drawError, options.drawError); + } + + // Save the type so super can fire, need to do this as cannot do this.TYPE :( + this.type = L.Draw.Polyline.TYPE; + + L.Draw.Feature.prototype.initialize.call(this, map, options); + }, + + // @method addHooks(): void + // Add listener hooks to this handler + addHooks: function () { + L.Draw.Feature.prototype.addHooks.call(this); + if (this._map) { + this._markers = []; + + this._markerGroup = new L.LayerGroup(); + this._map.addLayer(this._markerGroup); + + this._poly = new L.Polyline([], this.options.shapeOptions); + + this._tooltip.updateContent(this._getTooltipText()); + + // Make a transparent marker that will used to catch click events. These click + // events will create the vertices. We need to do this so we can ensure that + // we can create vertices over other map layers (markers, vector layers). We + // also do not want to trigger any click handlers of objects we are clicking on + // while drawing. + if (!this._mouseMarker) { + this._mouseMarker = L.marker(this._map.getCenter(), { + icon: L.divIcon({ + className: 'leaflet-mouse-marker', + iconAnchor: [20, 20], + iconSize: [40, 40] + }), + opacity: 0, + zIndexOffset: this.options.zIndexOffset + }); + } + + this._mouseMarker + .on('mouseout', this._onMouseOut, this) + .on('mousemove', this._onMouseMove, this) // Necessary to prevent 0.8 stutter + .on('mousedown', this._onMouseDown, this) + .on('mouseup', this._onMouseUp, this) // Necessary for 0.8 compatibility + .addTo(this._map); + + this._map + .on('mouseup', this._onMouseUp, this) // Necessary for 0.7 compatibility + .on('mousemove', this._onMouseMove, this) + .on('zoomlevelschange', this._onZoomEnd, this) + .on('touchstart', this._onTouch, this) + .on('zoomend', this._onZoomEnd, this); + + } + }, + + // @method removeHooks(): void + // Remove listener hooks from this handler. + removeHooks: function () { + L.Draw.Feature.prototype.removeHooks.call(this); + + this._clearHideErrorTimeout(); + + this._cleanUpShape(); + + // remove markers from map + this._map.removeLayer(this._markerGroup); + delete this._markerGroup; + delete this._markers; + + this._map.removeLayer(this._poly); + delete this._poly; + + this._mouseMarker + .off('mousedown', this._onMouseDown, this) + .off('mouseout', this._onMouseOut, this) + .off('mouseup', this._onMouseUp, this) + .off('mousemove', this._onMouseMove, this); + this._map.removeLayer(this._mouseMarker); + delete this._mouseMarker; + + // clean up DOM + this._clearGuides(); + + this._map + .off('mouseup', this._onMouseUp, this) + .off('mousemove', this._onMouseMove, this) + .off('zoomlevelschange', this._onZoomEnd, this) + .off('zoomend', this._onZoomEnd, this) + .off('touchstart', this._onTouch, this) + .off('click', this._onTouch, this); + }, + + // @method deleteLastVertex(): void + // Remove the last vertex from the polyline, removes polyline from map if only one point exists. + deleteLastVertex: function () { + if (this._markers.length <= 1) { + return; + } + + var lastMarker = this._markers.pop(), + poly = this._poly, + // Replaces .spliceLatLngs() + latlngs = poly.getLatLngs(), + latlng = latlngs.splice(-1, 1)[0]; + this._poly.setLatLngs(latlngs); + + this._markerGroup.removeLayer(lastMarker); + + if (poly.getLatLngs().length < 2) { + this._map.removeLayer(poly); + } + + this._vertexChanged(latlng, false); + }, + + // @method addVertex(): void + // Add a vertex to the end of the polyline + addVertex: function (latlng) { + var markersLength = this._markers.length; + // markersLength must be greater than or equal to 2 before intersections can occur + if (markersLength >= 2 && !this.options.allowIntersection && this._poly.newLatLngIntersects(latlng)) { + this._showErrorTooltip(); + return; + } + else if (this._errorShown) { + this._hideErrorTooltip(); + } + + this._markers.push(this._createMarker(latlng)); + + this._poly.addLatLng(latlng); + + if (this._poly.getLatLngs().length === 2) { + this._map.addLayer(this._poly); + } + + this._vertexChanged(latlng, true); + }, + + // @method completeShape(): void + // Closes the polyline between the first and last points + completeShape: function () { + if (this._markers.length <= 1) { + return; + } + + this._fireCreatedEvent(); + this.disable(); + + if (this.options.repeatMode) { + this.enable(); + } + }, + + _finishShape: function () { + var latlngs = this._poly._defaultShape ? this._poly._defaultShape() : this._poly.getLatLngs(); + var intersects = this._poly.newLatLngIntersects(latlngs[latlngs.length - 1]); + + if ((!this.options.allowIntersection && intersects) || !this._shapeIsValid()) { + this._showErrorTooltip(); + return; + } + + this._fireCreatedEvent(); + this.disable(); + if (this.options.repeatMode) { + this.enable(); + } + }, + + // Called to verify the shape is valid when the user tries to finish it + // Return false if the shape is not valid + _shapeIsValid: function () { + return true; + }, + + _onZoomEnd: function () { + if (this._markers !== null) { + this._updateGuide(); + } + }, + + _onMouseMove: function (e) { + var newPos = this._map.mouseEventToLayerPoint(e.originalEvent); + var latlng = this._map.layerPointToLatLng(newPos); + + // Save latlng + // should this be moved to _updateGuide() ? + this._currentLatLng = latlng; + + this._updateTooltip(latlng); + + // Update the guide line + this._updateGuide(newPos); + + // Update the mouse marker position + this._mouseMarker.setLatLng(latlng); + + L.DomEvent.preventDefault(e.originalEvent); + }, + + _vertexChanged: function (latlng, added) { + this._map.fire(L.Draw.Event.DRAWVERTEX, { layers: this._markerGroup }); + this._updateFinishHandler(); + + this._updateRunningMeasure(latlng, added); + + this._clearGuides(); + + this._updateTooltip(); + }, + + _onMouseDown: function (e) { + if (!this._clickHandled && !this._touchHandled && !this._disableMarkers) { + this._onMouseMove(e); + this._clickHandled = true; + this._disableNewMarkers(); + var originalEvent = e.originalEvent; + var clientX = originalEvent.clientX; + var clientY = originalEvent.clientY; + this._startPoint.call(this, clientX, clientY); + } + }, + + _startPoint: function (clientX, clientY) { + this._mouseDownOrigin = L.point(clientX, clientY); + }, + + _onMouseUp: function (e) { + var originalEvent = e.originalEvent; + var clientX = originalEvent.clientX; + var clientY = originalEvent.clientY; + this._endPoint.call(this, clientX, clientY, e); + this._clickHandled = null; + }, + + _endPoint: function (clientX, clientY, e) { + if (this._mouseDownOrigin) { + var dragCheckDistance = L.point(clientX, clientY) + .distanceTo(this._mouseDownOrigin); + var lastPtDistance = this._calculateFinishDistance(e.latlng); + if (lastPtDistance < 10 && L.Browser.touch) { + this._finishShape(); + } else if (Math.abs(dragCheckDistance) < 9 * (window.devicePixelRatio || 1)) { + this.addVertex(e.latlng); + } + this._enableNewMarkers(); // after a short pause, enable new markers + } + this._mouseDownOrigin = null; + }, + + // ontouch prevented by clickHandled flag because some browsers fire both click/touch events, + // causing unwanted behavior + _onTouch: function (e) { + var originalEvent = e.originalEvent; + var clientX; + var clientY; + if (originalEvent.touches && originalEvent.touches[0] && !this._clickHandled && !this._touchHandled && !this._disableMarkers) { + clientX = originalEvent.touches[0].clientX; + clientY = originalEvent.touches[0].clientY; + this._disableNewMarkers(); + this._touchHandled = true; + this._startPoint.call(this, clientX, clientY); + this._endPoint.call(this, clientX, clientY, e); + this._touchHandled = null; + } + this._clickHandled = null; + }, + + _onMouseOut: function () { + if (this._tooltip) { + this._tooltip._onMouseOut.call(this._tooltip); + } + }, + + // calculate if we are currently within close enough distance + // of the closing point (first point for shapes, last point for lines) + // this is semi-ugly code but the only reliable way i found to get the job done + // note: calculating point.distanceTo between mouseDownOrigin and last marker did NOT work + _calculateFinishDistance: function (potentialLatLng) { + var lastPtDistance + if (this._markers.length > 0) { + var finishMarker; + if (this.type === L.Draw.Polyline.TYPE) { + finishMarker = this._markers[this._markers.length - 1]; + } else if (this.type === L.Draw.Polygon.TYPE) { + finishMarker = this._markers[0]; + } else { + return Infinity; + } + var lastMarkerPoint = this._map.latLngToContainerPoint(finishMarker.getLatLng()), + potentialMarker = new L.Marker(potentialLatLng, { + icon: this.options.icon, + zIndexOffset: this.options.zIndexOffset * 2 + }); + var potentialMarkerPint = this._map.latLngToContainerPoint(potentialMarker.getLatLng()); + lastPtDistance = lastMarkerPoint.distanceTo(potentialMarkerPint); + } else { + lastPtDistance = Infinity; + } + return lastPtDistance; + }, + + _updateFinishHandler: function () { + var markerCount = this._markers.length; + // The last marker should have a click handler to close the polyline + if (markerCount > 1) { + this._markers[markerCount - 1].on('click', this._finishShape, this); + } + + // Remove the old marker click handler (as only the last point should close the polyline) + if (markerCount > 2) { + this._markers[markerCount - 2].off('click', this._finishShape, this); + } + }, + + _createMarker: function (latlng) { + var marker = new L.Marker(latlng, { + icon: this.options.icon, + zIndexOffset: this.options.zIndexOffset * 2 + }); + + this._markerGroup.addLayer(marker); + + return marker; + }, + + _updateGuide: function (newPos) { + var markerCount = this._markers ? this._markers.length : 0; + + if (markerCount > 0) { + newPos = newPos || this._map.latLngToLayerPoint(this._currentLatLng); + + // draw the guide line + this._clearGuides(); + this._drawGuide( + this._map.latLngToLayerPoint(this._markers[markerCount - 1].getLatLng()), + newPos + ); + } + }, + + _updateTooltip: function (latLng) { + var text = this._getTooltipText(); + + if (latLng) { + this._tooltip.updatePosition(latLng); + } + + if (!this._errorShown) { + this._tooltip.updateContent(text); + } + }, + + _drawGuide: function (pointA, pointB) { + var length = Math.floor(Math.sqrt(Math.pow((pointB.x - pointA.x), 2) + Math.pow((pointB.y - pointA.y), 2))), + guidelineDistance = this.options.guidelineDistance, + maxGuideLineLength = this.options.maxGuideLineLength, + // Only draw a guideline with a max length + i = length > maxGuideLineLength ? length - maxGuideLineLength : guidelineDistance, + fraction, + dashPoint, + dash; + + //create the guides container if we haven't yet + if (!this._guidesContainer) { + this._guidesContainer = L.DomUtil.create('div', 'leaflet-draw-guides', this._overlayPane); + } + + //draw a dash every GuildeLineDistance + for (; i < length; i += this.options.guidelineDistance) { + //work out fraction along line we are + fraction = i / length; + + //calculate new x,y point + dashPoint = { + x: Math.floor((pointA.x * (1 - fraction)) + (fraction * pointB.x)), + y: Math.floor((pointA.y * (1 - fraction)) + (fraction * pointB.y)) + }; + + //add guide dash to guide container + dash = L.DomUtil.create('div', 'leaflet-draw-guide-dash', this._guidesContainer); + dash.style.backgroundColor = + !this._errorShown ? this.options.shapeOptions.color : this.options.drawError.color; + + L.DomUtil.setPosition(dash, dashPoint); + } + }, + + _updateGuideColor: function (color) { + if (this._guidesContainer) { + for (var i = 0, l = this._guidesContainer.childNodes.length; i < l; i++) { + this._guidesContainer.childNodes[i].style.backgroundColor = color; + } + } + }, + + // removes all child elements (guide dashes) from the guides container + _clearGuides: function () { + if (this._guidesContainer) { + while (this._guidesContainer.firstChild) { + this._guidesContainer.removeChild(this._guidesContainer.firstChild); + } + } + }, + + _getTooltipText: function () { + var showLength = this.options.showLength, + labelText, distanceStr; + if (L.Browser.touch) { + showLength = false; // if there's a better place to put this, feel free to move it + } + if (this._markers.length === 0) { + labelText = { + text: L.drawLocal.draw.handlers.polyline.tooltip.start + }; + } else { + distanceStr = showLength ? this._getMeasurementString() : ''; + + if (this._markers.length === 1) { + labelText = { + text: L.drawLocal.draw.handlers.polyline.tooltip.cont, + subtext: distanceStr + }; + } else { + labelText = { + text: L.drawLocal.draw.handlers.polyline.tooltip.end, + subtext: distanceStr + }; + } + } + return labelText; + }, + + _updateRunningMeasure: function (latlng, added) { + var markersLength = this._markers.length, + previousMarkerIndex, distance; + + if (this._markers.length === 1) { + this._measurementRunningTotal = 0; + } else { + previousMarkerIndex = markersLength - (added ? 2 : 1); + distance = latlng.distanceTo(this._markers[previousMarkerIndex].getLatLng()); + + this._measurementRunningTotal += distance * (added ? 1 : -1); + } + }, + + _getMeasurementString: function () { + var currentLatLng = this._currentLatLng, + previousLatLng = this._markers[this._markers.length - 1].getLatLng(), + distance; + + // calculate the distance from the last fixed point to the mouse position + distance = this._measurementRunningTotal + currentLatLng.distanceTo(previousLatLng); + + return L.GeometryUtil.readableDistance(distance, this.options.metric, this.options.feet, this.options.nautic); + }, + + _showErrorTooltip: function () { + this._errorShown = true; + + // Update tooltip + this._tooltip + .showAsError() + .updateContent({ text: this.options.drawError.message }); + + // Update shape + this._updateGuideColor(this.options.drawError.color); + this._poly.setStyle({ color: this.options.drawError.color }); + + // Hide the error after 2 seconds + this._clearHideErrorTimeout(); + this._hideErrorTimeout = setTimeout(L.Util.bind(this._hideErrorTooltip, this), this.options.drawError.timeout); + }, + + _hideErrorTooltip: function () { + this._errorShown = false; + + this._clearHideErrorTimeout(); + + // Revert tooltip + this._tooltip + .removeError() + .updateContent(this._getTooltipText()); + + // Revert shape + this._updateGuideColor(this.options.shapeOptions.color); + this._poly.setStyle({ color: this.options.shapeOptions.color }); + }, + + _clearHideErrorTimeout: function () { + if (this._hideErrorTimeout) { + clearTimeout(this._hideErrorTimeout); + this._hideErrorTimeout = null; + } + }, + + // disable new markers temporarily; + // this is to prevent duplicated touch/click events in some browsers + _disableNewMarkers: function () { + this._disableMarkers = true; + }, + + // see _disableNewMarkers + _enableNewMarkers: function () { + setTimeout(function() { + this._disableMarkers = false; + }.bind(this), 50); + }, + + _cleanUpShape: function () { + if (this._markers.length > 1) { + this._markers[this._markers.length - 1].off('click', this._finishShape, this); + } + }, + + _fireCreatedEvent: function () { + var poly = new this.Poly(this._poly.getLatLngs(), this.options.shapeOptions); + L.Draw.Feature.prototype._fireCreatedEvent.call(this, poly); + } +}); + + + +/** + * @class L.Draw.Polygon + * @aka Draw.Polygon + * @inherits L.Draw.Polyline + */ +L.Draw.Polygon = L.Draw.Polyline.extend({ + statics: { + TYPE: 'polygon' + }, + + Poly: L.Polygon, + + options: { + showArea: false, + shapeOptions: { + stroke: true, + color: '#3388ff', + weight: 4, + opacity: 0.5, + fill: true, + fillColor: null, //same as color by default + fillOpacity: 0.2, + clickable: true + }, + metric: true // Whether to use the metric measurement system or imperial + }, + + // @method initialize(): void + initialize: function (map, options) { + L.Draw.Polyline.prototype.initialize.call(this, map, options); + + // Save the type so super can fire, need to do this as cannot do this.TYPE :( + this.type = L.Draw.Polygon.TYPE; + }, + + _updateFinishHandler: function () { + var markerCount = this._markers.length; + + // The first marker should have a click handler to close the polygon + if (markerCount === 1) { + this._markers[0].on('click', this._finishShape, this); + } + + // Add and update the double click handler + if (markerCount > 2) { + this._markers[markerCount - 1].on('dblclick', this._finishShape, this); + // Only need to remove handler if has been added before + if (markerCount > 3) { + this._markers[markerCount - 2].off('dblclick', this._finishShape, this); + } + } + }, + + _getTooltipText: function () { + var text, subtext; + + if (this._markers.length === 0) { + text = L.drawLocal.draw.handlers.polygon.tooltip.start; + } else if (this._markers.length < 3) { + text = L.drawLocal.draw.handlers.polygon.tooltip.cont; + } else { + text = L.drawLocal.draw.handlers.polygon.tooltip.end; + subtext = this._getMeasurementString(); + } + + return { + text: text, + subtext: subtext + }; + }, + + _getMeasurementString: function () { + var area = this._area; + + if (!area) { + return null; + } + + return L.GeometryUtil.readableArea(area, this.options.metric); + }, + + _shapeIsValid: function () { + return this._markers.length >= 3; + }, + + _vertexChanged: function (latlng, added) { + var latLngs; + + // Check to see if we should show the area + if (!this.options.allowIntersection && this.options.showArea) { + latLngs = this._poly.getLatLngs(); + + this._area = L.GeometryUtil.geodesicArea(latLngs); + } + + L.Draw.Polyline.prototype._vertexChanged.call(this, latlng, added); + }, + + _cleanUpShape: function () { + var markerCount = this._markers.length; + + if (markerCount > 0) { + this._markers[0].off('click', this._finishShape, this); + + if (markerCount > 2) { + this._markers[markerCount - 1].off('dblclick', this._finishShape, this); + } + } + } +}); + + + +L.SimpleShape = {}; +/** + * @class L.Draw.SimpleShape + * @aka Draw.SimpleShape + * @inherits L.Draw.Feature + */ +L.Draw.SimpleShape = L.Draw.Feature.extend({ + options: { + repeatMode: false + }, + + // @method initialize(): void + initialize: function (map, options) { + this._endLabelText = L.drawLocal.draw.handlers.simpleshape.tooltip.end; + + L.Draw.Feature.prototype.initialize.call(this, map, options); + }, + + // @method addHooks(): void + // Add listener hooks to this handler. + addHooks: function () { + L.Draw.Feature.prototype.addHooks.call(this); + if (this._map) { + this._mapDraggable = this._map.dragging.enabled(); + + if (this._mapDraggable) { + this._map.dragging.disable(); + } + + //TODO refactor: move cursor to styles + this._container.style.cursor = 'crosshair'; + + this._tooltip.updateContent({ text: this._initialLabelText }); + + this._map + .on('mousedown', this._onMouseDown, this) + .on('mousemove', this._onMouseMove, this) + .on('touchstart', this._onMouseDown, this) + .on('touchmove', this._onMouseMove, this); + } + }, + + // @method removeHooks(): void + // Remove listener hooks from this handler. + removeHooks: function () { + L.Draw.Feature.prototype.removeHooks.call(this); + if (this._map) { + if (this._mapDraggable) { + this._map.dragging.enable(); + } + + //TODO refactor: move cursor to styles + this._container.style.cursor = ''; + + this._map + .off('mousedown', this._onMouseDown, this) + .off('mousemove', this._onMouseMove, this) + .off('touchstart', this._onMouseDown, this) + .off('touchmove', this._onMouseMove, this); + + L.DomEvent.off(document, 'mouseup', this._onMouseUp, this); + L.DomEvent.off(document, 'touchend', this._onMouseUp, this); + + // If the box element doesn't exist they must not have moved the mouse, so don't need to destroy/return + if (this._shape) { + this._map.removeLayer(this._shape); + delete this._shape; + } + } + this._isDrawing = false; + }, + + _getTooltipText: function () { + return { + text: this._endLabelText + }; + }, + + _onMouseDown: function (e) { + this._isDrawing = true; + this._startLatLng = e.latlng; + + L.DomEvent + .on(document, 'mouseup', this._onMouseUp, this) + .on(document, 'touchend', this._onMouseUp, this) + .preventDefault(e.originalEvent); + }, + + _onMouseMove: function (e) { + var latlng = e.latlng; + + this._tooltip.updatePosition(latlng); + if (this._isDrawing) { + this._tooltip.updateContent(this._getTooltipText()); + this._drawShape(latlng); + } + }, + + _onMouseUp: function () { + if (this._shape) { + this._fireCreatedEvent(); + } + + this.disable(); + if (this.options.repeatMode) { + this.enable(); + } + } +}); + + +/** + * @class L.Draw.Rectangle + * @aka Draw.Rectangle + * @inherits L.Draw.SimpleShape + */ +L.Draw.Rectangle = L.Draw.SimpleShape.extend({ + statics: { + TYPE: 'rectangle' + }, + + options: { + shapeOptions: { + stroke: true, + color: '#3388ff', + weight: 4, + opacity: 0.5, + fill: true, + fillColor: null, //same as color by default + fillOpacity: 0.2, + showArea: true, + clickable: true + }, + metric: true // Whether to use the metric measurement system or imperial + }, + + // @method initialize(): void + initialize: function (map, options) { + // Save the type so super can fire, need to do this as cannot do this.TYPE :( + this.type = L.Draw.Rectangle.TYPE; + + this._initialLabelText = L.drawLocal.draw.handlers.rectangle.tooltip.start; + + L.Draw.SimpleShape.prototype.initialize.call(this, map, options); + }, + + _drawShape: function (latlng) { + if (!this._shape) { + this._shape = new L.Rectangle(new L.LatLngBounds(this._startLatLng, latlng), this.options.shapeOptions); + this._map.addLayer(this._shape); + } else { + this._shape.setBounds(new L.LatLngBounds(this._startLatLng, latlng)); + } + }, + + _fireCreatedEvent: function () { + var rectangle = new L.Rectangle(this._shape.getBounds(), this.options.shapeOptions); + L.Draw.SimpleShape.prototype._fireCreatedEvent.call(this, rectangle); + }, + + _getTooltipText: function () { + var tooltipText = L.Draw.SimpleShape.prototype._getTooltipText.call(this), + shape = this._shape, + showArea = this.options.showArea, + latLngs, area, subtext; + + if (shape) { + latLngs = this._shape._defaultShape ? this._shape._defaultShape() : this._shape.getLatLngs(); + area = L.GeometryUtil.geodesicArea(latLngs); + subtext = showArea ? L.GeometryUtil.readableArea(area, this.options.metric) : '' + } + + return { + text: tooltipText.text, + subtext: subtext + }; + } +}); + + + +/** + * @class L.Draw.Circle + * @aka Draw.Circle + * @inherits L.Draw.SimpleShape + */ +L.Draw.Circle = L.Draw.SimpleShape.extend({ + statics: { + TYPE: 'circle' + }, + + options: { + shapeOptions: { + stroke: true, + color: '#3388ff', + weight: 4, + opacity: 0.5, + fill: true, + fillColor: null, //same as color by default + fillOpacity: 0.2, + clickable: true + }, + showRadius: true, + metric: true, // Whether to use the metric measurement system or imperial + feet: true, // When not metric, use feet instead of yards for display + nautic: false // When not metric, not feet use nautic mile for display + }, + + // @method initialize(): void + initialize: function (map, options) { + // Save the type so super can fire, need to do this as cannot do this.TYPE :( + this.type = L.Draw.Circle.TYPE; + + this._initialLabelText = L.drawLocal.draw.handlers.circle.tooltip.start; + + L.Draw.SimpleShape.prototype.initialize.call(this, map, options); + }, + + _drawShape: function (latlng) { + if (!this._shape) { + this._shape = new L.Circle(this._startLatLng, this._startLatLng.distanceTo(latlng), this.options.shapeOptions); + this._map.addLayer(this._shape); + } else { + this._shape.setRadius(this._startLatLng.distanceTo(latlng)); + } + }, + + _fireCreatedEvent: function () { + var circle = new L.Circle(this._startLatLng, this._shape.getRadius(), this.options.shapeOptions); + L.Draw.SimpleShape.prototype._fireCreatedEvent.call(this, circle); + }, + + _onMouseMove: function (e) { + var latlng = e.latlng, + showRadius = this.options.showRadius, + useMetric = this.options.metric, + radius; + + this._tooltip.updatePosition(latlng); + if (this._isDrawing) { + this._drawShape(latlng); + + // Get the new radius (rounded to 1 dp) + radius = this._shape.getRadius().toFixed(1); + + var subtext = ''; + if (showRadius) { + subtext = L.drawLocal.draw.handlers.circle.radius + ': ' + + L.GeometryUtil.readableDistance(radius, useMetric, this.options.feet, this.options.nautic); + } + this._tooltip.updateContent({ + text: this._endLabelText, + subtext: subtext + }); + } + } +}); + + + +/** + * @class L.Draw.Marker + * @aka Draw.Marker + * @inherits L.Draw.Feature + */ +L.Draw.Marker = L.Draw.Feature.extend({ + statics: { + TYPE: 'marker' + }, + + options: { + icon: new L.Icon.Default(), + repeatMode: false, + zIndexOffset: 2000 // This should be > than the highest z-index any markers + }, + + // @method initialize(): void + initialize: function (map, options) { + // Save the type so super can fire, need to do this as cannot do this.TYPE :( + this.type = L.Draw.Marker.TYPE; + + L.Draw.Feature.prototype.initialize.call(this, map, options); + }, + + // @method addHooks(): void + // Add listener hooks to this handler. + addHooks: function () { + L.Draw.Feature.prototype.addHooks.call(this); + + if (this._map) { + this._tooltip.updateContent({ text: L.drawLocal.draw.handlers.marker.tooltip.start }); + + // Same mouseMarker as in Draw.Polyline + if (!this._mouseMarker) { + this._mouseMarker = L.marker(this._map.getCenter(), { + icon: L.divIcon({ + className: 'leaflet-mouse-marker', + iconAnchor: [20, 20], + iconSize: [40, 40] + }), + opacity: 0, + zIndexOffset: this.options.zIndexOffset + }); + } + + this._mouseMarker + .on('click', this._onClick, this) + .addTo(this._map); + + this._map.on('mousemove', this._onMouseMove, this); + this._map.on('click', this._onTouch, this); + } + }, + + // @method removeHooks(): void + // Remove listener hooks from this handler. + removeHooks: function () { + L.Draw.Feature.prototype.removeHooks.call(this); + + if (this._map) { + if (this._marker) { + this._marker.off('click', this._onClick, this); + this._map + .off('click', this._onClick, this) + .off('click', this._onTouch, this) + .removeLayer(this._marker); + delete this._marker; + } + + this._mouseMarker.off('click', this._onClick, this); + this._map.removeLayer(this._mouseMarker); + delete this._mouseMarker; + + this._map.off('mousemove', this._onMouseMove, this); + } + }, + + _onMouseMove: function (e) { + var latlng = e.latlng; + + this._tooltip.updatePosition(latlng); + this._mouseMarker.setLatLng(latlng); + + if (!this._marker) { + this._marker = new L.Marker(latlng, { + icon: this.options.icon, + zIndexOffset: this.options.zIndexOffset + }); + // Bind to both marker and map to make sure we get the click event. + this._marker.on('click', this._onClick, this); + this._map + .on('click', this._onClick, this) + .addLayer(this._marker); + } + else { + latlng = this._mouseMarker.getLatLng(); + this._marker.setLatLng(latlng); + } + }, + + _onClick: function () { + this._fireCreatedEvent(); + + this.disable(); + if (this.options.repeatMode) { + this.enable(); + } + }, + + _onTouch: function (e) { + // called on click & tap, only really does any thing on tap + this._onMouseMove(e); // creates & places marker + this._onClick(); // permanently places marker & ends interaction + }, + + _fireCreatedEvent: function () { + var marker = new L.Marker.Touch(this._marker.getLatLng(), { icon: this.options.icon }); + L.Draw.Feature.prototype._fireCreatedEvent.call(this, marker); + } +}); + + + +L.Edit = L.Edit || {}; + +/** + * @class L.Edit.Marker + * @aka Edit.Marker + */ +L.Edit.Marker = L.Handler.extend({ + // @method initialize(): void + initialize: function (marker, options) { + this._marker = marker; + L.setOptions(this, options); + }, + + // @method addHooks(): void + // Add listener hooks to this handler + addHooks: function () { + var marker = this._marker; + + marker.dragging.enable(); + marker.on('dragend', this._onDragEnd, marker); + this._toggleMarkerHighlight(); + }, + + // @method removeHooks(): void + // Remove listener hooks from this handler + removeHooks: function () { + var marker = this._marker; + + marker.dragging.disable(); + marker.off('dragend', this._onDragEnd, marker); + this._toggleMarkerHighlight(); + }, + + _onDragEnd: function (e) { + var layer = e.target; + layer.edited = true; + this._map.fire(L.Draw.Event.EDITMOVE, { layer: layer }); + }, + + _toggleMarkerHighlight: function () { + var icon = this._marker._icon; + + // Don't do anything if this layer is a marker but doesn't have an icon. Markers + // should usually have icons. If using Leaflet.draw with Leaflet.markercluster there + // is a chance that a marker doesn't. + if (!icon) { + return; + } + + // This is quite naughty, but I don't see another way of doing it. (short of setting a new icon) + icon.style.display = 'none'; + + if (L.DomUtil.hasClass(icon, 'leaflet-edit-marker-selected')) { + L.DomUtil.removeClass(icon, 'leaflet-edit-marker-selected'); + // Offset as the border will make the icon move. + this._offsetMarker(icon, -4); + + } else { + L.DomUtil.addClass(icon, 'leaflet-edit-marker-selected'); + // Offset as the border will make the icon move. + this._offsetMarker(icon, 4); + } + + icon.style.display = ''; + }, + + _offsetMarker: function (icon, offset) { + var iconMarginTop = parseInt(icon.style.marginTop, 10) - offset, + iconMarginLeft = parseInt(icon.style.marginLeft, 10) - offset; + + icon.style.marginTop = iconMarginTop + 'px'; + icon.style.marginLeft = iconMarginLeft + 'px'; + } +}); + +L.Marker.addInitHook(function () { + if (L.Edit.Marker) { + this.editing = new L.Edit.Marker(this); + + if (this.options.editable) { + this.editing.enable(); + } + } +}); + + + +L.Edit = L.Edit || {}; + +/** + * @class L.Edit.Polyline + * @aka L.Edit.Poly + * @aka Edit.Poly + */ +L.Edit.Poly = L.Handler.extend({ + options: {}, + + // @method initialize(): void + initialize: function (poly, options) { + + this.latlngs = [poly._latlngs]; + if (poly._holes) { + this.latlngs = this.latlngs.concat(poly._holes); + } + + this._poly = poly; + L.setOptions(this, options); + + this._poly.on('revert-edited', this._updateLatLngs, this); + }, + + // Compatibility method to normalize Poly* objects + // between 0.7.x and 1.0+ + _defaultShape: function () { + if (!L.Polyline._flat) { + return this._poly._latlngs; + } + return L.Polyline._flat(this._poly._latlngs) ? this._poly._latlngs : this._poly._latlngs[0]; + }, + + _eachVertexHandler: function (callback) { + for (var i = 0; i < this._verticesHandlers.length; i++) { + callback(this._verticesHandlers[i]); + } + }, + + // @method addHooks(): void + // Add listener hooks to this handler + addHooks: function () { + this._initHandlers(); + this._eachVertexHandler(function (handler) { + handler.addHooks(); + }); + }, + + // @method removeHooks(): void + // Remove listener hooks from this handler + removeHooks: function () { + this._eachVertexHandler(function (handler) { + handler.removeHooks(); + }); + }, + + // @method updateMarkers(): void + // Fire an update for each vertex handler + updateMarkers: function () { + this._eachVertexHandler(function (handler) { + handler.updateMarkers(); + }); + }, + + _initHandlers: function () { + this._verticesHandlers = []; + for (var i = 0; i < this.latlngs.length; i++) { + this._verticesHandlers.push(new L.Edit.PolyVerticesEdit(this._poly, this.latlngs[i], this.options)); + } + }, + + _updateLatLngs: function (e) { + this.latlngs = [e.layer._latlngs]; + if (e.layer._holes) { + this.latlngs = this.latlngs.concat(e.layer._holes); + } + } + +}); + +/** + * @class L.Edit.PolyVerticesEdit + * @aka Edit.PolyVerticesEdit + */ +L.Edit.PolyVerticesEdit = L.Handler.extend({ + options: { + icon: new L.DivIcon({ + iconSize: new L.Point(8, 8), + className: 'leaflet-div-icon leaflet-editing-icon' + }), + touchIcon: new L.DivIcon({ + iconSize: new L.Point(20, 20), + className: 'leaflet-div-icon leaflet-editing-icon leaflet-touch-icon' + }), + drawError: { + color: '#b00b00', + timeout: 1000 + } + + + }, + + // @method intialize(): void + initialize: function (poly, latlngs, options) { + // if touch, switch to touch icon + if (L.Browser.touch) { + this.options.icon = this.options.touchIcon; + } + this._poly = poly; + + if (options && options.drawError) { + options.drawError = L.Util.extend({}, this.options.drawError, options.drawError); + } + + this._latlngs = latlngs; + + L.setOptions(this, options); + }, + + // Compatibility method to normalize Poly* objects + // between 0.7.x and 1.0+ + _defaultShape: function () { + if (!L.Polyline._flat) { + return this._latlngs; + } + return L.Polyline._flat(this._latlngs) ? this._latlngs : this._latlngs[0]; + }, + + // @method addHooks(): void + // Add listener hooks to this handler. + addHooks: function () { + var poly = this._poly; + + if (!(poly instanceof L.Polygon)) { + poly.options.fill = false; + if (poly.options.editing) { + poly.options.editing.fill = false; + } + } + + poly.setStyle(poly.options.editing); + + if (this._poly._map) { + + this._map = this._poly._map; // Set map + + if (!this._markerGroup) { + this._initMarkers(); + } + this._poly._map.addLayer(this._markerGroup); + } + }, + + // @method removeHooks(): void + // Remove listener hooks from this handler. + removeHooks: function () { + var poly = this._poly; + + poly.setStyle(poly.options.original); + + if (poly._map) { + poly._map.removeLayer(this._markerGroup); + delete this._markerGroup; + delete this._markers; + } + }, + + // @method updateMarkers(): void + // Clear markers and update their location + updateMarkers: function () { + this._markerGroup.clearLayers(); + this._initMarkers(); + }, + + _initMarkers: function () { + if (!this._markerGroup) { + this._markerGroup = new L.LayerGroup(); + } + this._markers = []; + + var latlngs = this._defaultShape(), + i, j, len, marker; + + for (i = 0, len = latlngs.length; i < len; i++) { + + marker = this._createMarker(latlngs[i], i); + marker.on('click', this._onMarkerClick, this); + this._markers.push(marker); + } + + var markerLeft, markerRight; + + for (i = 0, j = len - 1; i < len; j = i++) { + if (i === 0 && !(L.Polygon && (this._poly instanceof L.Polygon))) { + continue; + } + + markerLeft = this._markers[j]; + markerRight = this._markers[i]; + + this._createMiddleMarker(markerLeft, markerRight); + this._updatePrevNext(markerLeft, markerRight); + } + }, + + _createMarker: function (latlng, index) { + // Extending L.Marker in TouchEvents.js to include touch. + var marker = new L.Marker.Touch(latlng, { + draggable: true, + icon: this.options.icon, + }); + + marker._origLatLng = latlng; + marker._index = index; + + marker + .on('dragstart', this._onMarkerDragStart, this) + .on('drag', this._onMarkerDrag, this) + .on('dragend', this._fireEdit, this) + .on('touchmove', this._onTouchMove, this) + .on('touchend', this._fireEdit, this) + .on('MSPointerMove', this._onTouchMove, this) + .on('MSPointerUp', this._fireEdit, this); + + this._markerGroup.addLayer(marker); + + return marker; + }, + + _onMarkerDragStart: function () { + this._poly.fire('editstart'); + }, + + _spliceLatLngs: function () { + var latlngs = this._defaultShape(); + var removed = [].splice.apply(latlngs, arguments); + this._poly._convertLatLngs(latlngs, true); + this._poly.redraw(); + return removed; + }, + + _removeMarker: function (marker) { + var i = marker._index; + + this._markerGroup.removeLayer(marker); + this._markers.splice(i, 1); + this._spliceLatLngs(i, 1); + this._updateIndexes(i, -1); + + marker + .off('dragstart', this._onMarkerDragStart, this) + .off('drag', this._onMarkerDrag, this) + .off('dragend', this._fireEdit, this) + .off('touchmove', this._onMarkerDrag, this) + .off('touchend', this._fireEdit, this) + .off('click', this._onMarkerClick, this) + .off('MSPointerMove', this._onTouchMove, this) + .off('MSPointerUp', this._fireEdit, this); + }, + + _fireEdit: function () { + this._poly.edited = true; + this._poly.fire('edit'); + this._poly._map.fire(L.Draw.Event.EDITVERTEX, { layers: this._markerGroup, poly: this._poly }); + }, + + _onMarkerDrag: function (e) { + var marker = e.target; + var poly = this._poly; + + L.extend(marker._origLatLng, marker._latlng); + + if (marker._middleLeft) { + marker._middleLeft.setLatLng(this._getMiddleLatLng(marker._prev, marker)); + } + if (marker._middleRight) { + marker._middleRight.setLatLng(this._getMiddleLatLng(marker, marker._next)); + } + + if (poly.options.poly) { + var tooltip = poly._map._editTooltip; // Access the tooltip + + // If we don't allow intersections and the polygon intersects + if (!poly.options.poly.allowIntersection && poly.intersects()) { + + var originalColor = poly.options.color; + poly.setStyle({ color: this.options.drawError.color }); + + // Manually trigger 'dragend' behavior on marker we are about to remove + // WORKAROUND: introduced in 1.0.0-rc2, may be related to #4484 + if (L.version.indexOf('0.7') !== 0) { + marker.dragging._draggable._onUp(e); + } + this._onMarkerClick(e); // Remove violating marker + // FIXME: Reset the marker to it's original position (instead of remove) + + if (tooltip) { + tooltip.updateContent({ + text: L.drawLocal.draw.handlers.polyline.error + }); + } + + // Reset everything back to normal after a second + setTimeout(function () { + poly.setStyle({ color: originalColor }); + if (tooltip) { + tooltip.updateContent({ + text: L.drawLocal.edit.handlers.edit.tooltip.text, + subtext: L.drawLocal.edit.handlers.edit.tooltip.subtext + }); + } + }, 1000); + } + } + + this._poly.redraw(); + this._poly.fire('editdrag'); + }, + + _onMarkerClick: function (e) { + + var minPoints = L.Polygon && (this._poly instanceof L.Polygon) ? 4 : 3, + marker = e.target; + + // If removing this point would create an invalid polyline/polygon don't remove + if (this._defaultShape().length < minPoints) { + return; + } + + // remove the marker + this._removeMarker(marker); + + // update prev/next links of adjacent markers + this._updatePrevNext(marker._prev, marker._next); + + // remove ghost markers near the removed marker + if (marker._middleLeft) { + this._markerGroup.removeLayer(marker._middleLeft); + } + if (marker._middleRight) { + this._markerGroup.removeLayer(marker._middleRight); + } + + // create a ghost marker in place of the removed one + if (marker._prev && marker._next) { + this._createMiddleMarker(marker._prev, marker._next); + + } else if (!marker._prev) { + marker._next._middleLeft = null; + + } else if (!marker._next) { + marker._prev._middleRight = null; + } + + this._fireEdit(); + }, + + _onTouchMove: function (e) { + + var layerPoint = this._map.mouseEventToLayerPoint(e.originalEvent.touches[0]), + latlng = this._map.layerPointToLatLng(layerPoint), + marker = e.target; + + L.extend(marker._origLatLng, latlng); + + if (marker._middleLeft) { + marker._middleLeft.setLatLng(this._getMiddleLatLng(marker._prev, marker)); + } + if (marker._middleRight) { + marker._middleRight.setLatLng(this._getMiddleLatLng(marker, marker._next)); + } + + this._poly.redraw(); + this.updateMarkers(); + }, + + _updateIndexes: function (index, delta) { + this._markerGroup.eachLayer(function (marker) { + if (marker._index > index) { + marker._index += delta; + } + }); + }, + + _createMiddleMarker: function (marker1, marker2) { + var latlng = this._getMiddleLatLng(marker1, marker2), + marker = this._createMarker(latlng), + onClick, + onDragStart, + onDragEnd; + + marker.setOpacity(0.6); + + marker1._middleRight = marker2._middleLeft = marker; + + onDragStart = function () { + marker.off('touchmove', onDragStart, this); + var i = marker2._index; + + marker._index = i; + + marker + .off('click', onClick, this) + .on('click', this._onMarkerClick, this); + + latlng.lat = marker.getLatLng().lat; + latlng.lng = marker.getLatLng().lng; + this._spliceLatLngs(i, 0, latlng); + this._markers.splice(i, 0, marker); + + marker.setOpacity(1); + + this._updateIndexes(i, 1); + marker2._index++; + this._updatePrevNext(marker1, marker); + this._updatePrevNext(marker, marker2); + + this._poly.fire('editstart'); + }; + + onDragEnd = function () { + marker.off('dragstart', onDragStart, this); + marker.off('dragend', onDragEnd, this); + marker.off('touchmove', onDragStart, this); + + this._createMiddleMarker(marker1, marker); + this._createMiddleMarker(marker, marker2); + }; + + onClick = function () { + onDragStart.call(this); + onDragEnd.call(this); + this._fireEdit(); + }; + + marker + .on('click', onClick, this) + .on('dragstart', onDragStart, this) + .on('dragend', onDragEnd, this) + .on('touchmove', onDragStart, this); + + this._markerGroup.addLayer(marker); + }, + + _updatePrevNext: function (marker1, marker2) { + if (marker1) { + marker1._next = marker2; + } + if (marker2) { + marker2._prev = marker1; + } + }, + + _getMiddleLatLng: function (marker1, marker2) { + var map = this._poly._map, + p1 = map.project(marker1.getLatLng()), + p2 = map.project(marker2.getLatLng()); + + return map.unproject(p1._add(p2)._divideBy(2)); + } +}); + +L.Polyline.addInitHook(function () { + + // Check to see if handler has already been initialized. This is to support versions of Leaflet that still have L.Handler.PolyEdit + if (this.editing) { + return; + } + + if (L.Edit.Poly) { + + this.editing = new L.Edit.Poly(this, this.options.poly); + + if (this.options.editable) { + this.editing.enable(); + } + } + + this.on('add', function () { + if (this.editing && this.editing.enabled()) { + this.editing.addHooks(); + } + }); + + this.on('remove', function () { + if (this.editing && this.editing.enabled()) { + this.editing.removeHooks(); + } + }); +}); + + + +L.Edit = L.Edit || {}; +/** + * @class L.Edit.SimpleShape + * @aka Edit.SimpleShape + */ +L.Edit.SimpleShape = L.Handler.extend({ + options: { + moveIcon: new L.DivIcon({ + iconSize: new L.Point(8, 8), + className: 'leaflet-div-icon leaflet-editing-icon leaflet-edit-move' + }), + resizeIcon: new L.DivIcon({ + iconSize: new L.Point(8, 8), + className: 'leaflet-div-icon leaflet-editing-icon leaflet-edit-resize' + }), + touchMoveIcon: new L.DivIcon({ + iconSize: new L.Point(20, 20), + className: 'leaflet-div-icon leaflet-editing-icon leaflet-edit-move leaflet-touch-icon' + }), + touchResizeIcon: new L.DivIcon({ + iconSize: new L.Point(20, 20), + className: 'leaflet-div-icon leaflet-editing-icon leaflet-edit-resize leaflet-touch-icon' + }), + }, + + // @method intialize(): void + initialize: function (shape, options) { + // if touch, switch to touch icon + if (L.Browser.touch) { + this.options.moveIcon = this.options.touchMoveIcon; + this.options.resizeIcon = this.options.touchResizeIcon; + } + + this._shape = shape; + L.Util.setOptions(this, options); + }, + + // @method addHooks(): void + // Add listener hooks to this handler + addHooks: function () { + var shape = this._shape; + if (this._shape._map) { + this._map = this._shape._map; + shape.setStyle(shape.options.editing); + + if (shape._map) { + this._map = shape._map; + if (!this._markerGroup) { + this._initMarkers(); + } + this._map.addLayer(this._markerGroup); + } + } + }, + + // @method removeHooks(): void + // Remove listener hooks from this handler + removeHooks: function () { + var shape = this._shape; + + shape.setStyle(shape.options.original); + + if (shape._map) { + this._unbindMarker(this._moveMarker); + + for (var i = 0, l = this._resizeMarkers.length; i < l; i++) { + this._unbindMarker(this._resizeMarkers[i]); + } + this._resizeMarkers = null; + + this._map.removeLayer(this._markerGroup); + delete this._markerGroup; + } + + this._map = null; + }, + + // @method updateMarkers(): void + // Remove the edit markers from this layer + updateMarkers: function () { + this._markerGroup.clearLayers(); + this._initMarkers(); + }, + + _initMarkers: function () { + if (!this._markerGroup) { + this._markerGroup = new L.LayerGroup(); + } + + // Create center marker + this._createMoveMarker(); + + // Create edge marker + this._createResizeMarker(); + }, + + _createMoveMarker: function () { + // Children override + }, + + _createResizeMarker: function () { + // Children override + }, + + _createMarker: function (latlng, icon) { + // Extending L.Marker in TouchEvents.js to include touch. + var marker = new L.Marker.Touch(latlng, { + draggable: true, + icon: icon, + zIndexOffset: 10 + }); + + this._bindMarker(marker); + + this._markerGroup.addLayer(marker); + + return marker; + }, + + _bindMarker: function (marker) { + marker + .on('dragstart', this._onMarkerDragStart, this) + .on('drag', this._onMarkerDrag, this) + .on('dragend', this._onMarkerDragEnd, this) + .on('touchstart', this._onTouchStart, this) + .on('touchmove', this._onTouchMove, this) + .on('MSPointerMove', this._onTouchMove, this) + .on('touchend', this._onTouchEnd, this) + .on('MSPointerUp', this._onTouchEnd, this); + }, + + _unbindMarker: function (marker) { + marker + .off('dragstart', this._onMarkerDragStart, this) + .off('drag', this._onMarkerDrag, this) + .off('dragend', this._onMarkerDragEnd, this) + .off('touchstart', this._onTouchStart, this) + .off('touchmove', this._onTouchMove, this) + .off('MSPointerMove', this._onTouchMove, this) + .off('touchend', this._onTouchEnd, this) + .off('MSPointerUp', this._onTouchEnd, this); + }, + + _onMarkerDragStart: function (e) { + var marker = e.target; + marker.setOpacity(0); + + this._shape.fire('editstart'); + }, + + _fireEdit: function () { + this._shape.edited = true; + this._shape.fire('edit'); + }, + + _onMarkerDrag: function (e) { + var marker = e.target, + latlng = marker.getLatLng(); + + if (marker === this._moveMarker) { + this._move(latlng); + } else { + this._resize(latlng); + } + + this._shape.redraw(); + this._shape.fire('editdrag'); + }, + + _onMarkerDragEnd: function (e) { + var marker = e.target; + marker.setOpacity(1); + + this._fireEdit(); + }, + + _onTouchStart: function (e) { + L.Edit.SimpleShape.prototype._onMarkerDragStart.call(this, e); + + if (typeof(this._getCorners) === 'function') { + // Save a reference to the opposite point + var corners = this._getCorners(), + marker = e.target, + currentCornerIndex = marker._cornerIndex; + + marker.setOpacity(0); + + // Copyed from Edit.Rectangle.js line 23 _onMarkerDragStart() + // Latlng is null otherwise. + this._oppositeCorner = corners[(currentCornerIndex + 2) % 4]; + this._toggleCornerMarkers(0, currentCornerIndex); + } + + this._shape.fire('editstart'); + }, + + _onTouchMove: function (e) { + var layerPoint = this._map.mouseEventToLayerPoint(e.originalEvent.touches[0]), + latlng = this._map.layerPointToLatLng(layerPoint), + marker = e.target; + + if (marker === this._moveMarker) { + this._move(latlng); + } else { + this._resize(latlng); + } + + this._shape.redraw(); + + // prevent touchcancel in IOS + // e.preventDefault(); + return false; + }, + + _onTouchEnd: function (e) { + var marker = e.target; + marker.setOpacity(1); + this.updateMarkers(); + this._fireEdit(); + }, + + _move: function () { + // Children override + }, + + _resize: function () { + // Children override + } +}); + + + +L.Edit = L.Edit || {}; +/** + * @class L.Edit.Rectangle + * @aka Edit.Rectangle + * @inherits L.Edit.SimpleShape + */ +L.Edit.Rectangle = L.Edit.SimpleShape.extend({ + _createMoveMarker: function () { + var bounds = this._shape.getBounds(), + center = bounds.getCenter(); + + this._moveMarker = this._createMarker(center, this.options.moveIcon); + }, + + _createResizeMarker: function () { + var corners = this._getCorners(); + + this._resizeMarkers = []; + + for (var i = 0, l = corners.length; i < l; i++) { + this._resizeMarkers.push(this._createMarker(corners[i], this.options.resizeIcon)); + // Monkey in the corner index as we will need to know this for dragging + this._resizeMarkers[i]._cornerIndex = i; + } + }, + + _onMarkerDragStart: function (e) { + L.Edit.SimpleShape.prototype._onMarkerDragStart.call(this, e); + + // Save a reference to the opposite point + var corners = this._getCorners(), + marker = e.target, + currentCornerIndex = marker._cornerIndex; + + this._oppositeCorner = corners[(currentCornerIndex + 2) % 4]; + + this._toggleCornerMarkers(0, currentCornerIndex); + }, + + _onMarkerDragEnd: function (e) { + var marker = e.target, + bounds, center; + + // Reset move marker position to the center + if (marker === this._moveMarker) { + bounds = this._shape.getBounds(); + center = bounds.getCenter(); + + marker.setLatLng(center); + } + + this._toggleCornerMarkers(1); + + this._repositionCornerMarkers(); + + L.Edit.SimpleShape.prototype._onMarkerDragEnd.call(this, e); + }, + + _move: function (newCenter) { + var latlngs = this._shape._defaultShape ? this._shape._defaultShape() : this._shape.getLatLngs(), + bounds = this._shape.getBounds(), + center = bounds.getCenter(), + offset, newLatLngs = []; + + // Offset the latlngs to the new center + for (var i = 0, l = latlngs.length; i < l; i++) { + offset = [latlngs[i].lat - center.lat, latlngs[i].lng - center.lng]; + newLatLngs.push([newCenter.lat + offset[0], newCenter.lng + offset[1]]); + } + + this._shape.setLatLngs(newLatLngs); + + // Reposition the resize markers + this._repositionCornerMarkers(); + + this._map.fire(L.Draw.Event.EDITMOVE, { layer: this._shape }); + }, + + _resize: function (latlng) { + var bounds; + + // Update the shape based on the current position of this corner and the opposite point + this._shape.setBounds(L.latLngBounds(latlng, this._oppositeCorner)); + + // Reposition the move marker + bounds = this._shape.getBounds(); + this._moveMarker.setLatLng(bounds.getCenter()); + + this._map.fire(L.Draw.Event.EDITRESIZE, { layer: this._shape }); + }, + + _getCorners: function () { + var bounds = this._shape.getBounds(), + nw = bounds.getNorthWest(), + ne = bounds.getNorthEast(), + se = bounds.getSouthEast(), + sw = bounds.getSouthWest(); + + return [nw, ne, se, sw]; + }, + + _toggleCornerMarkers: function (opacity) { + for (var i = 0, l = this._resizeMarkers.length; i < l; i++) { + this._resizeMarkers[i].setOpacity(opacity); + } + }, + + _repositionCornerMarkers: function () { + var corners = this._getCorners(); + + for (var i = 0, l = this._resizeMarkers.length; i < l; i++) { + this._resizeMarkers[i].setLatLng(corners[i]); + } + } +}); + +L.Rectangle.addInitHook(function () { + if (L.Edit.Rectangle) { + this.editing = new L.Edit.Rectangle(this); + + if (this.options.editable) { + this.editing.enable(); + } + } +}); + + + +L.Edit = L.Edit || {}; +/** + * @class L.Edit.Circle + * @aka Edit.Circle + * @inherits L.Edit.SimpleShape + */ +L.Edit.Circle = L.Edit.SimpleShape.extend({ + _createMoveMarker: function () { + var center = this._shape.getLatLng(); + + this._moveMarker = this._createMarker(center, this.options.moveIcon); + }, + + _createResizeMarker: function () { + var center = this._shape.getLatLng(), + resizemarkerPoint = this._getResizeMarkerPoint(center); + + this._resizeMarkers = []; + this._resizeMarkers.push(this._createMarker(resizemarkerPoint, this.options.resizeIcon)); + }, + + _getResizeMarkerPoint: function (latlng) { + // From L.shape.getBounds() + var delta = this._shape._radius * Math.cos(Math.PI / 4), + point = this._map.project(latlng); + return this._map.unproject([point.x + delta, point.y - delta]); + }, + + _move: function (latlng) { + var resizemarkerPoint = this._getResizeMarkerPoint(latlng); + + // Move the resize marker + this._resizeMarkers[0].setLatLng(resizemarkerPoint); + + // Move the circle + this._shape.setLatLng(latlng); + + this._map.fire(L.Draw.Event.EDITMOVE, { layer: this._shape }); + }, + + _resize: function (latlng) { + var moveLatLng = this._moveMarker.getLatLng(), + radius = moveLatLng.distanceTo(latlng); + + this._shape.setRadius(radius); + + this._map.fire(L.Draw.Event.EDITRESIZE, { layer: this._shape }); + } +}); + +L.Circle.addInitHook(function () { + if (L.Edit.Circle) { + this.editing = new L.Edit.Circle(this); + + if (this.options.editable) { + this.editing.enable(); + } + } + + this.on('add', function () { + if (this.editing && this.editing.enabled()) { + this.editing.addHooks(); + } + }); + + this.on('remove', function () { + if (this.editing && this.editing.enabled()) { + this.editing.removeHooks(); + } + }); +}); + + +L.Map.mergeOptions({ + touchExtend: true +}); + +/** + * @class L.Map.TouchExtend + * @aka TouchExtend + */ +L.Map.TouchExtend = L.Handler.extend({ + + // @method initialize(): void + // Sets TouchExtend private accessor variables + initialize: function (map) { + this._map = map; + this._container = map._container; + this._pane = map._panes.overlayPane; + }, + + // @method addHooks(): void + // Adds dom listener events to the map container + addHooks: function () { + L.DomEvent.on(this._container, 'touchstart', this._onTouchStart, this); + L.DomEvent.on(this._container, 'touchend', this._onTouchEnd, this); + L.DomEvent.on(this._container, 'touchmove', this._onTouchMove, this); + if (this._detectIE()) { + L.DomEvent.on(this._container, 'MSPointerDown', this._onTouchStart, this); + L.DomEvent.on(this._container, 'MSPointerUp', this._onTouchEnd, this); + L.DomEvent.on(this._container, 'MSPointerMove', this._onTouchMove, this); + L.DomEvent.on(this._container, 'MSPointerCancel', this._onTouchCancel, this); + + } else { + L.DomEvent.on(this._container, 'touchcancel', this._onTouchCancel, this); + L.DomEvent.on(this._container, 'touchleave', this._onTouchLeave, this); + } + }, + + // @method removeHooks(): void + // Removes dom listener events from the map container + removeHooks: function () { + L.DomEvent.off(this._container, 'touchstart', this._onTouchStart); + L.DomEvent.off(this._container, 'touchend', this._onTouchEnd); + L.DomEvent.off(this._container, 'touchmove', this._onTouchMove); + if (this._detectIE()) { + L.DomEvent.off(this._container, 'MSPointerDowm', this._onTouchStart); + L.DomEvent.off(this._container, 'MSPointerUp', this._onTouchEnd); + L.DomEvent.off(this._container, 'MSPointerMove', this._onTouchMove); + L.DomEvent.off(this._container, 'MSPointerCancel', this._onTouchCancel); + } else { + L.DomEvent.off(this._container, 'touchcancel', this._onTouchCancel); + L.DomEvent.off(this._container, 'touchleave', this._onTouchLeave); + } + }, + + _touchEvent: function (e, type) { + // #TODO: fix the pageX error that is do a bug in Android where a single touch triggers two click events + // _filterClick is what leaflet uses as a workaround. + // This is a problem with more things than just android. Another problem is touchEnd has no touches in + // its touch list. + var touchEvent = {}; + if (typeof e.touches !== 'undefined') { + if (!e.touches.length) { + return; + } + touchEvent = e.touches[0]; + } else if (e.pointerType === 'touch') { + touchEvent = e; + if (!this._filterClick(e)) { + return; + } + } else { + return; + } + + var containerPoint = this._map.mouseEventToContainerPoint(touchEvent), + layerPoint = this._map.mouseEventToLayerPoint(touchEvent), + latlng = this._map.layerPointToLatLng(layerPoint); + + this._map.fire(type, { + latlng: latlng, + layerPoint: layerPoint, + containerPoint: containerPoint, + pageX: touchEvent.pageX, + pageY: touchEvent.pageY, + originalEvent: e + }); + }, + + /** Borrowed from Leaflet and modified for bool ops **/ + _filterClick: function (e) { + var timeStamp = (e.timeStamp || e.originalEvent.timeStamp), + elapsed = L.DomEvent._lastClick && (timeStamp - L.DomEvent._lastClick); + + // are they closer together than 500ms yet more than 100ms? + // Android typically triggers them ~300ms apart while multiple listeners + // on the same event should be triggered far faster; + // or check if click is simulated on the element, and if it is, reject any non-simulated events + if ((elapsed && elapsed > 100 && elapsed < 500) || (e.target._simulatedClick && !e._simulated)) { + L.DomEvent.stop(e); + return false; + } + L.DomEvent._lastClick = timeStamp; + return true; + }, + + _onTouchStart: function (e) { + if (!this._map._loaded) { + return; + } + + var type = 'touchstart'; + this._touchEvent(e, type); + + }, + + _onTouchEnd: function (e) { + if (!this._map._loaded) { + return; + } + + var type = 'touchend'; + this._touchEvent(e, type); + }, + + _onTouchCancel: function (e) { + if (!this._map._loaded) { + return; + } + + var type = 'touchcancel'; + if (this._detectIE()) { + type = 'pointercancel'; + } + this._touchEvent(e, type); + }, + + _onTouchLeave: function (e) { + if (!this._map._loaded) { + return; + } + + var type = 'touchleave'; + this._touchEvent(e, type); + }, + + _onTouchMove: function (e) { + if (!this._map._loaded) { + return; + } + + var type = 'touchmove'; + this._touchEvent(e, type); + }, + + _detectIE: function () { + var ua = window.navigator.userAgent; + + var msie = ua.indexOf('MSIE '); + if (msie > 0) { + // IE 10 or older => return version number + return parseInt(ua.substring(msie + 5, ua.indexOf('.', msie)), 10); + } + + var trident = ua.indexOf('Trident/'); + if (trident > 0) { + // IE 11 => return version number + var rv = ua.indexOf('rv:'); + return parseInt(ua.substring(rv + 3, ua.indexOf('.', rv)), 10); + } + + var edge = ua.indexOf('Edge/'); + if (edge > 0) { + // IE 12 => return version number + return parseInt(ua.substring(edge + 5, ua.indexOf('.', edge)), 10); + } + + // other browser + return false; + } +}); + +L.Map.addInitHook('addHandler', 'touchExtend', L.Map.TouchExtend); + + +/** + * @class L.Marker.Touch + * @aka Marker.Touch + * + * This isn't full Touch support. This is just to get markers to also support dom touch events after creation + * #TODO: find a better way of getting markers to support touch. + */ +L.Marker.Touch = L.Marker.extend({ + + _initInteraction: function () { + if (!this.addInteractiveTarget) { + // 0.7.x support + return this._initInteractionLegacy(); + } + // TODO this may need be updated to re-add touch events for 1.0+ + return L.Marker.prototype._initInteraction.apply(this); + }, + + // This is an exact copy of https://github.com/Leaflet/Leaflet/blob/v0.7/src/layer/marker/Marker.js + // with the addition of the touch events + _initInteractionLegacy: function () { + + if (!this.options.clickable) { + return; + } + + // TODO refactor into something shared with Map/Path/etc. to DRY it up + + var icon = this._icon, + events = ['dblclick', + 'mousedown', + 'mouseover', + 'mouseout', + 'contextmenu', + 'touchstart', + 'touchend', + 'touchmove']; + if (this._detectIE) { + events.concat(['MSPointerDown', + 'MSPointerUp', + 'MSPointerMove', + 'MSPointerCancel']); + } else { + events.concat(['touchcancel']); + } + + L.DomUtil.addClass(icon, 'leaflet-clickable'); + L.DomEvent.on(icon, 'click', this._onMouseClick, this); + L.DomEvent.on(icon, 'keypress', this._onKeyPress, this); + + for (var i = 0; i < events.length; i++) { + L.DomEvent.on(icon, events[i], this._fireMouseEvent, this); + } + + if (L.Handler.MarkerDrag) { + this.dragging = new L.Handler.MarkerDrag(this); + + if (this.options.draggable) { + this.dragging.enable(); + } + } + }, + + _detectIE: function () { + var ua = window.navigator.userAgent; + + var msie = ua.indexOf('MSIE '); + if (msie > 0) { + // IE 10 or older => return version number + return parseInt(ua.substring(msie + 5, ua.indexOf('.', msie)), 10); + } + + var trident = ua.indexOf('Trident/'); + if (trident > 0) { + // IE 11 => return version number + var rv = ua.indexOf('rv:'); + return parseInt(ua.substring(rv + 3, ua.indexOf('.', rv)), 10); + } + + var edge = ua.indexOf('Edge/'); + if (edge > 0) { + // IE 12 => return version number + return parseInt(ua.substring(edge + 5, ua.indexOf('.', edge)), 10); + } + + // other browser + return false; + } +}); + + + +/** + * @class L.LatLngUtil + * @aka LatLngUtil + */ +L.LatLngUtil = { + // Clones a LatLngs[], returns [][] + + // @method cloneLatLngs(LatLngs[]): L.LatLngs[] + // Clone the latLng point or points or nested points and return an array with those points + cloneLatLngs: function (latlngs) { + var clone = []; + for (var i = 0, l = latlngs.length; i < l; i++) { + // Check for nested array (Polyline/Polygon) + if (Array.isArray(latlngs[i])) { + clone.push(L.LatLngUtil.cloneLatLngs(latlngs[i])); + } else { + clone.push(this.cloneLatLng(latlngs[i])); + } + } + return clone; + }, + + // @method cloneLatLng(LatLng): L.LatLng + // Clone the latLng and return a new LatLng object. + cloneLatLng: function (latlng) { + return L.latLng(latlng.lat, latlng.lng); + } +}; + + + +/** + * @class L.GeometryUtil + * @aka GeometryUtil + */ +L.GeometryUtil = L.extend(L.GeometryUtil || {}, { + // Ported from the OpenLayers implementation. See https://github.com/openlayers/openlayers/blob/master/lib/OpenLayers/Geometry/LinearRing.js#L270 + + // @method geodesicArea(): number + geodesicArea: function (latLngs) { + var pointsCount = latLngs.length, + area = 0.0, + d2r = Math.PI / 180, + p1, p2; + + if (pointsCount > 2) { + for (var i = 0; i < pointsCount; i++) { + p1 = latLngs[i]; + p2 = latLngs[(i + 1) % pointsCount]; + area += ((p2.lng - p1.lng) * d2r) * + (2 + Math.sin(p1.lat * d2r) + Math.sin(p2.lat * d2r)); + } + area = area * 6378137.0 * 6378137.0 / 2.0; + } + + return Math.abs(area); + }, + + // @method readableArea(area, isMetric): string + // Returns a readable area string in yards or metric + readableArea: function (area, isMetric) { + var areaStr; + + if (isMetric) { + if (area >= 10000) { + areaStr = (area * 0.0001).toFixed(2) + ' ha'; + } else { + areaStr = area.toFixed(2) + ' m²'; + } + } else { + area /= 0.836127; // Square yards in 1 meter + + if (area >= 3097600) { //3097600 square yards in 1 square mile + areaStr = (area / 3097600).toFixed(2) + ' mi²'; + } else if (area >= 4840) {//48040 square yards in 1 acre + areaStr = (area / 4840).toFixed(2) + ' acres'; + } else { + areaStr = Math.ceil(area) + ' yd²'; + } + } + + return areaStr; + }, + + // @method readableDistance(distance, units): string + // Converts a metric distance to one of [ feet, nauticalMile, metric or yards ] string + // + // @alternative + // @method readableDistance(distance, isMetric, useFeet, isNauticalMile): string + // Converts metric distance to distance string. + readableDistance: function (distance, isMetric, isFeet, isNauticalMile) { + var distanceStr, + units; + + if (typeof isMetric == "string") { + units = isMetric; + } else { + if (isFeet) { + units = 'feet'; + } else if (isNauticalMile) { + units = 'nauticalMile'; + } else if (isMetric) { + units = 'metric'; + } else { + units = 'yards'; + } + } + + switch (units) { + case 'metric': + // show metres when distance is < 1km, then show km + if (distance > 1000) { + distanceStr = (distance / 1000).toFixed(2) + ' km'; + } else { + distanceStr = Math.ceil(distance) + ' m'; + } + break; + case 'feet': + distance *= 1.09361 * 3; + distanceStr = Math.ceil(distance) + ' ft'; + + break; + case 'nauticalMile': + distance *= 0.53996; + distanceStr = (distance / 1000).toFixed(2) + ' nm'; + break; + case 'yards': + default: + distance *= 1.09361; + + if (distance > 1760) { + distanceStr = (distance / 1760).toFixed(2) + ' miles'; + } else { + distanceStr = Math.ceil(distance) + ' yd'; + } + break; + } + return distanceStr; + } +}); + + + +/** + * @class L.LineUtil + * @aka Util + * @aka L.Utils + */ +L.Util.extend(L.LineUtil, { + + // @method segmentsIntersect(): boolean + // Checks to see if two line segments intersect. Does not handle degenerate cases. + // http://compgeom.cs.uiuc.edu/~jeffe/teaching/373/notes/x06-sweepline.pdf + segmentsIntersect: function (/*Point*/ p, /*Point*/ p1, /*Point*/ p2, /*Point*/ p3) { + return this._checkCounterclockwise(p, p2, p3) !== + this._checkCounterclockwise(p1, p2, p3) && + this._checkCounterclockwise(p, p1, p2) !== + this._checkCounterclockwise(p, p1, p3); + }, + + // check to see if points are in counterclockwise order + _checkCounterclockwise: function (/*Point*/ p, /*Point*/ p1, /*Point*/ p2) { + return (p2.y - p.y) * (p1.x - p.x) > (p1.y - p.y) * (p2.x - p.x); + } +}); + + +/** + * @class L.Polyline + * @aka Polyline + */ +L.Polyline.include({ + + // @method intersects(): boolean + // Check to see if this polyline has any linesegments that intersect. + // NOTE: does not support detecting intersection for degenerate cases. + intersects: function () { + var points = this._getProjectedPoints(), + len = points ? points.length : 0, + i, p, p1; + + if (this._tooFewPointsForIntersection()) { + return false; + } + + for (i = len - 1; i >= 3; i--) { + p = points[i - 1]; + p1 = points[i]; + + + if (this._lineSegmentsIntersectsRange(p, p1, i - 2)) { + return true; + } + } + + return false; + }, + + // @method newLatLngIntersects(): boolean + // Check for intersection if new latlng was added to this polyline. + // NOTE: does not support detecting intersection for degenerate cases. + newLatLngIntersects: function (latlng, skipFirst) { + // Cannot check a polyline for intersecting lats/lngs when not added to the map + if (!this._map) { + return false; + } + + return this.newPointIntersects(this._map.latLngToLayerPoint(latlng), skipFirst); + }, + + // @method newPointIntersects(): boolean + // Check for intersection if new point was added to this polyline. + // newPoint must be a layer point. + // NOTE: does not support detecting intersection for degenerate cases. + newPointIntersects: function (newPoint, skipFirst) { + var points = this._getProjectedPoints(), + len = points ? points.length : 0, + lastPoint = points ? points[len - 1] : null, + // The previous previous line segment. Previous line segment doesn't need testing. + maxIndex = len - 2; + + if (this._tooFewPointsForIntersection(1)) { + return false; + } + + return this._lineSegmentsIntersectsRange(lastPoint, newPoint, maxIndex, skipFirst ? 1 : 0); + }, + + // Polylines with 2 sides can only intersect in cases where points are collinear (we don't support detecting these). + // Cannot have intersection when < 3 line segments (< 4 points) + _tooFewPointsForIntersection: function (extraPoints) { + var points = this._getProjectedPoints(), + len = points ? points.length : 0; + // Increment length by extraPoints if present + len += extraPoints || 0; + + return !points || len <= 3; + }, + + // Checks a line segment intersections with any line segments before its predecessor. + // Don't need to check the predecessor as will never intersect. + _lineSegmentsIntersectsRange: function (p, p1, maxIndex, minIndex) { + var points = this._getProjectedPoints(), + p2, p3; + + minIndex = minIndex || 0; + + // Check all previous line segments (beside the immediately previous) for intersections + for (var j = maxIndex; j > minIndex; j--) { + p2 = points[j - 1]; + p3 = points[j]; + + if (L.LineUtil.segmentsIntersect(p, p1, p2, p3)) { + return true; + } + } + + return false; + }, + + _getProjectedPoints: function () { + if (!this._defaultShape) { + return this._originalPoints; + } + var points = [], + _shape = this._defaultShape(); + + for (var i = 0; i < _shape.length; i++) { + points.push(this._map.latLngToLayerPoint(_shape[i])); + } + return points; + } +}); + + + +/** + * @class L.Polygon + * @aka Polygon + */ +L.Polygon.include({ + + // @method intersects(): boolean + // Checks a polygon for any intersecting line segments. Ignores holes. + intersects: function () { + var polylineIntersects, + points = this._getProjectedPoints(), + len, firstPoint, lastPoint, maxIndex; + + if (this._tooFewPointsForIntersection()) { + return false; + } + + polylineIntersects = L.Polyline.prototype.intersects.call(this); + + // If already found an intersection don't need to check for any more. + if (polylineIntersects) { + return true; + } + + len = points.length; + firstPoint = points[0]; + lastPoint = points[len - 1]; + maxIndex = len - 2; + + // Check the line segment between last and first point. Don't need to check the first line segment (minIndex = 1) + return this._lineSegmentsIntersectsRange(lastPoint, firstPoint, maxIndex, 1); + } +}); + + + +/** + * @class L.Control.Draw + * @aka L.Draw + */ +L.Control.Draw = L.Control.extend({ + + // Options + options: { + position: 'topleft', + draw: {}, + edit: false + }, + + // @method initialize(): void + // Initializes draw control, toolbars from the options + initialize: function (options) { + if (L.version < '0.7') { + throw new Error('Leaflet.draw 0.2.3+ requires Leaflet 0.7.0+. Download latest from https://github.com/Leaflet/Leaflet/'); + } + + L.Control.prototype.initialize.call(this, options); + + var toolbar; + + this._toolbars = {}; + + // Initialize toolbars + if (L.DrawToolbar && this.options.draw) { + toolbar = new L.DrawToolbar(this.options.draw); + + this._toolbars[L.DrawToolbar.TYPE] = toolbar; + + // Listen for when toolbar is enabled + this._toolbars[L.DrawToolbar.TYPE].on('enable', this._toolbarEnabled, this); + } + + if (L.EditToolbar && this.options.edit) { + toolbar = new L.EditToolbar(this.options.edit); + + this._toolbars[L.EditToolbar.TYPE] = toolbar; + + // Listen for when toolbar is enabled + this._toolbars[L.EditToolbar.TYPE].on('enable', this._toolbarEnabled, this); + } + L.toolbar = this; //set global var for editing the toolbar + }, + + // @method onAdd(): container + // Adds the toolbar container to the map + onAdd: function (map) { + var container = L.DomUtil.create('div', 'leaflet-draw'), + addedTopClass = false, + topClassName = 'leaflet-draw-toolbar-top', + toolbarContainer; + + for (var toolbarId in this._toolbars) { + if (this._toolbars.hasOwnProperty(toolbarId)) { + toolbarContainer = this._toolbars[toolbarId].addToolbar(map); + + if (toolbarContainer) { + // Add class to the first toolbar to remove the margin + if (!addedTopClass) { + if (!L.DomUtil.hasClass(toolbarContainer, topClassName)) { + L.DomUtil.addClass(toolbarContainer.childNodes[0], topClassName); + } + addedTopClass = true; + } + + container.appendChild(toolbarContainer); + } + } + } + + return container; + }, + + // @method onRemove(): void + // Removes the toolbars from the map toolbar container + onRemove: function () { + for (var toolbarId in this._toolbars) { + if (this._toolbars.hasOwnProperty(toolbarId)) { + this._toolbars[toolbarId].removeToolbar(); + } + } + }, + + // @method setDrawingOptions(options): void + // Sets options to all toolbar instances + setDrawingOptions: function (options) { + for (var toolbarId in this._toolbars) { + if (this._toolbars[toolbarId] instanceof L.DrawToolbar) { + this._toolbars[toolbarId].setOptions(options); + } + } + }, + + _toolbarEnabled: function (e) { + var enabledToolbar = e.target; + + for (var toolbarId in this._toolbars) { + if (this._toolbars[toolbarId] !== enabledToolbar) { + this._toolbars[toolbarId].disable(); + } + } + } +}); + +L.Map.mergeOptions({ + drawControlTooltips: true, + drawControl: false +}); + +L.Map.addInitHook(function () { + if (this.options.drawControl) { + this.drawControl = new L.Control.Draw(); + this.addControl(this.drawControl); + } +}); + + + +/** + * @class L.Draw.Toolbar + * @aka Toolbar + * + * The toolbar class of the API — it is used to create the ui + * This will be depreciated + * + * @example + * + * ```js + * var toolbar = L.Toolbar(); + * toolbar.addToolbar(map); + * ``` + * + * ### Disabling a toolbar + * + * If you do not want a particular toolbar in your app you can turn it off by setting the toolbar to false. + * + * ```js + * var drawControl = new L.Control.Draw({ + * draw: false, + * edit: { + * featureGroup: editableLayers + * } + * }); + * ``` + * + * ### Disabling a toolbar item + * + * If you want to turn off a particular toolbar item, set it to false. The following disables drawing polygons and + * markers. It also turns off the ability to edit layers. + * + * ```js + * var drawControl = new L.Control.Draw({ + * draw: { + * polygon: false, + * marker: false + * }, + * edit: { + * featureGroup: editableLayers, + * edit: false + * } + * }); + * ``` + */ +L.Toolbar = L.Class.extend({ + includes: [L.Mixin.Events], + + // @section Methods for modifying the toolbar + + // @method initialize(options): void + // Toolbar constructor + initialize: function (options) { + L.setOptions(this, options); + + this._modes = {}; + this._actionButtons = []; + this._activeMode = null; + }, + + // @method enabled(): boolean + // Gets a true/false of whether the toolbar is enabled + enabled: function () { + return this._activeMode !== null; + }, + + // @method disable(): void + // Disables the toolbar + disable: function () { + if (!this.enabled()) { + return; + } + + this._activeMode.handler.disable(); + }, + + // @method addToolbar(map): L.DomUtil + // Adds the toolbar to the map and returns the toolbar dom element + addToolbar: function (map) { + var container = L.DomUtil.create('div', 'leaflet-draw-section'), + buttonIndex = 0, + buttonClassPrefix = this._toolbarClass || '', + modeHandlers = this.getModeHandlers(map), + i; + + this._toolbarContainer = L.DomUtil.create('div', 'leaflet-draw-toolbar leaflet-bar'); + this._map = map; + + for (i = 0; i < modeHandlers.length; i++) { + if (modeHandlers[i].enabled) { + this._initModeHandler( + modeHandlers[i].handler, + this._toolbarContainer, + buttonIndex++, + buttonClassPrefix, + modeHandlers[i].title + ); + } + } + + // if no buttons were added, do not add the toolbar + if (!buttonIndex) { + return; + } + + // Save button index of the last button, -1 as we would have ++ after the last button + this._lastButtonIndex = --buttonIndex; + + // Create empty actions part of the toolbar + this._actionsContainer = L.DomUtil.create('ul', 'leaflet-draw-actions'); + + // Add draw and cancel containers to the control container + container.appendChild(this._toolbarContainer); + container.appendChild(this._actionsContainer); + + return container; + }, + + // @method removeToolbar(): void + // Removes the toolbar and drops the handler event listeners + removeToolbar: function () { + // Dispose each handler + for (var handlerId in this._modes) { + if (this._modes.hasOwnProperty(handlerId)) { + // Unbind handler button + this._disposeButton( + this._modes[handlerId].button, + this._modes[handlerId].handler.enable, + this._modes[handlerId].handler + ); + + // Make sure is disabled + this._modes[handlerId].handler.disable(); + + // Unbind handler + this._modes[handlerId].handler + .off('enabled', this._handlerActivated, this) + .off('disabled', this._handlerDeactivated, this); + } + } + this._modes = {}; + + // Dispose the actions toolbar + for (var i = 0, l = this._actionButtons.length; i < l; i++) { + this._disposeButton( + this._actionButtons[i].button, + this._actionButtons[i].callback, + this + ); + } + this._actionButtons = []; + this._actionsContainer = null; + }, + + _initModeHandler: function (handler, container, buttonIndex, classNamePredix, buttonTitle) { + var type = handler.type; + + this._modes[type] = {}; + + this._modes[type].handler = handler; + + this._modes[type].button = this._createButton({ + type: type, + title: buttonTitle, + className: classNamePredix + '-' + type, + container: container, + callback: this._modes[type].handler.enable, + context: this._modes[type].handler + }); + + this._modes[type].buttonIndex = buttonIndex; + + this._modes[type].handler + .on('enabled', this._handlerActivated, this) + .on('disabled', this._handlerDeactivated, this); + }, + + _createButton: function (options) { + + var link = L.DomUtil.create('a', options.className || '', options.container); + // Screen reader tag + var sr = L.DomUtil.create('span', 'sr-only', options.container); + + link.href = '#'; + link.appendChild(sr); + + if (options.title) { + link.title = options.title; + sr.innerHTML = options.title; + } + + if (options.text) { + link.innerHTML = options.text; + sr.innerHTML = options.text; + } + + L.DomEvent + .on(link, 'click', L.DomEvent.stopPropagation) + .on(link, 'mousedown', L.DomEvent.stopPropagation) + .on(link, 'dblclick', L.DomEvent.stopPropagation) + .on(link, 'touchstart', L.DomEvent.stopPropagation) + .on(link, 'click', L.DomEvent.preventDefault) + .on(link, 'click', options.callback, options.context); + + return link; + }, + + _disposeButton: function (button, callback) { + L.DomEvent + .off(button, 'click', L.DomEvent.stopPropagation) + .off(button, 'mousedown', L.DomEvent.stopPropagation) + .off(button, 'dblclick', L.DomEvent.stopPropagation) + .off(button, 'touchstart', L.DomEvent.stopPropagation) + .off(button, 'click', L.DomEvent.preventDefault) + .off(button, 'click', callback); + }, + + _handlerActivated: function (e) { + // Disable active mode (if present) + this.disable(); + + // Cache new active feature + this._activeMode = this._modes[e.handler]; + + L.DomUtil.addClass(this._activeMode.button, 'leaflet-draw-toolbar-button-enabled'); + + this._showActionsToolbar(); + + this.fire('enable'); + }, + + _handlerDeactivated: function () { + this._hideActionsToolbar(); + + L.DomUtil.removeClass(this._activeMode.button, 'leaflet-draw-toolbar-button-enabled'); + + this._activeMode = null; + + this.fire('disable'); + }, + + _createActions: function (handler) { + var container = this._actionsContainer, + buttons = this.getActions(handler), + l = buttons.length, + li, di, dl, button; + + // Dispose the actions toolbar (todo: dispose only not used buttons) + for (di = 0, dl = this._actionButtons.length; di < dl; di++) { + this._disposeButton(this._actionButtons[di].button, this._actionButtons[di].callback); + } + this._actionButtons = []; + + // Remove all old buttons + while (container.firstChild) { + container.removeChild(container.firstChild); + } + + for (var i = 0; i < l; i++) { + if ('enabled' in buttons[i] && !buttons[i].enabled) { + continue; + } + + li = L.DomUtil.create('li', '', container); + + button = this._createButton({ + title: buttons[i].title, + text: buttons[i].text, + container: li, + callback: buttons[i].callback, + context: buttons[i].context + }); + + this._actionButtons.push({ + button: button, + callback: buttons[i].callback + }); + } + }, + + _showActionsToolbar: function () { + var buttonIndex = this._activeMode.buttonIndex, + lastButtonIndex = this._lastButtonIndex, + toolbarPosition = this._activeMode.button.offsetTop - 1; + + // Recreate action buttons on every click + this._createActions(this._activeMode.handler); + + // Correctly position the cancel button + this._actionsContainer.style.top = toolbarPosition + 'px'; + + if (buttonIndex === 0) { + L.DomUtil.addClass(this._toolbarContainer, 'leaflet-draw-toolbar-notop'); + L.DomUtil.addClass(this._actionsContainer, 'leaflet-draw-actions-top'); + } + + if (buttonIndex === lastButtonIndex) { + L.DomUtil.addClass(this._toolbarContainer, 'leaflet-draw-toolbar-nobottom'); + L.DomUtil.addClass(this._actionsContainer, 'leaflet-draw-actions-bottom'); + } + + this._actionsContainer.style.display = 'block'; + }, + + _hideActionsToolbar: function () { + this._actionsContainer.style.display = 'none'; + + L.DomUtil.removeClass(this._toolbarContainer, 'leaflet-draw-toolbar-notop'); + L.DomUtil.removeClass(this._toolbarContainer, 'leaflet-draw-toolbar-nobottom'); + L.DomUtil.removeClass(this._actionsContainer, 'leaflet-draw-actions-top'); + L.DomUtil.removeClass(this._actionsContainer, 'leaflet-draw-actions-bottom'); + } +}); + + + +L.Draw = L.Draw || {}; +/** + * @class L.Draw.Tooltip + * @aka Tooltip + * + * The tooltip class — it is used to display the tooltip while drawing + * This will be depreciated + * + * @example + * + * ```js + * var tooltip = L.Draw.Tooltip(); + * ``` + * + */ +L.Draw.Tooltip = L.Class.extend({ + + // @section Methods for modifying draw state + + // @method initialize(map): void + // Tooltip constructor + initialize: function (map) { + this._map = map; + this._popupPane = map._panes.popupPane; + + this._container = map.options.drawControlTooltips ? + L.DomUtil.create('div', 'leaflet-draw-tooltip', this._popupPane) : null; + this._singleLineLabel = false; + + this._map.on('mouseout', this._onMouseOut, this); + }, + + // @method dispose(): void + // Remove Tooltip DOM and unbind events + dispose: function () { + this._map.off('mouseout', this._onMouseOut, this); + + if (this._container) { + this._popupPane.removeChild(this._container); + this._container = null; + } + }, + + // @method updateContent(labelText): this + // Changes the tooltip text to string in function call + updateContent: function (labelText) { + if (!this._container) { + return this; + } + labelText.subtext = labelText.subtext || ''; + + // update the vertical position (only if changed) + if (labelText.subtext.length === 0 && !this._singleLineLabel) { + L.DomUtil.addClass(this._container, 'leaflet-draw-tooltip-single'); + this._singleLineLabel = true; + } + else if (labelText.subtext.length > 0 && this._singleLineLabel) { + L.DomUtil.removeClass(this._container, 'leaflet-draw-tooltip-single'); + this._singleLineLabel = false; + } + + this._container.innerHTML = + (labelText.subtext.length > 0 ? + '' + labelText.subtext + '' + '
' : '') + + '' + labelText.text + ''; + + return this; + }, + + // @method updatePosition(latlng): this + // Changes the location of the tooltip + updatePosition: function (latlng) { + var pos = this._map.latLngToLayerPoint(latlng), + tooltipContainer = this._container; + + if (this._container) { + tooltipContainer.style.visibility = 'inherit'; + L.DomUtil.setPosition(tooltipContainer, pos); + } + + return this; + }, + + // @method showAsError(): this + // Applies error class to tooltip + showAsError: function () { + if (this._container) { + L.DomUtil.addClass(this._container, 'leaflet-error-draw-tooltip'); + } + return this; + }, + + // @method removeError(): this + // Removes the error class from the tooltip + removeError: function () { + if (this._container) { + L.DomUtil.removeClass(this._container, 'leaflet-error-draw-tooltip'); + } + return this; + }, + + _onMouseOut: function () { + if (this._container) { + this._container.style.visibility = 'hidden'; + } + } +}); + + + +/** + * @class L.DrawToolbar + * @aka Toolbar + */ +L.DrawToolbar = L.Toolbar.extend({ + + statics: { + TYPE: 'draw' + }, + + options: { + polyline: {}, + polygon: {}, + rectangle: {}, + circle: {}, + marker: {} + }, + + // @method initialize(): void + initialize: function (options) { + // Ensure that the options are merged correctly since L.extend is only shallow + for (var type in this.options) { + if (this.options.hasOwnProperty(type)) { + if (options[type]) { + options[type] = L.extend({}, this.options[type], options[type]); + } + } + } + + this._toolbarClass = 'leaflet-draw-draw'; + L.Toolbar.prototype.initialize.call(this, options); + }, + + // @method getModeHandlers(): object + // Get mode handlers information + getModeHandlers: function (map) { + return [ + { + enabled: this.options.polyline, + handler: new L.Draw.Polyline(map, this.options.polyline), + title: L.drawLocal.draw.toolbar.buttons.polyline + }, + { + enabled: this.options.polygon, + handler: new L.Draw.Polygon(map, this.options.polygon), + title: L.drawLocal.draw.toolbar.buttons.polygon + }, + { + enabled: this.options.rectangle, + handler: new L.Draw.Rectangle(map, this.options.rectangle), + title: L.drawLocal.draw.toolbar.buttons.rectangle + }, + { + enabled: this.options.circle, + handler: new L.Draw.Circle(map, this.options.circle), + title: L.drawLocal.draw.toolbar.buttons.circle + }, + { + enabled: this.options.marker, + handler: new L.Draw.Marker(map, this.options.marker), + title: L.drawLocal.draw.toolbar.buttons.marker + } + ]; + }, + + // @method getActions(): object + // Get action information + getActions: function (handler) { + return [ + { + enabled: handler.completeShape, + title: L.drawLocal.draw.toolbar.finish.title, + text: L.drawLocal.draw.toolbar.finish.text, + callback: handler.completeShape, + context: handler + }, + { + enabled: handler.deleteLastVertex, + title: L.drawLocal.draw.toolbar.undo.title, + text: L.drawLocal.draw.toolbar.undo.text, + callback: handler.deleteLastVertex, + context: handler + }, + { + title: L.drawLocal.draw.toolbar.actions.title, + text: L.drawLocal.draw.toolbar.actions.text, + callback: this.disable, + context: this + } + ]; + }, + + // @method setOptions(): void + // Sets the options to the toolbar + setOptions: function (options) { + L.setOptions(this, options); + + for (var type in this._modes) { + if (this._modes.hasOwnProperty(type) && options.hasOwnProperty(type)) { + this._modes[type].handler.setOptions(options[type]); + } + } + } +}); + + + +/*L.Map.mergeOptions({ + editControl: true + });*/ +/** + * @class L.EditToolbar + * @aka EditToolbar + */ +L.EditToolbar = L.Toolbar.extend({ + statics: { + TYPE: 'edit' + }, + + options: { + edit: { + selectedPathOptions: { + dashArray: '10, 10', + + fill: true, + fillColor: '#fe57a1', + fillOpacity: 0.1, + + // Whether to user the existing layers color + maintainColor: false + } + }, + remove: {}, + poly: null, + featureGroup: null /* REQUIRED! TODO: perhaps if not set then all layers on the map are selectable? */ + }, + + // @method intialize(): void + initialize: function (options) { + // Need to set this manually since null is an acceptable value here + if (options.edit) { + if (typeof options.edit.selectedPathOptions === 'undefined') { + options.edit.selectedPathOptions = this.options.edit.selectedPathOptions; + } + options.edit.selectedPathOptions = L.extend({}, this.options.edit.selectedPathOptions, options.edit.selectedPathOptions); + } + + if (options.remove) { + options.remove = L.extend({}, this.options.remove, options.remove); + } + + if (options.poly) { + options.poly = L.extend({}, this.options.poly, options.poly); + } + + this._toolbarClass = 'leaflet-draw-edit'; + L.Toolbar.prototype.initialize.call(this, options); + + this._selectedFeatureCount = 0; + }, + + // @method getModeHandlers(): object + // Get mode handlers information + getModeHandlers: function (map) { + var featureGroup = this.options.featureGroup; + return [ + { + enabled: this.options.edit, + handler: new L.EditToolbar.Edit(map, { + featureGroup: featureGroup, + selectedPathOptions: this.options.edit.selectedPathOptions, + poly: this.options.poly + }), + title: L.drawLocal.edit.toolbar.buttons.edit + }, + { + enabled: this.options.remove, + handler: new L.EditToolbar.Delete(map, { + featureGroup: featureGroup + }), + title: L.drawLocal.edit.toolbar.buttons.remove + } + ]; + }, + + // @method getActions(): object + // Get actions information + getActions: function () { + return [ + { + title: L.drawLocal.edit.toolbar.actions.save.title, + text: L.drawLocal.edit.toolbar.actions.save.text, + callback: this._save, + context: this + }, + { + title: L.drawLocal.edit.toolbar.actions.cancel.title, + text: L.drawLocal.edit.toolbar.actions.cancel.text, + callback: this.disable, + context: this + } + ]; + }, + + // @method addToolbar(map): L.DomUtil + // Adds the toolbar to the map + addToolbar: function (map) { + var container = L.Toolbar.prototype.addToolbar.call(this, map); + + this._checkDisabled(); + + this.options.featureGroup.on('layeradd layerremove', this._checkDisabled, this); + + return container; + }, + + // @method removeToolbar(): void + // Removes the toolbar from the map + removeToolbar: function () { + this.options.featureGroup.off('layeradd layerremove', this._checkDisabled, this); + + L.Toolbar.prototype.removeToolbar.call(this); + }, + + // @method disable(): void + // Disables the toolbar + disable: function () { + if (!this.enabled()) { + return; + } + + this._activeMode.handler.revertLayers(); + + L.Toolbar.prototype.disable.call(this); + }, + + _save: function () { + this._activeMode.handler.save(); + if (this._activeMode) { + this._activeMode.handler.disable(); + } + }, + + _checkDisabled: function () { + var featureGroup = this.options.featureGroup, + hasLayers = featureGroup.getLayers().length !== 0, + button; + + if (this.options.edit) { + button = this._modes[L.EditToolbar.Edit.TYPE].button; + + if (hasLayers) { + L.DomUtil.removeClass(button, 'leaflet-disabled'); + } else { + L.DomUtil.addClass(button, 'leaflet-disabled'); + } + + button.setAttribute( + 'title', + hasLayers ? + L.drawLocal.edit.toolbar.buttons.edit + : L.drawLocal.edit.toolbar.buttons.editDisabled + ); + } + + if (this.options.remove) { + button = this._modes[L.EditToolbar.Delete.TYPE].button; + + if (hasLayers) { + L.DomUtil.removeClass(button, 'leaflet-disabled'); + } else { + L.DomUtil.addClass(button, 'leaflet-disabled'); + } + + button.setAttribute( + 'title', + hasLayers ? + L.drawLocal.edit.toolbar.buttons.remove + : L.drawLocal.edit.toolbar.buttons.removeDisabled + ); + } + } +}); + + + +/** + * @class L.EditToolbar.Edit + * @aka EditToolbar.Edit + */ +L.EditToolbar.Edit = L.Handler.extend({ + statics: { + TYPE: 'edit' + }, + + includes: L.Mixin.Events, + + // @method intialize(): void + initialize: function (map, options) { + L.Handler.prototype.initialize.call(this, map); + + L.setOptions(this, options); + + // Store the selectable layer group for ease of access + this._featureGroup = options.featureGroup; + + if (!(this._featureGroup instanceof L.FeatureGroup)) { + throw new Error('options.featureGroup must be a L.FeatureGroup'); + } + + this._uneditedLayerProps = {}; + + // Save the type so super can fire, need to do this as cannot do this.TYPE :( + this.type = L.EditToolbar.Edit.TYPE; + }, + + // @method enable(): void + // Enable the edit toolbar + enable: function () { + if (this._enabled || !this._hasAvailableLayers()) { + return; + } + this.fire('enabled', { handler: this.type }); + //this disable other handlers + + this._map.fire(L.Draw.Event.EDITSTART, { handler: this.type }); + //allow drawLayer to be updated before beginning edition. + + L.Handler.prototype.enable.call(this); + this._featureGroup + .on('layeradd', this._enableLayerEdit, this) + .on('layerremove', this._disableLayerEdit, this); + }, + + // @method disable(): void + // Disable the edit toolbar + disable: function () { + if (!this._enabled) { + return; + } + this._featureGroup + .off('layeradd', this._enableLayerEdit, this) + .off('layerremove', this._disableLayerEdit, this); + L.Handler.prototype.disable.call(this); + this._map.fire(L.Draw.Event.EDITSTOP, { handler: this.type }); + this.fire('disabled', { handler: this.type }); + }, + + // @method addHooks(): void + // Add listener hooks for this handler + addHooks: function () { + var map = this._map; + + if (map) { + map.getContainer().focus(); + + this._featureGroup.eachLayer(this._enableLayerEdit, this); + + this._tooltip = new L.Draw.Tooltip(this._map); + this._tooltip.updateContent({ + text: L.drawLocal.edit.handlers.edit.tooltip.text, + subtext: L.drawLocal.edit.handlers.edit.tooltip.subtext + }); + + // Quickly access the tooltip to update for intersection checking + map._editTooltip = this._tooltip; + + this._updateTooltip(); + + this._map + .on('mousemove', this._onMouseMove, this) + .on('touchmove', this._onMouseMove, this) + .on('MSPointerMove', this._onMouseMove, this) + .on(L.Draw.Event.EDITVERTEX, this._updateTooltip, this); + } + }, + + // @method removeHooks(): void + // Remove listener hooks for this handler + removeHooks: function () { + if (this._map) { + // Clean up selected layers. + this._featureGroup.eachLayer(this._disableLayerEdit, this); + + // Clear the backups of the original layers + this._uneditedLayerProps = {}; + + this._tooltip.dispose(); + this._tooltip = null; + + this._map + .off('mousemove', this._onMouseMove, this) + .off('touchmove', this._onMouseMove, this) + .off('MSPointerMove', this._onMouseMove, this) + .off(L.Draw.Event.EDITVERTEX, this._updateTooltip, this); + } + }, + + // @method revertLayers(): void + // Revert each layer's geometry changes + revertLayers: function () { + this._featureGroup.eachLayer(function (layer) { + this._revertLayer(layer); + }, this); + }, + + // @method save(): void + // Save the layer geometries + save: function () { + var editedLayers = new L.LayerGroup(); + this._featureGroup.eachLayer(function (layer) { + if (layer.edited) { + editedLayers.addLayer(layer); + layer.edited = false; + } + }); + this._map.fire(L.Draw.Event.EDITED, { layers: editedLayers }); + }, + + _backupLayer: function (layer) { + var id = L.Util.stamp(layer); + + if (!this._uneditedLayerProps[id]) { + // Polyline, Polygon or Rectangle + if (layer instanceof L.Polyline || layer instanceof L.Polygon || layer instanceof L.Rectangle) { + this._uneditedLayerProps[id] = { + latlngs: L.LatLngUtil.cloneLatLngs(layer.getLatLngs()) + }; + } else if (layer instanceof L.Circle) { + this._uneditedLayerProps[id] = { + latlng: L.LatLngUtil.cloneLatLng(layer.getLatLng()), + radius: layer.getRadius() + }; + } else if (layer instanceof L.Marker) { // Marker + this._uneditedLayerProps[id] = { + latlng: L.LatLngUtil.cloneLatLng(layer.getLatLng()) + }; + } + } + }, + + _getTooltipText: function () { + return ({ + text: L.drawLocal.edit.handlers.edit.tooltip.text, + subtext: L.drawLocal.edit.handlers.edit.tooltip.subtext + }); + }, + + _updateTooltip: function () { + this._tooltip.updateContent(this._getTooltipText()); + }, + + _revertLayer: function (layer) { + var id = L.Util.stamp(layer); + layer.edited = false; + if (this._uneditedLayerProps.hasOwnProperty(id)) { + // Polyline, Polygon or Rectangle + if (layer instanceof L.Polyline || layer instanceof L.Polygon || layer instanceof L.Rectangle) { + layer.setLatLngs(this._uneditedLayerProps[id].latlngs); + } else if (layer instanceof L.Circle) { + layer.setLatLng(this._uneditedLayerProps[id].latlng); + layer.setRadius(this._uneditedLayerProps[id].radius); + } else if (layer instanceof L.Marker) { // Marker + layer.setLatLng(this._uneditedLayerProps[id].latlng); + } + + layer.fire('revert-edited', { layer: layer }); + } + }, + + _enableLayerEdit: function (e) { + var layer = e.layer || e.target || e, + pathOptions, poly; + + // Back up this layer (if haven't before) + this._backupLayer(layer); + + if (this.options.poly) { + poly = L.Util.extend({}, this.options.poly); + layer.options.poly = poly; + } + + // Set different style for editing mode + if (this.options.selectedPathOptions) { + pathOptions = L.Util.extend({}, this.options.selectedPathOptions); + + // Use the existing color of the layer + if (pathOptions.maintainColor) { + pathOptions.color = layer.options.color; + pathOptions.fillColor = layer.options.fillColor; + } + + layer.options.original = L.extend({}, layer.options); + layer.options.editing = pathOptions; + + } + + if (layer instanceof L.Marker) { + if (layer.editing) { + layer.editing.enable(); + } + layer.dragging.enable(); + layer + .on('dragend', this._onMarkerDragEnd) + // #TODO: remove when leaflet finally fixes their draggable so it's touch friendly again. + .on('touchmove', this._onTouchMove, this) + .on('MSPointerMove', this._onTouchMove, this) + .on('touchend', this._onMarkerDragEnd, this) + .on('MSPointerUp', this._onMarkerDragEnd, this); + } else { + layer.editing.enable(); + } + }, + + _disableLayerEdit: function (e) { + var layer = e.layer || e.target || e; + + layer.edited = false; + if (layer.editing) { + layer.editing.disable(); + } + + delete layer.options.editing; + delete layer.options.original; + // Reset layer styles to that of before select + if (this._selectedPathOptions) { + if (layer instanceof L.Marker) { + this._toggleMarkerHighlight(layer); + } else { + // reset the layer style to what is was before being selected + layer.setStyle(layer.options.previousOptions); + // remove the cached options for the layer object + delete layer.options.previousOptions; + } + } + + if (layer instanceof L.Marker) { + layer.dragging.disable(); + layer + .off('dragend', this._onMarkerDragEnd, this) + .off('touchmove', this._onTouchMove, this) + .off('MSPointerMove', this._onTouchMove, this) + .off('touchend', this._onMarkerDragEnd, this) + .off('MSPointerUp', this._onMarkerDragEnd, this); + } else { + layer.editing.disable(); + } + }, + + _onMouseMove: function (e) { + this._tooltip.updatePosition(e.latlng); + }, + + _onMarkerDragEnd: function (e) { + var layer = e.target; + layer.edited = true; + this._map.fire(L.Draw.Event.EDITMOVE, { layer: layer }); + }, + + _onTouchMove: function (e) { + var touchEvent = e.originalEvent.changedTouches[0], + layerPoint = this._map.mouseEventToLayerPoint(touchEvent), + latlng = this._map.layerPointToLatLng(layerPoint); + e.target.setLatLng(latlng); + }, + + _hasAvailableLayers: function () { + return this._featureGroup.getLayers().length !== 0; + } +}); + + + +/** + * @class L.EditToolbar.Delete + * @aka EditToolbar.Delete + */ +L.EditToolbar.Delete = L.Handler.extend({ + statics: { + TYPE: 'remove' // not delete as delete is reserved in js + }, + + includes: L.Mixin.Events, + + // @method intialize(): void + initialize: function (map, options) { + L.Handler.prototype.initialize.call(this, map); + + L.Util.setOptions(this, options); + + // Store the selectable layer group for ease of access + this._deletableLayers = this.options.featureGroup; + + if (!(this._deletableLayers instanceof L.FeatureGroup)) { + throw new Error('options.featureGroup must be a L.FeatureGroup'); + } + + // Save the type so super can fire, need to do this as cannot do this.TYPE :( + this.type = L.EditToolbar.Delete.TYPE; + }, + + // @method enable(): void + // Enable the delete toolbar + enable: function () { + if (this._enabled || !this._hasAvailableLayers()) { + return; + } + this.fire('enabled', { handler: this.type }); + + this._map.fire(L.Draw.Event.DELETESTART, { handler: this.type }); + + L.Handler.prototype.enable.call(this); + + this._deletableLayers + .on('layeradd', this._enableLayerDelete, this) + .on('layerremove', this._disableLayerDelete, this); + }, + + // @method disable(): void + // Disable the delete toolbar + disable: function () { + if (!this._enabled) { + return; + } + + this._deletableLayers + .off('layeradd', this._enableLayerDelete, this) + .off('layerremove', this._disableLayerDelete, this); + + L.Handler.prototype.disable.call(this); + + this._map.fire(L.Draw.Event.DELETESTOP, { handler: this.type }); + + this.fire('disabled', { handler: this.type }); + }, + + // @method addHooks(): void + // Add listener hooks to this handler + addHooks: function () { + var map = this._map; + + if (map) { + map.getContainer().focus(); + + this._deletableLayers.eachLayer(this._enableLayerDelete, this); + this._deletedLayers = new L.LayerGroup(); + + this._tooltip = new L.Draw.Tooltip(this._map); + this._tooltip.updateContent({ text: L.drawLocal.edit.handlers.remove.tooltip.text }); + + this._map.on('mousemove', this._onMouseMove, this); + } + }, + + // @method removeHooks(): void + // Remove listener hooks from this handler + removeHooks: function () { + if (this._map) { + this._deletableLayers.eachLayer(this._disableLayerDelete, this); + this._deletedLayers = null; + + this._tooltip.dispose(); + this._tooltip = null; + + this._map.off('mousemove', this._onMouseMove, this); + } + }, + + // @method revertLayers(): void + // Revert the deleted layers back to their prior state. + revertLayers: function () { + // Iterate of the deleted layers and add them back into the featureGroup + this._deletedLayers.eachLayer(function (layer) { + this._deletableLayers.addLayer(layer); + layer.fire('revert-deleted', { layer: layer }); + }, this); + }, + + // @method save(): void + // Save deleted layers + save: function () { + this._map.fire(L.Draw.Event.DELETED, { layers: this._deletedLayers }); + }, + + _enableLayerDelete: function (e) { + var layer = e.layer || e.target || e; + + layer.on('click', this._removeLayer, this); + }, + + _disableLayerDelete: function (e) { + var layer = e.layer || e.target || e; + + layer.off('click', this._removeLayer, this); + + // Remove from the deleted layers so we can't accidentally revert if the user presses cancel + this._deletedLayers.removeLayer(layer); + }, + + _removeLayer: function (e) { + var layer = e.layer || e.target || e; + + this._deletableLayers.removeLayer(layer); + + this._deletedLayers.addLayer(layer); + + layer.fire('deleted'); + }, + + _onMouseMove: function (e) { + this._tooltip.updatePosition(e.latlng); + }, + + _hasAvailableLayers: function () { + return this._deletableLayers.getLayers().length !== 0; + } +}); + + + +}(window, document)); +//# sourceMappingURL=leaflet.draw-src.map \ No newline at end of file diff --git a/static/leaflet/draw/leaflet.draw.css b/static/leaflet/draw/leaflet.draw.css new file mode 100644 index 00000000..9a6f2614 --- /dev/null +++ b/static/leaflet/draw/leaflet.draw.css @@ -0,0 +1,9 @@ +.leaflet-draw-section{position:relative}.leaflet-draw-toolbar{margin-top:12px}.leaflet-draw-toolbar-top{margin-top:0}.leaflet-draw-toolbar-notop a:first-child{border-top-right-radius:0}.leaflet-draw-toolbar-nobottom a:last-child{border-bottom-right-radius:0}.leaflet-draw-toolbar a{background-image:url('images/spritesheet.png');background-image:linear-gradient(transparent,transparent),url('images/spritesheet.svg');background-repeat:no-repeat;background-size:270px 30px;background-clip:padding-box}.leaflet-retina .leaflet-draw-toolbar a{background-image:url('images/spritesheet-2x.png');background-image:linear-gradient(transparent,transparent),url('images/spritesheet.svg')} +.leaflet-draw a{display:block;text-align:center;text-decoration:none}.leaflet-draw a .sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.leaflet-draw-actions{display:none;list-style:none;margin:0;padding:0;position:absolute;left:26px;top:0;white-space:nowrap}.leaflet-touch .leaflet-draw-actions{left:32px}.leaflet-right .leaflet-draw-actions{right:26px;left:auto}.leaflet-touch .leaflet-right .leaflet-draw-actions{right:32px;left:auto}.leaflet-draw-actions li{display:inline-block} +.leaflet-draw-actions li:first-child a{border-left:0}.leaflet-draw-actions li:last-child a{-webkit-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.leaflet-right .leaflet-draw-actions li:last-child a{-webkit-border-radius:0;border-radius:0}.leaflet-right .leaflet-draw-actions li:first-child a{-webkit-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.leaflet-draw-actions a{background-color:#919187;border-left:1px solid #AAA;color:#FFF;font:11px/19px "Helvetica Neue",Arial,Helvetica,sans-serif;line-height:28px;text-decoration:none;padding-left:10px;padding-right:10px;height:28px} +.leaflet-touch .leaflet-draw-actions a{font-size:12px;line-height:30px;height:30px}.leaflet-draw-actions-bottom{margin-top:0}.leaflet-draw-actions-top{margin-top:1px}.leaflet-draw-actions-top a,.leaflet-draw-actions-bottom a{height:27px;line-height:27px}.leaflet-draw-actions a:hover{background-color:#a0a098}.leaflet-draw-actions-top.leaflet-draw-actions-bottom a{height:26px;line-height:26px}.leaflet-draw-toolbar .leaflet-draw-draw-polyline{background-position:-2px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-draw-polyline{background-position:0 -1px} +.leaflet-draw-toolbar .leaflet-draw-draw-polygon{background-position:-31px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-draw-polygon{background-position:-29px -1px}.leaflet-draw-toolbar .leaflet-draw-draw-rectangle{background-position:-62px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-draw-rectangle{background-position:-60px -1px}.leaflet-draw-toolbar .leaflet-draw-draw-circle{background-position:-92px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-draw-circle{background-position:-90px -1px} +.leaflet-draw-toolbar .leaflet-draw-draw-marker{background-position:-122px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-draw-marker{background-position:-120px -1px}.leaflet-draw-toolbar .leaflet-draw-edit-edit{background-position:-152px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-edit-edit{background-position:-150px -1px}.leaflet-draw-toolbar .leaflet-draw-edit-remove{background-position:-182px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-edit-remove{background-position:-180px -1px} +.leaflet-draw-toolbar .leaflet-draw-edit-edit.leaflet-disabled{background-position:-212px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-edit-edit.leaflet-disabled{background-position:-210px -1px}.leaflet-draw-toolbar .leaflet-draw-edit-remove.leaflet-disabled{background-position:-242px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-edit-remove.leaflet-disabled{background-position:-240px -2px}.leaflet-mouse-marker{background-color:#fff;cursor:crosshair}.leaflet-draw-tooltip{background:#363636;background:rgba(0,0,0,0.5);border:1px solid transparent;-webkit-border-radius:4px;border-radius:4px;color:#fff;font:12px/18px "Helvetica Neue",Arial,Helvetica,sans-serif;margin-left:20px;margin-top:-21px;padding:4px 8px;position:absolute;visibility:hidden;white-space:nowrap;z-index:6} +.leaflet-draw-tooltip:before{border-right:6px solid black;border-right-color:rgba(0,0,0,0.5);border-top:6px solid transparent;border-bottom:6px solid transparent;content:"";position:absolute;top:7px;left:-7px}.leaflet-error-draw-tooltip{background-color:#f2dede;border:1px solid #e6b6bd;color:#b94a48}.leaflet-error-draw-tooltip:before{border-right-color:#e6b6bd}.leaflet-draw-tooltip-single{margin-top:-12px}.leaflet-draw-tooltip-subtext{color:#f8d5e4}.leaflet-draw-guide-dash{font-size:1%;opacity:.6;position:absolute;width:5px;height:5px} +.leaflet-edit-marker-selected{background-color:rgba(254,87,161,0.1);border:4px dashed rgba(254,87,161,0.6);-webkit-border-radius:4px;border-radius:4px;box-sizing:content-box}.leaflet-edit-move{cursor:move}.leaflet-edit-resize{cursor:pointer}.leaflet-oldie .leaflet-draw-toolbar{border:1px solid #999} \ No newline at end of file diff --git a/static/leaflet/draw/leaflet.draw.js b/static/leaflet/draw/leaflet.draw.js new file mode 100644 index 00000000..5f47fda4 --- /dev/null +++ b/static/leaflet/draw/leaflet.draw.js @@ -0,0 +1,9 @@ +/* + Leaflet.draw 0.4.9, a plugin that adds drawing and editing tools to Leaflet powered maps. + (c) 2012-2017, Jacob Toye, Jon West, Smartrak, Leaflet + + https://github.com/Leaflet/Leaflet.draw + http://leafletjs.com + */ +!function(t,e,i){L.drawVersion="0.4.9",L.Draw={},L.drawLocal={draw:{toolbar:{actions:{title:"Cancel drawing",text:"Cancel"},finish:{title:"Finish drawing",text:"Finish"},undo:{title:"Delete last point drawn",text:"Delete last point"},buttons:{polyline:"Draw a polyline",polygon:"Draw a polygon",rectangle:"Draw a rectangle",circle:"Draw a circle",marker:"Draw a marker"}},handlers:{circle:{tooltip:{start:"Click and drag to draw circle."},radius:"Radius"},marker:{tooltip:{start:"Click map to place marker."}},polygon:{tooltip:{start:"Click to start drawing shape.",cont:"Click to continue drawing shape.",end:"Click first point to close this shape."}},polyline:{error:"Error: shape edges cannot cross!",tooltip:{start:"Click to start drawing line.",cont:"Click to continue drawing line.",end:"Click last point to finish line."}},rectangle:{tooltip:{start:"Click and drag to draw rectangle."}},simpleshape:{tooltip:{end:"Release mouse to finish drawing."}}}},edit:{toolbar:{actions:{save:{title:"Save changes.",text:"Save"},cancel:{title:"Cancel editing, discards all changes.",text:"Cancel"}},buttons:{edit:"Edit layers.",editDisabled:"No layers to edit.",remove:"Delete layers.",removeDisabled:"No layers to delete."}},handlers:{edit:{tooltip:{text:"Drag handles, or marker to edit feature.",subtext:"Click cancel to undo changes."}},remove:{tooltip:{text:"Click on a feature to remove"}}}}},L.Draw.Event={},L.Draw.Event.CREATED="draw:created",L.Draw.Event.EDITED="draw:edited",L.Draw.Event.DELETED="draw:deleted",L.Draw.Event.DRAWSTART="draw:drawstart",L.Draw.Event.DRAWSTOP="draw:drawstop",L.Draw.Event.DRAWVERTEX="draw:drawvertex",L.Draw.Event.EDITSTART="draw:editstart",L.Draw.Event.EDITMOVE="draw:editmove",L.Draw.Event.EDITRESIZE="draw:editresize",L.Draw.Event.EDITVERTEX="draw:editvertex",L.Draw.Event.EDITSTOP="draw:editstop",L.Draw.Event.DELETESTART="draw:deletestart",L.Draw.Event.DELETESTOP="draw:deletestop",L.Draw=L.Draw||{},L.Draw.Feature=L.Handler.extend({includes:L.Mixin.Events,initialize:function(t,e){this._map=t,this._container=t._container,this._overlayPane=t._panes.overlayPane,this._popupPane=t._panes.popupPane,e&&e.shapeOptions&&(e.shapeOptions=L.Util.extend({},this.options.shapeOptions,e.shapeOptions)),L.setOptions(this,e)},enable:function(){this._enabled||(L.Handler.prototype.enable.call(this),this.fire("enabled",{handler:this.type}),this._map.fire(L.Draw.Event.DRAWSTART,{layerType:this.type}))},disable:function(){this._enabled&&(L.Handler.prototype.disable.call(this),this._map.fire(L.Draw.Event.DRAWSTOP,{layerType:this.type}),this.fire("disabled",{handler:this.type}))},addHooks:function(){var t=this._map;t&&(L.DomUtil.disableTextSelection(),t.getContainer().focus(),this._tooltip=new L.Draw.Tooltip(this._map),L.DomEvent.on(this._container,"keyup",this._cancelDrawing,this))},removeHooks:function(){this._map&&(L.DomUtil.enableTextSelection(),this._tooltip.dispose(),this._tooltip=null,L.DomEvent.off(this._container,"keyup",this._cancelDrawing,this))},setOptions:function(t){L.setOptions(this,t)},_fireCreatedEvent:function(t){this._map.fire(L.Draw.Event.CREATED,{layer:t,layerType:this.type})},_cancelDrawing:function(t){this._map.fire("draw:canceled",{layerType:this.type}),27===t.keyCode&&this.disable()}}),L.Draw.Polyline=L.Draw.Feature.extend({statics:{TYPE:"polyline"},Poly:L.Polyline,options:{allowIntersection:!0,repeatMode:!1,drawError:{color:"#b00b00",timeout:2500},icon:new L.DivIcon({iconSize:new L.Point(8,8),className:"leaflet-div-icon leaflet-editing-icon"}),touchIcon:new L.DivIcon({iconSize:new L.Point(20,20),className:"leaflet-div-icon leaflet-editing-icon leaflet-touch-icon"}),guidelineDistance:20,maxGuideLineLength:4e3,shapeOptions:{stroke:!0,color:"#3388ff",weight:4,opacity:.5,fill:!1,clickable:!0},metric:!0,feet:!0,nautic:!1,showLength:!0,zIndexOffset:2e3},initialize:function(t,e){L.Browser.touch&&(this.options.icon=this.options.touchIcon),this.options.drawError.message=L.drawLocal.draw.handlers.polyline.error,e&&e.drawError&&(e.drawError=L.Util.extend({},this.options.drawError,e.drawError)),this.type=L.Draw.Polyline.TYPE,L.Draw.Feature.prototype.initialize.call(this,t,e)},addHooks:function(){L.Draw.Feature.prototype.addHooks.call(this),this._map&&(this._markers=[],this._markerGroup=new L.LayerGroup,this._map.addLayer(this._markerGroup),this._poly=new L.Polyline([],this.options.shapeOptions),this._tooltip.updateContent(this._getTooltipText()),this._mouseMarker||(this._mouseMarker=L.marker(this._map.getCenter(),{icon:L.divIcon({className:"leaflet-mouse-marker",iconAnchor:[20,20],iconSize:[40,40]}),opacity:0,zIndexOffset:this.options.zIndexOffset})),this._mouseMarker.on("mouseout",this._onMouseOut,this).on("mousemove",this._onMouseMove,this).on("mousedown",this._onMouseDown,this).on("mouseup",this._onMouseUp,this).addTo(this._map),this._map.on("mouseup",this._onMouseUp,this).on("mousemove",this._onMouseMove,this).on("zoomlevelschange",this._onZoomEnd,this).on("touchstart",this._onTouch,this).on("zoomend",this._onZoomEnd,this))},removeHooks:function(){L.Draw.Feature.prototype.removeHooks.call(this),this._clearHideErrorTimeout(),this._cleanUpShape(),this._map.removeLayer(this._markerGroup),delete this._markerGroup,delete this._markers,this._map.removeLayer(this._poly),delete this._poly,this._mouseMarker.off("mousedown",this._onMouseDown,this).off("mouseout",this._onMouseOut,this).off("mouseup",this._onMouseUp,this).off("mousemove",this._onMouseMove,this),this._map.removeLayer(this._mouseMarker),delete this._mouseMarker,this._clearGuides(),this._map.off("mouseup",this._onMouseUp,this).off("mousemove",this._onMouseMove,this).off("zoomlevelschange",this._onZoomEnd,this).off("zoomend",this._onZoomEnd,this).off("touchstart",this._onTouch,this).off("click",this._onTouch,this)},deleteLastVertex:function(){if(!(this._markers.length<=1)){var t=this._markers.pop(),e=this._poly,i=e.getLatLngs(),o=i.splice(-1,1)[0];this._poly.setLatLngs(i),this._markerGroup.removeLayer(t),e.getLatLngs().length<2&&this._map.removeLayer(e),this._vertexChanged(o,!1)}},addVertex:function(t){var e=this._markers.length;return e>=2&&!this.options.allowIntersection&&this._poly.newLatLngIntersects(t)?void this._showErrorTooltip():(this._errorShown&&this._hideErrorTooltip(),this._markers.push(this._createMarker(t)),this._poly.addLatLng(t),2===this._poly.getLatLngs().length&&this._map.addLayer(this._poly),void this._vertexChanged(t,!0))},completeShape:function(){this._markers.length<=1||(this._fireCreatedEvent(),this.disable(),this.options.repeatMode&&this.enable())},_finishShape:function(){var t=this._poly._defaultShape?this._poly._defaultShape():this._poly.getLatLngs(),e=this._poly.newLatLngIntersects(t[t.length-1]);return!this.options.allowIntersection&&e||!this._shapeIsValid()?void this._showErrorTooltip():(this._fireCreatedEvent(),this.disable(),void(this.options.repeatMode&&this.enable()))},_shapeIsValid:function(){return!0},_onZoomEnd:function(){null!==this._markers&&this._updateGuide()},_onMouseMove:function(t){var e=this._map.mouseEventToLayerPoint(t.originalEvent),i=this._map.layerPointToLatLng(e);this._currentLatLng=i,this._updateTooltip(i),this._updateGuide(e),this._mouseMarker.setLatLng(i),L.DomEvent.preventDefault(t.originalEvent)},_vertexChanged:function(t,e){this._map.fire(L.Draw.Event.DRAWVERTEX,{layers:this._markerGroup}),this._updateFinishHandler(),this._updateRunningMeasure(t,e),this._clearGuides(),this._updateTooltip()},_onMouseDown:function(t){if(!this._clickHandled&&!this._touchHandled&&!this._disableMarkers){this._onMouseMove(t),this._clickHandled=!0,this._disableNewMarkers();var e=t.originalEvent,i=e.clientX,o=e.clientY;this._startPoint.call(this,i,o)}},_startPoint:function(t,e){this._mouseDownOrigin=L.point(t,e)},_onMouseUp:function(t){var e=t.originalEvent,i=e.clientX,o=e.clientY;this._endPoint.call(this,i,o,t),this._clickHandled=null},_endPoint:function(e,i,o){if(this._mouseDownOrigin){var n=L.point(e,i).distanceTo(this._mouseDownOrigin),s=this._calculateFinishDistance(o.latlng);s<10&&L.Browser.touch?this._finishShape():Math.abs(n)<9*(t.devicePixelRatio||1)&&this.addVertex(o.latlng),this._enableNewMarkers()}this._mouseDownOrigin=null},_onTouch:function(t){var e,i,o=t.originalEvent;!o.touches||!o.touches[0]||this._clickHandled||this._touchHandled||this._disableMarkers||(e=o.touches[0].clientX,i=o.touches[0].clientY,this._disableNewMarkers(),this._touchHandled=!0,this._startPoint.call(this,e,i),this._endPoint.call(this,e,i,t),this._touchHandled=null),this._clickHandled=null},_onMouseOut:function(){this._tooltip&&this._tooltip._onMouseOut.call(this._tooltip)},_calculateFinishDistance:function(t){var e;if(this._markers.length>0){var i;if(this.type===L.Draw.Polyline.TYPE)i=this._markers[this._markers.length-1];else{if(this.type!==L.Draw.Polygon.TYPE)return 1/0;i=this._markers[0]}var o=this._map.latLngToContainerPoint(i.getLatLng()),n=new L.Marker(t,{icon:this.options.icon,zIndexOffset:2*this.options.zIndexOffset}),s=this._map.latLngToContainerPoint(n.getLatLng());e=o.distanceTo(s)}else e=1/0;return e},_updateFinishHandler:function(){var t=this._markers.length;t>1&&this._markers[t-1].on("click",this._finishShape,this),t>2&&this._markers[t-2].off("click",this._finishShape,this)},_createMarker:function(t){var e=new L.Marker(t,{icon:this.options.icon,zIndexOffset:2*this.options.zIndexOffset});return this._markerGroup.addLayer(e),e},_updateGuide:function(t){var e=this._markers?this._markers.length:0;e>0&&(t=t||this._map.latLngToLayerPoint(this._currentLatLng),this._clearGuides(),this._drawGuide(this._map.latLngToLayerPoint(this._markers[e-1].getLatLng()),t))},_updateTooltip:function(t){var e=this._getTooltipText();t&&this._tooltip.updatePosition(t),this._errorShown||this._tooltip.updateContent(e)},_drawGuide:function(t,e){var i,o,n,s=Math.floor(Math.sqrt(Math.pow(e.x-t.x,2)+Math.pow(e.y-t.y,2))),a=this.options.guidelineDistance,r=this.options.maxGuideLineLength,h=s>r?s-r:a;for(this._guidesContainer||(this._guidesContainer=L.DomUtil.create("div","leaflet-draw-guides",this._overlayPane));h1&&this._markers[this._markers.length-1].off("click",this._finishShape,this)},_fireCreatedEvent:function(){var t=new this.Poly(this._poly.getLatLngs(),this.options.shapeOptions);L.Draw.Feature.prototype._fireCreatedEvent.call(this,t)}}),L.Draw.Polygon=L.Draw.Polyline.extend({statics:{TYPE:"polygon"},Poly:L.Polygon,options:{showArea:!1,shapeOptions:{stroke:!0,color:"#3388ff",weight:4,opacity:.5,fill:!0,fillColor:null,fillOpacity:.2,clickable:!0},metric:!0},initialize:function(t,e){L.Draw.Polyline.prototype.initialize.call(this,t,e),this.type=L.Draw.Polygon.TYPE},_updateFinishHandler:function(){var t=this._markers.length;1===t&&this._markers[0].on("click",this._finishShape,this),t>2&&(this._markers[t-1].on("dblclick",this._finishShape,this),t>3&&this._markers[t-2].off("dblclick",this._finishShape,this))},_getTooltipText:function(){var t,e;return 0===this._markers.length?t=L.drawLocal.draw.handlers.polygon.tooltip.start:this._markers.length<3?t=L.drawLocal.draw.handlers.polygon.tooltip.cont:(t=L.drawLocal.draw.handlers.polygon.tooltip.end,e=this._getMeasurementString()),{text:t,subtext:e}},_getMeasurementString:function(){var t=this._area;return t?L.GeometryUtil.readableArea(t,this.options.metric):null},_shapeIsValid:function(){return this._markers.length>=3},_vertexChanged:function(t,e){var i;!this.options.allowIntersection&&this.options.showArea&&(i=this._poly.getLatLngs(),this._area=L.GeometryUtil.geodesicArea(i)),L.Draw.Polyline.prototype._vertexChanged.call(this,t,e)},_cleanUpShape:function(){var t=this._markers.length;t>0&&(this._markers[0].off("click",this._finishShape,this),t>2&&this._markers[t-1].off("dblclick",this._finishShape,this))}}),L.SimpleShape={},L.Draw.SimpleShape=L.Draw.Feature.extend({options:{repeatMode:!1},initialize:function(t,e){this._endLabelText=L.drawLocal.draw.handlers.simpleshape.tooltip.end,L.Draw.Feature.prototype.initialize.call(this,t,e)},addHooks:function(){L.Draw.Feature.prototype.addHooks.call(this),this._map&&(this._mapDraggable=this._map.dragging.enabled(),this._mapDraggable&&this._map.dragging.disable(),this._container.style.cursor="crosshair",this._tooltip.updateContent({text:this._initialLabelText}),this._map.on("mousedown",this._onMouseDown,this).on("mousemove",this._onMouseMove,this).on("touchstart",this._onMouseDown,this).on("touchmove",this._onMouseMove,this))},removeHooks:function(){L.Draw.Feature.prototype.removeHooks.call(this),this._map&&(this._mapDraggable&&this._map.dragging.enable(),this._container.style.cursor="",this._map.off("mousedown",this._onMouseDown,this).off("mousemove",this._onMouseMove,this).off("touchstart",this._onMouseDown,this).off("touchmove",this._onMouseMove,this),L.DomEvent.off(e,"mouseup",this._onMouseUp,this),L.DomEvent.off(e,"touchend",this._onMouseUp,this),this._shape&&(this._map.removeLayer(this._shape),delete this._shape)),this._isDrawing=!1},_getTooltipText:function(){return{text:this._endLabelText}},_onMouseDown:function(t){this._isDrawing=!0,this._startLatLng=t.latlng,L.DomEvent.on(e,"mouseup",this._onMouseUp,this).on(e,"touchend",this._onMouseUp,this).preventDefault(t.originalEvent)},_onMouseMove:function(t){var e=t.latlng;this._tooltip.updatePosition(e),this._isDrawing&&(this._tooltip.updateContent(this._getTooltipText()),this._drawShape(e))},_onMouseUp:function(){this._shape&&this._fireCreatedEvent(),this.disable(),this.options.repeatMode&&this.enable()}}),L.Draw.Rectangle=L.Draw.SimpleShape.extend({statics:{TYPE:"rectangle"},options:{shapeOptions:{stroke:!0,color:"#3388ff",weight:4,opacity:.5,fill:!0,fillColor:null,fillOpacity:.2,showArea:!0,clickable:!0},metric:!0},initialize:function(t,e){this.type=L.Draw.Rectangle.TYPE,this._initialLabelText=L.drawLocal.draw.handlers.rectangle.tooltip.start,L.Draw.SimpleShape.prototype.initialize.call(this,t,e)},_drawShape:function(t){this._shape?this._shape.setBounds(new L.LatLngBounds(this._startLatLng,t)):(this._shape=new L.Rectangle(new L.LatLngBounds(this._startLatLng,t),this.options.shapeOptions),this._map.addLayer(this._shape))},_fireCreatedEvent:function(){var t=new L.Rectangle(this._shape.getBounds(),this.options.shapeOptions);L.Draw.SimpleShape.prototype._fireCreatedEvent.call(this,t)},_getTooltipText:function(){var t,e,i,o=L.Draw.SimpleShape.prototype._getTooltipText.call(this),n=this._shape,s=this.options.showArea;return n&&(t=this._shape._defaultShape?this._shape._defaultShape():this._shape.getLatLngs(),e=L.GeometryUtil.geodesicArea(t),i=s?L.GeometryUtil.readableArea(e,this.options.metric):""),{text:o.text,subtext:i}}}),L.Draw.Circle=L.Draw.SimpleShape.extend({statics:{TYPE:"circle"},options:{shapeOptions:{stroke:!0,color:"#3388ff",weight:4,opacity:.5,fill:!0,fillColor:null,fillOpacity:.2,clickable:!0},showRadius:!0,metric:!0,feet:!0,nautic:!1},initialize:function(t,e){this.type=L.Draw.Circle.TYPE,this._initialLabelText=L.drawLocal.draw.handlers.circle.tooltip.start,L.Draw.SimpleShape.prototype.initialize.call(this,t,e)},_drawShape:function(t){this._shape?this._shape.setRadius(this._startLatLng.distanceTo(t)):(this._shape=new L.Circle(this._startLatLng,this._startLatLng.distanceTo(t),this.options.shapeOptions),this._map.addLayer(this._shape))},_fireCreatedEvent:function(){var t=new L.Circle(this._startLatLng,this._shape.getRadius(),this.options.shapeOptions);L.Draw.SimpleShape.prototype._fireCreatedEvent.call(this,t)},_onMouseMove:function(t){var e,i=t.latlng,o=this.options.showRadius,n=this.options.metric;if(this._tooltip.updatePosition(i),this._isDrawing){this._drawShape(i),e=this._shape.getRadius().toFixed(1);var s="";o&&(s=L.drawLocal.draw.handlers.circle.radius+": "+L.GeometryUtil.readableDistance(e,n,this.options.feet,this.options.nautic)),this._tooltip.updateContent({text:this._endLabelText,subtext:s})}}}),L.Draw.Marker=L.Draw.Feature.extend({statics:{TYPE:"marker"},options:{icon:new L.Icon.Default,repeatMode:!1,zIndexOffset:2e3},initialize:function(t,e){this.type=L.Draw.Marker.TYPE,L.Draw.Feature.prototype.initialize.call(this,t,e)},addHooks:function(){L.Draw.Feature.prototype.addHooks.call(this),this._map&&(this._tooltip.updateContent({text:L.drawLocal.draw.handlers.marker.tooltip.start}),this._mouseMarker||(this._mouseMarker=L.marker(this._map.getCenter(),{icon:L.divIcon({className:"leaflet-mouse-marker",iconAnchor:[20,20],iconSize:[40,40]}),opacity:0,zIndexOffset:this.options.zIndexOffset})),this._mouseMarker.on("click",this._onClick,this).addTo(this._map),this._map.on("mousemove",this._onMouseMove,this),this._map.on("click",this._onTouch,this))},removeHooks:function(){L.Draw.Feature.prototype.removeHooks.call(this),this._map&&(this._marker&&(this._marker.off("click",this._onClick,this),this._map.off("click",this._onClick,this).off("click",this._onTouch,this).removeLayer(this._marker),delete this._marker),this._mouseMarker.off("click",this._onClick,this),this._map.removeLayer(this._mouseMarker),delete this._mouseMarker,this._map.off("mousemove",this._onMouseMove,this))},_onMouseMove:function(t){var e=t.latlng;this._tooltip.updatePosition(e),this._mouseMarker.setLatLng(e),this._marker?(e=this._mouseMarker.getLatLng(),this._marker.setLatLng(e)):(this._marker=new L.Marker(e,{icon:this.options.icon,zIndexOffset:this.options.zIndexOffset}),this._marker.on("click",this._onClick,this),this._map.on("click",this._onClick,this).addLayer(this._marker))},_onClick:function(){this._fireCreatedEvent(),this.disable(),this.options.repeatMode&&this.enable()},_onTouch:function(t){this._onMouseMove(t),this._onClick()},_fireCreatedEvent:function(){var t=new L.Marker.Touch(this._marker.getLatLng(),{icon:this.options.icon});L.Draw.Feature.prototype._fireCreatedEvent.call(this,t)}}),L.Edit=L.Edit||{},L.Edit.Marker=L.Handler.extend({initialize:function(t,e){this._marker=t,L.setOptions(this,e)},addHooks:function(){var t=this._marker;t.dragging.enable(),t.on("dragend",this._onDragEnd,t),this._toggleMarkerHighlight()},removeHooks:function(){var t=this._marker;t.dragging.disable(),t.off("dragend",this._onDragEnd,t),this._toggleMarkerHighlight()},_onDragEnd:function(t){var e=t.target;e.edited=!0,this._map.fire(L.Draw.Event.EDITMOVE,{layer:e})},_toggleMarkerHighlight:function(){var t=this._marker._icon;t&&(t.style.display="none",L.DomUtil.hasClass(t,"leaflet-edit-marker-selected")?(L.DomUtil.removeClass(t,"leaflet-edit-marker-selected"),this._offsetMarker(t,-4)):(L.DomUtil.addClass(t,"leaflet-edit-marker-selected"),this._offsetMarker(t,4)),t.style.display="")},_offsetMarker:function(t,e){var i=parseInt(t.style.marginTop,10)-e,o=parseInt(t.style.marginLeft,10)-e;t.style.marginTop=i+"px",t.style.marginLeft=o+"px"}}),L.Marker.addInitHook(function(){L.Edit.Marker&&(this.editing=new L.Edit.Marker(this),this.options.editable&&this.editing.enable())}),L.Edit=L.Edit||{},L.Edit.Poly=L.Handler.extend({options:{},initialize:function(t,e){this.latlngs=[t._latlngs],t._holes&&(this.latlngs=this.latlngs.concat(t._holes)),this._poly=t,L.setOptions(this,e),this._poly.on("revert-edited",this._updateLatLngs,this)},_defaultShape:function(){return L.Polyline._flat?L.Polyline._flat(this._poly._latlngs)?this._poly._latlngs:this._poly._latlngs[0]:this._poly._latlngs},_eachVertexHandler:function(t){for(var e=0;et&&(i._index+=e)})},_createMiddleMarker:function(t,e){var i,o,n,s=this._getMiddleLatLng(t,e),a=this._createMarker(s);a.setOpacity(.6),t._middleRight=e._middleLeft=a,o=function(){a.off("touchmove",o,this);var n=e._index;a._index=n,a.off("click",i,this).on("click",this._onMarkerClick,this),s.lat=a.getLatLng().lat,s.lng=a.getLatLng().lng,this._spliceLatLngs(n,0,s),this._markers.splice(n,0,a),a.setOpacity(1),this._updateIndexes(n,1),e._index++,this._updatePrevNext(t,a),this._updatePrevNext(a,e),this._poly.fire("editstart")},n=function(){a.off("dragstart",o,this),a.off("dragend",n,this),a.off("touchmove",o,this),this._createMiddleMarker(t,a),this._createMiddleMarker(a,e)},i=function(){o.call(this),n.call(this),this._fireEdit()},a.on("click",i,this).on("dragstart",o,this).on("dragend",n,this).on("touchmove",o,this),this._markerGroup.addLayer(a)},_updatePrevNext:function(t,e){t&&(t._next=e),e&&(e._prev=t)},_getMiddleLatLng:function(t,e){var i=this._poly._map,o=i.project(t.getLatLng()),n=i.project(e.getLatLng());return i.unproject(o._add(n)._divideBy(2))}}),L.Polyline.addInitHook(function(){this.editing||(L.Edit.Poly&&(this.editing=new L.Edit.Poly(this,this.options.poly),this.options.editable&&this.editing.enable()),this.on("add",function(){this.editing&&this.editing.enabled()&&this.editing.addHooks()}),this.on("remove",function(){this.editing&&this.editing.enabled()&&this.editing.removeHooks()}))}),L.Edit=L.Edit||{},L.Edit.SimpleShape=L.Handler.extend({options:{moveIcon:new L.DivIcon({iconSize:new L.Point(8,8),className:"leaflet-div-icon leaflet-editing-icon leaflet-edit-move"}),resizeIcon:new L.DivIcon({iconSize:new L.Point(8,8),className:"leaflet-div-icon leaflet-editing-icon leaflet-edit-resize"}),touchMoveIcon:new L.DivIcon({iconSize:new L.Point(20,20),className:"leaflet-div-icon leaflet-editing-icon leaflet-edit-move leaflet-touch-icon"}),touchResizeIcon:new L.DivIcon({iconSize:new L.Point(20,20),className:"leaflet-div-icon leaflet-editing-icon leaflet-edit-resize leaflet-touch-icon"})},initialize:function(t,e){L.Browser.touch&&(this.options.moveIcon=this.options.touchMoveIcon,this.options.resizeIcon=this.options.touchResizeIcon),this._shape=t,L.Util.setOptions(this,e)},addHooks:function(){var t=this._shape;this._shape._map&&(this._map=this._shape._map,t.setStyle(t.options.editing),t._map&&(this._map=t._map,this._markerGroup||this._initMarkers(),this._map.addLayer(this._markerGroup)))},removeHooks:function(){var t=this._shape;if(t.setStyle(t.options.original),t._map){this._unbindMarker(this._moveMarker);for(var e=0,i=this._resizeMarkers.length;e100&&i<500||t.target._simulatedClick&&!t._simulated?(L.DomEvent.stop(t),!1):(L.DomEvent._lastClick=e,!0)},_onTouchStart:function(t){if(this._map._loaded){var e="touchstart";this._touchEvent(t,e)}},_onTouchEnd:function(t){if(this._map._loaded){var e="touchend";this._touchEvent(t,e)}},_onTouchCancel:function(t){if(this._map._loaded){var e="touchcancel";this._detectIE()&&(e="pointercancel"),this._touchEvent(t,e)}},_onTouchLeave:function(t){if(this._map._loaded){var e="touchleave";this._touchEvent(t,e)}},_onTouchMove:function(t){if(this._map._loaded){var e="touchmove";this._touchEvent(t,e)}},_detectIE:function(){var e=t.navigator.userAgent,i=e.indexOf("MSIE ");if(i>0)return parseInt(e.substring(i+5,e.indexOf(".",i)),10);var o=e.indexOf("Trident/");if(o>0){var n=e.indexOf("rv:");return parseInt(e.substring(n+3,e.indexOf(".",n)),10)}var s=e.indexOf("Edge/");return s>0&&parseInt(e.substring(s+5,e.indexOf(".",s)),10)}}),L.Map.addInitHook("addHandler","touchExtend",L.Map.TouchExtend),L.Marker.Touch=L.Marker.extend({_initInteraction:function(){return this.addInteractiveTarget?L.Marker.prototype._initInteraction.apply(this):this._initInteractionLegacy()},_initInteractionLegacy:function(){if(this.options.clickable){var t=this._icon,e=["dblclick","mousedown","mouseover","mouseout","contextmenu","touchstart","touchend","touchmove"];this._detectIE?e.concat(["MSPointerDown","MSPointerUp","MSPointerMove","MSPointerCancel"]):e.concat(["touchcancel"]),L.DomUtil.addClass(t,"leaflet-clickable"),L.DomEvent.on(t,"click",this._onMouseClick,this),L.DomEvent.on(t,"keypress",this._onKeyPress,this);for(var i=0;i 0)return parseInt(e.substring(i+5,e.indexOf(".",i)),10);var o=e.indexOf("Trident/");if(o>0){var n=e.indexOf("rv:");return parseInt(e.substring(n+3,e.indexOf(".",n)),10)}var s=e.indexOf("Edge/");return s>0&&parseInt(e.substring(s+5,e.indexOf(".",s)),10)}}),L.LatLngUtil={cloneLatLngs:function(t){for(var e=[],i=0,o=t.length;i 2){for(var a=0;a =1e4?(1e-4*t).toFixed(2)+" ha":t.toFixed(2)+" m²":(t/=.836127,i=t>=3097600?(t/3097600).toFixed(2)+" mi²":t>=4840?(t/4840).toFixed(2)+" acres":Math.ceil(t)+" yd²"),i},readableDistance:function(t,e,i,o){var n,s;switch(s="string"==typeof e?e:i?"feet":o?"nauticalMile":e?"metric":"yards"){case"metric":n=t>1e3?(t/1e3).toFixed(2)+" km":Math.ceil(t)+" m";break;case"feet":t*=3.28083,n=Math.ceil(t)+" ft";break;case"nauticalMile":t*=.53996,n=(t/1e3).toFixed(2)+" nm";break;case"yards":default:t*=1.09361,n=t>1760?(t/1760).toFixed(2)+" miles":Math.ceil(t)+" yd"}return n}}),L.Util.extend(L.LineUtil,{segmentsIntersect:function(t,e,i,o){return this._checkCounterclockwise(t,i,o)!==this._checkCounterclockwise(e,i,o)&&this._checkCounterclockwise(t,e,i)!==this._checkCounterclockwise(t,e,o)},_checkCounterclockwise:function(t,e,i){return(i.y-t.y)*(e.x-t.x)>(e.y-t.y)*(i.x-t.x)}}),L.Polyline.include({intersects:function(){var t,e,i,o=this._getProjectedPoints(),n=o?o.length:0;if(this._tooFewPointsForIntersection())return!1;for(t=n-1;t>=3;t--)if(e=o[t-1],i=o[t],this._lineSegmentsIntersectsRange(e,i,t-2))return!0;return!1},newLatLngIntersects:function(t,e){return!!this._map&&this.newPointIntersects(this._map.latLngToLayerPoint(t),e)},newPointIntersects:function(t,e){var i=this._getProjectedPoints(),o=i?i.length:0,n=i?i[o-1]:null,s=o-2;return!this._tooFewPointsForIntersection(1)&&this._lineSegmentsIntersectsRange(n,t,s,e?1:0)},_tooFewPointsForIntersection:function(t){var e=this._getProjectedPoints(),i=e?e.length:0;return i+=t||0,!e||i<=3},_lineSegmentsIntersectsRange:function(t,e,i,o){var n,s,a=this._getProjectedPoints();o=o||0;for(var r=i;r>o;r--)if(n=a[r-1],s=a[r],L.LineUtil.segmentsIntersect(t,e,n,s))return!0;return!1},_getProjectedPoints:function(){if(!this._defaultShape)return this._originalPoints;for(var t=[],e=this._defaultShape(),i=0;i 0&&this._singleLineLabel&&(L.DomUtil.removeClass(this._container,"leaflet-draw-tooltip-single"),this._singleLineLabel=!1):(L.DomUtil.addClass(this._container,"leaflet-draw-tooltip-single"),this._singleLineLabel=!0),this._container.innerHTML=(t.subtext.length>0?''+t.subtext+"
":"")+""+t.text+"",this):this},updatePosition:function(t){var e=this._map.latLngToLayerPoint(t),i=this._container;return this._container&&(i.style.visibility="inherit",L.DomUtil.setPosition(i,e)),this},showAsError:function(){return this._container&&L.DomUtil.addClass(this._container,"leaflet-error-draw-tooltip"),this},removeError:function(){return this._container&&L.DomUtil.removeClass(this._container,"leaflet-error-draw-tooltip"),this},_onMouseOut:function(){this._container&&(this._container.style.visibility="hidden")}}),L.DrawToolbar=L.Toolbar.extend({statics:{TYPE:"draw"},options:{polyline:{},polygon:{},rectangle:{},circle:{},marker:{}},initialize:function(t){for(var e in this.options)this.options.hasOwnProperty(e)&&t[e]&&(t[e]=L.extend({},this.options[e],t[e]));this._toolbarClass="leaflet-draw-draw",L.Toolbar.prototype.initialize.call(this,t)},getModeHandlers:function(t){return[{enabled:this.options.polyline,handler:new L.Draw.Polyline(t,this.options.polyline),title:L.drawLocal.draw.toolbar.buttons.polyline},{enabled:this.options.polygon,handler:new L.Draw.Polygon(t,this.options.polygon),title:L.drawLocal.draw.toolbar.buttons.polygon},{enabled:this.options.rectangle,handler:new L.Draw.Rectangle(t,this.options.rectangle),title:L.drawLocal.draw.toolbar.buttons.rectangle},{enabled:this.options.circle,handler:new L.Draw.Circle(t,this.options.circle),title:L.drawLocal.draw.toolbar.buttons.circle},{enabled:this.options.marker,handler:new L.Draw.Marker(t,this.options.marker),title:L.drawLocal.draw.toolbar.buttons.marker}]},getActions:function(t){return[{enabled:t.completeShape,title:L.drawLocal.draw.toolbar.finish.title,text:L.drawLocal.draw.toolbar.finish.text,callback:t.completeShape,context:t},{enabled:t.deleteLastVertex,title:L.drawLocal.draw.toolbar.undo.title,text:L.drawLocal.draw.toolbar.undo.text,callback:t.deleteLastVertex,context:t},{title:L.drawLocal.draw.toolbar.actions.title,text:L.drawLocal.draw.toolbar.actions.text,callback:this.disable,context:this}]},setOptions:function(t){L.setOptions(this,t);for(var e in this._modes)this._modes.hasOwnProperty(e)&&t.hasOwnProperty(e)&&this._modes[e].handler.setOptions(t[e])}}),L.EditToolbar=L.Toolbar.extend({statics:{TYPE:"edit"},options:{edit:{selectedPathOptions:{dashArray:"10, 10",fill:!0,fillColor:"#fe57a1",fillOpacity:.1,maintainColor:!1}},remove:{},poly:null,featureGroup:null},initialize:function(t){t.edit&&("undefined"==typeof t.edit.selectedPathOptions&&(t.edit.selectedPathOptions=this.options.edit.selectedPathOptions),t.edit.selectedPathOptions=L.extend({},this.options.edit.selectedPathOptions,t.edit.selectedPathOptions)),t.remove&&(t.remove=L.extend({},this.options.remove,t.remove)),t.poly&&(t.poly=L.extend({},this.options.poly,t.poly)),this._toolbarClass="leaflet-draw-edit",L.Toolbar.prototype.initialize.call(this,t),this._selectedFeatureCount=0},getModeHandlers:function(t){var e=this.options.featureGroup;return[{enabled:this.options.edit,handler:new L.EditToolbar.Edit(t,{featureGroup:e,selectedPathOptions:this.options.edit.selectedPathOptions,poly:this.options.poly}),title:L.drawLocal.edit.toolbar.buttons.edit},{enabled:this.options.remove,handler:new L.EditToolbar.Delete(t,{featureGroup:e}),title:L.drawLocal.edit.toolbar.buttons.remove}]},getActions:function(){return[{title:L.drawLocal.edit.toolbar.actions.save.title,text:L.drawLocal.edit.toolbar.actions.save.text,callback:this._save,context:this},{title:L.drawLocal.edit.toolbar.actions.cancel.title,text:L.drawLocal.edit.toolbar.actions.cancel.text,callback:this.disable,context:this}]},addToolbar:function(t){var e=L.Toolbar.prototype.addToolbar.call(this,t);return this._checkDisabled(),this.options.featureGroup.on("layeradd layerremove",this._checkDisabled,this),e},removeToolbar:function(){this.options.featureGroup.off("layeradd layerremove",this._checkDisabled,this),L.Toolbar.prototype.removeToolbar.call(this)},disable:function(){this.enabled()&&(this._activeMode.handler.revertLayers(),L.Toolbar.prototype.disable.call(this))},_save:function(){this._activeMode.handler.save(),this._activeMode&&this._activeMode.handler.disable()},_checkDisabled:function(){var t,e=this.options.featureGroup,i=0!==e.getLayers().length;this.options.edit&&(t=this._modes[L.EditToolbar.Edit.TYPE].button,i?L.DomUtil.removeClass(t,"leaflet-disabled"):L.DomUtil.addClass(t,"leaflet-disabled"),t.setAttribute("title",i?L.drawLocal.edit.toolbar.buttons.edit:L.drawLocal.edit.toolbar.buttons.editDisabled)),this.options.remove&&(t=this._modes[L.EditToolbar.Delete.TYPE].button,i?L.DomUtil.removeClass(t,"leaflet-disabled"):L.DomUtil.addClass(t,"leaflet-disabled"),t.setAttribute("title",i?L.drawLocal.edit.toolbar.buttons.remove:L.drawLocal.edit.toolbar.buttons.removeDisabled))}}),L.EditToolbar.Edit=L.Handler.extend({statics:{TYPE:"edit"},includes:L.Mixin.Events,initialize:function(t,e){if(L.Handler.prototype.initialize.call(this,t),L.setOptions(this,e),this._featureGroup=e.featureGroup,!(this._featureGroup instanceof L.FeatureGroup))throw new Error("options.featureGroup must be a L.FeatureGroup");this._uneditedLayerProps={},this.type=L.EditToolbar.Edit.TYPE},enable:function(){!this._enabled&&this._hasAvailableLayers()&&(this.fire("enabled",{handler:this.type}),this._map.fire(L.Draw.Event.EDITSTART,{handler:this.type}),L.Handler.prototype.enable.call(this),this._featureGroup.on("layeradd",this._enableLayerEdit,this).on("layerremove",this._disableLayerEdit,this))},disable:function(){this._enabled&&(this._featureGroup.off("layeradd",this._enableLayerEdit,this).off("layerremove",this._disableLayerEdit,this),L.Handler.prototype.disable.call(this),this._map.fire(L.Draw.Event.EDITSTOP,{handler:this.type}),this.fire("disabled",{handler:this.type}))},addHooks:function(){var t=this._map;t&&(t.getContainer().focus(),this._featureGroup.eachLayer(this._enableLayerEdit,this),this._tooltip=new L.Draw.Tooltip(this._map),this._tooltip.updateContent({text:L.drawLocal.edit.handlers.edit.tooltip.text,subtext:L.drawLocal.edit.handlers.edit.tooltip.subtext}),t._editTooltip=this._tooltip,this._updateTooltip(),this._map.on("mousemove",this._onMouseMove,this).on("touchmove",this._onMouseMove,this).on("MSPointerMove",this._onMouseMove,this).on(L.Draw.Event.EDITVERTEX,this._updateTooltip,this))},removeHooks:function(){this._map&&(this._featureGroup.eachLayer(this._disableLayerEdit,this),this._uneditedLayerProps={},this._tooltip.dispose(),this._tooltip=null,this._map.off("mousemove",this._onMouseMove,this).off("touchmove",this._onMouseMove,this).off("MSPointerMove",this._onMouseMove,this).off(L.Draw.Event.EDITVERTEX,this._updateTooltip,this))},revertLayers:function(){this._featureGroup.eachLayer(function(t){this._revertLayer(t)},this)},save:function(){var t=new L.LayerGroup;this._featureGroup.eachLayer(function(e){e.edited&&(t.addLayer(e),e.edited=!1)}),this._map.fire(L.Draw.Event.EDITED,{layers:t})},_backupLayer:function(t){var e=L.Util.stamp(t);this._uneditedLayerProps[e]||(t instanceof L.Polyline||t instanceof L.Polygon||t instanceof L.Rectangle?this._uneditedLayerProps[e]={latlngs:L.LatLngUtil.cloneLatLngs(t.getLatLngs())}:t instanceof L.Circle?this._uneditedLayerProps[e]={latlng:L.LatLngUtil.cloneLatLng(t.getLatLng()),radius:t.getRadius()}:t instanceof L.Marker&&(this._uneditedLayerProps[e]={latlng:L.LatLngUtil.cloneLatLng(t.getLatLng())}))},_getTooltipText:function(){return{text:L.drawLocal.edit.handlers.edit.tooltip.text,subtext:L.drawLocal.edit.handlers.edit.tooltip.subtext}},_updateTooltip:function(){this._tooltip.updateContent(this._getTooltipText())},_revertLayer:function(t){var e=L.Util.stamp(t);t.edited=!1,this._uneditedLayerProps.hasOwnProperty(e)&&(t instanceof L.Polyline||t instanceof L.Polygon||t instanceof L.Rectangle?t.setLatLngs(this._uneditedLayerProps[e].latlngs):t instanceof L.Circle?(t.setLatLng(this._uneditedLayerProps[e].latlng),t.setRadius(this._uneditedLayerProps[e].radius)):t instanceof L.Marker&&t.setLatLng(this._uneditedLayerProps[e].latlng),t.fire("revert-edited",{layer:t}))},_enableLayerEdit:function(t){var e,i,o=t.layer||t.target||t;this._backupLayer(o),this.options.poly&&(i=L.Util.extend({},this.options.poly),o.options.poly=i),this.options.selectedPathOptions&&(e=L.Util.extend({},this.options.selectedPathOptions),e.maintainColor&&(e.color=o.options.color,e.fillColor=o.options.fillColor),o.options.original=L.extend({},o.options),o.options.editing=e),o instanceof L.Marker?(o.editing&&o.editing.enable(),o.dragging.enable(),o.on("dragend",this._onMarkerDragEnd).on("touchmove",this._onTouchMove,this).on("MSPointerMove",this._onTouchMove,this).on("touchend",this._onMarkerDragEnd,this).on("MSPointerUp",this._onMarkerDragEnd,this)):o.editing.enable()},_disableLayerEdit:function(t){var e=t.layer||t.target||t;e.edited=!1,e.editing&&e.editing.disable(),delete e.options.editing,delete e.options.original,this._selectedPathOptions&&(e instanceof L.Marker?this._toggleMarkerHighlight(e):(e.setStyle(e.options.previousOptions),delete e.options.previousOptions)),e instanceof L.Marker?(e.dragging.disable(),e.off("dragend",this._onMarkerDragEnd,this).off("touchmove",this._onTouchMove,this).off("MSPointerMove",this._onTouchMove,this).off("touchend",this._onMarkerDragEnd,this).off("MSPointerUp",this._onMarkerDragEnd,this)):e.editing.disable()},_onMouseMove:function(t){this._tooltip.updatePosition(t.latlng)},_onMarkerDragEnd:function(t){var e=t.target;e.edited=!0,this._map.fire(L.Draw.Event.EDITMOVE,{layer:e})},_onTouchMove:function(t){var e=t.originalEvent.changedTouches[0],i=this._map.mouseEventToLayerPoint(e),o=this._map.layerPointToLatLng(i);t.target.setLatLng(o)},_hasAvailableLayers:function(){return 0!==this._featureGroup.getLayers().length}}),L.EditToolbar.Delete=L.Handler.extend({statics:{TYPE:"remove"},includes:L.Mixin.Events,initialize:function(t,e){if(L.Handler.prototype.initialize.call(this,t),L.Util.setOptions(this,e),this._deletableLayers=this.options.featureGroup,!(this._deletableLayers instanceof L.FeatureGroup))throw new Error("options.featureGroup must be a L.FeatureGroup");this.type=L.EditToolbar.Delete.TYPE},enable:function(){!this._enabled&&this._hasAvailableLayers()&&(this.fire("enabled",{handler:this.type}),this._map.fire(L.Draw.Event.DELETESTART,{handler:this.type}),L.Handler.prototype.enable.call(this),this._deletableLayers.on("layeradd",this._enableLayerDelete,this).on("layerremove",this._disableLayerDelete,this))},disable:function(){this._enabled&&(this._deletableLayers.off("layeradd",this._enableLayerDelete,this).off("layerremove",this._disableLayerDelete,this),L.Handler.prototype.disable.call(this),this._map.fire(L.Draw.Event.DELETESTOP,{handler:this.type}),this.fire("disabled",{handler:this.type}))},addHooks:function(){var t=this._map;t&&(t.getContainer().focus(),this._deletableLayers.eachLayer(this._enableLayerDelete,this),this._deletedLayers=new L.LayerGroup,this._tooltip=new L.Draw.Tooltip(this._map),this._tooltip.updateContent({text:L.drawLocal.edit.handlers.remove.tooltip.text}),this._map.on("mousemove",this._onMouseMove,this))},removeHooks:function(){this._map&&(this._deletableLayers.eachLayer(this._disableLayerDelete,this),this._deletedLayers=null,this._tooltip.dispose(),this._tooltip=null,this._map.off("mousemove",this._onMouseMove,this))},revertLayers:function(){this._deletedLayers.eachLayer(function(t){this._deletableLayers.addLayer(t),t.fire("revert-deleted",{layer:t})},this)},save:function(){this._map.fire(L.Draw.Event.DELETED,{layers:this._deletedLayers})},_enableLayerDelete:function(t){var e=t.layer||t.target||t;e.on("click",this._removeLayer,this)},_disableLayerDelete:function(t){var e=t.layer||t.target||t;e.off("click",this._removeLayer,this),this._deletedLayers.removeLayer(e)},_removeLayer:function(t){var e=t.layer||t.target||t;this._deletableLayers.removeLayer(e),this._deletedLayers.addLayer(e),e.fire("deleted")},_onMouseMove:function(t){this._tooltip.updatePosition(t.latlng)},_hasAvailableLayers:function(){return 0!==this._deletableLayers.getLayers().length}})}(window,document); \ No newline at end of file diff --git a/static/leaflet/eventlistener.ie6-7.js b/static/leaflet/eventlistener.ie6-7.js new file mode 100644 index 00000000..cca49122 --- /dev/null +++ b/static/leaflet/eventlistener.ie6-7.js @@ -0,0 +1,140 @@ +// EventListener | MIT/GPL2 | github.com/jonathantneal/EventListener + +!this.addEventListener && this.attachEvent && (function (window, document) { + var registry = []; + + // add + function addEventListener(type, listener) { + var target = this; + + registry.unshift({ + __listener: function (event) { + event.currentTarget = target; + event.pageX = event.clientX + document.documentElement.scrollLeft; + event.pageY = event.clientY + document.documentElement.scrollTop; + event.preventDefault = function () { event.returnValue = false }; + event.relatedTarget = event.fromElement || null; + event.stopPropagation = function () { event.cancelBubble = true }; + event.relatedTarget = event.fromElement || null; + event.target = event.srcElement || target; + event.timeStamp = +new Date; + + listener.call(target, event); + }, + listener: listener, + target: target, + type: type + }); + + target.attachEvent("on" + type, registry[0].__listener); + } + + // remove + function removeEventListener(type, listener) { + for (var index = 0, length = registry.length; index < length; ++index) { + if (registry[index].target == this && registry[index].type == type && registry[index].listener == listener) { + return this.detachEvent("on" + type, registry.splice(index, 1)[0].__listener); + } + } + } + + // dispatch + function dispatchEvent(eventObject) { + try { + return this.fireEvent("on" + eventObject.type, eventObject); + } catch (error) { + for (var index = 0, length = registry.length; index < length; ++index) { + if (registry[index].target == this && registry[index].type == eventObject.type) { + registry[index].__listener.call(this, eventObject); + } + } + } + } + + // custom + function CustomEvent(type, canBubble, cancelable, detail) { + var event = document.createEventObject(), key; + + event.type = type; + event.returnValue = !cancelable; + event.cancelBubble = !canBubble; + + for (key in detail) { + event[key] = detail[key]; + } + + return event; + } + + function _patchNode(node) { + if (node.dispatchEvent) { + return; + } + + node.addEventListener = addEventListener; + node.removeEventListener = removeEventListener; + node.dispatchEvent = dispatchEvent; + + var appendChild = node.appendChild, createElement = node.createElement, insertBefore = node.insertBefore; + + if (appendChild) { + node.appendChild = function (node) { + var returnValue = appendChild(node); + + _patchNodeList(node.all); + + return returnValue; + }; + } + + if (createElement) { + node.createElement = function (nodeName) { + var returnValue = createElement(nodeName); + + _patchNodeList(node.all); + + return returnValue; + }; + } + + if (insertBefore) { + node.insertBefore = function (node, referenceElement) { + var returnValue = insertBefore(node, referenceElement); + + _patchNodeList(node.all); + + return returnValue; + }; + } + + if ("innerHTML" in node) { + node.attachEvent("onpropertychange", function (event) { + if (event.propertyName != "innerHTML") return; + + _patchNodeList(node.all); + }); + } + } + + function _patchNodeList(nodeList) { + for (var i = 0, node; node = nodeList[i]; ++i) { + _patchNode(node); + } + } + + document.attachEvent("onreadystatechange", function (event) { + if (document.readyState == "complete") { + _patchNodeList(document.all); + + // ready + document.dispatchEvent(new CustomEvent("DOMContentLoaded", false, false)); + } + }); + + _patchNode(window); + _patchNode(document); + + _patchNodeList(document.all); + + window.CustomEvent = CustomEvent; +})(this, document); \ No newline at end of file diff --git a/static/leaflet/eventlistener.ie8.js b/static/leaflet/eventlistener.ie8.js new file mode 100644 index 00000000..fa3423b2 --- /dev/null +++ b/static/leaflet/eventlistener.ie8.js @@ -0,0 +1,125 @@ +// EventListener | MIT/GPL2 | github.com/jonathantneal/EventListener + +this.Element && Element.prototype.attachEvent && !Element.prototype.addEventListener && (function () { + function addToPrototype(name, method) { + Window.prototype[name] = HTMLDocument.prototype[name] = Element.prototype[name] = method; + } + + // add + addToPrototype("addEventListener", function (type, listener) { + var + target = this, + listeners = target.addEventListener.listeners = target.addEventListener.listeners || {}, + typeListeners = listeners[type] = listeners[type] || []; + + // if no events exist, attach the listener + if (!typeListeners.length) { + target.attachEvent("on" + type, typeListeners.event = function (event) { + var documentElement = target.document && target.document.documentElement || target.documentElement || { scrollLeft: 0, scrollTop: 0 }; + + // polyfill w3c properties and methods + event.currentTarget = target; + event.pageX = event.clientX + documentElement.scrollLeft; + event.pageY = event.clientY + documentElement.scrollTop; + event.preventDefault = function () { event.returnValue = false }; + event.relatedTarget = event.fromElement || null; + event.stopImmediatePropagation = function () { immediatePropagation = false; event.cancelBubble = true }; + event.stopPropagation = function () { event.cancelBubble = true }; + event.target = event.srcElement || target; + event.timeStamp = +new Date; + + // create an cached list of the master events list (to protect this loop from breaking when an event is removed) + for (var i = 0, typeListenersCache = [].concat(typeListeners), typeListenerCache, immediatePropagation = true; immediatePropagation && (typeListenerCache = typeListenersCache[i]); ++i) { + // check to see if the cached event still exists in the master events list + for (var ii = 0, typeListener; typeListener = typeListeners[ii]; ++ii) { + if (typeListener == typeListenerCache) { + typeListener.call(target, event); + + break; + } + } + } + }); + } + + // add the event to the master event list + typeListeners.push(listener); + }); + + // remove + addToPrototype("removeEventListener", function (type, listener) { + var + target = this, + listeners = target.addEventListener.listeners = target.addEventListener.listeners || {}, + typeListeners = listeners[type] = listeners[type] || []; + + // remove the newest matching event from the master event list + for (var i = typeListeners.length - 1, typeListener; typeListener = typeListeners[i]; --i) { + if (typeListener == listener) { + typeListeners.splice(i, 1); + + break; + } + } + + // if no events exist, detach the listener + if (!typeListeners.length && typeListeners.event) { + target.detachEvent("on" + type, typeListeners.event); + } + }); + + // dispatch + addToPrototype("dispatchEvent", function (eventObject) { + var + target = this, + type = eventObject.type, + listeners = target.addEventListener.listeners = target.addEventListener.listeners || {}, + typeListeners = listeners[type] = listeners[type] || []; + + try { + return target.fireEvent("on" + type, eventObject); + } catch (error) { + if (typeListeners.event) { + typeListeners.event(eventObject); + } + + return; + } + }); + + // CustomEvent + Object.defineProperty(Window.prototype, "CustomEvent", { + get: function () { + var self = this; + + return function CustomEvent(type, eventInitDict) { + var event = self.document.createEventObject(), key; + + event.type = type; + for (key in eventInitDict) { + if (key == 'cancelable'){ + event.returnValue = !eventInitDict.cancelable; + } else if (key == 'bubbles'){ + event.cancelBubble = !eventInitDict.bubbles; + } else if (key == 'detail'){ + event.detail = eventInitDict.detail; + } + } + return event; + }; + } + }); + + // ready + function ready(event) { + if (ready.interval && document.body) { + ready.interval = clearInterval(ready.interval); + + document.dispatchEvent(new CustomEvent("DOMContentLoaded")); + } + } + + ready.interval = setInterval(ready, 1); + + window.addEventListener("load", ready); +})(); diff --git a/static/leaflet/images/layers-2x.png b/static/leaflet/images/layers-2x.png new file mode 100644 index 0000000000000000000000000000000000000000..200c333dca9652ac4cba004d609e5af4eee168c1 GIT binary patch literal 1259 zcmVFhCYNy;#0irRPomHqW|G1C*;4?@4#E?jH>?v@U%cy?3dQAc-DchXVErpOh~ z-jbon+tNbnl6hoEb;)TVk+%hTDDi_G%i3*RZ&15!$Fjr^f;Ke&A@|?=`2&+{zr+3a z{D*=t (`AXyS%X7N z%a#RZw6vD^t_rnM`L4E>m=U&R!A-&}nZIi$BOPvkhrCuUe@BN~-lRD)f44;J%TwgE zcze8 u!PQ_NR7?o(NylLXVTfDO zxs5=@|GsYEsNo4M#nT%N!UE(?dnS)t2+{ELYAFp*3=iF=|EQnTp`#vlSXuGVraYo? z+RCzXo6h3qA8{KG?S4nE(lM+;Eb4nT3XV;7gcAxUi5m)`k5tv}cPy()8ZR3TLW3I- zAS^}cq-IJvL7a4RgR!yk@~RT%$lA7{L5ES*hyx)M4(yxI$Ub(4f)K|^v1>zvwQY!_ zIrWw8q9GS^!Dp~}+?mbnB6jDF8mVlbQ!jFKDY;w=7;XO{9bq7>LXGK24WA`;rL)_Z z)&j}pbV (;6gY;VMhbxgvn`X;6x}VUEE-7 z%)7j-%t8S=ZL3yc)HbXDAqJZvBTPoiW_A-+a8m3_Z?v{DN7Tnr#O_VUMT0UBt$;p` zDh6JbGHN8JJ*JN%y2%msb97@_S>9!%Egwk;?PEkU9ntz&3uR}%Fj5d$JHQbQb3}a{ zSzFT^#n =VInPpcAS}CNxj?_ zVscANk5Cfz(51EI1pz};AWWb|kgbYNb4wCEGUn3+eMUMV?1-{=I4TlmLJMot@rd07 zZuo2hk1ccu{YmGkcYdWAVdk{Z4Nm?^cTD&}jGm+Q1SYIXMwmG*oO*83>l%nbR`G zhh=lZ%xIb7kU3#;TBbfECrnC9P=-XpL|TG2BoZdj61*XiFbW8?1Z_wp%#;>${SUIy V$8qr;L*)Pf002ovPDHLkV1hYLS~36t literal 0 HcmV?d00001 diff --git a/static/leaflet/images/layers.png b/static/leaflet/images/layers.png new file mode 100644 index 0000000000000000000000000000000000000000..1a72e5784b2b456eac5d7670738db80697af3377 GIT binary patch literal 696 zcmV;p0!RIcP) *@&l2<6p=!C&s@#ZL+%BQvF&b?w6S%wp=I>1QHj7AP5C)IWy#b znXXB;g;j=$a-tW89K%FbDceHVq&unY*Wx3L#=EGWH=rjqnp|4c_Ulec!ql3#G-5ZF zVlbBA@XP=)C8U&+Lrc)S4O5%1$&{(;7R^K(CSnvSr$v;+B$8q&7Bf|h$#PARo1^%M zf1H^nG-EiXVXr07OH(*8R)xa|F D;lXUlg_-%)~ZGsL2cX0NXaAzN2q%jqLRR6ruVk8`Jb7n#{`T;o@`F= z#3YcynIR^s83UNF3D!f5m#Mg)NJ24&Qfrqb&_z=yF;=B)#9Iq7u-@^O!(mW{D;qvr zPc)gVb%aowtS8m@ElL4A9G>w#ffQ~q{i&_i)*6f^)Sz|C?C>zb4Uo?H<-&Hz@a?J; z$ml@zGygWofb9$ZBj6aLjpLhsT2AzjOu=-*u_gSCU Luqp7Vds@BKcX=lOk~^Pb;%&wG9p3o}FL;Zuhp5D3)R z2yY2yCGfH2=LJ mOkQw^ 9n>daF6?Fz>oOD64$CM+_T`j0x%{zb|G zWolt{H|diO#S`|$6RM $ zYQuE4RW{2yZ`>fAt>jzyYyOB?)~HrQBlbbLT5yF%Xq8FEuzo80dd{%Q!{_)^mTE`^ z2$xe>TH$qiDJ+}(ajTp$Y*4vgGRrt^_?JwUO3+hm&{M b<8aRtf7%F@*!JJv* zmcB*cag=-t4U&g79u1krRAKHHm?ZXHP8z-#KdAM9?vU7sxldD%A5;r0Rk~kblro}5 z9YhoJP18m~=v^kMBWPltYTV$TD;r4n^eZVWmDs^6;ZN_RG+a#^(N18a+%xd;JvScL zu54_hiMdFR4767cmcp!KOryQBQG{$|3e)h(z_sY-NRM>A$84n-CdxAt6V242bQmV| z86*uGCJtVTXCvLyz=eM@jE-Vz#IeA4DY~VhqL`R_>D;YIh9amQX~+l$Sfbohb*X)d zKiDG!?8t|64T_+_Jzbv6K)P|KG-6qDVGPYUwpPqb#c;-juz~ZW0bFF4JlB>cOB#?3 z9XJ~@0J1u{T_(66oVpmpLOkqOk6}qY=vN7820OS|_L-o5(4!i~Ivv=j{IKzS2m>C_ zhm9Npo09&0s*wy#K%InNpSW)yCZOhAFhe UQtcXnn!x)WSjonNUm7@fguKPg0C3ESs~`Bd3Pyd$@XU8m z0JZWv0l=fZ{{jH?{!9Nt!mEGL|9_Oug?i>9H?4E!|Krk+(hy9WRiM;!>w8@J9&fq& z${#rK1z4j2$*KVGO=b{ivL6FFEPprv0No7|9RPB_H>dzW{;{(>P`XWmKn^Y#<8`e9 zc*;k@X>z(^khkvlh3UB1ICnF@RRHbZaQhkI;sl{txVGnBEzaFKZpw96Fm8qu^5@!a z+db!omc48o>}VvJr!j9Mpo^ZMPs2FKikZu-3edWhZ~5&Mp15G60gsVYic)|~eH4Q6 zF8d5^efqo~DD}CwR pRO|j91O-zygw(bv;<>V5MDzeC#nk zosJI@GCU;ylx)tp87H~!5Gl8^4UxdZ-ZLrRy7g=zwjIe|v>O(6W-QBuv-7h4HT Lcz&ce9H!^9o^4XLD_t08@f%uD+tdxMAHzHi z6>y1>XBw|wNRu9u6j`13s*X9iz%Z1zep^?+<}$-U*uzd9$?LD0QWc+GSyhyvx< ?!6YcvM{vC6CN2-dD>XyCsuOMe zdjA0H)tFMHvR%5Uqd_swkzDP0t5)bhy5xwusp(WsD}~`13N0NuN78MHcc03G_@3v- zZOvStb!W8+G+$o+mNh5)?USue0<9~5nql|l&C!mcb^cmUZGk2gF&p9IOMcs@2-WZX z+M_WESiwx34!IyuOY(`!=Sit;If5uuYqSJm`D>ogL1P7x5=v2W{zicaAxUs>WGzTn zQv?x3HR!VK$IB{-D-)cU&hLE;M2}umynSZBHRVLCW#WkaY>!>~#*V;;^Ck!H4Swwp zDHCGo7gMu}4-?)ga$s&da$6}|l&eSgpl~CnG5l
bg z7&|&nHy^@(l0;d(4qw!>Pc+03BPqwvhV@DjJr)KAb74dUY>mzPErgW+cGhAfAE(Hx zg7S551PZuugrt1qVHk*xE*1`NeDO|ZnOO1ye(Ps{N=r+Q=S*|(%4dYb+TIr5*H@Ka z&IFce5q4snQ7O4sQm?Pxu??B#U>#Bu+HC!Ti{Sl150Y#4pk06Ac+lU@`2YRqk-uHH zZoIWi#kr-H+gi|P?w*2JMQ7U)c>*fCAPTksemc#0N4+Zgz+o*bN1@=(#&Q(RLz+r2 zQx|up>q>^w^^^t*`_3bp*JBDwCvP3iT>oMu+dLrW{Yd*GhC1Kx;_L$zF%*j;?iDxZ zrao$m-Bw;}qtlD8Ts>}{*(A|it9iEx_ZRY$yVv3y#q}J<;l}p;3_y0NqKJBW%sac- z#s<-=rSr4%CNFQcuf<8$A3ba|hx+!=-B0jwr*} bFG1p0OLTqz#DYd z16dVY=E5n{UkaA*7{FAF7c$=SE0gV@(AxW_6rfOFvBFyfQpO=ChwyqQo?nZOT`6__ zP3(sCcoy|xktOO{hUoSFKDM)^*yWXvlS$9yTyC~k^q#t~$$O;oU_E7XGiY~S^b+mS zVh=RZHn+0(T-ooM5xx%AW =ZUqv zgKQURIr-z7x5ejdVPYlT>F)dyou|#!MM#5qXK_BVQyz*bJ!*A&^rr((=SaeGlUNwV z01+e{DcnsPPIth+gTfMc34NrqGRM-T5f0=)<0vZ6?K`I0Z1Y3GdqxI|$iyh%qoeNX UQO-*oc+)|Q_08}VdXD6O0C*xx%>V!Z literal 0 HcmV?d00001 diff --git a/static/leaflet/images/marker-icon.png b/static/leaflet/images/marker-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..950edf24677ded147df13b26f91baa2b0fa70513 GIT binary patch literal 1466 zcmV;r1x5OaP) P001cn1^@s6z>|W`000GnNkl GNuHDcIX17Zdjl&3`L?0sTjIws<{((Dh&g-s0<@jYQyl?D*X^?%13;ml^gy> ziMrY_^1WI=(g@LMizu=zCoA>C`6|QEq1eV92k*7m>G65*&@&6) aC&e}G zI)pf-Za|N`DT&Cn1J|o`19mumxW~hiKiKyc-P`S@q)rdTo84@QI@;0yXrG%9uhI>A zG5QHb6s4=<6xy{1 z@NMxEkryp{LS44%z$3lP^cX!9+2-;CTt3wM4(k*#C{aiIiLuB>jJj;KPhPzIC00bL zU3a#;aJld94lCW=`4&aAy8M7PY=HQ>O%$YEP4c4UY#CRxfgbE~(|uiI=YS8q;O9y6 zmIkXzR`}p7ti|PrM3a}WMnR=3NVnWdAAR>b9X@)DKL6=YsvmH%?I24wdq?Gh54_;# z$?_LvgjEdspdQlf t#4CQ z`2Zyvy?*)N1Ftw|{_hakhG9WjS?Az@I@+IZ8JbWewR !XUK4&6346+d#~gsE0SY(LX8&JfY>Aj)RxGy96nwhs2rv zzW6pTn MpFkDSkT*a*6Dx|u@ds6ISVn0@^RmIsKZ5Y ;bazbc;tTSq(kg(=481ODrPyNB6n z-$+U}(w$m6U6H$w17Bw+wDaFIe~GvNMYvnw31MpY0eQKT9l>SU``8k7w4)z!GZKMI z#_cEKq7k~i%nlK@6c-K?+R;B#5$?T#YpKD`t_4bAs^#E+@5QW$@OX3* `;(#{U^d-vY)&xEE>n5lYl&T?Amh876I}?PQWpJLz %)x;}A1s3+;FijJ6nrW+Pl4)+EiN1Z`Z};D5lx0BtvZnmddk_DqORVPG3@A z|MUAQ5d8CyG`n3U%W5w|$1lmUI>14Eit;<8S?vK{s*?{-Ss+T)u_t92ZJTA8?H?iF zR>;v&x7=gkU3dZlw!Q;_2%o}};F13pC@6@Z$Rj?JWwli(vf7`Uv_M3vmPaI6ZRyg? zwr>!?ft(K4bi>Np0r=D2_w3@8-GdNOt_Q!(r!eStxP|AMG^4f1MRn*m6B>vlqxDrZ z^e+Su+;WdVxV9T!t{sF|>)yKqtLo0f%X=)K?{|}VWzP^q==9)`cMLdQ#3#~>wy4qc zkwZdrRrSA+s#>ZLK#*s&!Gi5)Vb$&-com)GuwvIBL{{}fScMrv%gqq1Gu;L{wqS=D zA}ae~dCefKx&Wue^ipzWJZ7j zXOrbS%~*l+4B-|15TG$lD8hF1;}EmhTVCA{%ItO^Ul121NsW&?6bK%9NRoQ!5CUZB zO|YcG$Vgw>H3Xqp1%I_^QV@c+R-jF?y|mH 7qa2-(C z0ugQ8pDRw<4{n()NIJyegIV${HQGfO5|ixWN;7!nv;%PvB8%H$$MGRJ`}fQ6#kbes z>h&MMcTZrm#Ts3`fhrQak0q-hmbwn eh)8_kw~}aV#&vv&0DQ zTMfe^L}O-h6u4_rJ&^vk4ik*_)7p25=@J%{bu-Se@_xIv$v4hVrK*YPiWBP+oJ5Ir zr3moEi=@SPav_d;H&n2Pu<`*W>zjWJNOw_DD_r~j28`tsSACjsxMemoPU@AB>{HW| ztXJMqrYlUW(>mdwMq$Dp@GE$mom`;n2e&N-yI52)$YiOCq+5IXJdE|zd*KS>5aSRb z&c)UdVb4DKNOFD!$C<8h{hnqg;riV@!i3$}= UGTOZpIU)>0{iquGsT|d?Y#Yne-5SPQrxRe+$>w;#JvMh{Q|>l$k-X z*)S}8wwajRf-*gjo*13Ddi{I2mrq!J6XOcRiG#RlA-3m(|8_HzBcDkRaoI^2U;8Bj zLQ1 _>oIw~*j8y0k)gb88Fw%SV$TO(&Ik0F^e6@= zLHw>E1o&f_sL`n+G87&T9yqi}E?j2>(j)xilP|{{#Ely_L7r5{ctW;yF>b25Js }>`iO75R6BpgcE9|%%7ZRzD#1 @K!W4(uK@LfLHB`NAX3qZf^YwSvp;i9@cqQ*)vIMW(V~k@y^(GFR zk?hjYBY8Jfi-jYuvc KGw#YY%vDItj}3 z7Q|uPsUF8INEZfRw#oQYFK+5{*aoL3O>O}4)g`9<@EtcFTw-*g{8$|m2r9KG(G1~; z5e{y}MqI4=Zwu&dpd`7ElAEf=5>(R5d?a55G39=D1mdJp{meN=dkKNp1|7_pf2kQ< za(hP&%AULsujtTO-x4$UA&=)46DW!dAjWf} ei8-FW91wTuZPe2cpnffC0y4@sv2)Mw;_ci|bZ`gPMKR{MfO zKlGq*A1TRFnjioxLQU_S-3#-w^pgr|a kh3F*-@`3{jraTr2X0$DxU9J6 zonZ#3S7xR6ObDnNWJ8&AnybbQ=UC0Wae1hQ7p* {c(l)9RmncZ49Yhd#w`%) zJK~gOp5Ur+-DQnt#)LdUN8^=@2-enu2QF9ys>*XI-S-6QHw&K;_-m@Idn%23!X5>r z@k-;CZ74HNf_oumFy8=wOzyrX$n%YiOPc-`SB %=YvR_CYcdtRU%#lH0jzd^#(k@-di-hx~al>I_R&DnN#rm07DYJ+aF!NQPu( zbl)m=2e-pbn6kGOq%ozxMkhXFRl&@1RiUgum3Vj1u#)6jsv%5j<*IR6^t$emDShpN z7o|>QRl&?k@XAh_XN1|9@o5QNcLkjz8A* rcE*n}g+c*p5 z7~m;%`pNaTgO1TTk`ZiZ=Bn}0^D(8ryf5D9p^RE?AC-e7yN3;(S*bnf{JGme)u3~( zS$ORcqqFvg`t|$f)g;O&W!6%aW^l!m_k7b2$D02GPgh9AD1`()~cZ8Oj2AQyau(pP%|J;>TN`^P;r=4@@A|s=UjbU%Sr& zOCYBon0Iw=*%^D@^5cwU6_3(-JaB ke9$Lam@{1K@O ze*LXqlKQHiv=gx+V^Cbb2?z@ISBQ*3amF;9UJ3SBg(N|710TLamQmYZ&Qjn2LuO<* zCZlB4n%@pc&7NNnY1}x+NWpHlq`OJEo|`aYN9<`RBUB+79g;>dgb6YlfN#kGL?lO_ z!6~M^7s OnbsUkKk<@Ysie&`G>ruxH&Mgy&8;i=A zB9OO!xR{AyODw>DS-q5YM {0ExFEAzt zm>RdS+ssW(-8|?xr0(?$vBVB*%(xDLtq3Hf0I5yFm<_g=W2`QWAax{1rWVH=I!VrP zs(rTFX@W#t$hXNvbgX`gK&^w_YD;CQ!B@e0QbLIWaKAXQe2-kkloo;{iF#6}z!4=W zi$giRj1{ zt;2w`VSCF#WE&*ev7jpsC=6175@(~nTE2;7M-L((0bH@yG}-TB$R~WXd?tA$s3|%y zA`9$sA(>F%J3ioz<-LJl*^o1|w84l>HBR`>3l9c8$5Xr@xCiIQ7{x$fMCzOk_-M=% z+{a_Q#;42`#KfUte@$NT77uaTz?b-fBe)1s5XE$yA79fm?KqM^VgLXD07*qoM6N<$ Ef<_J(9smFU literal 0 HcmV?d00001 diff --git a/static/leaflet/images/reset-view.png b/static/leaflet/images/reset-view.png new file mode 100644 index 0000000000000000000000000000000000000000..8ae6bde0e88a9af88c90c42db512fc356a4c92e6 GIT binary patch literal 159 zcmeAS@N?(olHy`uVBq!ia0vp^oFL4|3?y&GPYDK6YymzYuK)l42QotsU9JOCj3q&S z!3+-1Zlr-YN#5=*3>~bp9zYIffk$L90|U1(2s1Lwnj--eWH0gbb!ET9!NScUx<6Yx u0VpKo>Eal|aXtCR{R0dx?16$20u0MFd7K{qcP$6XGI+ZBxvX K+x+eH(S-`hRCnqzo!E=?wB@hnw*G`kdWiF z;IObTJuR(SzEd3a qGjrp~Q>Sii+OWZ6)~s2-_Edg;<`Wxx_ZiRxjs3O1za9Me__(-`Nk?O2 OdUXHF6)Sca85(xZnmv2@zdt{NL6VHK9V7O#F<)Nh`})~~2Nnm;oH_I1`}_DB zuaDAKE?>T^tfKP7;e@~*w$k$Qa%pL4@%y_ mdKI;Vst00`cb?f?J) literal 0 HcmV?d00001 diff --git a/static/leaflet/images/zoom-in.png b/static/leaflet/images/zoom-in.png new file mode 100644 index 0000000000000000000000000000000000000000..7353c32e134583b535ca7f1072bddd972093ff88 GIT binary patch literal 960 zcmaJ=zi-n(7&TO?N~EqJm`+!*5&N7pHnC!=Bz9;}9U>=EGod)XOJb$=8T*2rj7%`{ z7cjAQWk3j4{(^!8BQpaMVhiUqNf`*1&i6y_z3<)kefP56dbppvlM@7Czj D|UX}5jSHTk?)EZ%&1{f)1#GrQGU2;bkLM|^jinLXB_LzXxZ5vrID2x$7w zh}guzv!|a(MG$s?*X^;Mb)vb{7o7|vrhdrTf>1e1L&u#E1_os01v>ok?gIoK*5PT% zLRMHOWAAt#k |bejN#D(P)Ri|;1lM6)ISSiE!E*tSL5sKSb|^)VG|u*n(A3? zP^S?AB@yLaq#&TkqFg*wRrMYyAQ?%hAjyTitZK!Ah7eeNkVlK~P_vE3Di+`AaLib! zNm7y|Vp0@oG?L_UxtwVf3VDvm$FqPrX+DS#)(QrR-N*}>M+1;4Is-aoI^<3-ui%Fp z*&tq}i6=}-olufRlwD~Jw5 9ubGp zs7vYDI*aWwWi%es5Y#&*aOwq^Ch-z&Sz0rQnG?9AY3PviMbE>UiA<%|sHw_fQI%yA zRjW1CL`XIjS*bUS@|tT !kqr`g&j@Zr)CbE8Yu G^xA=p&*)*!%y$iRGG4A(; eSD$;ouRVD$?4ApEp_+S JT}WnhDjhuZfk|XY31hG9o0z zAAy)yVPs%nW??N1Oo$aWCZupq 3l!*-uvEt-*+#%o%{QHclQ{E*>9id7M*wV zv9m@0mk)ojblUEo+6J8$i^c0NufEdNO=5L=3+DSf_<-*25Tj3e_=2QPjF^Up2MDwS zXNWB1c(cbJQJrD7fN%Fn-#k@Z9B@vK;j$p2Y=)^HXOZKM5di}<^h1^X`The7d{1T1 zDkd}|4UPPhd5n7Vj_uCJZp~wlk3c -1F~QeCQ7EVOI?Mo^JAU`O9&aO?3Jm$ z*##Po5vXvm H%O35rFPF4z&gF-<^i5vS7@o@-pMQ4DgL}jVdt1AT2 zS~g5pX`%_^GbiE&4(3-XfTsC>Xb`N?39-<%-+u}x_AElYg%UiCU0S${gFIBEXfbjK zj%|!5MHagwOmH&75zu-SaOQ^|PLn0tG?jLk5GQm|TUS}i=X~E&jG9>1pkAw%C0P&* z*p!-D)2K?7Rs%MxWv$@q*qsI_Bn8*I<~BBR^F|0F8d*oN{{(rh7zbc^Va4C m>tAIqj%e@X%XQPz&ARPTNP9h|P5tBf%Ww2S+-mC$d*{+Ue|BSg ca`>Bh`-s{7%*dVFpQQY~w2hAbrg{GK57=fU)c^nh literal 0 HcmV?d00001 diff --git a/static/leaflet/leaflet-src.js b/static/leaflet/leaflet-src.js new file mode 100644 index 00000000..d416065f --- /dev/null +++ b/static/leaflet/leaflet-src.js @@ -0,0 +1,13251 @@ +/* + Leaflet 1.0.3+ed36a04, a JS library for interactive maps. http://leafletjs.com + (c) 2010-2016 Vladimir Agafonkin, (c) 2010-2011 CloudMade +*/ +(function (window, document, undefined) { +var L = { + version: "1.0.3+ed36a04" +}; + +function expose() { + var oldL = window.L; + + L.noConflict = function () { + window.L = oldL; + return this; + }; + + window.L = L; +} + +// define Leaflet for Node module pattern loaders, including Browserify +if (typeof module === 'object' && typeof module.exports === 'object') { + module.exports = L; + +// define Leaflet as an AMD module +} else if (typeof define === 'function' && define.amd) { + define(L); +} + +// define Leaflet as a global L variable, saving the original L to restore later if needed +if (typeof window !== 'undefined') { + expose(); +} + + + +/* + * @namespace Util + * + * Various utility functions, used by Leaflet internally. + */ + +L.Util = { + + // @function extend(dest: Object, src?: Object): Object + // Merges the properties of the `src` object (or multiple objects) into `dest` object and returns the latter. Has an `L.extend` shortcut. + extend: function (dest) { + var i, j, len, src; + + for (j = 1, len = arguments.length; j < len; j++) { + src = arguments[j]; + for (i in src) { + dest[i] = src[i]; + } + } + return dest; + }, + + // @function create(proto: Object, properties?: Object): Object + // Compatibility polyfill for [Object.create](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/create) + create: Object.create || (function () { + function F() {} + return function (proto) { + F.prototype = proto; + return new F(); + }; + })(), + + // @function bind(fn: Function, …): Function + // Returns a new function bound to the arguments passed, like [Function.prototype.bind](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Function/bind). + // Has a `L.bind()` shortcut. + bind: function (fn, obj) { + var slice = Array.prototype.slice; + + if (fn.bind) { + return fn.bind.apply(fn, slice.call(arguments, 1)); + } + + var args = slice.call(arguments, 2); + + return function () { + return fn.apply(obj, args.length ? args.concat(slice.call(arguments)) : arguments); + }; + }, + + // @function stamp(obj: Object): Number + // Returns the unique ID of an object, assiging it one if it doesn't have it. + stamp: function (obj) { + /*eslint-disable */ + obj._leaflet_id = obj._leaflet_id || ++L.Util.lastId; + return obj._leaflet_id; + /*eslint-enable */ + }, + + // @property lastId: Number + // Last unique ID used by [`stamp()`](#util-stamp) + lastId: 0, + + // @function throttle(fn: Function, time: Number, context: Object): Function + // Returns a function which executes function `fn` with the given scope `context` + // (so that the `this` keyword refers to `context` inside `fn`'s code). The function + // `fn` will be called no more than one time per given amount of `time`. The arguments + // received by the bound function will be any arguments passed when binding the + // function, followed by any arguments passed when invoking the bound function. + // Has an `L.bind` shortcut. + throttle: function (fn, time, context) { + var lock, args, wrapperFn, later; + + later = function () { + // reset lock and call if queued + lock = false; + if (args) { + wrapperFn.apply(context, args); + args = false; + } + }; + + wrapperFn = function () { + if (lock) { + // called too soon, queue to call later + args = arguments; + + } else { + // call and lock until later + fn.apply(context, arguments); + setTimeout(later, time); + lock = true; + } + }; + + return wrapperFn; + }, + + // @function wrapNum(num: Number, range: Number[], includeMax?: Boolean): Number + // Returns the number `num` modulo `range` in such a way so it lies within + // `range[0]` and `range[1]`. The returned value will be always smaller than + // `range[1]` unless `includeMax` is set to `true`. + wrapNum: function (x, range, includeMax) { + var max = range[1], + min = range[0], + d = max - min; + return x === max && includeMax ? x : ((x - min) % d + d) % d + min; + }, + + // @function falseFn(): Function + // Returns a function which always returns `false`. + falseFn: function () { return false; }, + + // @function formatNum(num: Number, digits?: Number): Number + // Returns the number `num` rounded to `digits` decimals, or to 5 decimals by default. + formatNum: function (num, digits) { + var pow = Math.pow(10, digits || 5); + return Math.round(num * pow) / pow; + }, + + // @function trim(str: String): String + // Compatibility polyfill for [String.prototype.trim](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/Trim) + trim: function (str) { + return str.trim ? str.trim() : str.replace(/^\s+|\s+$/g, ''); + }, + + // @function splitWords(str: String): String[] + // Trims and splits the string on whitespace and returns the array of parts. + splitWords: function (str) { + return L.Util.trim(str).split(/\s+/); + }, + + // @function setOptions(obj: Object, options: Object): Object + // Merges the given properties to the `options` of the `obj` object, returning the resulting options. See `Class options`. Has an `L.setOptions` shortcut. + setOptions: function (obj, options) { + if (!obj.hasOwnProperty('options')) { + obj.options = obj.options ? L.Util.create(obj.options) : {}; + } + for (var i in options) { + obj.options[i] = options[i]; + } + return obj.options; + }, + + // @function getParamString(obj: Object, existingUrl?: String, uppercase?: Boolean): String + // Converts an object into a parameter URL string, e.g. `{a: "foo", b: "bar"}` + // translates to `'?a=foo&b=bar'`. If `existingUrl` is set, the parameters will + // be appended at the end. If `uppercase` is `true`, the parameter names will + // be uppercased (e.g. `'?A=foo&B=bar'`) + getParamString: function (obj, existingUrl, uppercase) { + var params = []; + for (var i in obj) { + params.push(encodeURIComponent(uppercase ? i.toUpperCase() : i) + '=' + encodeURIComponent(obj[i])); + } + return ((!existingUrl || existingUrl.indexOf('?') === -1) ? '?' : '&') + params.join('&'); + }, + + // @function template(str: String, data: Object): String + // Simple templating facility, accepts a template string of the form `'Hello {a}, {b}'` + // and a data object like `{a: 'foo', b: 'bar'}`, returns evaluated string + // `('Hello foo, bar')`. You can also specify functions instead of strings for + // data values — they will be evaluated passing `data` as an argument. + template: function (str, data) { + return str.replace(L.Util.templateRe, function (str, key) { + var value = data[key]; + + if (value === undefined) { + throw new Error('No value provided for variable ' + str); + + } else if (typeof value === 'function') { + value = value(data); + } + return value; + }); + }, + + templateRe: /\{ *([\w_\-]+) *\}/g, + + // @function isArray(obj): Boolean + // Compatibility polyfill for [Array.isArray](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray) + isArray: Array.isArray || function (obj) { + return (Object.prototype.toString.call(obj) === '[object Array]'); + }, + + // @function indexOf(array: Array, el: Object): Number + // Compatibility polyfill for [Array.prototype.indexOf](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf) + indexOf: function (array, el) { + for (var i = 0; i < array.length; i++) { + if (array[i] === el) { return i; } + } + return -1; + }, + + // @property emptyImageUrl: String + // Data URI string containing a base64-encoded empty GIF image. + // Used as a hack to free memory from unused images on WebKit-powered + // mobile devices (by setting image `src` to this string). + emptyImageUrl: 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=' +}; + +(function () { + // inspired by http://paulirish.com/2011/requestanimationframe-for-smart-animating/ + + function getPrefixed(name) { + return window['webkit' + name] || window['moz' + name] || window['ms' + name]; + } + + var lastTime = 0; + + // fallback for IE 7-8 + function timeoutDefer(fn) { + var time = +new Date(), + timeToCall = Math.max(0, 16 - (time - lastTime)); + + lastTime = time + timeToCall; + return window.setTimeout(fn, timeToCall); + } + + var requestFn = window.requestAnimationFrame || getPrefixed('RequestAnimationFrame') || timeoutDefer, + cancelFn = window.cancelAnimationFrame || getPrefixed('CancelAnimationFrame') || + getPrefixed('CancelRequestAnimationFrame') || function (id) { window.clearTimeout(id); }; + + + // @function requestAnimFrame(fn: Function, context?: Object, immediate?: Boolean): Number + // Schedules `fn` to be executed when the browser repaints. `fn` is bound to + // `context` if given. When `immediate` is set, `fn` is called immediately if + // the browser doesn't have native support for + // [`window.requestAnimationFrame`](https://developer.mozilla.org/docs/Web/API/window/requestAnimationFrame), + // otherwise it's delayed. Returns a request ID that can be used to cancel the request. + L.Util.requestAnimFrame = function (fn, context, immediate) { + if (immediate && requestFn === timeoutDefer) { + fn.call(context); + } else { + return requestFn.call(window, L.bind(fn, context)); + } + }; + + // @function cancelAnimFrame(id: Number): undefined + // Cancels a previous `requestAnimFrame`. See also [window.cancelAnimationFrame](https://developer.mozilla.org/docs/Web/API/window/cancelAnimationFrame). + L.Util.cancelAnimFrame = function (id) { + if (id) { + cancelFn.call(window, id); + } + }; +})(); + +// shortcuts for most used utility functions +L.extend = L.Util.extend; +L.bind = L.Util.bind; +L.stamp = L.Util.stamp; +L.setOptions = L.Util.setOptions; + + + + +// @class Class +// @aka L.Class + +// @section +// @uninheritable + +// Thanks to John Resig and Dean Edwards for inspiration! + +L.Class = function () {}; + +L.Class.extend = function (props) { + + // @function extend(props: Object): Function + // [Extends the current class](#class-inheritance) given the properties to be included. + // Returns a Javascript function that is a class constructor (to be called with `new`). + var NewClass = function () { + + // call the constructor + if (this.initialize) { + this.initialize.apply(this, arguments); + } + + // call all constructor hooks + this.callInitHooks(); + }; + + var parentProto = NewClass.__super__ = this.prototype; + + var proto = L.Util.create(parentProto); + proto.constructor = NewClass; + + NewClass.prototype = proto; + + // inherit parent's statics + for (var i in this) { + if (this.hasOwnProperty(i) && i !== 'prototype') { + NewClass[i] = this[i]; + } + } + + // mix static properties into the class + if (props.statics) { + L.extend(NewClass, props.statics); + delete props.statics; + } + + // mix includes into the prototype + if (props.includes) { + L.Util.extend.apply(null, [proto].concat(props.includes)); + delete props.includes; + } + + // merge options + if (proto.options) { + props.options = L.Util.extend(L.Util.create(proto.options), props.options); + } + + // mix given properties into the prototype + L.extend(proto, props); + + proto._initHooks = []; + + // add method for calling all hooks + proto.callInitHooks = function () { + + if (this._initHooksCalled) { return; } + + if (parentProto.callInitHooks) { + parentProto.callInitHooks.call(this); + } + + this._initHooksCalled = true; + + for (var i = 0, len = proto._initHooks.length; i < len; i++) { + proto._initHooks[i].call(this); + } + }; + + return NewClass; +}; + + +// @function include(properties: Object): this +// [Includes a mixin](#class-includes) into the current class. +L.Class.include = function (props) { + L.extend(this.prototype, props); + return this; +}; + +// @function mergeOptions(options: Object): this +// [Merges `options`](#class-options) into the defaults of the class. +L.Class.mergeOptions = function (options) { + L.extend(this.prototype.options, options); + return this; +}; + +// @function addInitHook(fn: Function): this +// Adds a [constructor hook](#class-constructor-hooks) to the class. +L.Class.addInitHook = function (fn) { // (Function) || (String, args...) + var args = Array.prototype.slice.call(arguments, 1); + + var init = typeof fn === 'function' ? fn : function () { + this[fn].apply(this, args); + }; + + this.prototype._initHooks = this.prototype._initHooks || []; + this.prototype._initHooks.push(init); + return this; +}; + + + +/* + * @class Evented + * @aka L.Evented + * @inherits Class + * + * A set of methods shared between event-powered classes (like `Map` and `Marker`). Generally, events allow you to execute some function when something happens with an object (e.g. the user clicks on the map, causing the map to fire `'click'` event). + * + * @example + * + * ```js + * map.on('click', function(e) { + * alert(e.latlng); + * } ); + * ``` + * + * Leaflet deals with event listeners by reference, so if you want to add a listener and then remove it, define it as a function: + * + * ```js + * function onClick(e) { ... } + * + * map.on('click', onClick); + * map.off('click', onClick); + * ``` + */ + + +L.Evented = L.Class.extend({ + + /* @method on(type: String, fn: Function, context?: Object): this + * Adds a listener function (`fn`) to a particular event type of the object. You can optionally specify the context of the listener (object the this keyword will point to). You can also pass several space-separated types (e.g. `'click dblclick'`). + * + * @alternative + * @method on(eventMap: Object): this + * Adds a set of type/listener pairs, e.g. `{click: onClick, mousemove: onMouseMove}` + */ + on: function (types, fn, context) { + + // types can be a map of types/handlers + if (typeof types === 'object') { + for (var type in types) { + // we don't process space-separated events here for performance; + // it's a hot path since Layer uses the on(obj) syntax + this._on(type, types[type], fn); + } + + } else { + // types can be a string of space-separated words + types = L.Util.splitWords(types); + + for (var i = 0, len = types.length; i < len; i++) { + this._on(types[i], fn, context); + } + } + + return this; + }, + + /* @method off(type: String, fn?: Function, context?: Object): this + * Removes a previously added listener function. If no function is specified, it will remove all the listeners of that particular event from the object. Note that if you passed a custom context to `on`, you must pass the same context to `off` in order to remove the listener. + * + * @alternative + * @method off(eventMap: Object): this + * Removes a set of type/listener pairs. + * + * @alternative + * @method off: this + * Removes all listeners to all events on the object. + */ + off: function (types, fn, context) { + + if (!types) { + // clear all listeners if called without arguments + delete this._events; + + } else if (typeof types === 'object') { + for (var type in types) { + this._off(type, types[type], fn); + } + + } else { + types = L.Util.splitWords(types); + + for (var i = 0, len = types.length; i < len; i++) { + this._off(types[i], fn, context); + } + } + + return this; + }, + + // attach listener (without syntactic sugar now) + _on: function (type, fn, context) { + this._events = this._events || {}; + + /* get/init listeners for type */ + var typeListeners = this._events[type]; + if (!typeListeners) { + typeListeners = []; + this._events[type] = typeListeners; + } + + if (context === this) { + // Less memory footprint. + context = undefined; + } + var newListener = {fn: fn, ctx: context}, + listeners = typeListeners; + + // check if fn already there + for (var i = 0, len = listeners.length; i < len; i++) { + if (listeners[i].fn === fn && listeners[i].ctx === context) { + return; + } + } + + listeners.push(newListener); + }, + + _off: function (type, fn, context) { + var listeners, + i, + len; + + if (!this._events) { return; } + + listeners = this._events[type]; + + if (!listeners) { + return; + } + + if (!fn) { + // Set all removed listeners to noop so they are not called if remove happens in fire + for (i = 0, len = listeners.length; i < len; i++) { + listeners[i].fn = L.Util.falseFn; + } + // clear all listeners for a type if function isn't specified + delete this._events[type]; + return; + } + + if (context === this) { + context = undefined; + } + + if (listeners) { + + // find fn and remove it + for (i = 0, len = listeners.length; i < len; i++) { + var l = listeners[i]; + if (l.ctx !== context) { continue; } + if (l.fn === fn) { + + // set the removed listener to noop so that's not called if remove happens in fire + l.fn = L.Util.falseFn; + + if (this._firingCount) { + /* copy array in case events are being fired */ + this._events[type] = listeners = listeners.slice(); + } + listeners.splice(i, 1); + + return; + } + } + } + }, + + // @method fire(type: String, data?: Object, propagate?: Boolean): this + // Fires an event of the specified type. You can optionally provide an data + // object — the first argument of the listener function will contain its + // properties. The event can optionally be propagated to event parents. + fire: function (type, data, propagate) { + if (!this.listens(type, propagate)) { return this; } + + var event = L.Util.extend({}, data, {type: type, target: this}); + + if (this._events) { + var listeners = this._events[type]; + + if (listeners) { + this._firingCount = (this._firingCount + 1) || 1; + for (var i = 0, len = listeners.length; i < len; i++) { + var l = listeners[i]; + l.fn.call(l.ctx || this, event); + } + + this._firingCount--; + } + } + + if (propagate) { + // propagate the event to parents (set with addEventParent) + this._propagateEvent(event); + } + + return this; + }, + + // @method listens(type: String): Boolean + // Returns `true` if a particular event type has any listeners attached to it. + listens: function (type, propagate) { + var listeners = this._events && this._events[type]; + if (listeners && listeners.length) { return true; } + + if (propagate) { + // also check parents for listeners if event propagates + for (var id in this._eventParents) { + if (this._eventParents[id].listens(type, propagate)) { return true; } + } + } + return false; + }, + + // @method once(…): this + // Behaves as [`on(…)`](#evented-on), except the listener will only get fired once and then removed. + once: function (types, fn, context) { + + if (typeof types === 'object') { + for (var type in types) { + this.once(type, types[type], fn); + } + return this; + } + + var handler = L.bind(function () { + this + .off(types, fn, context) + .off(types, handler, context); + }, this); + + // add a listener that's executed once and removed after that + return this + .on(types, fn, context) + .on(types, handler, context); + }, + + // @method addEventParent(obj: Evented): this + // Adds an event parent - an `Evented` that will receive propagated events + addEventParent: function (obj) { + this._eventParents = this._eventParents || {}; + this._eventParents[L.stamp(obj)] = obj; + return this; + }, + + // @method removeEventParent(obj: Evented): this + // Removes an event parent, so it will stop receiving propagated events + removeEventParent: function (obj) { + if (this._eventParents) { + delete this._eventParents[L.stamp(obj)]; + } + return this; + }, + + _propagateEvent: function (e) { + for (var id in this._eventParents) { + this._eventParents[id].fire(e.type, L.extend({layer: e.target}, e), true); + } + } +}); + +var proto = L.Evented.prototype; + +// aliases; we should ditch those eventually + +// @method addEventListener(…): this +// Alias to [`on(…)`](#evented-on) +proto.addEventListener = proto.on; + +// @method removeEventListener(…): this +// Alias to [`off(…)`](#evented-off) + +// @method clearAllEventListeners(…): this +// Alias to [`off()`](#evented-off) +proto.removeEventListener = proto.clearAllEventListeners = proto.off; + +// @method addOneTimeEventListener(…): this +// Alias to [`once(…)`](#evented-once) +proto.addOneTimeEventListener = proto.once; + +// @method fireEvent(…): this +// Alias to [`fire(…)`](#evented-fire) +proto.fireEvent = proto.fire; + +// @method hasEventListeners(…): Boolean +// Alias to [`listens(…)`](#evented-listens) +proto.hasEventListeners = proto.listens; + +L.Mixin = {Events: proto}; + + + +/* + * @namespace Browser + * @aka L.Browser + * + * A namespace with static properties for browser/feature detection used by Leaflet internally. + * + * @example + * + * ```js + * if (L.Browser.ielt9) { + * alert('Upgrade your browser, dude!'); + * } + * ``` + */ + +(function () { + + var ua = navigator.userAgent.toLowerCase(), + doc = document.documentElement, + + ie = 'ActiveXObject' in window, + + webkit = ua.indexOf('webkit') !== -1, + phantomjs = ua.indexOf('phantom') !== -1, + android23 = ua.search('android [23]') !== -1, + chrome = ua.indexOf('chrome') !== -1, + gecko = ua.indexOf('gecko') !== -1 && !webkit && !window.opera && !ie, + + win = navigator.platform.indexOf('Win') === 0, + + mobile = typeof orientation !== 'undefined' || ua.indexOf('mobile') !== -1, + msPointer = !window.PointerEvent && window.MSPointerEvent, + pointer = window.PointerEvent || msPointer, + + ie3d = ie && ('transition' in doc.style), + webkit3d = ('WebKitCSSMatrix' in window) && ('m11' in new window.WebKitCSSMatrix()) && !android23, + gecko3d = 'MozPerspective' in doc.style, + opera12 = 'OTransition' in doc.style; + + + var touch = !window.L_NO_TOUCH && (pointer || 'ontouchstart' in window || + (window.DocumentTouch && document instanceof window.DocumentTouch)); + + L.Browser = { + + // @property ie: Boolean + // `true` for all Internet Explorer versions (not Edge). + ie: ie, + + // @property ielt9: Boolean + // `true` for Internet Explorer versions less than 9. + ielt9: ie && !document.addEventListener, + + // @property edge: Boolean + // `true` for the Edge web browser. + edge: 'msLaunchUri' in navigator && !('documentMode' in document), + + // @property webkit: Boolean + // `true` for webkit-based browsers like Chrome and Safari (including mobile versions). + webkit: webkit, + + // @property gecko: Boolean + // `true` for gecko-based browsers like Firefox. + gecko: gecko, + + // @property android: Boolean + // `true` for any browser running on an Android platform. + android: ua.indexOf('android') !== -1, + + // @property android23: Boolean + // `true` for browsers running on Android 2 or Android 3. + android23: android23, + + // @property chrome: Boolean + // `true` for the Chrome browser. + chrome: chrome, + + // @property safari: Boolean + // `true` for the Safari browser. + safari: !chrome && ua.indexOf('safari') !== -1, + + + // @property win: Boolean + // `true` when the browser is running in a Windows platform + win: win, + + + // @property ie3d: Boolean + // `true` for all Internet Explorer versions supporting CSS transforms. + ie3d: ie3d, + + // @property webkit3d: Boolean + // `true` for webkit-based browsers supporting CSS transforms. + webkit3d: webkit3d, + + // @property gecko3d: Boolean + // `true` for gecko-based browsers supporting CSS transforms. + gecko3d: gecko3d, + + // @property opera12: Boolean + // `true` for the Opera browser supporting CSS transforms (version 12 or later). + opera12: opera12, + + // @property any3d: Boolean + // `true` for all browsers supporting CSS transforms. + any3d: !window.L_DISABLE_3D && (ie3d || webkit3d || gecko3d) && !opera12 && !phantomjs, + + + // @property mobile: Boolean + // `true` for all browsers running in a mobile device. + mobile: mobile, + + // @property mobileWebkit: Boolean + // `true` for all webkit-based browsers in a mobile device. + mobileWebkit: mobile && webkit, + + // @property mobileWebkit3d: Boolean + // `true` for all webkit-based browsers in a mobile device supporting CSS transforms. + mobileWebkit3d: mobile && webkit3d, + + // @property mobileOpera: Boolean + // `true` for the Opera browser in a mobile device. + mobileOpera: mobile && window.opera, + + // @property mobileGecko: Boolean + // `true` for gecko-based browsers running in a mobile device. + mobileGecko: mobile && gecko, + + + // @property touch: Boolean + // `true` for all browsers supporting [touch events](https://developer.mozilla.org/docs/Web/API/Touch_events). + // This does not necessarily mean that the browser is running in a computer with + // a touchscreen, it only means that the browser is capable of understanding + // touch events. + touch: !!touch, + + // @property msPointer: Boolean + // `true` for browsers implementing the Microsoft touch events model (notably IE10). + msPointer: !!msPointer, + + // @property pointer: Boolean + // `true` for all browsers supporting [pointer events](https://msdn.microsoft.com/en-us/library/dn433244%28v=vs.85%29.aspx). + pointer: !!pointer, + + + // @property retina: Boolean + // `true` for browsers on a high-resolution "retina" screen. + retina: (window.devicePixelRatio || (window.screen.deviceXDPI / window.screen.logicalXDPI)) > 1 + }; + +}()); + + + +/* + * @class Point + * @aka L.Point + * + * Represents a point with `x` and `y` coordinates in pixels. + * + * @example + * + * ```js + * var point = L.point(200, 300); + * ``` + * + * All Leaflet methods and options that accept `Point` objects also accept them in a simple Array form (unless noted otherwise), so these lines are equivalent: + * + * ```js + * map.panBy([200, 300]); + * map.panBy(L.point(200, 300)); + * ``` + */ + +L.Point = function (x, y, round) { + // @property x: Number; The `x` coordinate of the point + this.x = (round ? Math.round(x) : x); + // @property y: Number; The `y` coordinate of the point + this.y = (round ? Math.round(y) : y); +}; + +L.Point.prototype = { + + // @method clone(): Point + // Returns a copy of the current point. + clone: function () { + return new L.Point(this.x, this.y); + }, + + // @method add(otherPoint: Point): Point + // Returns the result of addition of the current and the given points. + add: function (point) { + // non-destructive, returns a new point + return this.clone()._add(L.point(point)); + }, + + _add: function (point) { + // destructive, used directly for performance in situations where it's safe to modify existing point + this.x += point.x; + this.y += point.y; + return this; + }, + + // @method subtract(otherPoint: Point): Point + // Returns the result of subtraction of the given point from the current. + subtract: function (point) { + return this.clone()._subtract(L.point(point)); + }, + + _subtract: function (point) { + this.x -= point.x; + this.y -= point.y; + return this; + }, + + // @method divideBy(num: Number): Point + // Returns the result of division of the current point by the given number. + divideBy: function (num) { + return this.clone()._divideBy(num); + }, + + _divideBy: function (num) { + this.x /= num; + this.y /= num; + return this; + }, + + // @method multiplyBy(num: Number): Point + // Returns the result of multiplication of the current point by the given number. + multiplyBy: function (num) { + return this.clone()._multiplyBy(num); + }, + + _multiplyBy: function (num) { + this.x *= num; + this.y *= num; + return this; + }, + + // @method scaleBy(scale: Point): Point + // Multiply each coordinate of the current point by each coordinate of + // `scale`. In linear algebra terms, multiply the point by the + // [scaling matrix](https://en.wikipedia.org/wiki/Scaling_%28geometry%29#Matrix_representation) + // defined by `scale`. + scaleBy: function (point) { + return new L.Point(this.x * point.x, this.y * point.y); + }, + + // @method unscaleBy(scale: Point): Point + // Inverse of `scaleBy`. Divide each coordinate of the current point by + // each coordinate of `scale`. + unscaleBy: function (point) { + return new L.Point(this.x / point.x, this.y / point.y); + }, + + // @method round(): Point + // Returns a copy of the current point with rounded coordinates. + round: function () { + return this.clone()._round(); + }, + + _round: function () { + this.x = Math.round(this.x); + this.y = Math.round(this.y); + return this; + }, + + // @method floor(): Point + // Returns a copy of the current point with floored coordinates (rounded down). + floor: function () { + return this.clone()._floor(); + }, + + _floor: function () { + this.x = Math.floor(this.x); + this.y = Math.floor(this.y); + return this; + }, + + // @method ceil(): Point + // Returns a copy of the current point with ceiled coordinates (rounded up). + ceil: function () { + return this.clone()._ceil(); + }, + + _ceil: function () { + this.x = Math.ceil(this.x); + this.y = Math.ceil(this.y); + return this; + }, + + // @method distanceTo(otherPoint: Point): Number + // Returns the cartesian distance between the current and the given points. + distanceTo: function (point) { + point = L.point(point); + + var x = point.x - this.x, + y = point.y - this.y; + + return Math.sqrt(x * x + y * y); + }, + + // @method equals(otherPoint: Point): Boolean + // Returns `true` if the given point has the same coordinates. + equals: function (point) { + point = L.point(point); + + return point.x === this.x && + point.y === this.y; + }, + + // @method contains(otherPoint: Point): Boolean + // Returns `true` if both coordinates of the given point are less than the corresponding current point coordinates (in absolute values). + contains: function (point) { + point = L.point(point); + + return Math.abs(point.x) <= Math.abs(this.x) && + Math.abs(point.y) <= Math.abs(this.y); + }, + + // @method toString(): String + // Returns a string representation of the point for debugging purposes. + toString: function () { + return 'Point(' + + L.Util.formatNum(this.x) + ', ' + + L.Util.formatNum(this.y) + ')'; + } +}; + +// @factory L.point(x: Number, y: Number, round?: Boolean) +// Creates a Point object with the given `x` and `y` coordinates. If optional `round` is set to true, rounds the `x` and `y` values. + +// @alternative +// @factory L.point(coords: Number[]) +// Expects an array of the form `[x, y]` instead. + +// @alternative +// @factory L.point(coords: Object) +// Expects a plain object of the form `{x: Number, y: Number}` instead. +L.point = function (x, y, round) { + if (x instanceof L.Point) { + return x; + } + if (L.Util.isArray(x)) { + return new L.Point(x[0], x[1]); + } + if (x === undefined || x === null) { + return x; + } + if (typeof x === 'object' && 'x' in x && 'y' in x) { + return new L.Point(x.x, x.y); + } + return new L.Point(x, y, round); +}; + + + +/* + * @class Bounds + * @aka L.Bounds + * + * Represents a rectangular area in pixel coordinates. + * + * @example + * + * ```js + * var p1 = L.point(10, 10), + * p2 = L.point(40, 60), + * bounds = L.bounds(p1, p2); + * ``` + * + * All Leaflet methods that accept `Bounds` objects also accept them in a simple Array form (unless noted otherwise), so the bounds example above can be passed like this: + * + * ```js + * otherBounds.intersects([[10, 10], [40, 60]]); + * ``` + */ + +L.Bounds = function (a, b) { + if (!a) { return; } + + var points = b ? [a, b] : a; + + for (var i = 0, len = points.length; i < len; i++) { + this.extend(points[i]); + } +}; + +L.Bounds.prototype = { + // @method extend(point: Point): this + // Extends the bounds to contain the given point. + extend: function (point) { // (Point) + point = L.point(point); + + // @property min: Point + // The top left corner of the rectangle. + // @property max: Point + // The bottom right corner of the rectangle. + if (!this.min && !this.max) { + this.min = point.clone(); + this.max = point.clone(); + } else { + this.min.x = Math.min(point.x, this.min.x); + this.max.x = Math.max(point.x, this.max.x); + this.min.y = Math.min(point.y, this.min.y); + this.max.y = Math.max(point.y, this.max.y); + } + return this; + }, + + // @method getCenter(round?: Boolean): Point + // Returns the center point of the bounds. + getCenter: function (round) { + return new L.Point( + (this.min.x + this.max.x) / 2, + (this.min.y + this.max.y) / 2, round); + }, + + // @method getBottomLeft(): Point + // Returns the bottom-left point of the bounds. + getBottomLeft: function () { + return new L.Point(this.min.x, this.max.y); + }, + + // @method getTopRight(): Point + // Returns the top-right point of the bounds. + getTopRight: function () { // -> Point + return new L.Point(this.max.x, this.min.y); + }, + + // @method getSize(): Point + // Returns the size of the given bounds + getSize: function () { + return this.max.subtract(this.min); + }, + + // @method contains(otherBounds: Bounds): Boolean + // Returns `true` if the rectangle contains the given one. + // @alternative + // @method contains(point: Point): Boolean + // Returns `true` if the rectangle contains the given point. + contains: function (obj) { + var min, max; + + if (typeof obj[0] === 'number' || obj instanceof L.Point) { + obj = L.point(obj); + } else { + obj = L.bounds(obj); + } + + if (obj instanceof L.Bounds) { + min = obj.min; + max = obj.max; + } else { + min = max = obj; + } + + return (min.x >= this.min.x) && + (max.x <= this.max.x) && + (min.y >= this.min.y) && + (max.y <= this.max.y); + }, + + // @method intersects(otherBounds: Bounds): Boolean + // Returns `true` if the rectangle intersects the given bounds. Two bounds + // intersect if they have at least one point in common. + intersects: function (bounds) { // (Bounds) -> Boolean + bounds = L.bounds(bounds); + + var min = this.min, + max = this.max, + min2 = bounds.min, + max2 = bounds.max, + xIntersects = (max2.x >= min.x) && (min2.x <= max.x), + yIntersects = (max2.y >= min.y) && (min2.y <= max.y); + + return xIntersects && yIntersects; + }, + + // @method overlaps(otherBounds: Bounds): Boolean + // Returns `true` if the rectangle overlaps the given bounds. Two bounds + // overlap if their intersection is an area. + overlaps: function (bounds) { // (Bounds) -> Boolean + bounds = L.bounds(bounds); + + var min = this.min, + max = this.max, + min2 = bounds.min, + max2 = bounds.max, + xOverlaps = (max2.x > min.x) && (min2.x < max.x), + yOverlaps = (max2.y > min.y) && (min2.y < max.y); + + return xOverlaps && yOverlaps; + }, + + isValid: function () { + return !!(this.min && this.max); + } +}; + + +// @factory L.bounds(topLeft: Point, bottomRight: Point) +// Creates a Bounds object from two coordinates (usually top-left and bottom-right corners). +// @alternative +// @factory L.bounds(points: Point[]) +// Creates a Bounds object from the points it contains +L.bounds = function (a, b) { + if (!a || a instanceof L.Bounds) { + return a; + } + return new L.Bounds(a, b); +}; + + + +/* + * @class Transformation + * @aka L.Transformation + * + * Represents an affine transformation: a set of coefficients `a`, `b`, `c`, `d` + * for transforming a point of a form `(x, y)` into `(a*x + b, c*y + d)` and doing + * the reverse. Used by Leaflet in its projections code. + * + * @example + * + * ```js + * var transformation = new L.Transformation(2, 5, -1, 10), + * p = L.point(1, 2), + * p2 = transformation.transform(p), // L.point(7, 8) + * p3 = transformation.untransform(p2); // L.point(1, 2) + * ``` + */ + + +// factory new L.Transformation(a: Number, b: Number, c: Number, d: Number) +// Creates a `Transformation` object with the given coefficients. +L.Transformation = function (a, b, c, d) { + this._a = a; + this._b = b; + this._c = c; + this._d = d; +}; + +L.Transformation.prototype = { + // @method transform(point: Point, scale?: Number): Point + // Returns a transformed point, optionally multiplied by the given scale. + // Only accepts actual `L.Point` instances, not arrays. + transform: function (point, scale) { // (Point, Number) -> Point + return this._transform(point.clone(), scale); + }, + + // destructive transform (faster) + _transform: function (point, scale) { + scale = scale || 1; + point.x = scale * (this._a * point.x + this._b); + point.y = scale * (this._c * point.y + this._d); + return point; + }, + + // @method untransform(point: Point, scale?: Number): Point + // Returns the reverse transformation of the given point, optionally divided + // by the given scale. Only accepts actual `L.Point` instances, not arrays. + untransform: function (point, scale) { + scale = scale || 1; + return new L.Point( + (point.x / scale - this._b) / this._a, + (point.y / scale - this._d) / this._c); + } +}; + + + +/* + * @namespace DomUtil + * + * Utility functions to work with the [DOM](https://developer.mozilla.org/docs/Web/API/Document_Object_Model) + * tree, used by Leaflet internally. + * + * Most functions expecting or returning a `HTMLElement` also work for + * SVG elements. The only difference is that classes refer to CSS classes + * in HTML and SVG classes in SVG. + */ + +L.DomUtil = { + + // @function get(id: String|HTMLElement): HTMLElement + // Returns an element given its DOM id, or returns the element itself + // if it was passed directly. + get: function (id) { + return typeof id === 'string' ? document.getElementById(id) : id; + }, + + // @function getStyle(el: HTMLElement, styleAttrib: String): String + // Returns the value for a certain style attribute on an element, + // including computed values or values set through CSS. + getStyle: function (el, style) { + + var value = el.style[style] || (el.currentStyle && el.currentStyle[style]); + + if ((!value || value === 'auto') && document.defaultView) { + var css = document.defaultView.getComputedStyle(el, null); + value = css ? css[style] : null; + } + + return value === 'auto' ? null : value; + }, + + // @function create(tagName: String, className?: String, container?: HTMLElement): HTMLElement + // Creates an HTML element with `tagName`, sets its class to `className`, and optionally appends it to `container` element. + create: function (tagName, className, container) { + + var el = document.createElement(tagName); + el.className = className || ''; + + if (container) { + container.appendChild(el); + } + + return el; + }, + + // @function remove(el: HTMLElement) + // Removes `el` from its parent element + remove: function (el) { + var parent = el.parentNode; + if (parent) { + parent.removeChild(el); + } + }, + + // @function empty(el: HTMLElement) + // Removes all of `el`'s children elements from `el` + empty: function (el) { + while (el.firstChild) { + el.removeChild(el.firstChild); + } + }, + + // @function toFront(el: HTMLElement) + // Makes `el` the last children of its parent, so it renders in front of the other children. + toFront: function (el) { + el.parentNode.appendChild(el); + }, + + // @function toBack(el: HTMLElement) + // Makes `el` the first children of its parent, so it renders back from the other children. + toBack: function (el) { + var parent = el.parentNode; + parent.insertBefore(el, parent.firstChild); + }, + + // @function hasClass(el: HTMLElement, name: String): Boolean + // Returns `true` if the element's class attribute contains `name`. + hasClass: function (el, name) { + if (el.classList !== undefined) { + return el.classList.contains(name); + } + var className = L.DomUtil.getClass(el); + return className.length > 0 && new RegExp('(^|\\s)' + name + '(\\s|$)').test(className); + }, + + // @function addClass(el: HTMLElement, name: String) + // Adds `name` to the element's class attribute. + addClass: function (el, name) { + if (el.classList !== undefined) { + var classes = L.Util.splitWords(name); + for (var i = 0, len = classes.length; i < len; i++) { + el.classList.add(classes[i]); + } + } else if (!L.DomUtil.hasClass(el, name)) { + var className = L.DomUtil.getClass(el); + L.DomUtil.setClass(el, (className ? className + ' ' : '') + name); + } + }, + + // @function removeClass(el: HTMLElement, name: String) + // Removes `name` from the element's class attribute. + removeClass: function (el, name) { + if (el.classList !== undefined) { + el.classList.remove(name); + } else { + L.DomUtil.setClass(el, L.Util.trim((' ' + L.DomUtil.getClass(el) + ' ').replace(' ' + name + ' ', ' '))); + } + }, + + // @function setClass(el: HTMLElement, name: String) + // Sets the element's class. + setClass: function (el, name) { + if (el.className.baseVal === undefined) { + el.className = name; + } else { + // in case of SVG element + el.className.baseVal = name; + } + }, + + // @function getClass(el: HTMLElement): String + // Returns the element's class. + getClass: function (el) { + return el.className.baseVal === undefined ? el.className : el.className.baseVal; + }, + + // @function setOpacity(el: HTMLElement, opacity: Number) + // Set the opacity of an element (including old IE support). + // `opacity` must be a number from `0` to `1`. + setOpacity: function (el, value) { + + if ('opacity' in el.style) { + el.style.opacity = value; + + } else if ('filter' in el.style) { + L.DomUtil._setOpacityIE(el, value); + } + }, + + _setOpacityIE: function (el, value) { + var filter = false, + filterName = 'DXImageTransform.Microsoft.Alpha'; + + // filters collection throws an error if we try to retrieve a filter that doesn't exist + try { + filter = el.filters.item(filterName); + } catch (e) { + // don't set opacity to 1 if we haven't already set an opacity, + // it isn't needed and breaks transparent pngs. + if (value === 1) { return; } + } + + value = Math.round(value * 100); + + if (filter) { + filter.Enabled = (value !== 100); + filter.Opacity = value; + } else { + el.style.filter += ' progid:' + filterName + '(opacity=' + value + ')'; + } + }, + + // @function testProp(props: String[]): String|false + // Goes through the array of style names and returns the first name + // that is a valid style name for an element. If no such name is found, + // it returns false. Useful for vendor-prefixed styles like `transform`. + testProp: function (props) { + + var style = document.documentElement.style; + + for (var i = 0; i < props.length; i++) { + if (props[i] in style) { + return props[i]; + } + } + return false; + }, + + // @function setTransform(el: HTMLElement, offset: Point, scale?: Number) + // Resets the 3D CSS transform of `el` so it is translated by `offset` pixels + // and optionally scaled by `scale`. Does not have an effect if the + // browser doesn't support 3D CSS transforms. + setTransform: function (el, offset, scale) { + var pos = offset || new L.Point(0, 0); + + el.style[L.DomUtil.TRANSFORM] = + (L.Browser.ie3d ? + 'translate(' + pos.x + 'px,' + pos.y + 'px)' : + 'translate3d(' + pos.x + 'px,' + pos.y + 'px,0)') + + (scale ? ' scale(' + scale + ')' : ''); + }, + + // @function setPosition(el: HTMLElement, position: Point) + // Sets the position of `el` to coordinates specified by `position`, + // using CSS translate or top/left positioning depending on the browser + // (used by Leaflet internally to position its layers). + setPosition: function (el, point) { // (HTMLElement, Point[, Boolean]) + + /*eslint-disable */ + el._leaflet_pos = point; + /*eslint-enable */ + + if (L.Browser.any3d) { + L.DomUtil.setTransform(el, point); + } else { + el.style.left = point.x + 'px'; + el.style.top = point.y + 'px'; + } + }, + + // @function getPosition(el: HTMLElement): Point + // Returns the coordinates of an element previously positioned with setPosition. + getPosition: function (el) { + // this method is only used for elements previously positioned using setPosition, + // so it's safe to cache the position for performance + + return el._leaflet_pos || new L.Point(0, 0); + } +}; + + +(function () { + // prefix style property names + + // @property TRANSFORM: String + // Vendor-prefixed fransform style name (e.g. `'webkitTransform'` for WebKit). + L.DomUtil.TRANSFORM = L.DomUtil.testProp( + ['transform', 'WebkitTransform', 'OTransform', 'MozTransform', 'msTransform']); + + + // webkitTransition comes first because some browser versions that drop vendor prefix don't do + // the same for the transitionend event, in particular the Android 4.1 stock browser + + // @property TRANSITION: String + // Vendor-prefixed transform style name. + var transition = L.DomUtil.TRANSITION = L.DomUtil.testProp( + ['webkitTransition', 'transition', 'OTransition', 'MozTransition', 'msTransition']); + + L.DomUtil.TRANSITION_END = + transition === 'webkitTransition' || transition === 'OTransition' ? transition + 'End' : 'transitionend'; + + // @function disableTextSelection() + // Prevents the user from generating `selectstart` DOM events, usually generated + // when the user drags the mouse through a page with text. Used internally + // by Leaflet to override the behaviour of any click-and-drag interaction on + // the map. Affects drag interactions on the whole document. + + // @function enableTextSelection() + // Cancels the effects of a previous [`L.DomUtil.disableTextSelection`](#domutil-disabletextselection). + if ('onselectstart' in document) { + L.DomUtil.disableTextSelection = function () { + L.DomEvent.on(window, 'selectstart', L.DomEvent.preventDefault); + }; + L.DomUtil.enableTextSelection = function () { + L.DomEvent.off(window, 'selectstart', L.DomEvent.preventDefault); + }; + + } else { + var userSelectProperty = L.DomUtil.testProp( + ['userSelect', 'WebkitUserSelect', 'OUserSelect', 'MozUserSelect', 'msUserSelect']); + + L.DomUtil.disableTextSelection = function () { + if (userSelectProperty) { + var style = document.documentElement.style; + this._userSelect = style[userSelectProperty]; + style[userSelectProperty] = 'none'; + } + }; + L.DomUtil.enableTextSelection = function () { + if (userSelectProperty) { + document.documentElement.style[userSelectProperty] = this._userSelect; + delete this._userSelect; + } + }; + } + + // @function disableImageDrag() + // As [`L.DomUtil.disableTextSelection`](#domutil-disabletextselection), but + // for `dragstart` DOM events, usually generated when the user drags an image. + L.DomUtil.disableImageDrag = function () { + L.DomEvent.on(window, 'dragstart', L.DomEvent.preventDefault); + }; + + // @function enableImageDrag() + // Cancels the effects of a previous [`L.DomUtil.disableImageDrag`](#domutil-disabletextselection). + L.DomUtil.enableImageDrag = function () { + L.DomEvent.off(window, 'dragstart', L.DomEvent.preventDefault); + }; + + // @function preventOutline(el: HTMLElement) + // Makes the [outline](https://developer.mozilla.org/docs/Web/CSS/outline) + // of the element `el` invisible. Used internally by Leaflet to prevent + // focusable elements from displaying an outline when the user performs a + // drag interaction on them. + L.DomUtil.preventOutline = function (element) { + while (element.tabIndex === -1) { + element = element.parentNode; + } + if (!element || !element.style) { return; } + L.DomUtil.restoreOutline(); + this._outlineElement = element; + this._outlineStyle = element.style.outline; + element.style.outline = 'none'; + L.DomEvent.on(window, 'keydown', L.DomUtil.restoreOutline, this); + }; + + // @function restoreOutline() + // Cancels the effects of a previous [`L.DomUtil.preventOutline`](). + L.DomUtil.restoreOutline = function () { + if (!this._outlineElement) { return; } + this._outlineElement.style.outline = this._outlineStyle; + delete this._outlineElement; + delete this._outlineStyle; + L.DomEvent.off(window, 'keydown', L.DomUtil.restoreOutline, this); + }; +})(); + + + +/* @class LatLng + * @aka L.LatLng + * + * Represents a geographical point with a certain latitude and longitude. + * + * @example + * + * ``` + * var latlng = L.latLng(50.5, 30.5); + * ``` + * + * All Leaflet methods that accept LatLng objects also accept them in a simple Array form and simple object form (unless noted otherwise), so these lines are equivalent: + * + * ``` + * map.panTo([50, 30]); + * map.panTo({lon: 30, lat: 50}); + * map.panTo({lat: 50, lng: 30}); + * map.panTo(L.latLng(50, 30)); + * ``` + */ + +L.LatLng = function (lat, lng, alt) { + if (isNaN(lat) || isNaN(lng)) { + throw new Error('Invalid LatLng object: (' + lat + ', ' + lng + ')'); + } + + // @property lat: Number + // Latitude in degrees + this.lat = +lat; + + // @property lng: Number + // Longitude in degrees + this.lng = +lng; + + // @property alt: Number + // Altitude in meters (optional) + if (alt !== undefined) { + this.alt = +alt; + } +}; + +L.LatLng.prototype = { + // @method equals(otherLatLng: LatLng, maxMargin?: Number): Boolean + // Returns `true` if the given `LatLng` point is at the same position (within a small margin of error). The margin of error can be overriden by setting `maxMargin` to a small number. + equals: function (obj, maxMargin) { + if (!obj) { return false; } + + obj = L.latLng(obj); + + var margin = Math.max( + Math.abs(this.lat - obj.lat), + Math.abs(this.lng - obj.lng)); + + return margin <= (maxMargin === undefined ? 1.0E-9 : maxMargin); + }, + + // @method toString(): String + // Returns a string representation of the point (for debugging purposes). + toString: function (precision) { + return 'LatLng(' + + L.Util.formatNum(this.lat, precision) + ', ' + + L.Util.formatNum(this.lng, precision) + ')'; + }, + + // @method distanceTo(otherLatLng: LatLng): Number + // Returns the distance (in meters) to the given `LatLng` calculated using the [Haversine formula](http://en.wikipedia.org/wiki/Haversine_formula). + distanceTo: function (other) { + return L.CRS.Earth.distance(this, L.latLng(other)); + }, + + // @method wrap(): LatLng + // Returns a new `LatLng` object with the longitude wrapped so it's always between -180 and +180 degrees. + wrap: function () { + return L.CRS.Earth.wrapLatLng(this); + }, + + // @method toBounds(sizeInMeters: Number): LatLngBounds + // Returns a new `LatLngBounds` object in which each boundary is `sizeInMeters/2` meters apart from the `LatLng`. + toBounds: function (sizeInMeters) { + var latAccuracy = 180 * sizeInMeters / 40075017, + lngAccuracy = latAccuracy / Math.cos((Math.PI / 180) * this.lat); + + return L.latLngBounds( + [this.lat - latAccuracy, this.lng - lngAccuracy], + [this.lat + latAccuracy, this.lng + lngAccuracy]); + }, + + clone: function () { + return new L.LatLng(this.lat, this.lng, this.alt); + } +}; + + + +// @factory L.latLng(latitude: Number, longitude: Number, altitude?: Number): LatLng +// Creates an object representing a geographical point with the given latitude and longitude (and optionally altitude). + +// @alternative +// @factory L.latLng(coords: Array): LatLng +// Expects an array of the form `[Number, Number]` or `[Number, Number, Number]` instead. + +// @alternative +// @factory L.latLng(coords: Object): LatLng +// Expects an plain object of the form `{lat: Number, lng: Number}` or `{lat: Number, lng: Number, alt: Number}` instead. + +L.latLng = function (a, b, c) { + if (a instanceof L.LatLng) { + return a; + } + if (L.Util.isArray(a) && typeof a[0] !== 'object') { + if (a.length === 3) { + return new L.LatLng(a[0], a[1], a[2]); + } + if (a.length === 2) { + return new L.LatLng(a[0], a[1]); + } + return null; + } + if (a === undefined || a === null) { + return a; + } + if (typeof a === 'object' && 'lat' in a) { + return new L.LatLng(a.lat, 'lng' in a ? a.lng : a.lon, a.alt); + } + if (b === undefined) { + return null; + } + return new L.LatLng(a, b, c); +}; + + + +/* + * @class LatLngBounds + * @aka L.LatLngBounds + * + * Represents a rectangular geographical area on a map. + * + * @example + * + * ```js + * var corner1 = L.latLng(40.712, -74.227), + * corner2 = L.latLng(40.774, -74.125), + * bounds = L.latLngBounds(corner1, corner2); + * ``` + * + * All Leaflet methods that accept LatLngBounds objects also accept them in a simple Array form (unless noted otherwise), so the bounds example above can be passed like this: + * + * ```js + * map.fitBounds([ + * [40.712, -74.227], + * [40.774, -74.125] + * ]); + * ``` + * + * Caution: if the area crosses the antimeridian (often confused with the International Date Line), you must specify corners _outside_ the [-180, 180] degrees longitude range. + */ + +L.LatLngBounds = function (corner1, corner2) { // (LatLng, LatLng) or (LatLng[]) + if (!corner1) { return; } + + var latlngs = corner2 ? [corner1, corner2] : corner1; + + for (var i = 0, len = latlngs.length; i < len; i++) { + this.extend(latlngs[i]); + } +}; + +L.LatLngBounds.prototype = { + + // @method extend(latlng: LatLng): this + // Extend the bounds to contain the given point + + // @alternative + // @method extend(otherBounds: LatLngBounds): this + // Extend the bounds to contain the given bounds + extend: function (obj) { + var sw = this._southWest, + ne = this._northEast, + sw2, ne2; + + if (obj instanceof L.LatLng) { + sw2 = obj; + ne2 = obj; + + } else if (obj instanceof L.LatLngBounds) { + sw2 = obj._southWest; + ne2 = obj._northEast; + + if (!sw2 || !ne2) { return this; } + + } else { + return obj ? this.extend(L.latLng(obj) || L.latLngBounds(obj)) : this; + } + + if (!sw && !ne) { + this._southWest = new L.LatLng(sw2.lat, sw2.lng); + this._northEast = new L.LatLng(ne2.lat, ne2.lng); + } else { + sw.lat = Math.min(sw2.lat, sw.lat); + sw.lng = Math.min(sw2.lng, sw.lng); + ne.lat = Math.max(ne2.lat, ne.lat); + ne.lng = Math.max(ne2.lng, ne.lng); + } + + return this; + }, + + // @method pad(bufferRatio: Number): LatLngBounds + // Returns bigger bounds created by extending the current bounds by a given percentage in each direction. + pad: function (bufferRatio) { + var sw = this._southWest, + ne = this._northEast, + heightBuffer = Math.abs(sw.lat - ne.lat) * bufferRatio, + widthBuffer = Math.abs(sw.lng - ne.lng) * bufferRatio; + + return new L.LatLngBounds( + new L.LatLng(sw.lat - heightBuffer, sw.lng - widthBuffer), + new L.LatLng(ne.lat + heightBuffer, ne.lng + widthBuffer)); + }, + + // @method getCenter(): LatLng + // Returns the center point of the bounds. + getCenter: function () { + return new L.LatLng( + (this._southWest.lat + this._northEast.lat) / 2, + (this._southWest.lng + this._northEast.lng) / 2); + }, + + // @method getSouthWest(): LatLng + // Returns the south-west point of the bounds. + getSouthWest: function () { + return this._southWest; + }, + + // @method getNorthEast(): LatLng + // Returns the north-east point of the bounds. + getNorthEast: function () { + return this._northEast; + }, + + // @method getNorthWest(): LatLng + // Returns the north-west point of the bounds. + getNorthWest: function () { + return new L.LatLng(this.getNorth(), this.getWest()); + }, + + // @method getSouthEast(): LatLng + // Returns the south-east point of the bounds. + getSouthEast: function () { + return new L.LatLng(this.getSouth(), this.getEast()); + }, + + // @method getWest(): Number + // Returns the west longitude of the bounds + getWest: function () { + return this._southWest.lng; + }, + + // @method getSouth(): Number + // Returns the south latitude of the bounds + getSouth: function () { + return this._southWest.lat; + }, + + // @method getEast(): Number + // Returns the east longitude of the bounds + getEast: function () { + return this._northEast.lng; + }, + + // @method getNorth(): Number + // Returns the north latitude of the bounds + getNorth: function () { + return this._northEast.lat; + }, + + // @method contains(otherBounds: LatLngBounds): Boolean + // Returns `true` if the rectangle contains the given one. + + // @alternative + // @method contains (latlng: LatLng): Boolean + // Returns `true` if the rectangle contains the given point. + contains: function (obj) { // (LatLngBounds) or (LatLng) -> Boolean + if (typeof obj[0] === 'number' || obj instanceof L.LatLng || 'lat' in obj) { + obj = L.latLng(obj); + } else { + obj = L.latLngBounds(obj); + } + + var sw = this._southWest, + ne = this._northEast, + sw2, ne2; + + if (obj instanceof L.LatLngBounds) { + sw2 = obj.getSouthWest(); + ne2 = obj.getNorthEast(); + } else { + sw2 = ne2 = obj; + } + + return (sw2.lat >= sw.lat) && (ne2.lat <= ne.lat) && + (sw2.lng >= sw.lng) && (ne2.lng <= ne.lng); + }, + + // @method intersects(otherBounds: LatLngBounds): Boolean + // Returns `true` if the rectangle intersects the given bounds. Two bounds intersect if they have at least one point in common. + intersects: function (bounds) { + bounds = L.latLngBounds(bounds); + + var sw = this._southWest, + ne = this._northEast, + sw2 = bounds.getSouthWest(), + ne2 = bounds.getNorthEast(), + + latIntersects = (ne2.lat >= sw.lat) && (sw2.lat <= ne.lat), + lngIntersects = (ne2.lng >= sw.lng) && (sw2.lng <= ne.lng); + + return latIntersects && lngIntersects; + }, + + // @method overlaps(otherBounds: Bounds): Boolean + // Returns `true` if the rectangle overlaps the given bounds. Two bounds overlap if their intersection is an area. + overlaps: function (bounds) { + bounds = L.latLngBounds(bounds); + + var sw = this._southWest, + ne = this._northEast, + sw2 = bounds.getSouthWest(), + ne2 = bounds.getNorthEast(), + + latOverlaps = (ne2.lat > sw.lat) && (sw2.lat < ne.lat), + lngOverlaps = (ne2.lng > sw.lng) && (sw2.lng < ne.lng); + + return latOverlaps && lngOverlaps; + }, + + // @method toBBoxString(): String + // Returns a string with bounding box coordinates in a 'southwest_lng,southwest_lat,northeast_lng,northeast_lat' format. Useful for sending requests to web services that return geo data. + toBBoxString: function () { + return [this.getWest(), this.getSouth(), this.getEast(), this.getNorth()].join(','); + }, + + // @method equals(otherBounds: LatLngBounds): Boolean + // Returns `true` if the rectangle is equivalent (within a small margin of error) to the given bounds. + equals: function (bounds) { + if (!bounds) { return false; } + + bounds = L.latLngBounds(bounds); + + return this._southWest.equals(bounds.getSouthWest()) && + this._northEast.equals(bounds.getNorthEast()); + }, + + // @method isValid(): Boolean + // Returns `true` if the bounds are properly initialized. + isValid: function () { + return !!(this._southWest && this._northEast); + } +}; + +// TODO International date line? + +// @factory L.latLngBounds(corner1: LatLng, corner2: LatLng) +// Creates a `LatLngBounds` object by defining two diagonally opposite corners of the rectangle. + +// @alternative +// @factory L.latLngBounds(latlngs: LatLng[]) +// Creates a `LatLngBounds` object defined by the geographical points it contains. Very useful for zooming the map to fit a particular set of locations with [`fitBounds`](#map-fitbounds). +L.latLngBounds = function (a, b) { + if (a instanceof L.LatLngBounds) { + return a; + } + return new L.LatLngBounds(a, b); +}; + + + +/* + * @namespace Projection + * @section + * Leaflet comes with a set of already defined Projections out of the box: + * + * @projection L.Projection.LonLat + * + * Equirectangular, or Plate Carree projection — the most simple projection, + * mostly used by GIS enthusiasts. Directly maps `x` as longitude, and `y` as + * latitude. Also suitable for flat worlds, e.g. game maps. Used by the + * `EPSG:3395` and `Simple` CRS. + */ + +L.Projection = {}; + +L.Projection.LonLat = { + project: function (latlng) { + return new L.Point(latlng.lng, latlng.lat); + }, + + unproject: function (point) { + return new L.LatLng(point.y, point.x); + }, + + bounds: L.bounds([-180, -90], [180, 90]) +}; + + + +/* + * @namespace Projection + * @projection L.Projection.SphericalMercator + * + * Spherical Mercator projection — the most common projection for online maps, + * used by almost all free and commercial tile providers. Assumes that Earth is + * a sphere. Used by the `EPSG:3857` CRS. + */ + +L.Projection.SphericalMercator = { + + R: 6378137, + MAX_LATITUDE: 85.0511287798, + + project: function (latlng) { + var d = Math.PI / 180, + max = this.MAX_LATITUDE, + lat = Math.max(Math.min(max, latlng.lat), -max), + sin = Math.sin(lat * d); + + return new L.Point( + this.R * latlng.lng * d, + this.R * Math.log((1 + sin) / (1 - sin)) / 2); + }, + + unproject: function (point) { + var d = 180 / Math.PI; + + return new L.LatLng( + (2 * Math.atan(Math.exp(point.y / this.R)) - (Math.PI / 2)) * d, + point.x * d / this.R); + }, + + bounds: (function () { + var d = 6378137 * Math.PI; + return L.bounds([-d, -d], [d, d]); + })() +}; + + + +/* + * @class CRS + * @aka L.CRS + * Abstract class that defines coordinate reference systems for projecting + * geographical points into pixel (screen) coordinates and back (and to + * coordinates in other units for [WMS](https://en.wikipedia.org/wiki/Web_Map_Service) services). See + * [spatial reference system](http://en.wikipedia.org/wiki/Coordinate_reference_system). + * + * Leaflet defines the most usual CRSs by default. If you want to use a + * CRS not defined by default, take a look at the + * [Proj4Leaflet](https://github.com/kartena/Proj4Leaflet) plugin. + */ + +L.CRS = { + // @method latLngToPoint(latlng: LatLng, zoom: Number): Point + // Projects geographical coordinates into pixel coordinates for a given zoom. + latLngToPoint: function (latlng, zoom) { + var projectedPoint = this.projection.project(latlng), + scale = this.scale(zoom); + + return this.transformation._transform(projectedPoint, scale); + }, + + // @method pointToLatLng(point: Point, zoom: Number): LatLng + // The inverse of `latLngToPoint`. Projects pixel coordinates on a given + // zoom into geographical coordinates. + pointToLatLng: function (point, zoom) { + var scale = this.scale(zoom), + untransformedPoint = this.transformation.untransform(point, scale); + + return this.projection.unproject(untransformedPoint); + }, + + // @method project(latlng: LatLng): Point + // Projects geographical coordinates into coordinates in units accepted for + // this CRS (e.g. meters for EPSG:3857, for passing it to WMS services). + project: function (latlng) { + return this.projection.project(latlng); + }, + + // @method unproject(point: Point): LatLng + // Given a projected coordinate returns the corresponding LatLng. + // The inverse of `project`. + unproject: function (point) { + return this.projection.unproject(point); + }, + + // @method scale(zoom: Number): Number + // Returns the scale used when transforming projected coordinates into + // pixel coordinates for a particular zoom. For example, it returns + // `256 * 2^zoom` for Mercator-based CRS. + scale: function (zoom) { + return 256 * Math.pow(2, zoom); + }, + + // @method zoom(scale: Number): Number + // Inverse of `scale()`, returns the zoom level corresponding to a scale + // factor of `scale`. + zoom: function (scale) { + return Math.log(scale / 256) / Math.LN2; + }, + + // @method getProjectedBounds(zoom: Number): Bounds + // Returns the projection's bounds scaled and transformed for the provided `zoom`. + getProjectedBounds: function (zoom) { + if (this.infinite) { return null; } + + var b = this.projection.bounds, + s = this.scale(zoom), + min = this.transformation.transform(b.min, s), + max = this.transformation.transform(b.max, s); + + return L.bounds(min, max); + }, + + // @method distance(latlng1: LatLng, latlng2: LatLng): Number + // Returns the distance between two geographical coordinates. + + // @property code: String + // Standard code name of the CRS passed into WMS services (e.g. `'EPSG:3857'`) + // + // @property wrapLng: Number[] + // An array of two numbers defining whether the longitude (horizontal) coordinate + // axis wraps around a given range and how. Defaults to `[-180, 180]` in most + // geographical CRSs. If `undefined`, the longitude axis does not wrap around. + // + // @property wrapLat: Number[] + // Like `wrapLng`, but for the latitude (vertical) axis. + + // wrapLng: [min, max], + // wrapLat: [min, max], + + // @property infinite: Boolean + // If true, the coordinate space will be unbounded (infinite in both axes) + infinite: false, + + // @method wrapLatLng(latlng: LatLng): LatLng + // Returns a `LatLng` where lat and lng has been wrapped according to the + // CRS's `wrapLat` and `wrapLng` properties, if they are outside the CRS's bounds. + // Only accepts actual `L.LatLng` instances, not arrays. + wrapLatLng: function (latlng) { + var lng = this.wrapLng ? L.Util.wrapNum(latlng.lng, this.wrapLng, true) : latlng.lng, + lat = this.wrapLat ? L.Util.wrapNum(latlng.lat, this.wrapLat, true) : latlng.lat, + alt = latlng.alt; + + return L.latLng(lat, lng, alt); + }, + + // @method wrapLatLngBounds(bounds: LatLngBounds): LatLngBounds + // Returns a `LatLngBounds` with the same size as the given one, ensuring + // that its center is within the CRS's bounds. + // Only accepts actual `L.LatLngBounds` instances, not arrays. + wrapLatLngBounds: function (bounds) { + var center = bounds.getCenter(), + newCenter = this.wrapLatLng(center), + latShift = center.lat - newCenter.lat, + lngShift = center.lng - newCenter.lng; + + if (latShift === 0 && lngShift === 0) { + return bounds; + } + + var sw = bounds.getSouthWest(), + ne = bounds.getNorthEast(), + newSw = L.latLng({lat: sw.lat - latShift, lng: sw.lng - lngShift}), + newNe = L.latLng({lat: ne.lat - latShift, lng: ne.lng - lngShift}); + + return new L.LatLngBounds(newSw, newNe); + } +}; + + + +/* + * @namespace CRS + * @crs L.CRS.Simple + * + * A simple CRS that maps longitude and latitude into `x` and `y` directly. + * May be used for maps of flat surfaces (e.g. game maps). Note that the `y` + * axis should still be inverted (going from bottom to top). `distance()` returns + * simple euclidean distance. + */ + +L.CRS.Simple = L.extend({}, L.CRS, { + projection: L.Projection.LonLat, + transformation: new L.Transformation(1, 0, -1, 0), + + scale: function (zoom) { + return Math.pow(2, zoom); + }, + + zoom: function (scale) { + return Math.log(scale) / Math.LN2; + }, + + distance: function (latlng1, latlng2) { + var dx = latlng2.lng - latlng1.lng, + dy = latlng2.lat - latlng1.lat; + + return Math.sqrt(dx * dx + dy * dy); + }, + + infinite: true +}); + + + +/* + * @namespace CRS + * @crs L.CRS.Earth + * + * Serves as the base for CRS that are global such that they cover the earth. + * Can only be used as the base for other CRS and cannot be used directly, + * since it does not have a `code`, `projection` or `transformation`. `distance()` returns + * meters. + */ + +L.CRS.Earth = L.extend({}, L.CRS, { + wrapLng: [-180, 180], + + // Mean Earth Radius, as recommended for use by + // the International Union of Geodesy and Geophysics, + // see http://rosettacode.org/wiki/Haversine_formula + R: 6371000, + + // distance between two geographical points using spherical law of cosines approximation + distance: function (latlng1, latlng2) { + var rad = Math.PI / 180, + lat1 = latlng1.lat * rad, + lat2 = latlng2.lat * rad, + a = Math.sin(lat1) * Math.sin(lat2) + + Math.cos(lat1) * Math.cos(lat2) * Math.cos((latlng2.lng - latlng1.lng) * rad); + + return this.R * Math.acos(Math.min(a, 1)); + } +}); + + + +/* + * @namespace CRS + * @crs L.CRS.EPSG3857 + * + * The most common CRS for online maps, used by almost all free and commercial + * tile providers. Uses Spherical Mercator projection. Set in by default in + * Map's `crs` option. + */ + +L.CRS.EPSG3857 = L.extend({}, L.CRS.Earth, { + code: 'EPSG:3857', + projection: L.Projection.SphericalMercator, + + transformation: (function () { + var scale = 0.5 / (Math.PI * L.Projection.SphericalMercator.R); + return new L.Transformation(scale, 0.5, -scale, 0.5); + }()) +}); + +L.CRS.EPSG900913 = L.extend({}, L.CRS.EPSG3857, { + code: 'EPSG:900913' +}); + + + +/* + * @namespace CRS + * @crs L.CRS.EPSG4326 + * + * A common CRS among GIS enthusiasts. Uses simple Equirectangular projection. + * + * Leaflet 1.0.x complies with the [TMS coordinate scheme for EPSG:4326](https://wiki.osgeo.org/wiki/Tile_Map_Service_Specification#global-geodetic), + * which is a breaking change from 0.7.x behaviour. If you are using a `TileLayer` + * with this CRS, ensure that there are two 256x256 pixel tiles covering the + * whole earth at zoom level zero, and that the tile coordinate origin is (-180,+90), + * or (-180,-90) for `TileLayer`s with [the `tms` option](#tilelayer-tms) set. + */ + +L.CRS.EPSG4326 = L.extend({}, L.CRS.Earth, { + code: 'EPSG:4326', + projection: L.Projection.LonLat, + transformation: new L.Transformation(1 / 180, 1, -1 / 180, 0.5) +}); + + + +/* + * @class Map + * @aka L.Map + * @inherits Evented + * + * The central class of the API — it is used to create a map on a page and manipulate it. + * + * @example + * + * ```js + * // initialize the map on the "map" div with a given center and zoom + * var map = L.map('map', { + * center: [51.505, -0.09], + * zoom: 13 + * }); + * ``` + * + */ + +L.Map = L.Evented.extend({ + + options: { + // @section Map State Options + // @option crs: CRS = L.CRS.EPSG3857 + // The [Coordinate Reference System](#crs) to use. Don't change this if you're not + // sure what it means. + crs: L.CRS.EPSG3857, + + // @option center: LatLng = undefined + // Initial geographic center of the map + center: undefined, + + // @option zoom: Number = undefined + // Initial map zoom level + zoom: undefined, + + // @option minZoom: Number = undefined + // Minimum zoom level of the map. Overrides any `minZoom` option set on map layers. + minZoom: undefined, + + // @option maxZoom: Number = undefined + // Maximum zoom level of the map. Overrides any `maxZoom` option set on map layers. + maxZoom: undefined, + + // @option layers: Layer[] = [] + // Array of layers that will be added to the map initially + layers: [], + + // @option maxBounds: LatLngBounds = null + // When this option is set, the map restricts the view to the given + // geographical bounds, bouncing the user back if the user tries to pan + // outside the view. To set the restriction dynamically, use + // [`setMaxBounds`](#map-setmaxbounds) method. + maxBounds: undefined, + + // @option renderer: Renderer = * + // The default method for drawing vector layers on the map. `L.SVG` + // or `L.Canvas` by default depending on browser support. + renderer: undefined, + + + // @section Animation Options + // @option zoomAnimation: Boolean = true + // Whether the map zoom animation is enabled. By default it's enabled + // in all browsers that support CSS3 Transitions except Android. + zoomAnimation: true, + + // @option zoomAnimationThreshold: Number = 4 + // Won't animate zoom if the zoom difference exceeds this value. + zoomAnimationThreshold: 4, + + // @option fadeAnimation: Boolean = true + // Whether the tile fade animation is enabled. By default it's enabled + // in all browsers that support CSS3 Transitions except Android. + fadeAnimation: true, + + // @option markerZoomAnimation: Boolean = true + // Whether markers animate their zoom with the zoom animation, if disabled + // they will disappear for the length of the animation. By default it's + // enabled in all browsers that support CSS3 Transitions except Android. + markerZoomAnimation: true, + + // @option transform3DLimit: Number = 2^23 + // Defines the maximum size of a CSS translation transform. The default + // value should not be changed unless a web browser positions layers in + // the wrong place after doing a large `panBy`. + transform3DLimit: 8388608, // Precision limit of a 32-bit float + + // @section Interaction Options + // @option zoomSnap: Number = 1 + // Forces the map's zoom level to always be a multiple of this, particularly + // right after a [`fitBounds()`](#map-fitbounds) or a pinch-zoom. + // By default, the zoom level snaps to the nearest integer; lower values + // (e.g. `0.5` or `0.1`) allow for greater granularity. A value of `0` + // means the zoom level will not be snapped after `fitBounds` or a pinch-zoom. + zoomSnap: 1, + + // @option zoomDelta: Number = 1 + // Controls how much the map's zoom level will change after a + // [`zoomIn()`](#map-zoomin), [`zoomOut()`](#map-zoomout), pressing `+` + // or `-` on the keyboard, or using the [zoom controls](#control-zoom). + // Values smaller than `1` (e.g. `0.5`) allow for greater granularity. + zoomDelta: 1, + + // @option trackResize: Boolean = true + // Whether the map automatically handles browser window resize to update itself. + trackResize: true + }, + + initialize: function (id, options) { // (HTMLElement or String, Object) + options = L.setOptions(this, options); + + this._initContainer(id); + this._initLayout(); + + // hack for https://github.com/Leaflet/Leaflet/issues/1980 + this._onResize = L.bind(this._onResize, this); + + this._initEvents(); + + if (options.maxBounds) { + this.setMaxBounds(options.maxBounds); + } + + if (options.zoom !== undefined) { + this._zoom = this._limitZoom(options.zoom); + } + + if (options.center && options.zoom !== undefined) { + this.setView(L.latLng(options.center), options.zoom, {reset: true}); + } + + this._handlers = []; + this._layers = {}; + this._zoomBoundLayers = {}; + this._sizeChanged = true; + + this.callInitHooks(); + + // don't animate on browsers without hardware-accelerated transitions or old Android/Opera + this._zoomAnimated = L.DomUtil.TRANSITION && L.Browser.any3d && !L.Browser.mobileOpera && + this.options.zoomAnimation; + + // zoom transitions run with the same duration for all layers, so if one of transitionend events + // happens after starting zoom animation (propagating to the map pane), we know that it ended globally + if (this._zoomAnimated) { + this._createAnimProxy(); + L.DomEvent.on(this._proxy, L.DomUtil.TRANSITION_END, this._catchTransitionEnd, this); + } + + this._addLayers(this.options.layers); + }, + + + // @section Methods for modifying map state + + // @method setView(center: LatLng, zoom: Number, options?: Zoom/pan options): this + // Sets the view of the map (geographical center and zoom) with the given + // animation options. + setView: function (center, zoom, options) { + + zoom = zoom === undefined ? this._zoom : this._limitZoom(zoom); + center = this._limitCenter(L.latLng(center), zoom, this.options.maxBounds); + options = options || {}; + + this._stop(); + + if (this._loaded && !options.reset && options !== true) { + + if (options.animate !== undefined) { + options.zoom = L.extend({animate: options.animate}, options.zoom); + options.pan = L.extend({animate: options.animate, duration: options.duration}, options.pan); + } + + // try animating pan or zoom + var moved = (this._zoom !== zoom) ? + this._tryAnimatedZoom && this._tryAnimatedZoom(center, zoom, options.zoom) : + this._tryAnimatedPan(center, options.pan); + + if (moved) { + // prevent resize handler call, the view will refresh after animation anyway + clearTimeout(this._sizeTimer); + return this; + } + } + + // animation didn't start, just reset the map view + this._resetView(center, zoom); + + return this; + }, + + // @method setZoom(zoom: Number, options: Zoom/pan options): this + // Sets the zoom of the map. + setZoom: function (zoom, options) { + if (!this._loaded) { + this._zoom = zoom; + return this; + } + return this.setView(this.getCenter(), zoom, {zoom: options}); + }, + + // @method zoomIn(delta?: Number, options?: Zoom options): this + // Increases the zoom of the map by `delta` ([`zoomDelta`](#map-zoomdelta) by default). + zoomIn: function (delta, options) { + delta = delta || (L.Browser.any3d ? this.options.zoomDelta : 1); + return this.setZoom(this._zoom + delta, options); + }, + + // @method zoomOut(delta?: Number, options?: Zoom options): this + // Decreases the zoom of the map by `delta` ([`zoomDelta`](#map-zoomdelta) by default). + zoomOut: function (delta, options) { + delta = delta || (L.Browser.any3d ? this.options.zoomDelta : 1); + return this.setZoom(this._zoom - delta, options); + }, + + // @method setZoomAround(latlng: LatLng, zoom: Number, options: Zoom options): this + // Zooms the map while keeping a specified geographical point on the map + // stationary (e.g. used internally for scroll zoom and double-click zoom). + // @alternative + // @method setZoomAround(offset: Point, zoom: Number, options: Zoom options): this + // Zooms the map while keeping a specified pixel on the map (relative to the top-left corner) stationary. + setZoomAround: function (latlng, zoom, options) { + var scale = this.getZoomScale(zoom), + viewHalf = this.getSize().divideBy(2), + containerPoint = latlng instanceof L.Point ? latlng : this.latLngToContainerPoint(latlng), + + centerOffset = containerPoint.subtract(viewHalf).multiplyBy(1 - 1 / scale), + newCenter = this.containerPointToLatLng(viewHalf.add(centerOffset)); + + return this.setView(newCenter, zoom, {zoom: options}); + }, + + _getBoundsCenterZoom: function (bounds, options) { + + options = options || {}; + bounds = bounds.getBounds ? bounds.getBounds() : L.latLngBounds(bounds); + + var paddingTL = L.point(options.paddingTopLeft || options.padding || [0, 0]), + paddingBR = L.point(options.paddingBottomRight || options.padding || [0, 0]), + + zoom = this.getBoundsZoom(bounds, false, paddingTL.add(paddingBR)); + + zoom = (typeof options.maxZoom === 'number') ? Math.min(options.maxZoom, zoom) : zoom; + + var paddingOffset = paddingBR.subtract(paddingTL).divideBy(2), + + swPoint = this.project(bounds.getSouthWest(), zoom), + nePoint = this.project(bounds.getNorthEast(), zoom), + center = this.unproject(swPoint.add(nePoint).divideBy(2).add(paddingOffset), zoom); + + return { + center: center, + zoom: zoom + }; + }, + + // @method fitBounds(bounds: LatLngBounds, options?: fitBounds options): this + // Sets a map view that contains the given geographical bounds with the + // maximum zoom level possible. + fitBounds: function (bounds, options) { + + bounds = L.latLngBounds(bounds); + + if (!bounds.isValid()) { + throw new Error('Bounds are not valid.'); + } + + var target = this._getBoundsCenterZoom(bounds, options); + return this.setView(target.center, target.zoom, options); + }, + + // @method fitWorld(options?: fitBounds options): this + // Sets a map view that mostly contains the whole world with the maximum + // zoom level possible. + fitWorld: function (options) { + return this.fitBounds([[-90, -180], [90, 180]], options); + }, + + // @method panTo(latlng: LatLng, options?: Pan options): this + // Pans the map to a given center. + panTo: function (center, options) { // (LatLng) + return this.setView(center, this._zoom, {pan: options}); + }, + + // @method panBy(offset: Point): this + // Pans the map by a given number of pixels (animated). + panBy: function (offset, options) { + offset = L.point(offset).round(); + options = options || {}; + + if (!offset.x && !offset.y) { + return this.fire('moveend'); + } + // If we pan too far, Chrome gets issues with tiles + // and makes them disappear or appear in the wrong place (slightly offset) #2602 + if (options.animate !== true && !this.getSize().contains(offset)) { + this._resetView(this.unproject(this.project(this.getCenter()).add(offset)), this.getZoom()); + return this; + } + + if (!this._panAnim) { + this._panAnim = new L.PosAnimation(); + + this._panAnim.on({ + 'step': this._onPanTransitionStep, + 'end': this._onPanTransitionEnd + }, this); + } + + // don't fire movestart if animating inertia + if (!options.noMoveStart) { + this.fire('movestart'); + } + + // animate pan unless animate: false specified + if (options.animate !== false) { + L.DomUtil.addClass(this._mapPane, 'leaflet-pan-anim'); + + var newPos = this._getMapPanePos().subtract(offset).round(); + this._panAnim.run(this._mapPane, newPos, options.duration || 0.25, options.easeLinearity); + } else { + this._rawPanBy(offset); + this.fire('move').fire('moveend'); + } + + return this; + }, + + // @method flyTo(latlng: LatLng, zoom?: Number, options?: Zoom/pan options): this + // Sets the view of the map (geographical center and zoom) performing a smooth + // pan-zoom animation. + flyTo: function (targetCenter, targetZoom, options) { + + options = options || {}; + if (options.animate === false || !L.Browser.any3d) { + return this.setView(targetCenter, targetZoom, options); + } + + this._stop(); + + var from = this.project(this.getCenter()), + to = this.project(targetCenter), + size = this.getSize(), + startZoom = this._zoom; + + targetCenter = L.latLng(targetCenter); + targetZoom = targetZoom === undefined ? startZoom : targetZoom; + + var w0 = Math.max(size.x, size.y), + w1 = w0 * this.getZoomScale(startZoom, targetZoom), + u1 = (to.distanceTo(from)) || 1, + rho = 1.42, + rho2 = rho * rho; + + function r(i) { + var s1 = i ? -1 : 1, + s2 = i ? w1 : w0, + t1 = w1 * w1 - w0 * w0 + s1 * rho2 * rho2 * u1 * u1, + b1 = 2 * s2 * rho2 * u1, + b = t1 / b1, + sq = Math.sqrt(b * b + 1) - b; + + // workaround for floating point precision bug when sq = 0, log = -Infinite, + // thus triggering an infinite loop in flyTo + var log = sq < 0.000000001 ? -18 : Math.log(sq); + + return log; + } + + function sinh(n) { return (Math.exp(n) - Math.exp(-n)) / 2; } + function cosh(n) { return (Math.exp(n) + Math.exp(-n)) / 2; } + function tanh(n) { return sinh(n) / cosh(n); } + + var r0 = r(0); + + function w(s) { return w0 * (cosh(r0) / cosh(r0 + rho * s)); } + function u(s) { return w0 * (cosh(r0) * tanh(r0 + rho * s) - sinh(r0)) / rho2; } + + function easeOut(t) { return 1 - Math.pow(1 - t, 1.5); } + + var start = Date.now(), + S = (r(1) - r0) / rho, + duration = options.duration ? 1000 * options.duration : 1000 * S * 0.8; + + function frame() { + var t = (Date.now() - start) / duration, + s = easeOut(t) * S; + + if (t <= 1) { + this._flyToFrame = L.Util.requestAnimFrame(frame, this); + + this._move( + this.unproject(from.add(to.subtract(from).multiplyBy(u(s) / u1)), startZoom), + this.getScaleZoom(w0 / w(s), startZoom), + {flyTo: true}); + + } else { + this + ._move(targetCenter, targetZoom) + ._moveEnd(true); + } + } + + this._moveStart(true); + + frame.call(this); + return this; + }, + + // @method flyToBounds(bounds: LatLngBounds, options?: fitBounds options): this + // Sets the view of the map with a smooth animation like [`flyTo`](#map-flyto), + // but takes a bounds parameter like [`fitBounds`](#map-fitbounds). + flyToBounds: function (bounds, options) { + var target = this._getBoundsCenterZoom(bounds, options); + return this.flyTo(target.center, target.zoom, options); + }, + + // @method setMaxBounds(bounds: Bounds): this + // Restricts the map view to the given bounds (see the [maxBounds](#map-maxbounds) option). + setMaxBounds: function (bounds) { + bounds = L.latLngBounds(bounds); + + if (!bounds.isValid()) { + this.options.maxBounds = null; + return this.off('moveend', this._panInsideMaxBounds); + } else if (this.options.maxBounds) { + this.off('moveend', this._panInsideMaxBounds); + } + + this.options.maxBounds = bounds; + + if (this._loaded) { + this._panInsideMaxBounds(); + } + + return this.on('moveend', this._panInsideMaxBounds); + }, + + // @method setMinZoom(zoom: Number): this + // Sets the lower limit for the available zoom levels (see the [minZoom](#map-minzoom) option). + setMinZoom: function (zoom) { + this.options.minZoom = zoom; + + if (this._loaded && this.getZoom() < this.options.minZoom) { + return this.setZoom(zoom); + } + + return this; + }, + + // @method setMaxZoom(zoom: Number): this + // Sets the upper limit for the available zoom levels (see the [maxZoom](#map-maxzoom) option). + setMaxZoom: function (zoom) { + this.options.maxZoom = zoom; + + if (this._loaded && (this.getZoom() > this.options.maxZoom)) { + return this.setZoom(zoom); + } + + return this; + }, + + // @method panInsideBounds(bounds: LatLngBounds, options?: Pan options): this + // Pans the map to the closest view that would lie inside the given bounds (if it's not already), controlling the animation using the options specific, if any. + panInsideBounds: function (bounds, options) { + this._enforcingBounds = true; + var center = this.getCenter(), + newCenter = this._limitCenter(center, this._zoom, L.latLngBounds(bounds)); + + if (!center.equals(newCenter)) { + this.panTo(newCenter, options); + } + + this._enforcingBounds = false; + return this; + }, + + // @method invalidateSize(options: Zoom/Pan options): this + // Checks if the map container size changed and updates the map if so — + // call it after you've changed the map size dynamically, also animating + // pan by default. If `options.pan` is `false`, panning will not occur. + // If `options.debounceMoveend` is `true`, it will delay `moveend` event so + // that it doesn't happen often even if the method is called many + // times in a row. + + // @alternative + // @method invalidateSize(animate: Boolean): this + // Checks if the map container size changed and updates the map if so — + // call it after you've changed the map size dynamically, also animating + // pan by default. + invalidateSize: function (options) { + if (!this._loaded) { return this; } + + options = L.extend({ + animate: false, + pan: true + }, options === true ? {animate: true} : options); + + var oldSize = this.getSize(); + this._sizeChanged = true; + this._lastCenter = null; + + var newSize = this.getSize(), + oldCenter = oldSize.divideBy(2).round(), + newCenter = newSize.divideBy(2).round(), + offset = oldCenter.subtract(newCenter); + + if (!offset.x && !offset.y) { return this; } + + if (options.animate && options.pan) { + this.panBy(offset); + + } else { + if (options.pan) { + this._rawPanBy(offset); + } + + this.fire('move'); + + if (options.debounceMoveend) { + clearTimeout(this._sizeTimer); + this._sizeTimer = setTimeout(L.bind(this.fire, this, 'moveend'), 200); + } else { + this.fire('moveend'); + } + } + + // @section Map state change events + // @event resize: ResizeEvent + // Fired when the map is resized. + return this.fire('resize', { + oldSize: oldSize, + newSize: newSize + }); + }, + + // @section Methods for modifying map state + // @method stop(): this + // Stops the currently running `panTo` or `flyTo` animation, if any. + stop: function () { + this.setZoom(this._limitZoom(this._zoom)); + if (!this.options.zoomSnap) { + this.fire('viewreset'); + } + return this._stop(); + }, + + // @section Geolocation methods + // @method locate(options?: Locate options): this + // Tries to locate the user using the Geolocation API, firing a [`locationfound`](#map-locationfound) + // event with location data on success or a [`locationerror`](#map-locationerror) event on failure, + // and optionally sets the map view to the user's location with respect to + // detection accuracy (or to the world view if geolocation failed). + // Note that, if your page doesn't use HTTPS, this method will fail in + // modern browsers ([Chrome 50 and newer](https://sites.google.com/a/chromium.org/dev/Home/chromium-security/deprecating-powerful-features-on-insecure-origins)) + // See `Locate options` for more details. + locate: function (options) { + + options = this._locateOptions = L.extend({ + timeout: 10000, + watch: false + // setView: false + // maxZoom: + // maximumAge: 0 + // enableHighAccuracy: false + }, options); + + if (!('geolocation' in navigator)) { + this._handleGeolocationError({ + code: 0, + message: 'Geolocation not supported.' + }); + return this; + } + + var onResponse = L.bind(this._handleGeolocationResponse, this), + onError = L.bind(this._handleGeolocationError, this); + + if (options.watch) { + this._locationWatchId = + navigator.geolocation.watchPosition(onResponse, onError, options); + } else { + navigator.geolocation.getCurrentPosition(onResponse, onError, options); + } + return this; + }, + + // @method stopLocate(): this + // Stops watching location previously initiated by `map.locate({watch: true})` + // and aborts resetting the map view if map.locate was called with + // `{setView: true}`. + stopLocate: function () { + if (navigator.geolocation && navigator.geolocation.clearWatch) { + navigator.geolocation.clearWatch(this._locationWatchId); + } + if (this._locateOptions) { + this._locateOptions.setView = false; + } + return this; + }, + + _handleGeolocationError: function (error) { + var c = error.code, + message = error.message || + (c === 1 ? 'permission denied' : + (c === 2 ? 'position unavailable' : 'timeout')); + + if (this._locateOptions.setView && !this._loaded) { + this.fitWorld(); + } + + // @section Location events + // @event locationerror: ErrorEvent + // Fired when geolocation (using the [`locate`](#map-locate) method) failed. + this.fire('locationerror', { + code: c, + message: 'Geolocation error: ' + message + '.' + }); + }, + + _handleGeolocationResponse: function (pos) { + var lat = pos.coords.latitude, + lng = pos.coords.longitude, + latlng = new L.LatLng(lat, lng), + bounds = latlng.toBounds(pos.coords.accuracy), + options = this._locateOptions; + + if (options.setView) { + var zoom = this.getBoundsZoom(bounds); + this.setView(latlng, options.maxZoom ? Math.min(zoom, options.maxZoom) : zoom); + } + + var data = { + latlng: latlng, + bounds: bounds, + timestamp: pos.timestamp + }; + + for (var i in pos.coords) { + if (typeof pos.coords[i] === 'number') { + data[i] = pos.coords[i]; + } + } + + // @event locationfound: LocationEvent + // Fired when geolocation (using the [`locate`](#map-locate) method) + // went successfully. + this.fire('locationfound', data); + }, + + // TODO handler.addTo + // TODO Appropiate docs section? + // @section Other Methods + // @method addHandler(name: String, HandlerClass: Function): this + // Adds a new `Handler` to the map, given its name and constructor function. + addHandler: function (name, HandlerClass) { + if (!HandlerClass) { return this; } + + var handler = this[name] = new HandlerClass(this); + + this._handlers.push(handler); + + if (this.options[name]) { + handler.enable(); + } + + return this; + }, + + // @method remove(): this + // Destroys the map and clears all related event listeners. + remove: function () { + + this._initEvents(true); + + if (this._containerId !== this._container._leaflet_id) { + throw new Error('Map container is being reused by another instance'); + } + + try { + // throws error in IE6-8 + delete this._container._leaflet_id; + delete this._containerId; + } catch (e) { + /*eslint-disable */ + this._container._leaflet_id = undefined; + /*eslint-enable */ + this._containerId = undefined; + } + + L.DomUtil.remove(this._mapPane); + + if (this._clearControlPos) { + this._clearControlPos(); + } + + this._clearHandlers(); + + if (this._loaded) { + // @section Map state change events + // @event unload: Event + // Fired when the map is destroyed with [remove](#map-remove) method. + this.fire('unload'); + } + + for (var i in this._layers) { + this._layers[i].remove(); + } + + return this; + }, + + // @section Other Methods + // @method createPane(name: String, container?: HTMLElement): HTMLElement + // Creates a new [map pane](#map-pane) with the given name if it doesn't exist already, + // then returns it. The pane is created as a children of `container`, or + // as a children of the main map pane if not set. + createPane: function (name, container) { + var className = 'leaflet-pane' + (name ? ' leaflet-' + name.replace('Pane', '') + '-pane' : ''), + pane = L.DomUtil.create('div', className, container || this._mapPane); + + if (name) { + this._panes[name] = pane; + } + return pane; + }, + + // @section Methods for Getting Map State + + // @method getCenter(): LatLng + // Returns the geographical center of the map view + getCenter: function () { + this._checkIfLoaded(); + + if (this._lastCenter && !this._moved()) { + return this._lastCenter; + } + return this.layerPointToLatLng(this._getCenterLayerPoint()); + }, + + // @method getZoom(): Number + // Returns the current zoom level of the map view + getZoom: function () { + return this._zoom; + }, + + // @method getBounds(): LatLngBounds + // Returns the geographical bounds visible in the current map view + getBounds: function () { + var bounds = this.getPixelBounds(), + sw = this.unproject(bounds.getBottomLeft()), + ne = this.unproject(bounds.getTopRight()); + + return new L.LatLngBounds(sw, ne); + }, + + // @method getMinZoom(): Number + // Returns the minimum zoom level of the map (if set in the `minZoom` option of the map or of any layers), or `0` by default. + getMinZoom: function () { + return this.options.minZoom === undefined ? this._layersMinZoom || 0 : this.options.minZoom; + }, + + // @method getMaxZoom(): Number + // Returns the maximum zoom level of the map (if set in the `maxZoom` option of the map or of any layers). + getMaxZoom: function () { + return this.options.maxZoom === undefined ? + (this._layersMaxZoom === undefined ? Infinity : this._layersMaxZoom) : + this.options.maxZoom; + }, + + // @method getBoundsZoom(bounds: LatLngBounds, inside?: Boolean): Number + // Returns the maximum zoom level on which the given bounds fit to the map + // view in its entirety. If `inside` (optional) is set to `true`, the method + // instead returns the minimum zoom level on which the map view fits into + // the given bounds in its entirety. + getBoundsZoom: function (bounds, inside, padding) { // (LatLngBounds[, Boolean, Point]) -> Number + bounds = L.latLngBounds(bounds); + padding = L.point(padding || [0, 0]); + + var zoom = this.getZoom() || 0, + min = this.getMinZoom(), + max = this.getMaxZoom(), + nw = bounds.getNorthWest(), + se = bounds.getSouthEast(), + size = this.getSize().subtract(padding), + boundsSize = L.bounds(this.project(se, zoom), this.project(nw, zoom)).getSize(), + snap = L.Browser.any3d ? this.options.zoomSnap : 1; + + var scale = Math.min(size.x / boundsSize.x, size.y / boundsSize.y); + zoom = this.getScaleZoom(scale, zoom); + + if (snap) { + zoom = Math.round(zoom / (snap / 100)) * (snap / 100); // don't jump if within 1% of a snap level + zoom = inside ? Math.ceil(zoom / snap) * snap : Math.floor(zoom / snap) * snap; + } + + return Math.max(min, Math.min(max, zoom)); + }, + + // @method getSize(): Point + // Returns the current size of the map container (in pixels). + getSize: function () { + if (!this._size || this._sizeChanged) { + this._size = new L.Point( + this._container.clientWidth || 0, + this._container.clientHeight || 0); + + this._sizeChanged = false; + } + return this._size.clone(); + }, + + // @method getPixelBounds(): Bounds + // Returns the bounds of the current map view in projected pixel + // coordinates (sometimes useful in layer and overlay implementations). + getPixelBounds: function (center, zoom) { + var topLeftPoint = this._getTopLeftPoint(center, zoom); + return new L.Bounds(topLeftPoint, topLeftPoint.add(this.getSize())); + }, + + // TODO: Check semantics - isn't the pixel origin the 0,0 coord relative to + // the map pane? "left point of the map layer" can be confusing, specially + // since there can be negative offsets. + // @method getPixelOrigin(): Point + // Returns the projected pixel coordinates of the top left point of + // the map layer (useful in custom layer and overlay implementations). + getPixelOrigin: function () { + this._checkIfLoaded(); + return this._pixelOrigin; + }, + + // @method getPixelWorldBounds(zoom?: Number): Bounds + // Returns the world's bounds in pixel coordinates for zoom level `zoom`. + // If `zoom` is omitted, the map's current zoom level is used. + getPixelWorldBounds: function (zoom) { + return this.options.crs.getProjectedBounds(zoom === undefined ? this.getZoom() : zoom); + }, + + // @section Other Methods + + // @method getPane(pane: String|HTMLElement): HTMLElement + // Returns a [map pane](#map-pane), given its name or its HTML element (its identity). + getPane: function (pane) { + return typeof pane === 'string' ? this._panes[pane] : pane; + }, + + // @method getPanes(): Object + // Returns a plain object containing the names of all [panes](#map-pane) as keys and + // the panes as values. + getPanes: function () { + return this._panes; + }, + + // @method getContainer: HTMLElement + // Returns the HTML element that contains the map. + getContainer: function () { + return this._container; + }, + + + // @section Conversion Methods + + // @method getZoomScale(toZoom: Number, fromZoom: Number): Number + // Returns the scale factor to be applied to a map transition from zoom level + // `fromZoom` to `toZoom`. Used internally to help with zoom animations. + getZoomScale: function (toZoom, fromZoom) { + // TODO replace with universal implementation after refactoring projections + var crs = this.options.crs; + fromZoom = fromZoom === undefined ? this._zoom : fromZoom; + return crs.scale(toZoom) / crs.scale(fromZoom); + }, + + // @method getScaleZoom(scale: Number, fromZoom: Number): Number + // Returns the zoom level that the map would end up at, if it is at `fromZoom` + // level and everything is scaled by a factor of `scale`. Inverse of + // [`getZoomScale`](#map-getZoomScale). + getScaleZoom: function (scale, fromZoom) { + var crs = this.options.crs; + fromZoom = fromZoom === undefined ? this._zoom : fromZoom; + var zoom = crs.zoom(scale * crs.scale(fromZoom)); + return isNaN(zoom) ? Infinity : zoom; + }, + + // @method project(latlng: LatLng, zoom: Number): Point + // Projects a geographical coordinate `LatLng` according to the projection + // of the map's CRS, then scales it according to `zoom` and the CRS's + // `Transformation`. The result is pixel coordinate relative to + // the CRS origin. + project: function (latlng, zoom) { + zoom = zoom === undefined ? this._zoom : zoom; + return this.options.crs.latLngToPoint(L.latLng(latlng), zoom); + }, + + // @method unproject(point: Point, zoom: Number): LatLng + // Inverse of [`project`](#map-project). + unproject: function (point, zoom) { + zoom = zoom === undefined ? this._zoom : zoom; + return this.options.crs.pointToLatLng(L.point(point), zoom); + }, + + // @method layerPointToLatLng(point: Point): LatLng + // Given a pixel coordinate relative to the [origin pixel](#map-getpixelorigin), + // returns the corresponding geographical coordinate (for the current zoom level). + layerPointToLatLng: function (point) { + var projectedPoint = L.point(point).add(this.getPixelOrigin()); + return this.unproject(projectedPoint); + }, + + // @method latLngToLayerPoint(latlng: LatLng): Point + // Given a geographical coordinate, returns the corresponding pixel coordinate + // relative to the [origin pixel](#map-getpixelorigin). + latLngToLayerPoint: function (latlng) { + var projectedPoint = this.project(L.latLng(latlng))._round(); + return projectedPoint._subtract(this.getPixelOrigin()); + }, + + // @method wrapLatLng(latlng: LatLng): LatLng + // Returns a `LatLng` where `lat` and `lng` has been wrapped according to the + // map's CRS's `wrapLat` and `wrapLng` properties, if they are outside the + // CRS's bounds. + // By default this means longitude is wrapped around the dateline so its + // value is between -180 and +180 degrees. + wrapLatLng: function (latlng) { + return this.options.crs.wrapLatLng(L.latLng(latlng)); + }, + + // @method wrapLatLngBounds(bounds: LatLngBounds): LatLngBounds + // Returns a `LatLngBounds` with the same size as the given one, ensuring that + // its center is within the CRS's bounds. + // By default this means the center longitude is wrapped around the dateline so its + // value is between -180 and +180 degrees, and the majority of the bounds + // overlaps the CRS's bounds. + wrapLatLngBounds: function (latlng) { + return this.options.crs.wrapLatLngBounds(L.latLngBounds(latlng)); + }, + + // @method distance(latlng1: LatLng, latlng2: LatLng): Number + // Returns the distance between two geographical coordinates according to + // the map's CRS. By default this measures distance in meters. + distance: function (latlng1, latlng2) { + return this.options.crs.distance(L.latLng(latlng1), L.latLng(latlng2)); + }, + + // @method containerPointToLayerPoint(point: Point): Point + // Given a pixel coordinate relative to the map container, returns the corresponding + // pixel coordinate relative to the [origin pixel](#map-getpixelorigin). + containerPointToLayerPoint: function (point) { // (Point) + return L.point(point).subtract(this._getMapPanePos()); + }, + + // @method layerPointToContainerPoint(point: Point): Point + // Given a pixel coordinate relative to the [origin pixel](#map-getpixelorigin), + // returns the corresponding pixel coordinate relative to the map container. + layerPointToContainerPoint: function (point) { // (Point) + return L.point(point).add(this._getMapPanePos()); + }, + + // @method containerPointToLatLng(point: Point): LatLng + // Given a pixel coordinate relative to the map container, returns + // the corresponding geographical coordinate (for the current zoom level). + containerPointToLatLng: function (point) { + var layerPoint = this.containerPointToLayerPoint(L.point(point)); + return this.layerPointToLatLng(layerPoint); + }, + + // @method latLngToContainerPoint(latlng: LatLng): Point + // Given a geographical coordinate, returns the corresponding pixel coordinate + // relative to the map container. + latLngToContainerPoint: function (latlng) { + return this.layerPointToContainerPoint(this.latLngToLayerPoint(L.latLng(latlng))); + }, + + // @method mouseEventToContainerPoint(ev: MouseEvent): Point + // Given a MouseEvent object, returns the pixel coordinate relative to the + // map container where the event took place. + mouseEventToContainerPoint: function (e) { + return L.DomEvent.getMousePosition(e, this._container); + }, + + // @method mouseEventToLayerPoint(ev: MouseEvent): Point + // Given a MouseEvent object, returns the pixel coordinate relative to + // the [origin pixel](#map-getpixelorigin) where the event took place. + mouseEventToLayerPoint: function (e) { + return this.containerPointToLayerPoint(this.mouseEventToContainerPoint(e)); + }, + + // @method mouseEventToLatLng(ev: MouseEvent): LatLng + // Given a MouseEvent object, returns geographical coordinate where the + // event took place. + mouseEventToLatLng: function (e) { // (MouseEvent) + return this.layerPointToLatLng(this.mouseEventToLayerPoint(e)); + }, + + + // map initialization methods + + _initContainer: function (id) { + var container = this._container = L.DomUtil.get(id); + + if (!container) { + throw new Error('Map container not found.'); + } else if (container._leaflet_id) { + throw new Error('Map container is already initialized.'); + } + + L.DomEvent.addListener(container, 'scroll', this._onScroll, this); + this._containerId = L.Util.stamp(container); + }, + + _initLayout: function () { + var container = this._container; + + this._fadeAnimated = this.options.fadeAnimation && L.Browser.any3d; + + L.DomUtil.addClass(container, 'leaflet-container' + + (L.Browser.touch ? ' leaflet-touch' : '') + + (L.Browser.retina ? ' leaflet-retina' : '') + + (L.Browser.ielt9 ? ' leaflet-oldie' : '') + + (L.Browser.safari ? ' leaflet-safari' : '') + + (this._fadeAnimated ? ' leaflet-fade-anim' : '')); + + var position = L.DomUtil.getStyle(container, 'position'); + + if (position !== 'absolute' && position !== 'relative' && position !== 'fixed') { + container.style.position = 'relative'; + } + + this._initPanes(); + + if (this._initControlPos) { + this._initControlPos(); + } + }, + + _initPanes: function () { + var panes = this._panes = {}; + this._paneRenderers = {}; + + // @section + // + // Panes are DOM elements used to control the ordering of layers on the map. You + // can access panes with [`map.getPane`](#map-getpane) or + // [`map.getPanes`](#map-getpanes) methods. New panes can be created with the + // [`map.createPane`](#map-createpane) method. + // + // Every map has the following default panes that differ only in zIndex. + // + // @pane mapPane: HTMLElement = 'auto' + // Pane that contains all other map panes + + this._mapPane = this.createPane('mapPane', this._container); + L.DomUtil.setPosition(this._mapPane, new L.Point(0, 0)); + + // @pane tilePane: HTMLElement = 200 + // Pane for `GridLayer`s and `TileLayer`s + this.createPane('tilePane'); + // @pane overlayPane: HTMLElement = 400 + // Pane for vector overlays (`Path`s), like `Polyline`s and `Polygon`s + this.createPane('shadowPane'); + // @pane shadowPane: HTMLElement = 500 + // Pane for overlay shadows (e.g. `Marker` shadows) + this.createPane('overlayPane'); + // @pane markerPane: HTMLElement = 600 + // Pane for `Icon`s of `Marker`s + this.createPane('markerPane'); + // @pane tooltipPane: HTMLElement = 650 + // Pane for tooltip. + this.createPane('tooltipPane'); + // @pane popupPane: HTMLElement = 700 + // Pane for `Popup`s. + this.createPane('popupPane'); + + if (!this.options.markerZoomAnimation) { + L.DomUtil.addClass(panes.markerPane, 'leaflet-zoom-hide'); + L.DomUtil.addClass(panes.shadowPane, 'leaflet-zoom-hide'); + } + }, + + + // private methods that modify map state + + // @section Map state change events + _resetView: function (center, zoom) { + L.DomUtil.setPosition(this._mapPane, new L.Point(0, 0)); + + var loading = !this._loaded; + this._loaded = true; + zoom = this._limitZoom(zoom); + + this.fire('viewprereset'); + + var zoomChanged = this._zoom !== zoom; + this + ._moveStart(zoomChanged) + ._move(center, zoom) + ._moveEnd(zoomChanged); + + // @event viewreset: Event + // Fired when the map needs to redraw its content (this usually happens + // on map zoom or load). Very useful for creating custom overlays. + this.fire('viewreset'); + + // @event load: Event + // Fired when the map is initialized (when its center and zoom are set + // for the first time). + if (loading) { + this.fire('load'); + } + }, + + _moveStart: function (zoomChanged) { + // @event zoomstart: Event + // Fired when the map zoom is about to change (e.g. before zoom animation). + // @event movestart: Event + // Fired when the view of the map starts changing (e.g. user starts dragging the map). + if (zoomChanged) { + this.fire('zoomstart'); + } + return this.fire('movestart'); + }, + + _move: function (center, zoom, data) { + if (zoom === undefined) { + zoom = this._zoom; + } + var zoomChanged = this._zoom !== zoom; + + this._zoom = zoom; + this._lastCenter = center; + this._pixelOrigin = this._getNewPixelOrigin(center); + + // @event zoom: Event + // Fired repeatedly during any change in zoom level, including zoom + // and fly animations. + if (zoomChanged || (data && data.pinch)) { // Always fire 'zoom' if pinching because #3530 + this.fire('zoom', data); + } + + // @event move: Event + // Fired repeatedly during any movement of the map, including pan and + // fly animations. + return this.fire('move', data); + }, + + _moveEnd: function (zoomChanged) { + // @event zoomend: Event + // Fired when the map has changed, after any animations. + if (zoomChanged) { + this.fire('zoomend'); + } + + // @event moveend: Event + // Fired when the center of the map stops changing (e.g. user stopped + // dragging the map). + return this.fire('moveend'); + }, + + _stop: function () { + L.Util.cancelAnimFrame(this._flyToFrame); + if (this._panAnim) { + this._panAnim.stop(); + } + return this; + }, + + _rawPanBy: function (offset) { + L.DomUtil.setPosition(this._mapPane, this._getMapPanePos().subtract(offset)); + }, + + _getZoomSpan: function () { + return this.getMaxZoom() - this.getMinZoom(); + }, + + _panInsideMaxBounds: function () { + if (!this._enforcingBounds) { + this.panInsideBounds(this.options.maxBounds); + } + }, + + _checkIfLoaded: function () { + if (!this._loaded) { + throw new Error('Set map center and zoom first.'); + } + }, + + // DOM event handling + + // @section Interaction events + _initEvents: function (remove) { + if (!L.DomEvent) { return; } + + this._targets = {}; + this._targets[L.stamp(this._container)] = this; + + var onOff = remove ? 'off' : 'on'; + + // @event click: MouseEvent + // Fired when the user clicks (or taps) the map. + // @event dblclick: MouseEvent + // Fired when the user double-clicks (or double-taps) the map. + // @event mousedown: MouseEvent + // Fired when the user pushes the mouse button on the map. + // @event mouseup: MouseEvent + // Fired when the user releases the mouse button on the map. + // @event mouseover: MouseEvent + // Fired when the mouse enters the map. + // @event mouseout: MouseEvent + // Fired when the mouse leaves the map. + // @event mousemove: MouseEvent + // Fired while the mouse moves over the map. + // @event contextmenu: MouseEvent + // Fired when the user pushes the right mouse button on the map, prevents + // default browser context menu from showing if there are listeners on + // this event. Also fired on mobile when the user holds a single touch + // for a second (also called long press). + // @event keypress: KeyboardEvent + // Fired when the user presses a key from the keyboard while the map is focused. + L.DomEvent[onOff](this._container, 'click dblclick mousedown mouseup ' + + 'mouseover mouseout mousemove contextmenu keypress', this._handleDOMEvent, this); + + if (this.options.trackResize) { + L.DomEvent[onOff](window, 'resize', this._onResize, this); + } + + if (L.Browser.any3d && this.options.transform3DLimit) { + this[onOff]('moveend', this._onMoveEnd); + } + }, + + _onResize: function () { + L.Util.cancelAnimFrame(this._resizeRequest); + this._resizeRequest = L.Util.requestAnimFrame( + function () { this.invalidateSize({debounceMoveend: true}); }, this); + }, + + _onScroll: function () { + this._container.scrollTop = 0; + this._container.scrollLeft = 0; + }, + + _onMoveEnd: function () { + var pos = this._getMapPanePos(); + if (Math.max(Math.abs(pos.x), Math.abs(pos.y)) >= this.options.transform3DLimit) { + // https://bugzilla.mozilla.org/show_bug.cgi?id=1203873 but Webkit also have + // a pixel offset on very high values, see: http://jsfiddle.net/dg6r5hhb/ + this._resetView(this.getCenter(), this.getZoom()); + } + }, + + _findEventTargets: function (e, type) { + var targets = [], + target, + isHover = type === 'mouseout' || type === 'mouseover', + src = e.target || e.srcElement, + dragging = false; + + while (src) { + target = this._targets[L.stamp(src)]; + if (target && (type === 'click' || type === 'preclick') && !e._simulated && this._draggableMoved(target)) { + // Prevent firing click after you just dragged an object. + dragging = true; + break; + } + if (target && target.listens(type, true)) { + if (isHover && !L.DomEvent._isExternalTarget(src, e)) { break; } + targets.push(target); + if (isHover) { break; } + } + if (src === this._container) { break; } + src = src.parentNode; + } + if (!targets.length && !dragging && !isHover && L.DomEvent._isExternalTarget(src, e)) { + targets = [this]; + } + return targets; + }, + + _handleDOMEvent: function (e) { + if (!this._loaded || L.DomEvent._skipped(e)) { return; } + + var type = e.type === 'keypress' && e.keyCode === 13 ? 'click' : e.type; + + if (type === 'mousedown') { + // prevents outline when clicking on keyboard-focusable element + L.DomUtil.preventOutline(e.target || e.srcElement); + } + + this._fireDOMEvent(e, type); + }, + + _fireDOMEvent: function (e, type, targets) { + + if (e.type === 'click') { + // Fire a synthetic 'preclick' event which propagates up (mainly for closing popups). + // @event preclick: MouseEvent + // Fired before mouse click on the map (sometimes useful when you + // want something to happen on click before any existing click + // handlers start running). + var synth = L.Util.extend({}, e); + synth.type = 'preclick'; + this._fireDOMEvent(synth, synth.type, targets); + } + + if (e._stopped) { return; } + + // Find the layer the event is propagating from and its parents. + targets = (targets || []).concat(this._findEventTargets(e, type)); + + if (!targets.length) { return; } + + var target = targets[0]; + if (type === 'contextmenu' && target.listens(type, true)) { + L.DomEvent.preventDefault(e); + } + + var data = { + originalEvent: e + }; + + if (e.type !== 'keypress') { + var isMarker = target instanceof L.Marker; + data.containerPoint = isMarker ? + this.latLngToContainerPoint(target.getLatLng()) : this.mouseEventToContainerPoint(e); + data.layerPoint = this.containerPointToLayerPoint(data.containerPoint); + data.latlng = isMarker ? target.getLatLng() : this.layerPointToLatLng(data.layerPoint); + } + + for (var i = 0; i < targets.length; i++) { + targets[i].fire(type, data, true); + if (data.originalEvent._stopped || + (targets[i].options.nonBubblingEvents && L.Util.indexOf(targets[i].options.nonBubblingEvents, type) !== -1)) { return; } + } + }, + + _draggableMoved: function (obj) { + obj = obj.dragging && obj.dragging.enabled() ? obj : this; + return (obj.dragging && obj.dragging.moved()) || (this.boxZoom && this.boxZoom.moved()); + }, + + _clearHandlers: function () { + for (var i = 0, len = this._handlers.length; i < len; i++) { + this._handlers[i].disable(); + } + }, + + // @section Other Methods + + // @method whenReady(fn: Function, context?: Object): this + // Runs the given function `fn` when the map gets initialized with + // a view (center and zoom) and at least one layer, or immediately + // if it's already initialized, optionally passing a function context. + whenReady: function (callback, context) { + if (this._loaded) { + callback.call(context || this, {target: this}); + } else { + this.on('load', callback, context); + } + return this; + }, + + + // private methods for getting map state + + _getMapPanePos: function () { + return L.DomUtil.getPosition(this._mapPane) || new L.Point(0, 0); + }, + + _moved: function () { + var pos = this._getMapPanePos(); + return pos && !pos.equals([0, 0]); + }, + + _getTopLeftPoint: function (center, zoom) { + var pixelOrigin = center && zoom !== undefined ? + this._getNewPixelOrigin(center, zoom) : + this.getPixelOrigin(); + return pixelOrigin.subtract(this._getMapPanePos()); + }, + + _getNewPixelOrigin: function (center, zoom) { + var viewHalf = this.getSize()._divideBy(2); + return this.project(center, zoom)._subtract(viewHalf)._add(this._getMapPanePos())._round(); + }, + + _latLngToNewLayerPoint: function (latlng, zoom, center) { + var topLeft = this._getNewPixelOrigin(center, zoom); + return this.project(latlng, zoom)._subtract(topLeft); + }, + + _latLngBoundsToNewLayerBounds: function (latLngBounds, zoom, center) { + var topLeft = this._getNewPixelOrigin(center, zoom); + return L.bounds([ + this.project(latLngBounds.getSouthWest(), zoom)._subtract(topLeft), + this.project(latLngBounds.getNorthWest(), zoom)._subtract(topLeft), + this.project(latLngBounds.getSouthEast(), zoom)._subtract(topLeft), + this.project(latLngBounds.getNorthEast(), zoom)._subtract(topLeft) + ]); + }, + + // layer point of the current center + _getCenterLayerPoint: function () { + return this.containerPointToLayerPoint(this.getSize()._divideBy(2)); + }, + + // offset of the specified place to the current center in pixels + _getCenterOffset: function (latlng) { + return this.latLngToLayerPoint(latlng).subtract(this._getCenterLayerPoint()); + }, + + // adjust center for view to get inside bounds + _limitCenter: function (center, zoom, bounds) { + + if (!bounds) { return center; } + + var centerPoint = this.project(center, zoom), + viewHalf = this.getSize().divideBy(2), + viewBounds = new L.Bounds(centerPoint.subtract(viewHalf), centerPoint.add(viewHalf)), + offset = this._getBoundsOffset(viewBounds, bounds, zoom); + + // If offset is less than a pixel, ignore. + // This prevents unstable projections from getting into + // an infinite loop of tiny offsets. + if (offset.round().equals([0, 0])) { + return center; + } + + return this.unproject(centerPoint.add(offset), zoom); + }, + + // adjust offset for view to get inside bounds + _limitOffset: function (offset, bounds) { + if (!bounds) { return offset; } + + var viewBounds = this.getPixelBounds(), + newBounds = new L.Bounds(viewBounds.min.add(offset), viewBounds.max.add(offset)); + + return offset.add(this._getBoundsOffset(newBounds, bounds)); + }, + + // returns offset needed for pxBounds to get inside maxBounds at a specified zoom + _getBoundsOffset: function (pxBounds, maxBounds, zoom) { + var projectedMaxBounds = L.bounds( + this.project(maxBounds.getNorthEast(), zoom), + this.project(maxBounds.getSouthWest(), zoom) + ), + minOffset = projectedMaxBounds.min.subtract(pxBounds.min), + maxOffset = projectedMaxBounds.max.subtract(pxBounds.max), + + dx = this._rebound(minOffset.x, -maxOffset.x), + dy = this._rebound(minOffset.y, -maxOffset.y); + + return new L.Point(dx, dy); + }, + + _rebound: function (left, right) { + return left + right > 0 ? + Math.round(left - right) / 2 : + Math.max(0, Math.ceil(left)) - Math.max(0, Math.floor(right)); + }, + + _limitZoom: function (zoom) { + var min = this.getMinZoom(), + max = this.getMaxZoom(), + snap = L.Browser.any3d ? this.options.zoomSnap : 1; + if (snap) { + zoom = Math.round(zoom / snap) * snap; + } + return Math.max(min, Math.min(max, zoom)); + }, + + _onPanTransitionStep: function () { + this.fire('move'); + }, + + _onPanTransitionEnd: function () { + L.DomUtil.removeClass(this._mapPane, 'leaflet-pan-anim'); + this.fire('moveend'); + }, + + _tryAnimatedPan: function (center, options) { + // difference between the new and current centers in pixels + var offset = this._getCenterOffset(center)._floor(); + + // don't animate too far unless animate: true specified in options + if ((options && options.animate) !== true && !this.getSize().contains(offset)) { return false; } + + this.panBy(offset, options); + + return true; + }, + + _createAnimProxy: function () { + + var proxy = this._proxy = L.DomUtil.create('div', 'leaflet-proxy leaflet-zoom-animated'); + this._panes.mapPane.appendChild(proxy); + + this.on('zoomanim', function (e) { + var prop = L.DomUtil.TRANSFORM, + transform = proxy.style[prop]; + + L.DomUtil.setTransform(proxy, this.project(e.center, e.zoom), this.getZoomScale(e.zoom, 1)); + + // workaround for case when transform is the same and so transitionend event is not fired + if (transform === proxy.style[prop] && this._animatingZoom) { + this._onZoomTransitionEnd(); + } + }, this); + + this.on('load moveend', function () { + var c = this.getCenter(), + z = this.getZoom(); + L.DomUtil.setTransform(proxy, this.project(c, z), this.getZoomScale(z, 1)); + }, this); + }, + + _catchTransitionEnd: function (e) { + if (this._animatingZoom && e.propertyName.indexOf('transform') >= 0) { + this._onZoomTransitionEnd(); + } + }, + + _nothingToAnimate: function () { + return !this._container.getElementsByClassName('leaflet-zoom-animated').length; + }, + + _tryAnimatedZoom: function (center, zoom, options) { + + if (this._animatingZoom) { return true; } + + options = options || {}; + + // don't animate if disabled, not supported or zoom difference is too large + if (!this._zoomAnimated || options.animate === false || this._nothingToAnimate() || + Math.abs(zoom - this._zoom) > this.options.zoomAnimationThreshold) { return false; } + + // offset is the pixel coords of the zoom origin relative to the current center + var scale = this.getZoomScale(zoom), + offset = this._getCenterOffset(center)._divideBy(1 - 1 / scale); + + // don't animate if the zoom origin isn't within one screen from the current center, unless forced + if (options.animate !== true && !this.getSize().contains(offset)) { return false; } + + L.Util.requestAnimFrame(function () { + this + ._moveStart(true) + ._animateZoom(center, zoom, true); + }, this); + + return true; + }, + + _animateZoom: function (center, zoom, startAnim, noUpdate) { + if (startAnim) { + this._animatingZoom = true; + + // remember what center/zoom to set after animation + this._animateToCenter = center; + this._animateToZoom = zoom; + + L.DomUtil.addClass(this._mapPane, 'leaflet-zoom-anim'); + } + + // @event zoomanim: ZoomAnimEvent + // Fired on every frame of a zoom animation + this.fire('zoomanim', { + center: center, + zoom: zoom, + noUpdate: noUpdate + }); + + // Work around webkit not firing 'transitionend', see https://github.com/Leaflet/Leaflet/issues/3689, 2693 + setTimeout(L.bind(this._onZoomTransitionEnd, this), 250); + }, + + _onZoomTransitionEnd: function () { + if (!this._animatingZoom) { return; } + + L.DomUtil.removeClass(this._mapPane, 'leaflet-zoom-anim'); + + this._animatingZoom = false; + + this._move(this._animateToCenter, this._animateToZoom); + + // This anim frame should prevent an obscure iOS webkit tile loading race condition. + L.Util.requestAnimFrame(function () { + this._moveEnd(true); + }, this); + } +}); + +// @section + +// @factory L.map(id: String, options?: Map options) +// Instantiates a map object given the DOM ID of a ` ` element +// and optionally an object literal with `Map options`. +// +// @alternative +// @factory L.map(el: HTMLElement, options?: Map options) +// Instantiates a map object given an instance of a `` HTML element +// and optionally an object literal with `Map options`. +L.map = function (id, options) { + return new L.Map(id, options); +}; + + + + +/* + * @class Layer + * @inherits Evented + * @aka L.Layer + * @aka ILayer + * + * A set of methods from the Layer base class that all Leaflet layers use. + * Inherits all methods, options and events from `L.Evented`. + * + * @example + * + * ```js + * var layer = L.Marker(latlng).addTo(map); + * layer.addTo(map); + * layer.remove(); + * ``` + * + * @event add: Event + * Fired after the layer is added to a map + * + * @event remove: Event + * Fired after the layer is removed from a map + */ + + +L.Layer = L.Evented.extend({ + + // Classes extending `L.Layer` will inherit the following options: + options: { + // @option pane: String = 'overlayPane' + // By default the layer will be added to the map's [overlay pane](#map-overlaypane). Overriding this option will cause the layer to be placed on another pane by default. + pane: 'overlayPane', + nonBubblingEvents: [], // Array of events that should not be bubbled to DOM parents (like the map), + + // @option attribution: String = null + // String to be shown in the attribution control, describes the layer data, e.g. "© Mapbox". + attribution: null + }, + + /* @section + * Classes extending `L.Layer` will inherit the following methods: + * + * @method addTo(map: Map): this + * Adds the layer to the given map + */ + addTo: function (map) { + map.addLayer(this); + return this; + }, + + // @method remove: this + // Removes the layer from the map it is currently active on. + remove: function () { + return this.removeFrom(this._map || this._mapToAdd); + }, + + // @method removeFrom(map: Map): this + // Removes the layer from the given map + removeFrom: function (obj) { + if (obj) { + obj.removeLayer(this); + } + return this; + }, + + // @method getPane(name? : String): HTMLElement + // Returns the `HTMLElement` representing the named pane on the map. If `name` is omitted, returns the pane for this layer. + getPane: function (name) { + return this._map.getPane(name ? (this.options[name] || name) : this.options.pane); + }, + + addInteractiveTarget: function (targetEl) { + this._map._targets[L.stamp(targetEl)] = this; + return this; + }, + + removeInteractiveTarget: function (targetEl) { + delete this._map._targets[L.stamp(targetEl)]; + return this; + }, + + // @method getAttribution: String + // Used by the `attribution control`, returns the [attribution option](#gridlayer-attribution). + getAttribution: function () { + return this.options.attribution; + }, + + _layerAdd: function (e) { + var map = e.target; + + // check in case layer gets added and then removed before the map is ready + if (!map.hasLayer(this)) { return; } + + this._map = map; + this._zoomAnimated = map._zoomAnimated; + + if (this.getEvents) { + var events = this.getEvents(); + map.on(events, this); + this.once('remove', function () { + map.off(events, this); + }, this); + } + + this.onAdd(map); + + if (this.getAttribution && map.attributionControl) { + map.attributionControl.addAttribution(this.getAttribution()); + } + + this.fire('add'); + map.fire('layeradd', {layer: this}); + } +}); + +/* @section Extension methods + * @uninheritable + * + * Every layer should extend from `L.Layer` and (re-)implement the following methods. + * + * @method onAdd(map: Map): this + * Should contain code that creates DOM elements for the layer, adds them to `map panes` where they should belong and puts listeners on relevant map events. Called on [`map.addLayer(layer)`](#map-addlayer). + * + * @method onRemove(map: Map): this + * Should contain all clean up code that removes the layer's elements from the DOM and removes listeners previously added in [`onAdd`](#layer-onadd). Called on [`map.removeLayer(layer)`](#map-removelayer). + * + * @method getEvents(): Object + * This optional method should return an object like `{ viewreset: this._reset }` for [`addEventListener`](#evented-addeventlistener). The event handlers in this object will be automatically added and removed from the map with your layer. + * + * @method getAttribution(): String + * This optional method should return a string containing HTML to be shown on the `Attribution control` whenever the layer is visible. + * + * @method beforeAdd(map: Map): this + * Optional method. Called on [`map.addLayer(layer)`](#map-addlayer), before the layer is added to the map, before events are initialized, without waiting until the map is in a usable state. Use for early initialization only. + */ + + +/* @namespace Map + * @section Layer events + * + * @event layeradd: LayerEvent + * Fired when a new layer is added to the map. + * + * @event layerremove: LayerEvent + * Fired when some layer is removed from the map + * + * @section Methods for Layers and Controls + */ +L.Map.include({ + // @method addLayer(layer: Layer): this + // Adds the given layer to the map + addLayer: function (layer) { + var id = L.stamp(layer); + if (this._layers[id]) { return this; } + this._layers[id] = layer; + + layer._mapToAdd = this; + + if (layer.beforeAdd) { + layer.beforeAdd(this); + } + + this.whenReady(layer._layerAdd, layer); + + return this; + }, + + // @method removeLayer(layer: Layer): this + // Removes the given layer from the map. + removeLayer: function (layer) { + var id = L.stamp(layer); + + if (!this._layers[id]) { return this; } + + if (this._loaded) { + layer.onRemove(this); + } + + if (layer.getAttribution && this.attributionControl) { + this.attributionControl.removeAttribution(layer.getAttribution()); + } + + delete this._layers[id]; + + if (this._loaded) { + this.fire('layerremove', {layer: layer}); + layer.fire('remove'); + } + + layer._map = layer._mapToAdd = null; + + return this; + }, + + // @method hasLayer(layer: Layer): Boolean + // Returns `true` if the given layer is currently added to the map + hasLayer: function (layer) { + return !!layer && (L.stamp(layer) in this._layers); + }, + + /* @method eachLayer(fn: Function, context?: Object): this + * Iterates over the layers of the map, optionally specifying context of the iterator function. + * ``` + * map.eachLayer(function(layer){ + * layer.bindPopup('Hello'); + * }); + * ``` + */ + eachLayer: function (method, context) { + for (var i in this._layers) { + method.call(context, this._layers[i]); + } + return this; + }, + + _addLayers: function (layers) { + layers = layers ? (L.Util.isArray(layers) ? layers : [layers]) : []; + + for (var i = 0, len = layers.length; i < len; i++) { + this.addLayer(layers[i]); + } + }, + + _addZoomLimit: function (layer) { + if (isNaN(layer.options.maxZoom) || !isNaN(layer.options.minZoom)) { + this._zoomBoundLayers[L.stamp(layer)] = layer; + this._updateZoomLevels(); + } + }, + + _removeZoomLimit: function (layer) { + var id = L.stamp(layer); + + if (this._zoomBoundLayers[id]) { + delete this._zoomBoundLayers[id]; + this._updateZoomLevels(); + } + }, + + _updateZoomLevels: function () { + var minZoom = Infinity, + maxZoom = -Infinity, + oldZoomSpan = this._getZoomSpan(); + + for (var i in this._zoomBoundLayers) { + var options = this._zoomBoundLayers[i].options; + + minZoom = options.minZoom === undefined ? minZoom : Math.min(minZoom, options.minZoom); + maxZoom = options.maxZoom === undefined ? maxZoom : Math.max(maxZoom, options.maxZoom); + } + + this._layersMaxZoom = maxZoom === -Infinity ? undefined : maxZoom; + this._layersMinZoom = minZoom === Infinity ? undefined : minZoom; + + // @section Map state change events + // @event zoomlevelschange: Event + // Fired when the number of zoomlevels on the map is changed due + // to adding or removing a layer. + if (oldZoomSpan !== this._getZoomSpan()) { + this.fire('zoomlevelschange'); + } + + if (this.options.maxZoom === undefined && this._layersMaxZoom && this.getZoom() > this._layersMaxZoom) { + this.setZoom(this._layersMaxZoom); + } + if (this.options.minZoom === undefined && this._layersMinZoom && this.getZoom() < this._layersMinZoom) { + this.setZoom(this._layersMinZoom); + } + } +}); + + + +/* + * @namespace DomEvent + * Utility functions to work with the [DOM events](https://developer.mozilla.org/docs/Web/API/Event), used by Leaflet internally. + */ + +// Inspired by John Resig, Dean Edwards and YUI addEvent implementations. + + + +var eventsKey = '_leaflet_events'; + +L.DomEvent = { + + // @function on(el: HTMLElement, types: String, fn: Function, context?: Object): this + // Adds a listener function (`fn`) to a particular DOM event type of the + // element `el`. You can optionally specify the context of the listener + // (object the `this` keyword will point to). You can also pass several + // space-separated types (e.g. `'click dblclick'`). + + // @alternative + // @function on(el: HTMLElement, eventMap: Object, context?: Object): this + // Adds a set of type/listener pairs, e.g. `{click: onClick, mousemove: onMouseMove}` + on: function (obj, types, fn, context) { + + if (typeof types === 'object') { + for (var type in types) { + this._on(obj, type, types[type], fn); + } + } else { + types = L.Util.splitWords(types); + + for (var i = 0, len = types.length; i < len; i++) { + this._on(obj, types[i], fn, context); + } + } + + return this; + }, + + // @function off(el: HTMLElement, types: String, fn: Function, context?: Object): this + // Removes a previously added listener function. If no function is specified, + // it will remove all the listeners of that particular DOM event from the element. + // Note that if you passed a custom context to on, you must pass the same + // context to `off` in order to remove the listener. + + // @alternative + // @function off(el: HTMLElement, eventMap: Object, context?: Object): this + // Removes a set of type/listener pairs, e.g. `{click: onClick, mousemove: onMouseMove}` + off: function (obj, types, fn, context) { + + if (typeof types === 'object') { + for (var type in types) { + this._off(obj, type, types[type], fn); + } + } else { + types = L.Util.splitWords(types); + + for (var i = 0, len = types.length; i < len; i++) { + this._off(obj, types[i], fn, context); + } + } + + return this; + }, + + _on: function (obj, type, fn, context) { + var id = type + L.stamp(fn) + (context ? '_' + L.stamp(context) : ''); + + if (obj[eventsKey] && obj[eventsKey][id]) { return this; } + + var handler = function (e) { + return fn.call(context || obj, e || window.event); + }; + + var originalHandler = handler; + + if (L.Browser.pointer && type.indexOf('touch') === 0) { + this.addPointerListener(obj, type, handler, id); + + } else if (L.Browser.touch && (type === 'dblclick') && this.addDoubleTapListener && + !(L.Browser.pointer && L.Browser.chrome)) { + // Chrome >55 does not need the synthetic dblclicks from addDoubleTapListener + // See #5180 + this.addDoubleTapListener(obj, handler, id); + + } else if ('addEventListener' in obj) { + + if (type === 'mousewheel') { + obj.addEventListener('onwheel' in obj ? 'wheel' : 'mousewheel', handler, false); + + } else if ((type === 'mouseenter') || (type === 'mouseleave')) { + handler = function (e) { + e = e || window.event; + if (L.DomEvent._isExternalTarget(obj, e)) { + originalHandler(e); + } + }; + obj.addEventListener(type === 'mouseenter' ? 'mouseover' : 'mouseout', handler, false); + + } else { + if (type === 'click' && L.Browser.android) { + handler = function (e) { + return L.DomEvent._filterClick(e, originalHandler); + }; + } + obj.addEventListener(type, handler, false); + } + + } else if ('attachEvent' in obj) { + obj.attachEvent('on' + type, handler); + } + + obj[eventsKey] = obj[eventsKey] || {}; + obj[eventsKey][id] = handler; + + return this; + }, + + _off: function (obj, type, fn, context) { + + var id = type + L.stamp(fn) + (context ? '_' + L.stamp(context) : ''), + handler = obj[eventsKey] && obj[eventsKey][id]; + + if (!handler) { return this; } + + if (L.Browser.pointer && type.indexOf('touch') === 0) { + this.removePointerListener(obj, type, id); + + } else if (L.Browser.touch && (type === 'dblclick') && this.removeDoubleTapListener) { + this.removeDoubleTapListener(obj, id); + + } else if ('removeEventListener' in obj) { + + if (type === 'mousewheel') { + obj.removeEventListener('onwheel' in obj ? 'wheel' : 'mousewheel', handler, false); + + } else { + obj.removeEventListener( + type === 'mouseenter' ? 'mouseover' : + type === 'mouseleave' ? 'mouseout' : type, handler, false); + } + + } else if ('detachEvent' in obj) { + obj.detachEvent('on' + type, handler); + } + + obj[eventsKey][id] = null; + + return this; + }, + + // @function stopPropagation(ev: DOMEvent): this + // Stop the given event from propagation to parent elements. Used inside the listener functions: + // ```js + // L.DomEvent.on(div, 'click', function (ev) { + // L.DomEvent.stopPropagation(ev); + // }); + // ``` + stopPropagation: function (e) { + + if (e.stopPropagation) { + e.stopPropagation(); + } else if (e.originalEvent) { // In case of Leaflet event. + e.originalEvent._stopped = true; + } else { + e.cancelBubble = true; + } + L.DomEvent._skipped(e); + + return this; + }, + + // @function disableScrollPropagation(el: HTMLElement): this + // Adds `stopPropagation` to the element's `'mousewheel'` events (plus browser variants). + disableScrollPropagation: function (el) { + return L.DomEvent.on(el, 'mousewheel', L.DomEvent.stopPropagation); + }, + + // @function disableClickPropagation(el: HTMLElement): this + // Adds `stopPropagation` to the element's `'click'`, `'doubleclick'`, + // `'mousedown'` and `'touchstart'` events (plus browser variants). + disableClickPropagation: function (el) { + var stop = L.DomEvent.stopPropagation; + + L.DomEvent.on(el, L.Draggable.START.join(' '), stop); + + return L.DomEvent.on(el, { + click: L.DomEvent._fakeStop, + dblclick: stop + }); + }, + + // @function preventDefault(ev: DOMEvent): this + // Prevents the default action of the DOM Event `ev` from happening (such as + // following a link in the href of the a element, or doing a POST request + // with page reload when a `