././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1714825027.0269916 hdf_compass-0.7b15/0000777000000000000000000000000014615423503011113 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/COPYING0000666000000000000000000001322514544243330012150 0ustar00 Copyright Notice and License Terms for HDF Compass - Viewer for HDF5 and other file formats ----------------------------------------------------------------------------- HDF Compass Copyright 2014-2017 by The HDF Group. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted for any purpose (including commercial purposes) provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions, and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions, and the following disclaimer in the documentation and/or materials provided with the distribution. 3. In addition, redistributions of modified forms of the source or binary code must carry prominent notices stating that the original code was changed and the date of the change. 4. All publications or advertising materials mentioning features or use of this software are asked, but not required, to acknowledge that it was developed by The HDF Group and credit the contributors. 5. Neither the name of The HDF Group, nor the name of any Contributor may be used to endorse or promote products derived from this software without specific prior written permission from The HDF Group or the Contributor, respectively. DISCLAIMER: THIS SOFTWARE IS PROVIDED BY THE HDF GROUP AND THE CONTRIBUTORS "AS IS" WITH NO WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED. In no event shall The HDF Group or the Contributors be liable for any damages suffered by the users arising out of the use of this software, even if advised of the possibility of such damage. ----------------------------------------------------------------------------- Additional Copyright and License Information Copyright and license terms for the following software can be found in the adjacent subdirectory Additional_Legal/. This information was obtained from sources provided by each software package provider at the location specified under "Original source:" and was current as of 19 December 2014. HDF Compass uses selected icons from the following icon sets: KDE Oxygen icon set Provided by: KDE Community Copyright and license information: Additional_Legal/KDE_Oxygen_Icon_Set_Copyright_and_License.txt Original source: http://www.gnu.org/copyleft/lesser.html (via link from https://techbase.kde.org/Projects/Oxygen/Licensing) License type: GNU LGPL version 3 license HydroPro icon set Provided by: Ben Fleming via MediaDesign Copyright and license information: Additional_Legal/HydroPro_Icons_Terms.txt Original source: http://www.iconarchive.com/icons/media-design/hydropro/readme.txt License type: Freeware Pre-built HDF Compass binaries include the following software. None of this software is present in whole or in part in the HDF Compass source code. Python interpreter Provided by: Python Software Foundation Copyright and license information: additional_legal/Python_Copyright_and_License.txt Original source: https://docs.python.org/3/license.html License type: BSD-style license wxPython GUI layer Provided by: Julian Smart, Vadim Zeitlin, Stefan Csomor, Robert Roebling, and other members of the wxWidgets team Copyright and license information: additional_legal/wxWidgets_Copyrights_and_Licenses.txt Original source: http://docs.wxwidgets.org/3.0/page_copyright.html License type: GNU LGPL version 2 license with exception PyInstaller runtime support code (Windows & Linux only) Provided by: PyInstaller Development Team Copyright and license information: additional_legal/PyInstaller_Copyrights_and_Licenses.txt Original source: https://github.com/pyinstaller/pyinstaller/blob/develop/COPYING.txt License type: GNU GPL version 2 license with exception HDF5 for Python (h5py) Provided by: Andrew Collette and contributors Copyright and license information: additional_legal/h5py_Copyrights_and_Licenses.txt Original source: http://docs.h5py.org/en/latest/licenses.html License type: BSD-style license HydrOffice BAG (hyo2.bag) Provided by: G.Masetti, B.R.Calder, and contributors Copyright and license information: additional_legal/hydroffice_bag_Copyrights_and_Licenses.txt Original source: https://github.com/hydroffice/hyo2_bag/raw/master/COPYING License type: LGPL v3 license NumPy Provided by: NumPy Developers Copyright and license information: additional_legal/NumPy_Copyright_and_License.txt Original source: http://www.numpy.org/license.html License type: BSD-style license matplotlib Provided by: Matplotlib Development Team Copyright and license information: additional_legal/matplotlib_Copyright_and_License.txt Original source: http://matplotlib.org/users/license.html License type: BSD-style license PyDAP Provided by: Roberto De Almeida Copyright and license information: additional_legal/PyDAP_Copyright_and_License.txt Original source: http://www.pydap.org/license.html License type: BSD-style license ADIOS Provided by: ADIOS Developers (ORNL), and contributors Copyright and license information: additional_legal/ADIOS_Copyrights_and_Licenses.txt Original source: https://www.olcf.ornl.gov/center-projects/adios/ License type: BSD-style license ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/HDFCompass.desktop0000666000000000000000000000051214544243123014432 0ustar00[Desktop Entry] Version=1.0 Type=Application Name=HDFCompass GenericName=HDFCompass Comment=Viewer program for HDF5 and related formats TryExec=HDFCompass Exec=HDFCompass %f Categories=Development;Science; Keywords=Python;HDF;science;viewer; Icon=HDFCompass Terminal=false StartupNotify=true MimeType=text/x-python; ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/HDFCompass.icns0000666000000000000000000050751214544243123013731 0ustar00icnsJic09B jP ftypjp2 jp2 Ojp2hihdrcolr"cdefjp2cOQ2d#Creator: JasPer Version 1.900.1R \@@HHPHHPHHPHHPHHP]@@HHPHHPHHPHHPHHP]@@HHPHHPHHPHHPHHP]@@HHPHHPHHPHHPHHP ߅O_ߏK%ǓRMDO Mx47ڢ$#aՀyY9K? <> Jw9Y2h%@]c߅O_|ÅcڥjkL`Ö0TT`lh_ Q8]㸏K>CwONWrX=ﺃDWS~&rQ5_7¿GF(h˨]-7cU6,:Y iʲcg֪٫jX7 >Y|m4cD:6POܽcxcׂ!sDb ?a.,5L.`>Qi"_y>G[߅O`* H;8vx`LV_q{Ų4-8?Oo?#w nxxJJ]N¬m3MXSW&W"X8w`Fd? Ag1\1ɚ9)Q!4rlݸѠlOwԔ Oqމ=EksL1(ѨO@Wƨ¹q?;-ߕz6&Nɜo>-OsQlÑ<__|K:\0\Ygwy*nx)(E%TtH~tkJ ni0lt( K3:h;vb\^ Aхpx[4Pa4 7p &m or`x"ԒSU$kb@m]am%A\;#I)ZR,mƕ?º{-ߔ(ܥ/ ؒ&*\QoQ+7TΖb9L!U#{u+OL8ŰX˵ 1Et_\?J $u/Ԇ!)$}#lcI+|-)BS]R:ߺD^T? ])Fi |, x.M~lŰZLjާW ɥ7 ;SQ`ʖNߚޥا (׶dM:CK–ob&]a8Z$N{TnY~ba"}d)!`&/+VMP%dlM~[ ˀrWbno#%>\gǫKApHd[n'@?r &qXͨk G妨{&1H#$nF@*5Eꩠ^C5ӌ=Uc нiᰧaɱ`퉵>u¿^3`po+^@y0-YUv"$X{ ?p'1U|.K…?:g0ޥAJA"$/)uۇ~!wO%Fe\H`WՌ+7 _ĥB|NICk"lr'تY0jw#.Q%쭎R]@1DmrK6i30\[LYNO.9.I+)5Y3wQ0t~LZJ9`Ph`ޕ>1TqA"_TaF% HX:ĪfG7{y>*]ы,+!pK-AD L=Hg` :ޯ)?R; jV&}}Xek%1Qx |zI$[ Ά:;+fDMi\G|yH)M`W@*^~ E K.Rqx?ât9?>R.|_I+ ;R@[T٣[lQjFݗG,ܧXHg )832_YPW[|&Z5Ԣ&x0<ͽJJ}*a&zjo]k&u_* Kޢ  | lʮ՛XÉ%I-^ Elq2RWl79EknZ+V֟Jf.Ja;Kцee\'`K¡Ī\RB6y?Z`Ť-`QͯW\G Gm%M*sFp" "zO6f2UW{]2Jr 8|߳MaJah IJ\o6Ѕz?*y -jَS#fe"ŢW7A{ch Δ9 WQOZjwyJ6Ιk-VOqe<Ƚ[# =LOƋJ|cdUF/Br/7`fi-3V&oj&r08|o 8S}yz&c؉&8e3]6Z6vw}k> #2XY/l5zoąp̣(tC +%vѯ yy.m}lV8SN(d=q̉c6M)}>E)V))GRo5?:mץ T[tb< TvWSq[[$x7 zE.K&J4g qtbVI[WٿgA{ }cw'㡣j!@8_vFS7)Pk$-ިϗ7ۼpw.>/ <[nDj4׻Cŋa3ee+AfȢ@DT[8BɒKOƞ? $|L21ӟ^њMqщ6,oG 턶Z.+6| ipH۴S藋Z <̒w8 h4ߨ2pC-/jU@ܫ ojrĔ~ E\M'X&K6≲ӟfh6<ڎyIQ; .҅&CRkԼh Zyb:60zO؞©9!O9RrbP֕UO)#U' w_:U/%]"iުS}xe Rą!mdbsO}pi2y$V ~a='Ns/>#A$օji>t0A$Bʒn_}Ϳ|^@e9p{_QrGәFjA ʟβ𭛭T+%=){0*q[ltog%߳ Z-A-w-j4EE_$^D4 (,|xBRΩ{''i`kO>9,r_rI? xF֑`ߌʲ J tK%ꅿqQLPvZ_툮1g6z,e O Ħ്WátC?>!9M(~Lu)}+zD8lrl g#˺lWK 3Z;zA#JEӝxDԉvNz Ԡ['ˉsM` Ί;K&MQ]3Rfi$<(f /lO ^O,>y[rZ|QV\ʱgRV T{P_Dvz@IqdA^¯j0|O` LI\wc"3\&8H۴7X77'U n@BMIx8φUR⸁D,}iQj"||mEZ!m*̠ʵ,N/o3~x;mQxFeߺ/ $bq3yWtcߵ2CdtN9=8=T-@̱kC(gzn%]p4@b߻33`Z*Ünڣ( G'u@F|<^)M/aHP^Cɨ[TXQJN,]0abDiQY`x@.~%&fD};uE*b Jrk_ e[C^WQδ<}JnCe?ZS1Bs@lh%o7TL h{$: â@e~vDF\o+" 4+cC,Vxj}?NDdIq-ڠIs%rDMlQL-J9[Iqmfr2{Fс٘@i(w:y ux7P4ЉW=ͤSsKK fP.IQO'/jqAuaK^kFpY-?e.(W>K'Z)1> 1ohw%)ǫ+=WNRwS/.,#`f+8Q +ܯZ][A$I1[ nSH&f[7Nr0H<]>\cXl:7]VMH)}c?<|8[A*;Jߩ^\mr2Ձӧ,-e;* xZ|eZMA/p%J +>RUJ(݋GF^ K-}|ytѧhO Q%ijFJ gY )o U qo쏉s IÐȪAeK25c-Ifދ~^~~I*M0VXwW AvYݻ\}2 \9=ȶyC܌"=L)6ofbH cFW_)*,J (-IJW 3]#+5Z313ݲ'P؆1/롟wK넉d#}L.7_' LhBU'qq*Ȁ̌z|#:²/!pR\/E8c9Tp3z''*G>`$<7"a{jĄBțK1]LDf:%qhFMz18 Dbchro=Lȣ8b\6"fZ 0t*ԝ\LƛwЄ[o{Næ ni)=lWxJ ޠP+,p1zxd#/g*=q$gcZ/,E@c bIP !c%Se+@kxHtgpUfog|AЗY^·\ejْp%I"\-нl{)4|9[pj$xgȧT*B9-[v}Ȓ|/DS+ )3V0D#5y8}a#zp$l2 3ۢ̔N+ΧUI BZfRrTN_]f,Eym日j w;o'pƑhy0R3GnE7eC`MUyCgᡳpo~~Z_Re_Æka`Msrd{th(,?C1`O ?9!gA axtj҆8}ĭG_,N+3Οom:1PSRPOp*&n6|6@pjt<͵NHP"ܝ6OaPٕV+GzcI ӥ`}KnVGGZ-^~L͒)k^q ZV!_of.Α&@5X4Dْ% oU]ų($ ktKHS@բ9B9 s!~庬sb8 Ҭc5t s2?qPz(Bpq{;}}j_2WeIaH,3OxĖ8 q6 :y5ď<`JMfRUD9D[=>;?  { Jߟ#Hg̜\D)KJnabWtȗĝ3 m!=`̲̀;C9:$|R-$HP PiFf< ,!#w^! +}l 8þ 2_{ҍu@́6ް\,pu>i /N*./\l?/kcMmCZ-a Lwb;WVIP&v;>dx*T$vڋ!W Am!b>]i!'N*}/2՛2[O"N4+SL&>rNpfY Iu7f텄 O6'̄'Iқb&{sn߮FBy: mrC)Tpq=xHb ]uJ=TR;.bBO҈ѯbVj["oCO ܬxaX#&׿B 9VX"oO)!l$eᄿ֮cH\f?@Ӧ0ؤ=HR4 M~+k3>p#F@,q:6~ׇ=Fi( 2W3P0^ PW:&[N2L%n/LoBpJQtw;#?rȄ^^zR&k =OE0ՉyT7pՓҷc|l<#VԶ a4Y^]fWA!(#jvأФW İcm: Z&VZ:~{BKʂ8#Dyha7)KyaH4Dj9,uxekqyFOL|XSɟ1GVU͈ۛ꽔_4WF8c}v9ӆr鉭Q=gmkؑxpD7Νaj1( T^em??uXRhͳl4 @ڋn:'HE882 ,0rﺄIBPLS\lBؿE)wA8ԐO|f_԰\>eo.a[-dt11^{.Vkn>yQqf\Cl*ӻ^31X1-wW韾)Ǜ9K:kMo'2s3tYu= ķ!O"KW\t9-Sܜ>*x])VBW֗ͭFmSj@77,%7[,NYJ(46T&/ea?݇c 볹hW}(Hwu3(,f3i El_fjoq%@CpeLUڳ#ScVTM=KD1ټ#2LyͿ 8W$W+3 BJT%=1Lp]Y &'tn/;UBm>*Q>#v "jL1ܩ[2DEmWPI:?kd*ugD VBk~Ǜ-(UXe:q{/Z:  fcS|mf!eЫXdQ `$1 }@ISS+&EVpӺ/Y0䶀]~6׈@j$ӒiO훕qs k1pU?KxUϤOmgg^^G.蘿VJgESƼjFe;k;~f{pGYxh0^c6(_/%FȺLѵ-Ҡ]ә'{ca4aV#@C3"+7L7_'W0<,m&1)=AI9s׉0pHҙ!Rc4fEUcFJy_2",4ou. ;U+5S*V$aHw>|A`>DH'^]@zG6E@>Ήakb2 KQ,TzCZE0K6˾`gCpv|M ?ЌZc;5=pX]N}:˟V pk|P"e t˽Js7j؝B X4eFɃi@+e`&57,A}J.O;RhV:7YT- {aF1v"JvBD}OԓdpʄdP.[ ʠW'-X@>ȅϺ+foΚ *b`/C\#͈#3E3Gf22- Vm * تq5. D䅗-.I[nsE5? szWd`%IMkΨ>EJhzDPk=qػLE!'pz_{ [Mk_Q#㷦U_|H1!S3 D#*wMՀ1,ϝZ+ibufj2ޣ sJ W &J5 8wGAA}r]0|vNo1._lɾ"l!;KoH.JY-.~{$Ֆ1QRgZ#"O]*FvDe&<N2gUZW1r* :}k1 aX6vln>D]ak0#A.X^p`M#$1)6Q7A QklS%0"LP3Ta`COv'gD;Y#wnb\U< 4DGլ%6 3E1MS)jM;f.&fW8~hMsSOބ4$?}Ȳx&\7U Xp iæyLis7Jp =,.Sc*ЩBe"DgB́6%b1utK/R?/BŽ*͘ "rFjT(! b^kUܓ߳.q)O}.7(:r (Koaf|P)JPP!ʣ3[%i[mG1E;技(2ΒGwC"/DZrTZ xW4qH~&4nX(g+KV!dBv?FޖbM2- esΓ1U-l?APPM|i0w jOneMKl8[5tjAy܋1:AkZ鲀z\ LY X凲H"MTu)3MV^## Ɋ(|%S<4dxaMJq҃BNg6@Fk 4<`' GHb 픻7 Zqb4ad̈́~(FAg #]҉[-S*MX'Dƾnnd$o̔TjZ |ZrH |WURh4XCT4~] `Gݳ,w':Cڷf*ڠ߿12TEB*Q՘=+,3ڑrV`Z d3໯ _8/+I t4Dn;wp2=^& {C[Oė{y478>.xۨF~h_O3~ޅYWdpCpHL܋Ob=IW? i/t=ۥO?`†s^ۡ\I0;Z'-\Rtr5#zF%{㫑^ &q (6B=\ή"kr\GF{97hlm|2 s!|2 sfL5'4p\"x KZkzYW|*s{p,EPƹ 'Y*})92@#*3!C:s걜&lTkCZwGv_dWAjѴqT9:vg tJ'1@նy*ʱr̸>У)M&TK~+lODBwEeFm={uivkypKR`$EۺxtYS cKu]~2ܼ,VdqvϘ-2<҉|c:dFi~ܜI~ 3 4'P<* U6]3:HěD\ܭ/f;>2EN|baUm2fǹ1lö*ɸZI-!ɥ Ϊ a+b^Z%KTqp3O_ <;wQg΁-eR9OS.@ęƔF|[ᥟܪ&>#)ݬT"ZJ= *2nV ˨8hv_CĞ%`ŪWM+=+I):AC='mI5YFvtB8:i!/1W%̐ t^N8{X1+լ!uJ9q觬ُLR2k6=B=⦼$T?MdIfJS (ТHW1?"j 8ųMk\/Wc[W1 =U%XEюȽr`'LcoeI&'{,{-O! RY@ᠽ|P85W%{D &H0OG quO_VYu]0fڧÞ?x e(NoCwW$9~A#3{}@0R.Ӹ/rFЇFcmQs(<QJ#l;+S)쥷@+% Gz#+ZXvq޼ τ`{^ ^7+m9Mj'[@*(Uv2"́ѡ6)(L~C3JW$ 2-iLShd; [QX`f ~!E ]tn. i62|"û,jBV| ⧩!khGQ5*qS@'qPSP̌W4+;uFDOYitoǤZpSw3F̅v\JA]-wl=WNBe@oՙ[d|ZI;VTҤD}c6O"I |9#u+dUFqMm}-%3oL#5C=4>N{F ?~?S"S3?X/0Wq6\F[p{yVI_Ӹ.?l}v{t?v^ ;ǙBMmَ0Z^lu"8?}* W*KufY$m@wέ b3:?W9l>R촒i,(Xǡ[/ZB'NۯJ244J.3{&:Vɸɓu4"mFWD$٪srQ.ln=)O{>q2q0[ HA,Jt\WѨzo1IlT- odj,ӿ*5OV\XwWvhXxӶҲ# ku5]3r%d hjNm)8))񊐷\T C3/V_: اpzSHCJ޻-]*.lz6ѕK˘=I G2˙-rĿ80,"l2)+t3Ї z@EIlGR`wrt:( QQ|w<u @OY-|Ju[Tm|i&EF{^Ţ5{-d^u;Y P$@uKSfH+Sci6q>>] TP $FxB0LEר"2 lӭ#̚I&hNї{wбg [xPt'I=eLQ^r#×X*Hu4xG:GB,E9[f(Uښ l@!꿎[CL o\?fx')7/>;G.֙H}o)4/u;v TZlX9\5INoQvʿHV ?ڐz⪈UxJ9{tiDcd"H7ܧݲ@e- əS)u&K3J@e"Fdg0Xk:qiWV'{)Dx />E KH4 [YV^#)pŢFXaKZ `VҸMݐz:VäM\Cu6,ڠ@%xD QY1}JS{hTB<,6q!,\٠_ F5ԨGSXZ$D9TXJJ3Ғ`HDn}bFy@/|UoYK]ctA iLHMu@<5n;R2:xHJ󱚥*u Y\/Ɗf ;\0_SEqtil#Kڐ m(ΩmsBim*`~aBgdRg۫+LU!iԏzEA-5'n25n:0! 0ی Cy,nd=#4DF,p}UGr\ܚ/lw ϔ1u41JY Φ˹gs$랩vBc_z3R` 3Dͳ__ i-@ I"# mmB%XgK? ~]^/aEWE:xZ̶yrŨtױ*j EA.i*H޽%InI# k0%֭vC6G2ÚevV!y;M|l3b/KGRY@ؑ<,4-YZe_JGMg [rdP7$^!0\7KZUTš9'bnx&柇qb3O=Cehd/ i"gbf͍cX@OAt;=ߡn L&Wfv$RMHrj*l<}wO]([0ib+ "6%c6| ,sVޘx{`2_vaXPe# \Go [$; >iBmtmsaYV^Wl 61Z<'ff20VSN$w2iCk3C 1;K P|!OxroYGB7`MO8'C=\Lj;Uຜpjӽ7D29P1 ORMy?4'HyڳV"+&'!6sP5:h3xu4VhPGe 7 <ɾk~/83$‘LIáq4/f8@!3Y!]mNi4Өnl/MQ9-Ѹ9W`b8m2l|=xk:eb_~MnXﴈ`K]1Oyg9zC38{[BKgx6z hrp.nKJ[nRrI\C3]aՑG#xH!puN>f,?9cyЅT1zW2jf$DLUOQ*|!ll,.-yBINoH9*1-䯍N9y!,$%G{)R%h$*rS,aeΦVw*Zj?O1`Vp򤅜Lpgq>yIH܆./^w+ꜹN'MX&*ɛiOxƕHG0mg1#66!,zi b_P=|:Ɍ_qe"LN@d^M8ⴼ6ިٚ85$Ff5 t&qMF-FЀ;h 鈖+EaO2hKe nSmԷrK4rgD@KF~(*&%h,mP@J"KZ3&+l)Tԫv=j(f[r#nўmniL@$àt)?>@u{XmvqQE%|ejxsU+WF}>5JVAcˌr1X6\0gO^0x}κlGQ!һEmel)10\/`2]J S(?R eҢ9|~nz6P؆FPoɮ~KS\\h7VDQk2՟ʴY$~U<&9HgSxl)Z38oiBPB"8e9%i`+E9e_EeJ$>81[M5.#ht r&zѵVXXr#Zdqn(Eb6s34 Z|^/6|D^n;/AotY*[t VXuĠ Sͻ21HT5d[(J,47Fe^ec5 ፡ Ri$*Icf?Uy{>y (zfuPbQQ:Go" aC.RynŠMW$azf}d&޳[NWE a#`-w90cg"*}Ouţ5J&dWf ==׍y炃&g~OŸvC7ǠE#FFoEgYE?plSXzэl1S4h7wNCHd zχoMcD,#ES.K'BEu (`E]0fz1iI3fL󍾲,? 9$Fխbw:-_zg m&`AbAR88LI]a*%9.tHuҳLʘ;CM>'}K&qn:% 6nE%|o } #Cs9!!LdlUvbX&IaJQ܅|!{U,k;l ݕwfέl[ڂocՂLRW=±~#"!+0"#U<>m9wipƧf:9nmhoXb<ڀ[5-5o`4퀃BrI=%#|ygyeүz`ov~?ܾيI;mq., W Ĺ#gTSPd ܒ!SbMׄnhg]A+ЋZ1*Űy"&T#wBNVdO繋znCdށT7sc,rQRєOy:䅖/\a#2{p㛑q|!žR#K]_Ō*viIPLHyA"ݎM׵WoS䧠#Ks+G{DtRaCHUqMluәQh?Js5HhۙzO]$H!ɑwz}7:@tB{ϥxćf4cL|idENBgy[(X @qQӕq~rdJ2̫anf"eO*'|֢s<߻,"σk^PCEVDeah8LFrT?I(qeidv0'D$8C`%,ޮ;=(jZ2GT,}3rO%{QAٿEA-"'xflҨ z`Xŋt<(S` `]Z?OǿkjP&>=sTjХKPA+p}F}Jcp)\G2 `I_[#jV,Qi",(ZH X;/q688te9`Vv/kY-:8$IJ$u pN(\=xpv 5w+M oMLO1ӓ9Lֹa&AƍK3UPȿ67Y+(۶Da7N w@{`EXGun }) ߲!&&Qv"K5)N̾R\/NL l|? !.><Ũ Xm\}Ww:^z+GrOY8'Ѫ!@%S`̢Ӌ 3cjʜ~C_ 铵eu⥎l#' |B0&G:P%Od;{.n rҹxTFz-qʡR4&񒤤GZ 7 i (rU`F (z>mо_TGq%xht2h '7ݶ|U-UN Mj;gR©m lTw>XaҟGۘZsڲ &YQ, dkw?`Zٮˣڐ|4I*~ل5dàJ'FDX|Z._5ӃU D[~^p%Q;.z]Mõ$$P_uUA[UqM9 O "2Bp;[eoӨ.iYo SJۂ%t*vW^NH6<Ԫ]emB0B("4ȅ!HBvq3sfjQL:jpsRuusQ?" '԰3NsWc3xGr2^jIµ;#H{VE}N̈́?E4m|۱Epsfi-86h#Mc'߃P=77qrk] 'S~6I^v+*0E )eL/P"7Jjdae6IenL$ϞI9dxMplaY_W^L}UwyB~OfVn!~b$ L 4ai.w)~.&J`n32 OFDćtgun_W;]Jdږ}_2c~&IE_M_-)*ЉvfD3AYфk2ʵ 3g# ށӇ5pwyr7Ψ<]1)#~qBH@pΎ%dBmY9cRp0{4̭X:3WJz`.(V( /,>8W*S"˕8GRuCw_8TOksKFo,[Xdlɾ(B*"tc s`?'?z>~9u1$`HZ6sdd.iǩ[lj {N8l\"['Z]|yK{N0d q55-T[FTCF-q_8/%i+ 05'XLQeՊB"_ΪT%-8 P}U p65NG4f'"ilG`1Kc_xm RmKEp{t֢GZr|D0zI闏S@*"sIVCϽ7Yċb]"VޞWVM^4%`cxjxZg"lx-$ީ-TsQ)ګsU8 ^'BuÎ=s˸)n9+SVy )@(*p9c=xx\G*~-ߦ3bc}8g3"K?%_u%[$,,$DcСfIݖݝ3nQTp– M؇2xL*ȸ+h (ώ ve-5>'/+7ҍzփd+hD=/+=՜)s_%g_"JO2WgzdF>"ǍkF 5W3Y̕vD BmD rA1{_'Ir'?oi'p2Wp^ԋD_2)WxiI,o7 ^- |&wzxҒ#VsStޙW<4eE eˍ[ШNx5{Z_OF WG?-&V Umo/@Ȑ Bt0mBE& 8ʴ!;z٧|(lc^si_w0B*l\ˎPG:{֎eSՆ\mn3[# >IƦD84MPj1%n*ӳۡ rX_CYQ[?I:cF9")y/TZXi}}H31ͬT:DPRC]|ms\.)i]P׏;ۻyy tM߃/&dkt\H/|??\4\ gr,-27u@XMgZR Qɲy_}\0>w9\l26M χP@y(В2fL|e{X qT-ΜV 1pPA'] X[xa)NZ2rzrx@Z4_dN ܾG|?}+)&"NEPBeœqwArd&o Ej8p$xpʈ Ԑp@$ }+8ِK.vHHmgR@%7pI5-/ BlYZgOO~#kLS#Yof2;neݺ.k8.D(xbMԯ p>[ N梋o~o+]')R< WW)Q\(Ko҄Lx;ߌs6B[tӏPADk݇Ԣ-abс1ՉVC||O؆$"X3~}p4QgWih;JyzN1roMVѤ/-Z}3O4f McH:Zo*6Ųo ef HGjp_.8*e@5<6ȭYG߻'}*.54}b5R>%>7wA F};=>}vbJs00 3}ibw+}}{9*JԷGؒdk]҅?ސpӀ_,yEE$o_II2:@*0 Oﺭ!_*o 0m~x{T@8ȀUDQ;r&4C =OhlҚϋjSbݴtlG FTñjo\*yKW!Lh(ͦ:QwHP'j[^ּ,=/?>[Focp]g` )f>݉x ,oOHҖ.sYGgq^{N)ö .nk6'co]nZki9W+b{Yt {5Pؘ8V \#WCsحP}n ڒ#}00*xm]+5A'LmD qOS,} O>MBEEڸ|iZAG!\خ2y@F>Q_D gHrOϖKplu+ݴp/n^ݩ A$4Q%mVqR˗/SM[5E7 \M=4Q6VsIH}Z貫\?MGh9?PwJXCa}ʎ''Xo^Q^7*~){NLSW!3Ncħ "w3?׶ad\OlHR\pj#_ Hӝ7 ; ,kx=D`?å<>t3S9uU >Yqk¾X*$@0Nr._,LEnfxǁ2@4zěv ZH>$r4jN†ktkڿOH@wpXS1 nKً:A qv)@RdғS)+$ÍvvMKiW| Ad>yMHI.#NoiPp<ޒE5qv5+q#54wuIEn݂71/5dn:xRҵ.MZ9*Pl\} .>=NN1pa2G |W>fehYA 5䫔WQt~!j^ hUĽu4ͣ<'/hViVZm,(,\a.S4_?Y,x?Jq?%j<|/q͟n`- tdMlt-ur FpE>[|sMX= kѡxS ٸր6䚲h@ӑs%)#>HVnLy@s ~vETr*r'j4Jpnմh+`KQo9}bj_0}`{e[9{ʙXDrJjN|X2qabDYJ؋ъp`(Ro9$%bo|8哀#9 W/خxo qs'_ N9_:i1y4K:,){qh@7R;v]!D2/P(VD6۷k`'=R ><Ã(X⋭Lj)B߹2)Jgs %Yzq2Ө\#~/doXqA @{,j'$EɦI!J{-fTI%vӧ9R|Vmw8Lcŏ&u+k묏l @ ::%7&K"ُA85d͌!X]20sEo5ɟZp e%_"+d/w2MMMk8rcƝZ5#FTԈF#_ .Rj* Dw 3o)~Z`<^Łެzos͈vI2/|v+c*ZzgQ,͔Tf- D.ޑ8`.+E &\#ڒZ7\!qm6faɆnrwhp>Xn|V`,m}G`Ra~Mn͹XZn%[ S߼gk'ж%?Gŀ^nFRa3o0(Rx0J'5yԙOmΜ酔ѱO\yT+E7)d0P^Op"h)`DFyT}n}B&j])S:,t!ب|"va[j{tz̬E, YV65:jL (Ό9?-͗> N+$⣤lݾyAB8إLz1س]wc>ʎ- ]``"hU)V> aT ɡk8!- ]` v-(:P%>zurY|?$>rd4 `j d.[Ae3K'`zh)9uюf+aCլ-=uPrq̈;fe\&2FtzTn i{> `b5۫Ŏ=Igh,p&}їi4hr\dtiM[|" }G9qSϭ|lq.D֞T*WM@ ):Ոjtg+@N?ɖ <)≓oUO=dL=Tj׽FA E:t|ڢq((** O޳tgatECyoe|X\Ps;cm7NK;1l #ЁH>k&[-B7R> #AbQ ;;Om'3thƼ@"-Si‚3PYaĈEQ |!QZ&oY AUs>rlXIw,5si%aU.O;;0rd}oY8j?4qϚpͨHn֮ c<%ys-m*,'a?2! %pJ;ɧ̆KOv0j])AC'9̶GK` Ӫ#x1}o ;;U;hzحdrUms}1]2޷_ŴF~Xι__2s]yPEJfWudf5̑aSSNB]HZ<.BfXl?;oJ F0ȫn:ȟчFw.`%$bѓ0Ko)@d+Ҋ#/˵(AРH-ibH1],"zLC#.i\+@U'*a6 ,jMB-ʙddaȯ a+vagX%Q46/dbzPo Y@7spO ~mbpp6W&dBpI& zQ-]l2U*_&z4\HHISiMP@N3Txt3ٰ̈jh5V;ʯM4ѻ%l|(cc ;Džl4>AO]-+m@W[ 'go/C<OQLcQ0s') l_JMKNG' GTڨ BdksW GN1QLby.:+:E]xen5 kd oj[LϥveC4i ՗5xrt92r0 1xyA%¤)DjڳwHOL < OTr^c0i3=ݶ:k1߸wjE >W`~ʱo<[@&-j -ty܀oWN0.N4=#D_DzDFEV%/g?3_GBFĐ[3H:0$\uTQ 5\v][z5KrLf!$жŚj{X$Ӑs[\2N2䷐c-H^;^V! 1t%r6L<-zK p;:),|%*$J4w[4O!VRjP^9JMgMTK#sܝ֔U)jD^Տdzۗb)A4.mhq'odEFŘvԆ~XI[OV`3tC,po/L:ڮliu_l(>yjr~*Uƺ1bݵu7GIlM?VTW`fenѣ'=㻬a}&f$ tQis!G"ǬOgi8+*0mߚ I罧u?UGT8/[UV$#And!q0g4dV&Bq}H?}]%m1՛4p%(Ι@A'PҢuR^nKO݇((=XBjxH"3b#U.F UH#v 8lLM^G%C@Yzj8n?u/t䇏TfX6곐Eq=#~,ӫT#Ji>VW'P<3F|i% ? "Ke{~ mv2n2T`8Tc`yhM O (Kr#X r:'FtPR B裆[>Yx~|j QnE"„2ݕGaG_h~B/vhE?FGgu?\typWA~ֈCHͭ]H -ZUF#l^l\7 E7-p%γo]r# Lh/.ZvTI>bzi˾f&:8}7QkHGl` bf{(J8S %im˹gծbf<=a߫Ʒ͈na;"7E>]VW{FR; r+͇7X7 io!ᏃO.͕p*K hV|{0[y+Xk>L* [[W\Qgp_#l6avCǝ'C8'BbONaMvZ2>3HFoMh֤؊x骦KXCzQoxޞ?B[tRvc;5:ǼO:MIMެvM&P$N?Cu 8k3gP!A8,bBAk/k$G([$o]'_ϐ Uk6FKavAׯE;}JoֶW"!Ĩe*pn%U%T.| '5cWnBy+XNJfO3/Еy+7}JI"+@AR=f(2z 7P]tWCd5؝2FK-j*w(h|M^bA,p< :whzigˈ*%i 7U8 a&iX=wޏ",[q,Q>ZVcZ)ӾU`Dw53rE3 .Lh%VZi"'%;3zB vF/n%LX^0MiZ5ʋ0g_~`J?wj+^ս{XChdO}`dsë2jSE-r+3_T*(@F.R+r*Yҷ'gp_EW{v:k!nE&B 7t:*c{fhB{Nj(kzw46]ﲡU1%þΤ'7ej7}$ѨǟZ6ȸq0Ҳouég!\ $C_:"ZveO-~yGOe xjnQًO/ׯg`e󢴑UB78bP>V> 7%Mi";jȕ8r_sd.0SYۉq֐h mRQ*)nk+ 4Q>݈UP25z\Z9ߪxLEdtj]w Sz,sW$ont+GE*`lr=6#&9DZo0᚛8ӊf]^^ SAr.5Wr>'^]֋_bzn劄JIͿ0zAFc*gSh).ܸ7(ݤݿ츼$zƊ eWBvp`b'В|J;3Ғ ~ Rp;{;RdOGtJ yZ`6z:'0S2?A~e@Bv`ZVK,vHE|MGŀ'EOgYtħnJϐTPEV]JnNX+'"F . K I]~ndLo}s($KT"B*p60(< 2{VoPFb)f@V)ᴍS Q*x y03~Xi!X.|C`VRe诵 HZyec_ۤԑӥvj*=23Ƃ%Ruop~5>E'-,|bI?W;-;u.j|qjM#o7R7gj1A\ܮ_C,K=JκH@_x)mhhNV 6v c/v«,((X,MY?xƐfjԷ(J%A[䮮,GQ4[%eU Zx5eU Y'E$p'6߹S|+Ƽdei  X('?T`n iOq=v*$A:g2H5C>}9J|Gt\k\.־$/]nj]jXi6*OW>Cmbl'6r1PhGpķ|ij<>WX8EA$lKa ia5E?$,: oEHA4>Eh#މ}dlw5{O6bQ` $:-sJ4j)U5^B!Ȉv-FލUhhQN_Efsv6#qmHim> Yx2J4GݒJԁO ζ*Y9/G@lj OZ3a 9zƪRAu%v<$+'P6.\^S5WGPExm4ȈXt^ھ5Hߍ|sNn&QɃ۪k02Ze =ZѹƔc.aL19mn~JU`.HH51$%EE.7%=xDNUw>xίa9æ+Z.y[ |ʽrEA_):za3 M|[!41oT#7$† EsxrJyi_Krk?3f>zL41O6T׈1\VZS|;&"Ghߣ)?i>7VΘn l0#Y8P䋡hxdR[tS훚nӓ:v +}Be*]Ļ!rujqS<*O`,ƘJanǞi#AكWӞph߹{yo_&;1w[C_wcR!N'jTs&\ԍmy7H , L'jN$=Ϡo_\O*gr=<,2 0W  jJNsmyʝdёgL?FQP XjuIlTT3 {2S(*s5f _NȫïHU/wp]SP $OK 7 ]9/5W-& ]"wFOyw ǔW;ߟ ?&/Ȣ츖gkvn_3]_ ǯ3#*=1dġ.R`R~jQ}͸5L(sRl;bڱpP9z˻i5(HeU؉wrC t8…O)xV \y[FuNA%b"@Cχ1XK>%lJ#H}P^;#?ޑ[v=;mTQνwOϕ0t< 3!Tbgԟdpϔ5 5|)bE4QgZ* 5K-/lQWnˑaRjji 0ԔtkwHM#w4zI3e!|%1-b=?>g3RˤJ۳ܷ!Tbky4Vam&8 6N:C_|$ժT14 GF";xo\mU{IEGB1N|J#_ƥ7>_uJt1dSVJ+)Uq^ cޞYpJL\ cKXw:o(_Kk%NAMK3*+.Tg%p^ل$nQ(=N`e9,sZP%2g{UP#0k>"hz"&+o>9@}ڋ=@Z tz)QuIk en+롁N9fX,wގiUHU&mޮ 5^h,"D3o Eø%Gj?uRa@Lbۋ5uO1R;Q ۪m rܕn_W6#Q+'Zd<ʂ٫׷f'n"AgO@uhdz(!R֎pC$)/ސp2| vC]c.%`[{/{d7\_]S";> myŜΝ_ KYvb5;2Ɔuy1n0K*#hyTޤu{ض`Ie}0ұC3TYo3;:W3)"bmf7+U&.B-I&S֛t}/,2āP7h95M"a[P7^#I7Uǯ5 pSuQ H{♡q-gڣ2% A [(n4,mYy Q--4Ċ\tH?ֻ^h/&aDo{20{-ɺ5$oG3HZ~{ *y3V:w6L R)11VT=HVPmO]L2cYT9UNH-ezB>Vb9&WLpF|(.IR6Iyf"kkXƭAL'Qצ'0ث:'n. P Ȑ?db^^jKsM+Ubq1 ]Ց/CTUl`fw&AsJKDMG"rP=ȹC//TiX-nAcVCf+bjm&!xPȧ *"a p\Dr)tJ}&W0?Vė@榒:h8my~'حuh!XXŧ.5դLSʝ vsD:Egjo `⡜ u .$hsaWUPlx_`z -b ^GM8:}^xI(0zg۲W1nRרmpYu|d ~: p=jt͒ Å?I$v슔+3j]J]v (p84xEm~T.jgfA;ȋzd&ݼrHj*/hBso7V)p _ V(ï?9m(J%Dԡ͘QGjQ'E}2:=+jQF;X2jh'nC"ͰaY=,H'GֈoW)Nx+;Kl'Kn\Vyc!Y} i0km#bH:V̄{ڿe( nc%5 hk a#ֽGv PyhKڒt@\@' mw 7x,8i6՗@t\θBa!Stn VUpv|o@"ǛJ)`¹Z bm%5B ,/`I7;m*z!}wUle˄ܐ\Vu}b䅯VCqYv*h345/io>w? D˩ʗ.G?}P'd&N72viAejׁCaɠcp"F$G0G8]Nӑ򹧘Z |bTYAW GwtCS/V]j=!!ꇁA,&v&^4GLɪQ Mʎ͵F" ېQfn)|^!v5t9f:{6Jn[|Cz1U:$Y%$ANad/)o'+B૸7EbmDhK[ B XZS'T4e$wBi7C>D5^ 0+M⦐(mtAu1^(S(4PCs JeR@ RoÀ0rcXv2AcX֗_& _O Ffͫq_2:|p:֚4"L2PK\ P hO;/Q+V߅7MNe`Zj`̶³d8'IzX$5n 5id ?D_ܚAS%ZoE&L "Z+k2X%!p_N"Sz7ԃO)>_ӫ{ozÝAٓ_f`: E^ yoN}1g Fģ<f0q8ADz}=hWU|.Ms*kJ6es [~aw@ep $WGov8o^[<^]7@$qP-"'f?!$%LBXKsa ON>Kr iW J*jqו-b3N\uSjgO-."BK#%JrAܟψWHvKBUI&b",W h͝ a`fr|}M%4\'[[Q Q&qݽT%f'RY !4ͮv'u) k_K6gP*IQ}WJp#Vi/@ I 8s^4b.щ8̭E[,La SP_ `5>j{.e>aKzJ nʳL+'(&(( He6|w)d}MrUE'yhm%=5|;F88o]t,b 3D(F=e.ZJų߭%{;#R-}9o}u&JH:ȝ 㒅O@nM~C3TRPz6x*yxgTqj`Cŝ[Aڇh5%j@7՝nR~Q i=h م] 6Km빤K V{j@NZl}ܡN~_|Łl\`4$3jqxgpPS ~ef-3[oML9K]?镫PSS!%qhضwA;R޽͌l ?#Ly=cuLSJkD>ёmܐ-rczF{"{j& f1O$zz=3"yba5ό.NA[ăvn υP#WY6}Aoyoj PP7]15: 2W#%AECbI"\Z$Q KPe'atb1=T'Fiv:aQg,T0!+xdv~ Z/3X.y%й||IAgd^Pzz* a#q3Űq|RD0: ֍aVupLj}/2K0рe-ͬdn PBqvi&"zڭrhs덆FIc_)䥂-w8,vɲ;U}#C:" \ܜlI~]a-BpztmtT12giUV%FsWmG!yZ("*SIvr8B W-&n1=}'s}2 !L-R\2V%PfuNPs29f|47%eX!UQ+TKG 5WE8`-]( 24Z#/(1t+_C O2{e<&v9|3YmYeD_^HnfˤJAAѡ59̹-JjfVbk뙼1 =o19(&|\FP})>̔KX:١BjVfUN.] * !ӘCM^m?Eo9.(kfx IdD,/p&[w/6AICѲ]CbrXDvglT>~'Yd11$rŒ %=?(:-* 4:ӡ?KÆ윏^\iP1E׮2r^TŽؙ͟SJ#niakF1bYNVܿ,/,15Ͷ-bkֱ袚Rf\";,WAE@js%/Yu.;~іc)1BqEYvrzčg( iG1a@P=kPm w2q"?IZLzSjVbI9̡d[%Lϗt<dC!r5 k]^튽9]9-Y7T=j&tTAP} zEnFa &OWp$-/e zԱлy[Z&74ǟ5K;!%O4FifRQduע~ST5(Ū&z0QJnM<t8[|:]dGUFte⑁qN6b)̎T*Zr Z/,d+!SCweYƀiA"#$J Q8WEqߑ;ɉIj(CK9^3bIb4桿/>l|H?bqsݕG}`',jvR2EڼǛآ+o2慺y^aF`9憭eefk0hF9+z" E:a|S!%-MƏS1S*,v{7f6}W&BvYod?f=|[{/'iX64#|AZ[ ~>+<2[#Mߪ%O5S?“R #I$P抉{0m-xȒEz?sxNifg&Q1kI3zo}z) ИtRvc;5=d m~ZZ,stٶBJڦ-hExY~"h +%vt6F1/$p4EU@><7sd^@L*&}+3V%qbcA1 CFjIᙃ6;@ ~y%t5U1!hK$G_9Zzy|<)S7ݑ&t'a\pΣ'[8(G3D` kk):K:X]φ|ku"NGAM*@azD>&7[q/!}2\?!pRЫ]uBqw yOWدk";VDW2ۉan5sirG*..5 '!93ʶԑF -\TZ|ʴaj>øw;1H^ɦRTON97yj粒:.Xo[OSv3ɣ oh2@ @DkLC1M UGNI?\7}C:[ɌIޝ"&1b5|i;&A,O%+QqJJYyނKHV Wqx';ηCPR0QjMRDߴ*dҡn|,K[iBk_0WSO+*=/C$S`|6͡G(ǢmT?k֠&;gO6t+He7|}Qe{|6sP +R%;K$7Kśҕq$"Ap94vٛSATi80<&~^c6vYa|]rsm[ m$S8t.܃]WZH֔/vbLH` /9[ V 0|wHB ?7=m6*2P(T"@`A.̽wD ^㑃 '%.?g#XFc|A٨tVـ,Y)C+Z*Stwb[xCG Pj"UU}m,).r]qt,Ԕk WAyv"۫`@)&3!YH•!} <ޥY^@QNuk2E'pB4D. Fk#h=V=k ⤔3jꑽg њErO)݅\<ݡ,]tY){Ƕe6uk|Cݔ=.ewxԤbsc?}Nd ^aRQLnm(D"AAwFg\o%v[ie^; @D7-na{D5|Sl˷4d&ts{pE *lC^ 6) rKsXIMegv%̹oi4ޙ&؞t+v(J#jV6[z\Qb,P̀F4r1YR"zTN"&B.$wB.z?IR@jGڤ!5j=ڛ񭶞T68ZUR+'⵸ME;sɨK mVQR*3TfmRFPK%c8֟9ti4^@x)W(7Ud2tg@lh-Vʚ vؤV.Og0}\r\nOVv R@ˋ0sWGh}*, =Kr2 "It+6R /4xD1B8R.<@C ӄ.֝mқQ]_:,pη: 9,.貶Wl[Q"K4?lJPB1 1:V(4%aK? c .]szl}6C/O§h'nu5`Za z*cT{?)5^gm'\r0OftR o9*E̳yT8"IS!?.M4 f'EjN NDȍj+O#xY߂^y??!@p4z.?M(SGfV"Z`[TFŻCušH H3[B&RN33ϥIV:i׿pxoϲ ,ƭoOd?_% r⭠7J.FR/UMTH7M.Lo? mL!ycZ.HjYWQ,}}RpX˘*%ʘc38d45uGٺӒgIԠ0Z.s;wMzZ^önRnRgB@0,J6F--1L9S=5B=#jfs2aua$XְB^)P酂KđsB=0Jӳj8!)"j  `ARo+#3eV2棯Nioi|d4 Ά})^Q'\rײ+UB/SU9 c=RЗјQ瘶eretҞ<-pAHIɻk]{k7pzNXJlVAܤeHeнQ}D{q ›[ ėC*gj%KkMvJHE523m:龫PpNuRFkcXԴ KO#+ Ut5ШU픭rpvr-=? Pߠ$^ۺT2p e"×;Y10fG U>`Hgtw/Qz-Kp '?] 0 dl iŠa?ӷ#¤ @rF̈3k"1 ;tt[1(C2R?10HvAn_3BIQtUyR91anܳ=@f̳B`LOJFHC ʞpK,ObT" a!6 X`bA[LqY #X%JMdwʍ`VyG@ e5wm)>Uz ʜ| #@T~\%С+zQ҆8cGaw)3XIJ^[!&j2;t.~AQJEz!6xQ2#;X5(ֹFf\6DjHGMD n~TCB= 2"c 2I $8P<ü aTӑzUp#\%;r#H\麚BȠ Xh!yg(֕߮WNu+uy;7ʟo81!\)C}9o QcV \Ct|=vVTE4i$ x6)J#3:O` EՕ]w/lr yn7Q7/؛%rF#Ꟗ_6qnkեe *A J8洕褸j5=G{;zC߅k!_WPES}fw[;QEYh zqq(P&)yޒ|> ގG [ ߥB748<)"hRFV3;?-9[ۋ'ţFG/^8Ԁĩ9Xے@MgڲˮiP0$'w@idycmTQ/Y`U7fŦ̤]&bQ&*j4'ÂqC+gĐ&g[PVW 빰smRM1j1SpN8=O}FJӐ+v`m8)/E2ǭy*t3 ksp\eittDۨP`9snn=7Uwt>Ws]q ;J! ǵa.W.$B$÷!Y1s*3B5\ Ӌ[B$~gecF(̈́j sY ˌ~߮jO#㐂L(JSN`47刋6w+B2sudt> (Ԅ $Uda^7R}1;|/+# cRz-8< -H$yT,|F`uҪf%T7pc)WZeMȧo)tɨ]ddAi %8eˆՆdultrso QUvv;+2 DG^; ΡצojLIKdy/HrlQޭƏ,VG} ''$]_EIOF֠/W&Ade4edn}9C%fj"g)Y]:䙸˫ Ԍvf(P>9M%9 B|/ב~; giLHwJϷ"t5 ^|hS>1[bJķXd|AeA*9OW"*yoA;cnG\Yk4[j q1N%&sUDCgN[K֐;CSvYNaimT?kiB`H{K%Գwσ*$ h,5RS 35.Ic/L_9݉P#3Uv'+]*Ng;M@}5X] ,^oF&Qhe_6AF<QYN]5$<0 G&i{G‹+Oa2F1U˴- ,+h$Ի7B)Ϊ%Hvyv^_(DT\P0^poɩ|M߄2ü[qpF A'anDBEM.bj */\fcP^^LkK0$}*2!Ϩl`̿W{ vICUbǼ\ >O{{]_l!#Ek^-/ž^,&2kz RG+(l;k< `f1 b83#1 3tA k_47=QHu An XOpT =1Ys|+^?BKB|߅>3 E Wbd U Ar{8!X(giË/_f> bWy;8e7 v D4q ]ĈÅ|(~Uq:Me-8K=D&S1Sҝ`)A-CKgpT[Nen[|px5kqjL`bwɚJ\`ӧG+(YPJ.o&gE4`oI)\C}I Oe#6@0)ohG?M|dvTj&֙Z"JfsLyO>+om,hf|6 [Ubݠd#6U&4iqf?'Tꟻ+!ub8,Iv`D.;O:ynaɨ~*9m2mxZ stDK)XyԈ:f7K{\"2#%rvy;\"Zhd3|i!~RwL&\dوonY[h\,yVpz3g/(;Se4CP.ʩs[JUjA $0O0sW.7ehL;Ec7C $' Qxݖ7g$߳1SB? u)35́7W|6t]<Ђ22J/sZSD #̸PWUu5w\z]P0n!׊9o}if56d-"\2Q+…V0 նBVR3M9f=,+.nSܭLOiGRL.p\91s"x*e0GKmm"լ!7^4 B,X@6H oB$K>zL.쭇GW^x% A1z|t稃ʽ Q_Sos8XFO8R'VSpLޞ֮&qК?TO*Q\eV5,-~=sNMVrݕP읊 >xk' IY;6`) @U,zqkCK h`,w$~m5Sq@W./UGA[t5j?+h r"oLE- 4f/~Mr{(iz@NAPM[RX@5+4BZ)]Y?mpgﮓ8"e[ ѥ4J]Ȩæ79]u]=i:nDq2ItF`z7C!-!oڨw`cE:e} nXq}Koˑ2W׌, 31ف<:N. m}qZ#DIe"\%Rvɾ1BNWkRH% ./hI+#=U*z@s ;A2mDĪ +R)"ňKD)]FU+ӆAV=>ȳP"j>^0w-f>X=w~ Qϗ9%7WȬ>w =HSN 0WT( py>r0qZP{\Y{;aazܣ m3.pl,*e3hx:ZNan1^O[F R,7h[;4 g vanp3&bU ieo +70!gaqAo1hp vO^N[^3(F0i_i>%LU`,5)Y^x7Z@\(_v4^ _0:ӸoZ ^`x'&F7} ؤ 5'Sݦli"rֿ/y6^/,gԂ̋3A]h+[$l=lql^2S:@>҅׊(E5(շa(`6=)5s?18 V٠(廆kˆ6ԠE˛ߜUypiF fpm3s W,Wo8X(z Gz3/|}#c5[;1sd2"Pb° P(u4dHS[ = pvo \^@Jydyzi*=Xk~| 6~}XYV +{S3)8Q3SiMï@0Z4# UT/ȒPA"lex)@K6;о!>=CrOP759S,K&39D'K%b>PCq/#⍸B >HAKVf㲭\%lXAtyR-a,Ao-X,&X\ܝOI>D3MP! Y5B:Uabc)h4P.+y*Q[?X[Y@ (a0e@țh]`Xzz8ycE|!^c{ (a"q؉$•Wꛐ^Ã7,ìo+sr_LL%9M9~n֞'_q>~\SijB|lu%.Q&gdV,Xr$#}cig_*S"!vaɅ6?qLY_vOF/zMi?v분p. D+L% ,PVid^J(w#کU=BL{/^m/`ꥺt'drzg#0+\\,ӂS|#YϐǥoړjD.E,>*_iSv+;l~eû@"bß@TRZWYygyfe$~.P%$7*aEj,pg>J&Odi)J]=| 4ɘooP\%UW0_P'@YEܲxZPUw7 PWw_KY{h|ɍfӾ. uhlZ?հ-$u#)%=fNe^Nۼ9aч;}͍y*9LS1ɰVh d޴;gH=F"6J0K`m+};#tls}.^ȿ_b,UI(o| l A(uqp%vNꎕ1AMdЏ_ +/}h'y `A0#

dU6@Lonf1-;,S炔_Xݓ3)n|; RA":CA[(Q)Qu"DD9*NQ^p6%v=vCATbfˎlDĺռJ3 ylmk@+fo&uq#(T8G)H/Q:l9ˆ]wgSapYu0B`o> W9.t{90_f!dz}SJF>Z 5sT{ű;6{Qc5a7C1,E\G.-@BKHCŨj=Ń, .}h. 芔ylhx2.Y00Cl0T1c Bآ]lQGЖ:]!0 ,:u׶V\:-|6Yd쯜 d_ݔ1Ô7>V)/k}M)jHFx\m.V.wa_ ϱӷsO˵`yևyiQd{' 3$SUm] Z8(xcLbE1-#M)eMgN.8Lo*'<ً->])uڳl)SjDZ%5i%hl aH4Iqc3a5}Qy8uϧ64ۛZ@5=26;C F;d:[_<(x%ڦ[yzrH%抛X|acB^3`:yFH4E9r(BⶌNTš1NְEsE{2~b`jf 7䣍fRV7y5SdsnGu-oNLQt!nBwcVOP`?TWqWc1c} Jr!G'T } 7怩$~?QNw 6 AI$E=2Ӎ:)6c)U\٘%AO{PqvIժC wi ?/e&h4mw3>&~࣢@򡫴ի'jn {&"g8d?Y6%8*l'*ތb`b'"yCe,*|ƒG6Y:0! eK{eHR w?H]@clY .9BP]R&Du6wgz9=hi=W&1L7g%<6wSA);lxvWQuK/Ԟ~Q{~\t_n] wjPOA\q:lPezI)_ 6LdyEOWJTAP\77 [ǕR٣u3fmB]ul %U_|3 `!L^ve3;ؠ\m2LwIӪoab1e[*EKEYnG'g/]^]בϜ0wyFIi<]_[6@v0m#О䫱ŰbT֪i[&at R&uk@Ax Fhpy㷨U:v2<؛"^UTᛗMJ|E9!&ll0̗t35p<4K7Ban1;i7}Klp  {I+0Jkx@f_}8q~ǔ^0:FNB, /X%3P};:t3gyXZqg[ 3=\a:gLs6Z (&/4_|Ժ-R-Aׄ(-,d^Ҕ :aZ>F÷'`~H9`e&LSusB#{F`ْFZwsnČ2q:V[w?Nm#eX!,v8|BR{n5ߋ93@7$iAS7C 3{Cv͐j8:?NQe;s .ٲڥ=: eiE%e0-@aV+^G pb'w#f5N=uXuO.+퍄NH,$ZӿZ|׏5kȗۇDVB%1T7=Ym1.7PBnDI3@ሶ#uhY~7 @;l];)hP\{ Rא&8B)])D] ѥL+ zf#=z”AbbHC6=uWx D Dl/jn<5mڰwzs'ߐnV),3d&FkٮZ6fa: G%үN0?]`u(BBeXR >Kzrp.y_:⣘%c9<\x F?}]3o}zo_WL?Y7k>M a/=//Q/Vzޯ{{߷C~o(4w?óz.u۾ԽoLl]~ut}TK~n}4pg Lh8WpXU2v} c1ah~Dc[u5[lL*I.PVQ#,vzfbú1/>v[ 0\l7HWtQ1$c^PjSGk \а^[_u[ҽ0422+zj❢{MA=/`%Hᴾch&| $O^dax9߳ʒ( ŔUܸ "s^=_ZT=ܖM_Sal-Ș#5lĚ$rÊynt@G$ ikȠy7 O*..y.~a͝[ WsHC|~}o@m\.rb 4<?ITצJmHy)<I7bϔ!5ʚW>Ô:z"#sg9-iB,уn\W0X[4~u ױ]k@V6{Uq103ٳM$x$ F;/@k:}lBdçFj@Lـ6Px%(("8a\k)@ `^T{~)?!UxˑtBNN-@u2=gn>S~}~'*V)J8xG01_m ՆUt Sf[C،X^>ASY#|j̑ߕIk*! c`֛S9td~6,s- ]@yebQ'm݈;$QcxoUᔋА짅s7Uyh[X;9Dǔ4| pZQ&Z.˄޸\Eh>!GN{VT vce^WԍP*h`xc|-xhJZ̝7`2dZǧT8x++ӒDևD'^LiZG7%Ie\#FR<-"s@ NINdj8&[)'{A 'n\y?kcNt]oW{8ĸlc_RʃS` se݄"?g/mGF?erp4M蔫/e1ѦVtc;󭢼vs Aܾ b*Xj:M^sS&2%?, dw,;kh!|;Hw~&g ջ2+\9>H:)L\ь؛cĺ>8y}?l@4k _U6 Pu}J_");3PE Ru[RdQA]pxR3:uOygぽ,yОWK:aC-%=g)_CSc/#Kn5w!XThsh|U޹<;pf@)Ϻ෤Qz:sTQiunk usxjYG[*fo6)(v:5OF͛u%Ob!k%d^ɮٌvœk|SInXP=UOHR״Xn{`,79$B-[9G8& \Nt]v|$st8B \af̨;2Jϝn9O@h"U~ALtsڨڏ93#EWRۖOb?SUT CdrܳZ;f+fD<:]> X~+" ], ~j6XWt;l,sj^E{svqS"\j~G}V 3"% 'k~Y`ot|tg|d&*;?2K!1i",q|ˢ"m Pv% 9;ho&e!/XSƻ7#=T?ssקQN?!t DX7.$%Q2^#zNq)KR^CܲԘdiG-+y">>ZbcZf=y1((yEs\kQ CnG{?T?!)J;h$M(L\Wa󻲴,1oQpFɉ q`8'p|ÑCf!@ kae?"5%xhrIKC➼rXӠXu~"ISAA)"+j|pG{|iTo]mKVq)pk0I2["Ho-WUΐ0bQIDRId$lx;ሑx,ˮ$>}sכbsX4Q9rϿFf?q]a"NBF4g =%VFl)pD|Tq!"=k7=-|wQ1ež3r /r@t3Gג4ij4#Ą8{ͫId"?ԀCxO6d @mExkWpDߠ=w*W~st?Xd|BĜy6.*qX&9C T,3 37ѯ0A Kex wK7FS■\_Y 2 'r4J֔sb2L&38bBj-ڂt^RB8Qo3:bԅ00+-{ E.l45`l|= ^S siqe4F0.0WB-PS *&1-^=î^8F bO ,o-EIdN^FAb4k?kwNΌr!¥`']:H2Si^=k%%`R c-bӤ.3FB}q/;?=$]1)ub^<]hVY:.!G-BkK993&:uĵas [23fŗZ2ߘ mIT 47>8h;A!etX| [|0b*M![ۣ$rcG|r=[ބCIۃ()@j Á78(:5Ci%6FE@۹C>Y Dң@}Zeie%"7"1trE72Vՠb*/r#Fq =f~ N!;iQ= Fπ-Kش6*lϖc tr:aC={ ( 4}'s*M,/\9\3b6 QuTLK`S4$z~?q]矬)q\Ն]"E$~üv_JwY ^nϥP_Ql?nƲ .TDi \PA؎d 'IN$} OIK3kI`  ɫ2,"IR<M _4lx*u%6hw._ԁLu)0z^$n3t/gE(O7+9gkĊw;|Qjl˵ºeNWAcz޷v^9ru̦caƺOE (,Զbu3A-Fwmʘᖔź9I|=~LL@)=LS@.npǴ70Y.qjd֯ C2lQV|"ԃ-zԷK27Z{>,62\c??D&'Kj_e=NmJf1(^uLgAY{ 2ߏ!EoM߄0xb]9j+3=Ỷ]/Axa͘OV};st4Ǥ}!'kI)7xO;'H阧5t8oXmDž~{IeO~c-8}R݄ JTL Z#Nt𾵊wAZ;WѴYĀp8t~ ޟ\OKzH8a.i''ӿbJ]gm@['}_sYJ0Y խٖY&=4 o*] .,X"\+w/JQ Ă1f:@w? pW᎞Z#~Hl6itt+ KRO ,oj!)I%D`#D>-QRiq]SvXm'ĭMetVdƝ,w?q)2|΁); Z|JȞLXTʥ(k6t³)zE0M(8F mQq>z0}XtA/+hs_^W0(YIMqW>4Év@I@|`dk0+i| nQdɌy$u  yvyKS\IJbݖ4h:|I6˛I|{v2ҫ⦄S9KUڟq5.Ԯ#]W7.gw'ilQS$`"ї/@ D<^pw9qaO&5_؈u.|q]uq,9(u`Z- wnf|dM^>n{&<Mp@E*iKq%4"S LGP/ jg'D\FRPcc}A' _$\Kxu4'hz4X|@ZI%qF(@VjD-0@v*Xk%3?, T9G/N@z7e&L+kNΣ%􅜯- J7 <`w5m\͙MVr^lQQxEIFϋH.ۄAWLj_i~~Jyw&uH.T_(`H+:K>{ӥ(jQ/漘|?#򉊖COЫJTEK"M5:Xgd5xu%Dj @ŏڦ%ON)1{[AETȈ9<"ir[v s‡@W9^$ɈEMIAϺ^MY1<1h ͟lGcKMq埈uuShg׳4%:5 q6 ΅>ϳ/F?6>1ɸC(0V$GHeܩw vL|)OQׯWhO|Qm% &9T-}"8F3 RGrvJNՒoqO? 9R+p3 d2H^%ҏGk{r5 [x_mz[YhpP3qj"S>"&@ 1SL љW,?)pH@LYȃ m=UwVEQI1C9a " U~C*xW$8Tu՘k>=ySΈ`=[Öe:B ^ jӨs*e1 $}@ȏM+Puۅ^mfI AqH% uN;e(}q0> a5< TT~Fz)S\ Eb S3\zVe <|z5= rwg iƋ40 h9K.&1xʼn#V'[%ޔ> OtX˗72bXWSzd h-1vQHByY9]6XJ6-crF>)rpRcKL5BJ__-%;oj1A2.'$ӫ@?M REւ>& WT ;N W a5k~P-bsy20>4攎w,W7Y 0,|# oB%PÐ * =UF7w# ǫ 1+RIb)0&}7{%!!f-,gNɄ &0&BK\o0V%cOKc7PyW׀PO٢6MtZ.&rd>ާ zl9bȹ:CBCyfY6<>^rOT -A-)n )Ҕ8{_|5ηS[@" G%CѝeJ08RNz HJQS[Cy0q/8bAT,ebM_g^j>ToK2{{=!;;/D!z3VsE_O59|Ev~, EU5Ƹ-rWpEQ%GOrP|RP!xz)0+P%S<#Y{S#_q"yFdwktDԅ$lqh& $!ӣc{_S\p<*xpg[.LO/yOwDV60i8DB1+?q( "`e_hus!O[PyG9i(înl~V䢭GjmI_;@3n#ȝۃ58 [KXD?W0H~"=ꧧ&; ʟtXqAnۗ>(_"A 8$+J'r効xj}k#/moCYgU_qqkʃ>>P~\0 =+b:T wON+63>E1`UmWK#@D+$hk1JC!ף>!HjN~37 l B'u6^`ℑHLT{Mǰ 50W1f~&8+3(T>|tBTl=U@g5Ťhl"#-z1d!7Yŋ3F|NYJ,xŀ KQg2~֢un떗7~> Uܦ1Ҷ;&Gs%/v72&-H&CŘ<8%Q+Mō7`Ml'_"a*\5,򠤮zLPvXQl4\ϕZˆl_B,  G9;Fl6nfڀTZ=TU@ V -VA!`DWa`&TZʢVHߩQVbٻL w䐗A9uVfR⧚J |5Olݭoki$p1*JCy+i.>L5_CkӃN+!ʲaV /ݢ)A<+kUb69)a,ehuWOCMzU<]:JNM HVѰibT.=7FR L#wrA&Ы'I϶&6)G"dyӫGnE-xUU@a˂;H:[Dz MҀ$ipnֹepsg6vӥ$Hz Wlg }'5ۃil28-N #Gݦ0"2Hw.:HNbtEȁn6ޙ% 1,c  w|f] kӑϴTn|ɝڿ骽 ;E!0L<)Gs4;Q R.I7D8᭿g׎_!̈́FR {4n"|<?r6%χ|.]( &ϦaиUGF&\D0%`g'>#w2b.fHTGJΛ9ŠdnX& "14zsWh%ScKkpyGk1Zv"ΓTk \C5uv4 ~_g1QJa129F+Ko4vEײ1rCvdm]@a̓,V& JRTSoM!/Y_N )= "7>Ҵd!ƞ0%$^Gq#gﵭ2J|=@d}ꬲ~SZ:$v+F] ๺=PJfOTs/R9^'JDthKǺWE ! ^r]x>c?jZ=4x]00G25w_}ik#KR%CgAaQCUR!zhpuk 鏴n9Ȉc=L[GzsYDvBhe*d"GZEOQ!M:kR8c˄$)[nY0_K k%,p75\&}P۪* w4vsF-OGliꌝu-by%]ݍm:4QAE])O La2[zZXHZ u4X;A?1YSmyS8}ϑ%A?-W%~gPʤ)䢄7-4'?#$!.de&x z8YֽoLƉWwu߱K-QHgG; :qW0$ ͍S(}M*H"+{X<ӌda( GZ\}OT$8z-_N _S?5CEOg4MIb"Zu8$%CevJaw,LߔxA-c$E ]o8pd7ܧL,!(X <1l2k>SӐoX]JjJ QU]Aext+9;3{BO(ݳa5b(D@ȸA3߮I4TIUI5t<_t/)t$oheb>ML*V&H᩽DmOdCȿ<&cY wvU+Qɸ3s{*4ûFkB {3(:Oͽ/ho^*`h!rW!U%>eu%n@;"ᷣ3Eu9U/Ќp8MjorcRii+hoL0$2ZR.g&8 6;;W6QqjiVkw}|X!owEsBGiGZRR:fBl+rPn8 q$q4ܟ9V2?S4,8.H^BW[|@0k-pK˴'cHΑv w?4mHӼu~N8O|HnBczxlc8 6iw ?byhnn^`R 9wG`#mI~.{ gFv1ӂU|`K78!$pKu\!j~l''e$h[bK^a_cpWMyΥyZm 6 i,2횞-Q`CDP?E7)&K?@;lN&900O{rg'b`92+ߗg]cUCKL&qCM A<"|Z?ڂbQ}<擔nXɸe)9V@iȏC0Ń/79'>L*.5(a NY˶}留Qo[onޕ9p< =4szVjJO(q Է`V&4ۦqudDJ[W2PHQ-V,*"Yl* JB^ʄߎBUb`,7ݪa MR а\.Vm<[Bl5~f_{'ex/D /S՞|Ϸhkz.LX68KFSu! RY27! />YZcӒك#i#r׳K ȦXMSWRN7-8L<".z~{;>?Ỏ?x U@!5k`S527*tk~lYu~BJf׌[z;}g:}8hv`6]PlWj_ l!\HڕEt5&Ťs a>M1UG#MH,4DQ~u;?Y{+sf45FgJO_PpTb<ӘmWUdjN-YDT= ex?bP$'Jf(эCZ] oIJf#{ cvxXGcP^AOF͍0}R2F06JU6Jqfn SkVQjN_#~/u3RnQމ(` Qiu>۲s\R`􇿌+[+ʀ>k\@:C/&:6^0AQ: e4iE&|YcWbU\E_r^:)C~iթ~Wm#_}afK/ "UQFhŷ v"$j JQnP`CgjrPEn eVW : ̘A|ǯ^X@LDQ)4J`liߑWh6&,@pBK٢Vi>92:+de-K$ X2LT-Q24={q$M0ץC8_PPܸ?-MG:o1sm.j8Vqa3#^<\+g0Tg+t+hls,o?EhO,iȨ m-#腾d \QM`f]) mW^ˉ8qUE%}fǔ (!,MB8tG Rkzy&+r1gOѠa%Vh+S* >o%&y\.a\l<;P@6}?BΕCA* * % q][3 i\z>P˶V\KƄj.6Q3Ɉb=B9*iZ衐j:ܗFe<7{gT-.[_`.U Nv(=Q˖~p U~}! v% 1 u '% 8_owcx=4XC +ǎȺhXH8oTkU>3Tơ ǐdWqa?fbh&$Gt [0`T۴Rp:́;޷F|Z S\Rմv3R@^q~S T}3߿Gë>'uuo`bϑNMtJxYT{6*')z~/{mH ;CQ^p1<5iBb3sO.\]wF>U}\Hz.Ä!vFkX%UNA$։}XM]d //M0je JV# w&}$u9kXff#Ssߠ%& YQ3aAܯ6vpjNQf<MκNgmS`D*K̚^Y./M*mV45ur a $X>7e:8ϙ^ȫ^eS*\^n=5IG)ېNV?[lB,#e*]?Ѭ6E%2oU$2WQxuat}'5^*A]TqDuH@&/I/Q@hiKL`Jo~BTOF^c3QO$UR2:OLŃ:woZlp..^Q|Hs B-N@; tΚz\cm)|" J#ڞ D=8 !~b^O&e1A@:eG@ߝf*i82!NKtȒ Y?cuc[=7.BEV税jWGn\ȵKQX9sw28P1\4Nԁ/j*ʕdBT3W m"2A&jvi֯6NyF :%hyD}>sB=4=}ޔ{RWf%LruyS!;d` m vE\Y ox} %aAӾ Oe8k?"lf[h@ m =@~삲mLF! < `ł9=|!̨C/S0 S%>@(;>HWo5n)ysB IIY|XJ}spH>A\\ OWs>GӘс0迀c)G:a+@ Mmޞ< zv)PhsOy1q3e:P-^#E!*_o< Bds1ª̍ b.q| V2Y@fHm+ȥ:K4;>gěeqvm/w2D(H?* %߄,X2sK \6b㞯]2Z_g=н+}Al*AaTp` Qr\J|a>Dlk_;NxQt| <&ט2Ez=}n6൫Cˮf!UL$<E7*_npQjtj7XfwuLeD|Piԏ2,nz<$#-q@)l*SP@Gx 9x(KO.4VۘTfS܍6*͝S_VԭX /b$t#f8 ͸-G_R0"9|<>dA(0%I?R1,@FdI;:p&݆qT͹|s"-)P΋TaoHӡX #On(I89684"nZX5]#}?)Ӟ0XpiY8}Dp3q .EኜOa/?,>i|ޟXBHe)K;($Б;f  9EtKy-Z x"OQ},s?r[a-Lu$LˆER`nV"XS lfM4˦PƝ-$=6uRx} Boc1mq닂Dbk yF KqՅ *a֙pmg/:Gb҇A˒ AE.IfzX2)T[ \@C^v+GGI)3Jrx# 'k&~/Jr'+R&{=31= O;:Ȉʬ2!ZS@'tid6xAo0uYUவfkgsR wp0nh.F,BYVzZC,* KAo)\",4BSyRӲɊZf]AhBN zryĮ@`)'J0iBA<8qͷ?f&Mٿ/gκ!Qd\PHmMdcR<^ ~eHa!tM仗n< Hon iϿ=M0tbmQ[Q' WO q\T2VAw;"(J)8޲{8 jg`#: {i}-"M{('F; gRk-式Sj;]/ėGʹڮ7oaP5\Dx(S@s8FtSLPn͝zU@K856-"5T|YD 鿬/k3ȱD%3=ցNJ)};^FPUf2x$+8mcL\.X=lIaL0@+;"NRngjH8} ndS\1l.@P1ѺF|)ҹRt'{2 ֙Ssں%C!?S %;ڟ-m n A(OYH G>= }]'fFAvZw8+ٮ53 ew4jSU[K9!eڬfou1=z?U͌xf@nq˔RmM{r30G*ȃ6ݡu.,WgMo*>'̹UB#Uyy>+VëQKn;j(Eo[60 %]+';$sq'=D+-5`u"c"F#&Ttx@dAZm)L;V>l fl-0~$u-PYW̥a4nB)T9NpY5 r9>>!>hVW.#&2YϽ*,#-8]{(zCō<қͼ~UArEU@\VO+"]uq?uj!gf1M1Ә}pТ!vS;.Qi9zmu|EIq>˻vfuL պGk˶-E"ŲUP'd݇VRxPw%G{p X⭛s}ծ6&g&Rs$ 膅 "&PJer 2o h\ ~;wFq#`;rsMӽ1.Q~RK@V$X#g ֫~`T6E횲Up6ib9C: W9*^&`DBu2b}۳!i'%}3ܿ6ơCTg"8$go< [2Yxҟq] MQоʩMUӕ|½%Ck4T}zb7,x%'xsFLڜIiQFoTPRJs׾< Bb3B:حN:{$s^ Z}nq+rN`[ ۂr8?)8g:sF;&[+(*Z*䥇Ԏ4S`g?,>X5׳2ڬ6 ~7&íZ~zIBq Un|Y2ޭɏGaY0-@9rb, 6) ~_}T]F YYwHsZpE#+O8V?A6&s:x:", тfe2t!|z!^Dr/ Y1Fm|Pkq>.H{pMps}حkƬ,c3NkʅJ@4ܷ0uZfw{<Ѡ`Jl 9.+M#0M qբNR▸݉[t}yG՜+ט~׻!%gl\ateD np졋(Dp*a/$ÚJl_ ;۟5@gǓq:燇 ~<}#،_sy?f#w1vvU \e| i5G!@+Yg5xgl\= j_qޓL[`S^@D6En{'':{[ko\cAPV3 DQÈ>e &DQISqSD݂c^=$F{$-%33jdWqanp M@01Ym F AtHܜ֢/Zml1剺d<`ICՂ%oY)s3śVR_M"t|Hr< YvY! ڑK?DWKIh0 ;J(Cc䗞R@{y3OˆHe!u]J$I|jXO VWvX$Zn `\pW}ۏⅶ8wKӢ!@!񁱀y&4ү6*~6w /\MC8^{frkePmvY@eZM lʞt"̅70˷ꢏ6RȰ+i+wfL"}qXwf{[M%;Dz]$4VN/xh߀G'lf31C6,k9k?iam&}/D_ZKĐ7&U bt?PC+e+ҵLw#Sdd @1 40 gO\Je$0e&Ib.}O mQ e Kr&>wvDTlGLkQT_Zۘq5'}`j~kKWy4GN-v0\jnX| u aʋ9ȔX]OVNRLvD`^~@׆bLjLCn̤MZT4էCb K&f݊߰u}6{Z#fqC P53JM8-,9ϒ@`kdo{z07FDm3AK^nHpi zSoUDdmeb+$[F cSŶczktNuVf|ߚqJ{ _\"w ֦6|gf9j5!]epM3i{VcĶzA"Pbwi:nDž5RgY k"ȦˎB-ЧZfT''cXp%~u d%]E Dt0Jljqgu-֧NiU6",ToF j=-_1뉥ʅ*N+[ШwB#-Q7 PVg3h1$N(baa=Z=t1Fe9\EBٴJz~f;XNB>۝Z=-XUXI3 0YSm TODJt)Qjc){X \\T1X]-E (.b"zpBA_f\RP ^醬Ks¬xet$`tl&t.\VF =S-:NDS$Q%]0R`ugDX4O\wo^^^@<~~\$I KAyʶ`k;sd'TXևqVěgW(ʮ6d4L@uDBr,$.m<|*Ej7T}J1Z1 U SBHW)l[وF5Q9 8VwJ@?h{䧅*#i#^,-[y ,ٔϯR3k2Gb$;0#_ HIgQ]D&vR!P p diXxfS$[wi2clJ"Ư `.8stxœ'wKNM -ntծDКF a$a&4ۤg",'pPSq΂p/#X}yQ]fDzU klFqBzh5Qpqn# ?(Hm֭|`1b#K1"0^V:әKj{C4*sO@//YWd2XTkzHAS8ӟ _`C_YZ삑W)H{ʘoT:\3M2,j*aqes_*c=$k"z,4gvSټuk(j[6l71d[ ߓS]yq5%AݷtadXvVIg҆E)..ť3Vt۽NBF\xEQc x9tyEV*~Tx%Ærn>;̳N+}vdVA4vӳ`H2Ulqdڞ"Z~E'bGۆʤHדqmrP]*'/Э_HQޕ 62K >"4hos1ؖyfpSM{]z5nJu5Qԃ!fFa`YBM[zLcJӒ+p>(d=hM4|q踚tҭCv0&g).c5 ˶8dO-_%Ĵ&p6js9323G6 r郆짠e-seeavMW%Ćnډ7F̥ /Fo*'PMLo? L, )1H}$%51KVB^J;3,vv{ l=Jjd.V k 0)F,'yrz3Ԭk41Dӕb;gx1r}`lAn0 ]pJ1>s͎/uof Ӂv<: *jE;6``先V?u󀊨!M(՛X~rBZO{ zv BhHs?w0Z$nɌП==<:g Zd9Ex}傼)FB,<0Qpkt'lw!26ÂH(̣jתʱR9R(ǛP[m Pi"s`K͢\MZT&yocLS-#=w+BUșI}I;LM9-xBgn&zV #Cƃ'7wba%O?[WPd-XlX>\PYrs uۅNr‚Ҵ/ x^wl]iׅ@*JyI 7騞QxrI l2ƻBtA)  JXYaɳxJ;5 (BiأŢ{;, .v#Z`-zc5{qQC +"S%t a¡Q $4K [uv]$mOq F'UqA5v?ncR9NT@<8)怍+`„vcճGyY` WCDفw(ۭб[P $f4$kXVmM\^2GtCLbgTk%$% evPJsyb5//­Ẓ>b$| _6Sb3ݼAM/퐧#P?~=x[k,c[m]qu-8U yst\4-xka+Th&U׃PpATqE?,ښ]>υvDbR/xtOKlAK}oԟ>z3uG#u{|כQotK}]Wޮ7]v1&~/4w?>֍ݟ~+j?WK~n}4pg Lh8Węo[r_'/GsX:n6_K3c4mGBcB]$|8/4s.ߝXW5 OÜc'K0,U*Bpxb]I?qG^Z*FxԕJ1%+1N"ז`uy.wna u`%a& _~zI"y\n tG֜tĪq[tXB~JOT]"UL" YX Cl>!J87rVtu6 3֝т {UȭX@}Csq7oHFxTfQJ>Xj hk/۱4"kJz(PniJ{)5VHb9zj(BgC<}/ ) qc5?Wԍ 't܁9a(ŧhO]Gk$ HFx=cAc*7,0mw|AbƿH'miUf2`S ׎IcZ= E w5!iR`CוV:cg J ƫw+B,VYy\~.ֲJ@vh:e;O.Y;Ur*0,jr I=  vlL9 s:{,$/ԑ%/`¦9,'1K9+cE^1*h ;Oh?KҊ8;,.|Ur;EY`t+wEL_HP.?LN.8G~ni߼62YVw?O*]`}z)ZCȀM0XȺfns̛ ô'm !BY -!M1}U8Qx)面IoarRWT% g=ڨgme1 &4سlj84[Ulcɦ>5^0^@M߼s₳MhWhh~ ra S^ȍ}ޜhBsD ߲.J4Z ]-40x2z':^KаzC $I jTOLu`)fY'3O׍ F`Zz%#\Fby\bys_8!#OssקQapZ<$H*wN`bP'NeevA>'æ|#wŇ`v+m7<*qo2%xӰRdo2$mZz nooliE99Sv踵7&+p ZM -oky(o aE*T*we\wȓ/O\`Ǯzq)<ڼd`\/Vd&'#G[xP{w -|1m`-iki*A 9ȘL8rݩOiN-1 /%(iƄJ'ysy"jhJo")syxuYU($9[vi`6+DjҰ9-ta%) mKmn~H ][)m7~M8P.ة#&2z&]6,8`:m0}Y[ilEEuL|%s1 9 E0X׬Kq6t49Dݔ 2T }CžJK|iݸu!oYĢw|Q2'JT}-N g9qT7#JQ=5&`2wKOgp8UlRⅡ/MQn/ A1g$ݛ4EK*) 1cz+JRv/4څS$ru@z2^ţ ^ћǙ}_[It_ڵbr̿|_BTqڬ gЋ k~~u'8Z B)~DIޝODROv|H֍G >;iq *mKw]>E=)aJ?T>;e-׹UM~c+̀F(>޶SGV.,M=Hꇌ>젨9.C !$)0\2:/]k4)Jw[b$ h"呻O=>䱬po-Qh7徍~$SrS5ٱ((2?Tۺa1 X*'ߔoK\at׼NυL.oU<=`5if3eNPFhOcI(ck6M7 >Oy?FpY)I"E6%u}(݊ӃӸ zܽڕ6j!tMI U蟵í5p8uuq,R&L,zKJo= qtj@4 +ƒgIɠ\|iXX&zQ]Y:) 2_mN\m6\[ާ7$aW%wXmo/cRK(bD@w?B[|4dI zr Iǰ@CXdw)%ߏTܡ bΤ$F4c~#r \O5&$Y"7T+, 'hf7IJyӤw=/ůв&p]lIX#>V%)\95*$$i2[XwEj5ctZaT)`7\İhMlVz Oet ;µ,<bWR( ,.$4Cѳ@=@-RrhAİO4rU%ۢ :T1OAE#Vz(^rO=v{/,nEϩ>8 Ђ~_ç m l[ +EЀpyZ^QK"A?xB >LƋ;d+^r5Qg}om+ iL<3^[_0b`pn#>]w5Futܬ h9~0$X5a%DtKI` a6R}eY"@KJl63h;S kti\CB[ 8-wP ?Jd#Zcζ=P>NhRd'Q& %! 0|}EMkUUY#i0)Ʊ'~n5 f^uF-|dQf͊9XӁnj9aZTceԞM@dzkw&<*|V%^u_!AҺfgz3 䐋 ǡ'O{ ncHXd;d!5qN;Du0ve^ B Pa8;~Z4 D.EdQPYIWxYnCW`ay9ߎqG!wWڹ,h[(S؄yz(99Scc&^h E̐)thi3Ua0M}'΋aCou|2poU}Ex 3$$~O pyNclsU1 j 4 J!w}&Wf&5 8.L\2qPY'LTjB?4ܜ)Ⱦ}6}G^]4N  syk룀b[ h\tC4q(PM,b)׽2cH+M=Lʓ5Nq8ҋwF ' l b끁E` 6`}fZ\!DX0zSX!Cɼ<IQױc%WIGr BN#elVڼy:s v*Ud*8uPDw2?y ˭8+9L 7b kx 'o( >>.H;Rk "aʊ"àeL>L':vv d d ZB`^=pZ!n=%잛*0mMN;nxQ|0zaguhD4rH!rA$uCM(HwuAx H s^?|l '#o&El?75QcLN㭊gUPX FjPH%x4Kߛ %5DJJF-n,LPMBQ(F6p\Y"E_KH^n Z,m Wx6A:yl/zs.~38h9dSF>-f׫.LU4F.&: sp/ 5T'L*EjǷ$-acy/i1TدvOrp?VU*67(X0@ǶCl\K 1os w^ɖDox xؗs@cB|^eYeVn!;`o7>&(5rqZMAz`7 T*N6T%'YA\`"6 W>|/7߾((_RPE$ɞ+}MvQʰt: LGR{!8$ےu|N*ɷԒ[0 (,[לgnٲԴ%@[Z|c*VWYoz]KI V s)˘JpZ(0`Vqe`'axlr_ uq?8w ˨%m]X6~>gyg`%<;}no>H*$GANQ{zE@5 #y7~DR0 ?: {kW3c<>' zn.;}6& q4DpsYidK9$Ʊ~3 &OkEcnP2Lo\y3o8QYUG`AGХdtVxdMGy?h1/*V$6SC;{r٢U5׹y7bV C6n5N}=N{:{' V_QNr:BUR#mߎ>tĪ\R\妞|?uJ87HOQA8"~SsaU .bM\wX) jECh>lu4-D;wSqyNJz;ꍉ ߭κY d,<=wQHӁ9X8!DfFJƒiB@oU&RGmZl,;^POXĵG- ҙku6!&? DT?>pӔ і!ݝWpS$2 zPgw p79b=*;Ľڌ)$1B͒YP:.ϬpX)v=~Yg"Q-k`bf\c- `3bmH OAPD1b_}n /+ԛq.Y:n'qC X}}w0'LN\?d:O7~-fӄb3ߛ*b.m*h}ihv5lZ#`蚟h.x8e+jXyܾU .vLzJ\+[j6i"\nk ќ*@JM|.宆ڃkOqyzVY@JߐM9'LLSo:L(g6<fՄ,:jlA*:WZ^Q8™M(/ȟ!.^Fhue6lqݽ.R 4Xv!Ӌs{ؕ]܊0EPjzhF6IB&R=$}V:ԋT8ٯ#n n9rֶ]"%7ҒE!,MPLj{K"37Gs~Gl^bEO9ߓMpD?52䖞HlE },+j@&ס|lM0[4O X\:dr#^(Z5q0%y&>%,صrraSҭ<$- B<(WTKc9sH3xOJITp)fCyelJVϨ8yƯp`8!̔L8BYy[{NS]!}~VwP +q0B#u\"$r +󗽹 jry@mZ[_`NF11Wj RNp5!S%5c餒~pM+I9 cN`ĐVmgT<.@8GgÈDuCj.}ki !jjrqNn [eT䗼+Mnƽ=Сlg>`(lR# a+ٱÐn"[;TH#ә#aj3Jaf&sȀciо?N*;?T2-WՏLGc]QncS0.xW9Dq1HII .1a&5 H8^esV+_OLh{]tϣeE)k:O'NAY͢AlO^b㾣XZ6~ڟ> n{SYË/e_\k&kJZ|'?@:21 kZyW;A~PsGgaӚ\^ơU;“^e@A/Nv$vi~,GcQ41uV#sۤ|yD׌-_z+LGͶ Uɋy}BF59EB_A#}[1[fac`.o^0:%'F鐺@JdP't5_[yi9o& OG {6Wl(zb6 ,&#:oS[o i[Pe>MKߏkᄌ\[RO;-8M•4'S۽^P *uꓲ-De{HҐHj>SO.itPڨWzMM4zOdɈ-dy9:)Wq3lH\htH30 9:vƏH}Ko@$O\k})7`ԉ@~{4{+"*E}-sw@6]r.Tʜ 6@&ssꉝo3wtTK"ظT8kU*pJRpI9z  )Nߕ:7mS}/W~f Vn@;"0KG8l~ <[mfʅwwnєezZ`3u a8ِS3뀳 밋0Ir$|A5 ;ٔ` [[S460D,o&N%{Ze5 S\%/?lTQ g 7'!zݢt_xMu16-G Ed'rTNT&<+H 2+Z,;B M&(x1Sdjj+l~IgXBB?Z 9CLdsq@x7bQ)>{Cb/#(8UK[t9I&qyB7U "縡&t9G!v*+ f xSt .9%h=5 0@7bdb:r0"90agG'V#Xj]uǙ?+,H2m"7dА5iQӀ>Z]UN:lp#H H!HF?5<6 X 8#woŏwmGCfiC$<) SPWJ%$?-ƦFZbutm; {|adAҦ5'wB^vPw_z bྃk2:T9M mhэ+p>H4'{͊ ls ༟!׉YٛUG6'j7GPλ/'RiPz,2ͪwik2g_Nr~ Iߚhj`p +6lNMi5 3+)s"Ƿɇ+b{H9]^brڇ pP-.w͗ITXϚ̮v] b\#>w*#n5E+^=Xqfh:RqN0h9?oI ׊8#{n7h3C!r,l`/ؾF*߹KBZ3^6]i;J]dwD ͮrjNSMFeowFC`KEנ \Ƌ6M{&]h7 q{rqٴ a ~4CHZZv{ }q18HKwMHU:vuSs_y!Vj(ͮL"K)#a== )bQN9.}&eQ#|NU:3`??j"9ύ (CʪX}Iy.@"7ܝmHz Llf.DXBLFLck+".VQ"_dz Q%pFz] 1ȥX? 4ʏO zbD+aNaS*鳛*d <ba3xeIt <8O1/πPAXuUU>Wx^Nt?7T-/.HYm1ouweFߩ^7Q!/-0-CjcI'(W/-7Ov52~N\#;^ucM0҇9.Y?Bu;N> 쓮1,~ksOpt;{]CȰLw!Mb}1uX?Orr"h֜+}z朢I^HtpXKq4^ MaIӛMs~$ڦ)qfgq!kU^ u$/ +5|459<.-yq+/f>O9bh-)vY$/*Xɠf;H)zLOiq棭bQ c|:5[\=H`Ҳc4p쑚lywlt'DԐ,zwsPGB0nw>Sh%LlFy^WSz.s 3Fc*M1m<0CRS ~9d^8SAoܞr|u - P_^2M2dD(?H(hI{C"A+3FzgT*>q-:ངT尸(0,v,sJA壗yBH&HǡQX@"S]gi B-Q2]NxQX^`pvP-NjmՑQR8ahG!4i1͉ʣa.)7ѹ(`*At5.Ki#n)k:7+ZFd_⾺I%(xMF$~$$M%8.CbiGXSbN)5VV9e.O:T,7Ppm ,h8cU(zܠGxstD/knhД-LMLs"4~KpE32ԙm{ZnY֪ O"+ HJ BV-˄-Wf/nFeqx6 x3UArƥ+H /"L0\ɯQwF䗒}.vsm)0*EUY̿ASel|@3dr {źV`!!- x&П-!]0%+ sCpR#_t"Z.y 8"XRzE]hMEBƦM|jeyEFa\iB5G`t1Hyovݪ ކHN ;r'-ㆈ&Ȋt_/&:7DwIpmѷӒQg^$7\~lmt.rl0,[j,8$o~Rt*pFf)zlNTozY \< Rv A3-n/V% gf-wNb7[Z5Ud ԘP?־9I$[`䪉6#>FGO (2ܱnh*ϋ̍fYvu,ѼTL"`;Ѐy'<^}|V# k6ϾXpl]~j;#Jb5iӾ8CЍ ~Lr]P='tc9.ɓTKT% 'N@pN$t;TQ. @Oߌ=H2c=?ga8D>bl)pXd(f ni5v3>3@f:}i,.LvJ9J$ryET'Hm LTNV3+rUj"x>BXsDM [=#ֳ?=$@TgtҲx?&$iN1*°nY⵳!d]-FdE ːR3zZ$iVζb ϴ#b ق ?g|o$B i`) TˢPUcXSO(k+9%Ѕ k^j_Һ CƘ= ;UsC.shZxs^(q-"Te1r+ 82K2ҟp׏ 1e8 KYMg1%1%vp UXデU-fp[]rO@Y$ѧāHḻ֙{_z`7d~HШ˭l;15VٹߟةhQwLoΚa$ b׹ hƼRB~p7աa F ^4 m;o2S ߝ_!*K3 G,3?U_4S1;Ays^9ZVl)/|.OH'Θ>-S>kx;d\.TL ug%cćvfBR'3@ߟ2nNx3}Xܓ2 =<~g!ϫÌ} xP;SD7V*~^iYl:6dzOY)v12lcNm~StHQd8/(OBۺGPfPwl]6gdy&Zd w'2'~C(76Ѿ=PدsRamrd$=1ɷsIݐ|4 D7z3>t!fNN:͟Nr@hg)OQ26׿dCwL]cى>znO 0UWGߪsLq߬.͘r614XT K94xHbA/+D/ fP#+ȖMcl(!I1c'kAAˊhqPLvhLL07LbH,(5YE&!j͹E4l^ GNȃaO\{nskjr" W*$MHqHwm1*E)lOʌKdMAr$n(皇Sf3Tu֞4 ~q@6qE] H&M7wDȌcJֈSFQ +YV>jU;en=rbO_vG~0zXc7"A9(5 .STʧ6%$FD߹(c*g0C*m#pX@<]SlCqNe|PNA6ᢷ_58 v1YN pȀ6Pt,~T*O1Eq8 ]M;=uߒqm2iMk<]Y;eЄs}3sL`^ ,;gJc(FQeT''E,&ljt  ktQpOl֗PB*ԯ<' Fѵ]c'/ dX2^v)BxPld]8ƍLI1#13uJQME!\;ܢ/tZ͖NrηKW4eY댰sX (eeВZx+!9آ8/Qd-;焕Ȥ=ܫOX%7(x9 a5[Аw3aeUwJdaK8GF\Hs{5´X3{I^t6Ob!6pK)v`(',CD^9-Ho:!]; RX>+a\T1g'b7W5h;/Wn XZR3>%Kz?7z_wl<9ۻsQ ڱ6(xޞ-"Zz9a|/_;uǍ,Tff|'DY&솜 f.D:'2Dx`߻~9ofGmh`9wf9?6ʀcYjѾyHB: ]ZM {W"4 780X`큑hL=~*8Kh>'b/]0|d0bV"ϵӻꊉt[E2vo g j['pq"!=-pp>i+6[B"EA D6w }PtEaժ8ɧ/y H! # ]%y"a}䍔NUhd#a=iqyN?W%R 'gJ v?mGž43JF e|B3sxhef$eE'SVuOAZ' pt$=r3e+[Jzh>JjAU~N=Kg̫&?(O2"SI|Xr89L}?=GZsRS|x}.6!-^vm$3?o*K:]B'zoG pyXꩀ bnZcOp*}Fz.+'Vv` P > + jv'/O|= "ڌ_6#@GOM 캝@/d-Po_1c!AA]9״nMC2zZD-LI5[K1yZ8nj.oNU=-P`tQ!!2\<̲K&g#j*!4Qay$Fb]M7t*/Ev4Fxqz(9i&&~r A?/e F*C2FFnP=h rgXoW`LjlH{;8CǓxjw> Vv?Ec"jd*:9L'^&iY;RD,RhIX eF7fyb<= *NkM0?1zn F#!({K!a@s.} ʯmN.rd4MO:aDk%VHvz{b2Vp}̔k:KAZ6UQT'QoԞd1 A ϳ Dgqt# Vr H kn%UrfIRf6WD^'SZ*PȻep93{y(9Hlh2[, ՇzN j_\>OuYI&ku)bx,FSTb nZT R|GfK^j9ĩ iX̊k@{BMPީC/U_>̉ٸD ;PL6+@.qXeB"2טw{Gf_4=參Loz_1%# AEvA531YևD-}h0Cb*a1g̱)^9._R1nJn*GZK_EDel,w?{>U`qheEsȠJI"T 5+I"_/X617{|,eAY!H`? %,vOIӜ']{'!=I9i{QxQ"!X˱Dj;AE fҋSNgB>4Ggx[(f% m\Pb$գbaI?F< bLL\-õ<B>4ܵsX+K+_2.b Ȓ)[m r]jm:G\%9ﻛtꉨ$*v% I m.r2y܏7\IOO!ߔE~j!U񀵦RKNX)P#v&iIێ[Ha?%^/poPn:|{_>ExL}B[(2d Ӭ5R\iYoJ;FAvZw8+go%O/cH\+a.UEcC^د&@6N|;L]f 1B/aPtRnb6 _[.'r `%c3[9}%ѤVD>.4p~!g s҉WIB%)KReU0OÝIcū ')ZM>ߎu[i qH CߘѫT:x"7AʑX$㌒Fs?p7a^-SZ C!ܕy>փ0&^&4]'D.<4lmu!ZEH+o gddoVY( !KT=YAg5ly\|ST,O~&e&M 6C.lk=>Ux~lD`V&#eyV< Dе-nQX܏5mg R9_R-&`_Kpɾ,Yzx. {A&7ǿەb+=q/M}pamЦ\uoz)>S+nW*{h=NIÿ sL`<4&JaUŢ2_S卑5?I|T\:6uV5fP{p(RwLCY9}Z5\c{?3@93'>@h$ ccWŽיNm:7`ck v)tƧ`L ;V2KL ./+@ /`>g(*Mk[q+놹2Yܿm'즌c#Vb (AR38jk"JKPt”t R0(m9xz(TT d 8'ikM`(V%m[Pɍ_%yu ~r\z.xm:$u;0BW ;Hq[#_/Rcnd$p8z,O^)9(=|dOUSaNJk-A7TG<'"6\ T☋!үHRsPD&2^W׶Zu6VXqMҚ=<%ъ7WL=5xI/+ݞ9[Lzsҋ ""fN&A s 5 ,ܟf\"BB.dI)tOX}5n&!:y3Z+]j64ET# AMB|v(91,x(v _l=e'M2`Q?PŒ%ƿH}|ڱ*^|X8Ө!=dtyMYao< ֑m#g}U΋ Q᪸t4N\t&d#Q"7/('$/Y)FX:grD?m9TB N? ⼽%N< ݷǛGr>jW3ܻp.*: @SnP*HhK~hh):~&;([h``n*.n!lh_ح*2d͗Jׂ{M͒~+VêC MD-^!Ci}LY <է%'{)ă^=6JxIlAjɰ "gdfLA.LpwI6DRRĮw&_|˪{0ͤ7YTB;6mܒ'iRޮuU=p)ΩG(r5pZ(=qC~3'xD܃5ǥmP8?J8vȻsd?_n{淙чz#B XRO' :g(mIySV45vDR^f@<%Bß5 &T<]r;-0Fa#XWg?Ey4DZϋ5m;n+GJ=Ew4A-}/%qOpʾ¾AE] |[L$L,-|?{4k ht38lg:-[0+WcZvĄ#3 }p b[u_?F"լM7.7,D$:z_Ǥ% ҳ̳@ !(e8 34ŀx+!9QL9,0$<eRn~AY9k{>l-;4`}:6 )x2~DPD?;ĭc!Uٴ~V_Q#dZPo;nGR:7+c,bg'; ҈,w-j(&ԟ|~u|=\5*Ivd5<-B]UOBsG⒆ lkWie6| 2v3}WַbpSnbgʑۇA>Px+(#`,3 GU42]T;sSֽ/8#WY]=^5^LƈAId,YʆsD}#*#ܵ[FQwIE} r4LZ *:ռB(SN :ھlcr0x)Ypϊ2*?ugĶ/ 1 GT*yOco%(V䲛61OFO} -hrV(#"T` Ib)y!<+%y/& Hu!P-LjDPJ\3 s[TIB6ohݝ|s͓!1D`I/oI1zFS$yw_ڇJ|1m,XJ&EM赿 MA:dr),O.̤{dޘ[PjGL{]Ǭal@ _Og$0̥9/Dtƾ_X xGv2[' sz:T#5!#4+!E6["k$c ܅XXM.8H Ͼo*:aW(ʗ Џf~îKMKA/:Ms9wrD=w#K*CR1n95sBsF6ZV~ x;c59ᐥG|vx6|K9?4%Whbrh =hc0ћFȫ!g7|􄀦7V~:MM42}|֧D [HENx/t .OE/xЕuSK1_h+uА@[A ׅn VG/DTW&90:0^_~$Ox|BODyg(x_f93xZ,O/wΫQ'0MW* 'ߺM@&ЃQP0r2䔒~N31O7d wj|1P؎z_қcfm{!>0#G`<>˛ð̻=Y4h=FPN%P=U]: + R#xP lR9zpwI0΀|l Iof;m7vIp I&r'& W\nVԡH3MeE fޭD؀RX۠o8YIԵ9 jX{0 l>꽷ܖ R~\X ?:JHﭕe٣UPD$8C%%E-]j?pGYęuueWPNTƜBڷ闷G,̊,Éc`KMsq[bxEii}*iKZ {$gE 2Ŭ8A jK,cqbIC YHM̶ eO'6ed=ܫpʼnM% n-tP[p% KaCIXkI¦kWA4UA`[b2<=Ut|9.KW]% ]O.tH.(%|8cR^8symF / uJs2o 6^ }πIi[?sۣ"ݞnWL<7|]E<ӫ:W+Mʾ z+{XEJءwNѭS?Qڕ4Ҽd5Vkcu9$[C18nqfj kNDQ7⦹1Z Ԫ 8e+twүNt(?h|K Tp{B.-_Ji.qΣQv8BjX*M1m& DYqmltՇ[RQN".er|[ER J%D60/&GÆ=: 2+cr/tfi5k=UjytIAH?gS~m#B[m$u,m/eU.1r9qfW3_7}F߫oAVbQ )?G4Uɔ܍;^&)8/0,T猣R7:Du3G h{X: |Dmfq,>KuIzbob4& ~`iNE$Kr2ҭ)v(xV~hCIv7;q fݠ* A,pg? ;1"$(fGa 9Pz c{M{6!bj&I2=JaЖ `FK2Ȩ3&cJͪz*89:Oط[%RP]aU͸#@I)l[U!Up(jQKeo#6w{UR$oNЁnSȎG c5`0l<@w)ˌB@c3!%s!T~ Xie}.Ace^pk_-[O' űi u24%1"WwζXyF='"t6#]:Ώ{/aa}_%.ِMH#3b꽱>4`<]эIBŠm;d.Iʾ' Ds vl)ZD+ԡ8HE/# –3NSbKN8;H"7f"Ƌ0hb'L,߹ÅZ w$Q$kݽ+}SGN`S4iY} Q8#0"@J[Qk4EŶɩK9.X3#*%Dz2nopHy.rT_y@Sڷ1,ʕ{PbOgr!:?̾^yn|dɇ/ё E- lo0'faDT.FC٠1ԩU#:D&W^J_s}N ߋТWֆ24 돶Q){"Q DWAͅKTIԣc_t7SF]&s‚_vD`H;1"ȣj%؛{iU+n,5 (}t旑If~Wchm'~S) A{ W;Gok<|*z4x0V5n U*rȬ.Qi3Ȣ6{_9{:ktftlĦ{ c0 ~RaqV ) T׾-b+jw SN㈣I:^ez8̻-eT#8#յU;dbH}[aVJ5$tS)^utevlm{UTqȷޞr%O 61G: ")<ΞFC#6:A7}dLoPkn\lԡV=r>M "Z+y}7efO,0Bीļ(,auOZ ~>l2Q*#8@c}IZ)nQO͊DC^},Z.rvsߤ&^9Ȳƥ پlyUllf4َk=*Mde=%(7\?߃۹%FĔ QʵU"Q;ɒjjP&*w\h ^-őg;uqgx];a7ye?9g&3՞_6«CsAX8eK{4g֏3SW5hވϺ2}.<lV^9rIz 8Fsq݈L{~Ʊ`HZpǂ5l.h\#Y?WA2_w3>BQ!Ixj@=3:/V|"{5s ^_敞6Q)F9%%5+^~>I$??H<2D.]<6uQZaq@ ]6+\nCPd0 LHվӱ};K{: |zn(A0*=K܃bt[| >'-H4S0nA +ksnmݔ@V^7Kٌ,9ehͷEmByWt ApJvMk_-Sc{IE8NllZ $Ajwns$t]`lt4j ڌ !DQ3aBBf@gj*&] l٭. {O]x?rlܓsVGRj^m,X%{g ϼ+KEIۮ|9"KH+&  j 8\/B""wҽR>ڲY>1k/e`>7G%hEqщh e!bJY(5O3t:BaOlХ]Jןj#UG\\6!^R;5H+G>NE,]xcF|FCOidiLOTᗕ>> 9?4CxmZZnŕ'̸R`3q5fr-vhIEBCV$^j{fYs[衞B4ĩCE_or^lM\T:@u1X#ip`>e_ łeZ6ReJҳjksmnz:e8r9~*#[IӤĎٖؿ#I6^|;{>ޣP7r8b 85LM~ӯޟ?GmK~KX Uh#zY 77s_ PR ;LT&[J~3G}hP<lxz,)0/`(18"&>ܙ S7ǴndBshN\~D՝ AٚlĐ=1模]&D)s"<H$#WqX "ϗ L󟔇!K*buRsJnu_gktU}<2;3uLT!. >BXk^̹-:ށ^u)Yrܚ VsVep~]U-yjT IWV >NJ,hacs#3l&y8_$/)R` ˅z_j..e,_>훼BRLq=% EF[؏*_#q^Xn1@,,Rf=Otz8"m_޵MPqMSMKj̻hx3B"1͛X#(Hh,Co=T #bƃ>9JfEmc?, bLEAMM) ̦R$g:b>BLMQqlULrhH<ՆgDm5oWj"li3d;Ap:aRec ΅`h NRGMON-0eq1}ʧ5E0G {]iR]5^hԝiWdV>ͭ(#(jE}T'fۋs1)m?1[Ws)mlRâGHQ3^Kjb(3;ocЙ97~t$-Rq?t&&\O7y 1f=k3^6&⩍Q&VǓEo($u 6AGm >+5U ѶS>+:tTd%.m>Jo٧6TH iCST! y<\_zLL٨YQ@Y3:_u~KcKg(>ә1AD &,M{UwT =FRG3K6KcKT&1H6(!Z)†{ Ao4]F{,%h Cк -QZ)➁k`"<|挕wQ1Gh ee~'Y0Dpzl{DVlP?mKpal_ÂT?!'*O3reBN߄IqmS[ǵ:$ɡ(B[,̄baZ`ʁ||B`1^6.l5ߟ5,A"D환kReųl̬G÷*y[ai ^&{JL^UߛO--|(l'9!U.P{ Hw`A*]8(͛/zv#9 Sh1J!$Z[#"{ftp3YЌ9Kt^ي V ~ 2@Ơ-c_VDX[8dQ#"Keϥl#hYGg. "ؾϾI+I 'Eh8i:f":?7#;aQztԆF]323UIkxAaI+.E|~M0=2XOKԓ Nq`uEq9ƌS1^/oچPb7PC}&J&=%ytZvcYGϝmrݨ%$T(k|Q2K~Y^|nB7(p}Ԃpz#gSLcUr+8 Q` 0~_ݗW ܱKE#?R<bWR( ,.$4ӫV&N0iIfDșm\ 3|"peED3Uc!C'{؂a|ZEz:! +։HLN^ԛzp_2Wɖ#429^&l}PbNIDoepx qaczlJp@W)4S >:a /cYxj(HN8@~wt%2o@"ϵ$L`y҅R=…NAWִ.aB˞%^LZѪʌ.j['5?85B·\ ^9+|R&@3vvuVBiZ5>Ip5 KIJhE95j_tѐ`^͛hn"Ya.ː@Eo W5PYƍv/vI% l@?-"YegnK2c|Vs_Ԟvj .6m(wi3:h{i r+&R7:DS 8} R]3do˸do)` O:W?[G cev5aNr$ngFyv̄| 8ǻ!D}Qj6TQgx>]/0f (Uk C&r悖Kp eSK ƭ_^HHMBgI(2Ap̙;T^LoS0yIsJBS@{|8J,ܜv>n]1mf?rzޒެLZwö1G7(|S|p;$4|EI˵QyYXABp+3bY+zNstUy [S$.okҀ&v?h|m!1MfFdY= $q1wB0 @~IM~@cZ +zii׍>?Bll7J$ݤm Q2mW ꊚݳƩM8\Wրl4ų:"Xe@RjO+'1a >A}`e~r@%M4$@0^ɝAgMbi,7_fOl-6p7s I@ e;º2Ps6`6{ܺʚ_}:E\KcYKlG 4@d#-mυK o0^m0% ɘ&0;PH |J߮t?E aRVȸ"r#_l  trk-;k=ZW%'S? D6vQMsd!9Hx6N/N߬v~ ͬOse4L- j] A)r#5;ke.fslj)[YVqI$[[|F|my|1K4Ql!AɈj)ɐ\Ԟd:.C582sW*5 <1zVأ/&SIN3 ٝU"HSܟH 21(Z6Qnd$1wBh.T Jؽ jxD#=zBg[(R/CWO/(U2R:Z!6 a|:9m{]b"1 ~1)b1e yކ'<~j[ݬN ~ON*1 R\™ݭJp2nb0Jx=d|YxN-ZhiXUJhrZ !2 4 yYXO,= jo% NRԦl {ٙ- b^-cG :U y}# (k2[? $Cه Ul zYquxQCK#nFtA zjLJؾ'QT .U%Xs9|eCoG a6jTr!Q /.Q>x>/oKmxPuj{=k g `ZZFgeұ3:;ԔșDTf>e ZdEYDMNtFN^GN2n%9\g8Hܿɋvo$i:%R˺׬J4z -㇉2bfd_sNpT߀u!vD'mϊ<ΓnfV+XB]%sw͙Y).. )z_ۘvze -2dGaI&ɻY"sĴS)+h~Wx@bS`[&vlϼRTMdH}VsO0j+.陉F$S~}. ~C^Qzzz_yeM x| m``囸32qO'QZLj'#"Nhܹ`h\M!}M?=&󑊛'm"2Ff!unJ }t-7S[/|z5E^Ui/֧l.!K1Ë#buK6!d Kh @i$CX1IئBm@8ՍJ6#?a5w6() *{W̓^'UuW<536W`Uݫ"{Y)886Vs5Y/h3LkيDf_Fk ˑZ1[yv ux_U4]o?&.VmOJn$&8{dX|I4Jk!G0gy矡{{=Kuq̮r<Я"#_+"+17AU)D!Rpn, ͙Gc8KH ÈC[1H\&~y/PHC tIA ZmN%KVFe3Qw1q<d Vj].í٪y]_H *}}6hlD&U'#TZVd]fWzo#2b0^^l&2^AUmM!(0h#RM 9T[NA*yHs}ǿmB_nd$=UyЄ~;̐ϩ ph z,ga俘z.v< ^W5`lsBa{󤄤oX Ћ|ʐ_$8XJ/YcR Dh"Kka (ўA zFW;Vdi]턴V^: ƚޗ `'n nKMA8#`FOama3:CY:DF50yt09w&iY-Z/_|'n=R,M)]8`t*n=2m3w*̛WaVd}j% A%Y4z"' ZOAҮl%UDݮϋAp_zhD2.%T{0Kd(Fuxm5 S:K 8#- s}&pV ޒvÀ8C73JEaW^[ǟjɡ#X~p 6S':mV{Xe ׽{3$:r_C#<2P ,5h!J)l@:SՁAb.#"kh 6g%K52qN'nՎȩ!M4ȌEfdٵ^ܹPNKS4.@ @Oj`͗_M)_[Z#T̓R`%cThdG&O mSuhy0_>Sr1 _B_Dr؉J Vs i:@H `!9+d $+YH(c3'\~0.$960X୤`OI0]S~(opUWD%A'ki f!xJ+3L雅ӏTJ0O/Z%_Q|SD\2NUQ#6N7y@( _h')'cG0OgHBJ+H`i+]^@)yj47=8WNe$q-Mɹ I 5`&/ ?Ff3H-L^\- Jnӵ?7{@cxUpщLrpHp].64ڢ$ Fgϒ;6_k_nM\[bz]oHZ*&]$_ОJ9m[?4iͅMFmL;ya#pafw'XgBR |XD6a@k27ViO |lyzge9`L[4ܣ~$|K_[0$ P5cdIgEp($8)y9xm_*5<'ȧ@ V%BcdѴQ/Mz(q9j\o 76#C4 m ăpss6/惙3|1VF%TfJ)'=95p/p]Dkb";[WI+&D8#gIdc)Ò߽ $5U8́zoB!ݭ@wWllRMI,=' ":&˜ug!j k4{,ѝ\j _{_WdA[C :^^.Ed+$wַ9`Vm(gTѪfjSp`vބ4ǣwVU Ęf$7j}." {LKFxJC^^|<*;cp8d'w]),P"KFM?UBF=Κ[w -8e?RF?Jt#I٢z3~t|]i˴1үƾn ~(rО# 1`v}>_8'Tn{ⷜA(K=f,1$bA&RnZ8kqw`RzW-Y |@`Ce6:K`ҳFKZW"uU&W/ЅYvJqKaߤʯb0)&ZLR )]oznXBɠ° Vڇ싈ըh4bn˴拓k(&HɸW`ziPʀ+^1rb$u&AѰ6ڧdlH,݁~)e^ЏCd6 'nXW6JOZ]놽 Z"#!,N!֧8/oZ^GlZb,aDm݋~hfGEI[1_Kk`W HlDB':1?=%=dK7\>;Bz%ې8Au:-)w}w@**Z9AE}W t?o(٬΋uYOєf%٪Jcc 3ցQbwNkǚ{[iY`1i:`t\>$Q e@jojr=BӏTSNIt J<^ _qs)@eԬ,_]ݣ\JԵ+DfG"'5K/|+0P'h mJg #-žcp'b ˸/&I'Q0 ~k\W҃& ^4Ӣ$n]z}KT0sc?z:J؟M}%zneKt'wR#˘WĊqB.>xtu&UFXL· qQ fSx=~|&YCnvςOh'5 Hg™eÂX m3YkWc=u24?iYcd |ʚxa==ᡮۼN< Pi܅b i[}dE -:]Vep1.I[5( sSW~  2>IN9{XN(Z[P`RY˴v`ʋC`.LHӊz)djLے5U\7t PQ)oaj;c$#nBrơU%.@R;Z,N?ϰer36R`1lD'Qgj3pn#zw5q:<݌چFSj>>GPT5n3 :$H Xޒl)?{^MN#G(z8ǷdCB^ƒ@97XٿR;]:q}I#8M ju_e=,eav̬΢fb1C2x)շ]͎+'M`.oY 8{o463IAir Iu[c>dbgj [_ [ FDab S=fw&sCr!Cߦ4* zB[^R1wB *`kxn)UgtP{Y2-}yQ9(הZ)㎗]7` 42R.DzG290q7 ûDoKr,Sp ߔ*}q$&Hߤ_PĮ+ -PiJ}T"wig^RYهt{g ?Gk ;hEb3JÜk8uy a2}S-3j+dձb" >7L10U]XV$[>p5_WWվmG_aА@h'i;xLlĔ=4vɨ|t]R;a{5O.,Wa9ZiISerܢ ٿ  !q*Ɉ/0s 'Tv+yHL'/Mr/ JZ-Bu1/~[);%L=7&T<~FJf|c*Mi @RVÚ82d+z)H֋ ,SO/¨>\_1_.a>e&ZWY.f D rQfg5pB !&ʁ7)u? ?C)5cfh}tpy$Q2H!J(fWOJ \<ޤ1 |?]T*9:4]֤2L>ʶNlbzO7%U[T5n4#mE`czʏ,,{J\ueZq5r전|RxP50\'U D ג-+4I1*/ "Di+_CwYڮqtN)qvÆ. tG|ۺ[#u՚*R$ƷFd 4buo&GqEr̾G^2yu~W.%ϠZ3S(7TDe5 zENRDݯ+>(̔F,C<׭ bѧ;mWM݆ϸaCN)kY ZCWY ẻs#ye7e}Y:At>z\i7͊GDi.hG:]UDDlI"(*{ &cclRGt{o1K*=J윩 ҍsYΝ&ٞH*n8(}(?`sN,z~LKijdg W SsiS}˥cS`Lњ <\HvP]7shw/o] &q{iM rz/;4 ~$ĕU3MN+qtU9 6 b ׾jgG o0^qLxM#@Pb ,2pBoAC. VF$tV#O!^^>)( 'R(  "!@'VJ`׫ߎǡQX@"S2Ȑ^!zNVsye|~qbz`X?|lEv NIk?I~vE`px0 U}T+K 4S.ޠc]"Be7-%iۓ*XkI ՗|bETke)Z"ʵ%donz(>D+%5:ozȯD>?8:C?QaEadۛS^fЉϼS)7A=yH+gWi,R klmjF堏XVK5,UYԂgu՘)Vf G !yU%+"OtQF:l({yAv0yi0S@{CEآ]Y_\iiVICwZ*֯8LAsIZͯwog؎7rYvBʿt KW\HMuDRUQM6-U50-[X>~g&wd4LKًrB5] k\kYZ MpQAe~ZJ O(%ِؒkÉTyj̾`:΍N4<Ju>z&:OyBzٹAҨįX8-l4|,vfn~Ml|iL`Bp*RvvnvCr0:F+cn5\ً DI֏+d?}չ x: "೓y6L.iDfM%O+wU§Ya&":(hn!LxIe- Z=F,a(m3%G4x(,pP=~uNoXjII=uc)F/sb])Q'DGWwp2f79ثRx񽠿^/o*6UaՉOnfy), ?p>FSh4婟YVfe+X|I^#Beߔw)鯊!~hkea)8ZvD0*p>֡cVtp:SQ o^)sb5Ut}yg2/SQ)W::Ѡ$b)/Zn<Ee=Gi) {VK|lXH*(]>* P(0qɲyd;27kjMB8~,+D){2fNJ^f3J9*wt7x"8ķ S?i&f|ŒPq XôD N@;ADp5{W.v {NvZ r3 4IG_k뺞0xepX ;nꪚD%cItgN9G0Og2A.]C O >S?yxb}b_6zRSING:f5."^y) 5 f]̑Ay߰P-s{^I$%J'tuEVyT moEޅUNDj/)oH<4-VsIC_TV.`#{w+69F(\Uq(] [5! @3D9ry;cA1XZp}@68]R34׋)<|U5Mll ]h<٫ U:T6팬ԠaD/*+e?*$/$fHD\˖S7%L *1!c*enL[7d=$v!2#]EFB1Jj WGX ;SDVb4.stgL-\ӽ->i.dX6p_ b~n2'h*kEOf(S’:%*'kuU`'G'Œ 2ࡧx5J Ug.l0{3; ImYѺˇS\BIhT̿![y&|CU9 y~!gM5ƝQLR,zJDwF^&>5 qx HKa8Xj- O~o1X֟QCI\CV#m8ɗ_=G]D!'ڳ)K_I:\`[jT~U:W'qIj7i򅥖R*y:%<-Up(4$9r=4@} t NSXHۛz6{F/sKHu S273 Yv:t܂I ~a 1@K@(R{vLbw|E+\f~5X>(P=z2B ~n cUP+a/Ņ`|["e:N6KV} < x LD 511֏ <5hC*рm|<{:^`Bן tyIcJ6pV^*sY1J| 'ZOx8(՛=u gɆG56'PXlݳEg]Hdi 3?.9QI6Xɰ Vf75cрo%s,^NZ8"9jH4Spa;&蛘:-9͚C]yőpUkVRvTPb]-emQnt>qK4՜HgyO›3MZgrA>+V(o<`: 嗋=0ռsì]k D"B)(n?H>e)9w{)rx)GۛIB!y=$SG=%cB .[_g; ךZSy8cxj*`_tpwy>z9r͠H 9.ϰZ%C6;F SITJ$N[cw4A5$|-恀Q2^UpJ<U$UoĕGg#զ i0KAlb$hc*vϬ ^u!#4bΒNV-F `@ 0I4/C+\[Vk?m.瓫y /ƛ1;7?*B\6kEk(€+G$%1]:ׯ!E_φ|Jn~͜Wls?A)O ! b?BHV`pz-r:[RQ$Tكꓣr |Jݓ^ C67Ƀ] CnKY|PW@bmZj+LnlCG >dw&{Ƹ\#:nK]H%7,͆Jo:bZ/z OkOݯG/ΰH}QaVvNXv 5A٤>;!LmZ⤁@ *@r Z\qxK6 o[v eӻր"(.v}5RmLIMEDUt6>s=TQ4g7pxyvRb\.39pʃ%Lv<|py9;P'MG+Fw4cd}]!8'5iNDyQz8xc5)e)s5dૈWƓ" bXɍz>;GըJ`t-֮kZY3Qb+K!W藡iң(8Nb,r0b qQ5k' F3KJ|`C׊tSٲ-Yrb*,NU(ət̎X֒GjE1;֒h XNʇ5+4'z *2/ o#W.\*&/&DR[X;[ݝ7Ps1Eu˰]%Vwx y#/s݂BiqJHcZ_΋HpT"B oyO5toB8wjZQȜ_cl'&̀Fw.^QG <.4!dLc6-_ ߇)LF6Y]S-EGJ N~Jxt}"oDEӊ %C_[.uЖ4VN?Ց˃81$.=T//C@/PG _Cp8K3Pˊg]s܊ _Im1*7lLhyO2#+p,ZjoPP!Ѥ(qR@RX۵gSB(w֭R{pnI)0Poۇanx3ŷa#Ypa0S־zÔ2֠麉H0,! wPj6\P1a}=B(~闱k00pr5{bѝcܚ@3ոpay=r\r`%)Fsؑ8.5E1ESLC\r K}۹6NAo*KC_3QXab4_* %Ki!mv!])t9!!\Q%vxٮKiB=!ZH Zߧj7:XYD4-?eJ2:YG<P@ID f]PQy a[E3G#.1dHNG>o!=kbhl;&Qza v GL:6QĚKY:P hyD? *y/I$D>NnhD#}˃YO! ,{~৞Cc7t(1<T.lo%csXɭ[f =hv`W0j3\=(]?`gZy1H׋FIQ<0.Pc"'<ť:]gsȷRV8}mR |  Arl D9>:#*SQK M_]kx<=WED}>QM3XdE4Y SwZS9A@t5jrΟ#N@-}ӦU cOJhe3ppsv0ڎ)'N t)NLf<;!-0%ibci7{;&Fdb6]ˣJ O&@tG1rD={d[#T7VU\ RTk6ջC"lK}ts41e;s ^=so ︖mv(m[HJ2-)8{Lv`00kxb%v֍~ydj\<./JG~g j-͸Pſgi}]3 bnwF}̎9rd{Q$FSdzƹEU; y$AL)F.lewV,:m [p؎SHVEJ9}dD)w0uiLM )]r1.Zo,jCH峕4Ԥ=])CS LX`Ew j[3U #MN:ֶ7yXS:~)Er|òN4rs7@a,IssgaYK`KIxM ?+Sùch}E4}t>* O@~uBh,yPVb6:߳^ҩ#14,3"rW]),Vy1Z+y"-)"͐EE:N|m%鵫plPYf$^]R܅XV)擵r6y 1m}jSkI8dPlfּ㸗)_ͬuBa_ Rl307rlQt:Tܷ< ˁXOU9f#[iR{뿌/dR=(cz ېȥhB@$u96rG*0kAOVmN6LOb/vBhkWEG &"!]e"~8>3Se.,UcUTz&wuzHԭ]qu4Tώʗ!>/Rfe'$2q3yHO.$i~\;5q èN92J˷pO<֏2"RhzIK],UGo1ԼC@Ђ=KXj +2ާ͸[C2sX7$f{ y^Y%)% ̋/? EbIcCɸ}Dm9:Q߭v!.tKBxOwS ZOkO6?iiǻZ#Т ęEnG+q> 2?%eDO2X XXǯu=nd 9; T!3!0h.+ndXy& pWN])Um\؎cygX"p?I}JoWRe%4rz@=zA\ÄlV7:&6[eW dBXm:؞> r{K#e!_+.5媰\J]JYzsG J{ d-. kjXw n/]xΎUj8:`ZYM'_76IǴm&ns5%?ũ^ #l܆' d'1V{](⳼F.8/j\R|?<_aV2ٗqH`~Ch ~ⷨLx5qk>մ/* e4A,!'-fBݽ[T >"z=Zb q70FjKmsrM&T>fNľfV(" ?.AcMS#i .>(U{_.u;b`j u+ݷVou>#ڏ(L u$~:ZOA çPSv?%t> bRe~*>|9{,W(򆩥nNZdkVlجD#"@9A@6pd?Zp!Թ4NLb N)WIȽp8Nnl ^}2F'uև]o/e[hc8DzH VL߶vꄠ,-}o8tJ .*'dy,ҡsڻ~fʒ|o1H90l,hn|!m-H4rߠ?'+h_|gB}KSzY4`A-}.LȒ&^tex]8U TI,iG.lQ'7җr3#vdH4P7.o~@c6Dp1#95'gYD8X%O +k嘚hӲa(u/^ CáN,Lf x{P}zÝs[$tt}}SU?CM(dQK]L$f4_lc;بt-;]ro hl#mBrY@IKބm+?aE# n+/Mrܵ~7$p|Rn1t&΋-{M3ݕn;yklq9 LK + mAllNC^?1u0lx7LA$'Ê]Oq fo=}$0upݷͧ޺vjE?H>/4A(VTP*_Z;d6S[3Ó3YDz%Zx`AЗ.l5'ŏGdr3^tЗ MZ'Z<_]YVcU.e@nt9򝂗e:Zm([b~$5iNx2[S-&,ت~ː<:$å~Ob{O. zJ74EǕE r{ c3_D+-.-L~!^xHy3׎4~2!|(4-_t 0lHOe!zA9}*$MceG;'&H5MD{iP/nӥҝhsl$ qzԤϖ&=hP~Lʃ]w93?y'PA/?OvTe_,qYYQ,w6M}FfD? N?"1G D ؄O<sY-tp#~^)ٞ7dPpiPlH&ONT *{|{K ?k,0k#?ez6Tߖ z=u_%@l@ɸk$G{HB'p F#bmF|"kA!V0Ti*K.!*FpY5Vp )^Kǃ2--?h} ],w fggVz8G2D6OEE-A(EP 5덟i]p,vf Gi ر%O% "f!dk@ P.3g+]^'ԧm;Q!æ1UU e9aK+9_j0O0d5Mj6Gl\E786j!F9X*sb8"72|j9pS~|_ZE#)A4UA`{}g+1 ÎP}# D1s|Ot\Q;}h,ywJ@7+\q͝:@+}i&Tq~ #h%{Zi,e}^L@1ԂYHkn=8BgݪH(W4(͟?&5-4!e[# ;B-gY,#0aH`玴:x?)(XWI-z=\Qh /X4v )ѦUK1A ~}lPF+9Գ3ӮY-(ia#^$2 JN۟\3-%WE =g5 eE/<|E9Ko5^FRSOUCAdo~q^%7ϐڕIe}3^!%)";:-q KbB_7Z`L8# 8.UiKK>NNE)ɿnN o' Fe\u$uXlT0>Jo _EY&PWNC{.lIJ(UN@Uxcgv;7`1Sc nCP]He&!}B?J>PIҹ$*FK9@U}Yۃl>n>6 `Ay|07Pd"|s".*,ēM;ǟˬ?L*yfTF y$Z+k C7Иl>xJ#E\i 'V@$~WBU##ѹQ͜UmKmZ%>:I|_M%j]0_4ޗy&l'kP^"y2Dmŭ]4W9qOD8C 77ZˍІ\u͢71MҤ\`yQ`Ó|L߻t#y^2 ђtm]'!hfES\{RIU:4טPV&+zXoނ7ka[B _?J'wRV**Y&eګFBu<>J;$Uw"z%R=G d`Cym6YA绂nٕ/^o5u*P`'cSб$vv8_Ϳ|' "ȥc/VT4ūg 8tys Í_,*SkWKs;&ͲĸT0Ej很֍3Y /U;aS=eǎ^M>;4b4cVAsA9 ],P1{ˊ ]<)xxRiO%DهF2+WcP</y@jE9 aƖhIM=KF~YgF׻ű' :wo_̢ ^:OmܤnqL"͒u[:ڳjJI+ 2,eAY5*VJD>WORjK|ĸBT~5 -Uxv_(hߟ(Bkl]nJ ^J>hc^^qC^^׼J,1J{4~\M与mU r Nq\,P'fHl _Իh)) F&C]_@eq`eo սߚ9:+SむEoUeq5!; ۠:w"e-mmv)F"3P%6R9OC`g>g[5vH.)xTs7fѕEB SGKV9n$!pDa\ֆI5-:ֿI I!qC:LgOܞ%_Xzs_Z3eY,^2Y!ߚ\1Uբ:`ϵoïv̶KSqƦzhƦm/Dw+\VD̠jw E{CS:KCĿ6's UZ)%x'{iJN3X7t9KwfFYCcr0,i Db<5EI&U;,)Y A\ @nRkk%`L|J(z /c$2B#8T$RrMrtStC3z]= tAle1ǀ HG az~1/63Չ^'KFg9)`5f}fGĐ=e慟>O/m޼/m0 Gi@Ѐc&'w$ucc8] [tp0]* /;3oQO.RS;3?Jz%id>",/`}!2(K ]j>0f[Aְue?1*yvScGj 9J[gjͱC:ؼaEVZYo`'Ω4]*(@{* (r8߭gG #"pd;'G\i"0n${nL2wH ,1Հl#qCT2b_7Ԑ^&T#]fɘ@hG”lE"0N9CGn]p`GPK~ FAEW0(k(F^+׸hnA "o%zրTa;5;]_=p\x#0w0(;_Dg~z.^NU0ӈo D4x$~ ǚ@T@^zRA7- mPIӱF=]D*0Fb ]?\/ȷ7VQbhiwߋ@B9;p7;}h&eΉ3۫h ,PGㄧioS'(Ѩ bzsb)G_WUGӏ Lޚhx}LUQP Ѥ{|H*N&Z+wKRæc(稏W&5x&=,}{Xe4BEb $v Gj"~6~A!|;:fEX"sV a*1n/kI{O^9w~kbyV2` ; }꣹S+ LaK}T| t^BB9TP}T[Okq@wH2Fﶊ[]WFX:h|F,3#kԦ;{BdB婖vmq)%R/qj\8MeV H':S'JًFO2p(ś1{t:Vu y')P 2oxvRDwckۆ~kQ ”-vkټl:bD@޲i 1m+K h+Z\[*fo/G7c8 G_bt`ꅊt{x-R`\vwov.B'@Bs$qT$7+oD5a9RaԗL%RaIR s@kڢ"JWKƮ*6~1ML%5_T\n(SV p /YXUr \/zm]h{r E%<>^>UM@cN L1u`?& ɿ)(D€9BW+UvXW.5SD?v=g@M>+cbCrCTNHhuxJ9H._/qpGsmyW v,^.e.]Pk3,…(P+5Kh4̸=& Z; Mmj 犊Z?iK4lI㌀ypcٳ} iEQJ6t&7T,HF{YI7Ek1{㈏]O4kc9uTQH 3 %ۭ=1@C*bR+f*zqcT},SqBAx%=p /z_;VAn59yT碑Ld`Φ{2 dŒ8o0 ]CaF(iCUܕaMUJkωl\^#Fu̶!.?K/5|~!7rFwA1.ޝ(pi^OUŞ be, "  *a8n.:kjL6YLN"h8k6OR<'A"a'*L"|o)W\QS}{5 \l?OӢ/F!nFC>o Z54Oŏ.m-=+1^!d0ol^6cѺ \;ҥHǘBv |6784:?v!oE5Iz~۶ɥM==멗$抸NxvtlN~9?O) tSg {КPcf!3{ē,zXx, !=Ed2l d5zJwIjU7G#igWe#9.9l- 2F6>7F2{ `LBa@'Ta"WovV1%R|ӂវem7 Q,/G2ʹR rH;k6z^\ NbMiU|aIge_!TFr折>τYHZܰ6W&gRk 5 r9$z v:"7_Ԣ5P::0O/a93Ǩ[Ma1:ՌJd-3=7 E&(.vnOMʆK*Q ? t˅$^)3kP(C yu%ݍ;tD|Q}zSΜ!)}R=k T?̽LTyY&DŽ+IxM&894(S{C%0*|AS #4R)DC!^ 6*i=|cfN|A$P_88d>&0{1EYԟ&U|L%l"KNԕZ׏֐qp`lvߏ]/tlg8 |h‹ns^o$9X9JX:Hβ+z @AG^I4]=SL9X)kt+^P:MEs\4tr;>1PpKB!4Q44 ؟]p/kDh;x~q=< n~ TcDkl,Q+:8 c)1A^+ ٳXݰ:&80MJ]-M O(R>` ڑ3jx03TU<4C(qx+ٺ}pf&/<:nlpFxqK.ed.\I`A#`f"c%i4 4 ۛˈOvq.v[W 1׵ lz*Z#-E%/U} ]|'rD[F)&hJ~)f|_ ?'mJ58S|:ف0/-D4 Ï@lOȗ8g+cBpƸAѢ%<@Y᯻ǡ:|*G RSoY´ymD#\AqKV]ooG8'=]A{;Szb|\M @V-p1hl V Ls-\ɾLE- wPy%7BM"˥idxQŁC$AH#e!)#^ooѧ<{u-mBhr$87ɠu|= kPYoL'=rџߎloFloY µC8cnRq5T…*݉׫./(NGg>D=GwOVV#R=T|;X&(v72ft=j҄خM>2?Ui&# )``|JHwjHJcR4Ã̝[РN:RBit^J jHEdT-$*K>I&LuNe0,Ghz)5%Yq!J)3oq &#U=*KVud.o1 E:!yj ߘ*YΚ??C.f$Z!v6!P|ε~P8.[!v-/Q*‚7A7+s:0*=4O[}y././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/HDFCompass.ico0000666000000000000000000020615214544243123013543 0ustar00 V00 %*   z hPNG  IHDR\rf IDATx}w]Uws;&IO )B EE@DT@A4XOS~**E@Q JHH#I&Mv޳]ϝIH|&}٧w74)MiJSҔ4)MiJSҔ4)MiJSҔ4)MiJSҔ4)MiJSҔ4)MiJSҔ4)MiJSnt𴻶v֭[G] M$#d 2"BB}~!w~+67f;je̫eWG~ُdErK61K~OIip O>4aɎP$@W'?"j? \!䆐BĻ#.tN'(Q1Eά OΆto Maɟ zv#>V`y@ٝhiF:VDԖ`x@DiS @0 D (@;m{Q6`x[>46J#]_C;wOIvv%8»} 9YzDK{iO*o \s÷~ A 9a eܖ16ʚwk_`YL#>ڪZ/<95J >Rv_o.=[0nT ,AlV/|WJ&RM eZ!i":6\M,h,m XטDiCENuW4T.X$9  yǠ np~]s\Y.z=F@J(`ؐrYްRJ$ @Bޞ=i6BDZpc1ot#h^߶k[J##D|,®֦/Db$3 C+'"DQ1d`Nx,X;֗ 1ˊ-.)h@ byz hAM]W+!t@ H}vג?=/48tZ^{sWy2YR֖7(/Wc5?ȋhvwP|GxvRY~Wh@; *?D݀K-nNtL\F)~)_]!!8N?8|SPVe6G?mm(O ~KyKɨOU2mVQlH͡|4Lip{<{G|O91 _?uZ KƑ^v_^Ƙ*"xrCQ1c01n$Dmxi20=lb>fdl~@B{A Ao5=9^$ sQZRG~<[hjio {y <YʇJOԋ^fow/CZiҔ6F]X=o;npDփV<΄*cB aR\w8k [5njOˊgܜL{#w-`Β*Y'' ۟5;T]׎?rs2KxZǻoœ%Uhi-R^oa>!w%ƟPf|gYESZ8t1\1()m܍6KN1^~@pDžqgE9xW:ں~5W748_]qo Tz{iQa|"Bk[VmY+wVJy^~Chy8fTUc:dbźjs! Z;5(}jPS߆N_aj{"@tOCt&8nq1ˈ`Gu# r S+4 rr~!8.r"|[$;!0vdKO\ Q#o= =wiDpFW'09ٙ}1ttv?5hhltjzpٔ2yr/^C]C1Xo??Y:.5y'rY_B :NFkJA ZKS P` xgףO>\s.!kK+ {.~k9@'.@6Ñ'6tuu~+.о^hSbYY.S80cjY`co=C4#G}.< H$9:bF )C<`FZ.DE+pﶎ.<% N>]'vT asr#2qH('#408C?AcKw~Y{SQRh@k<yC$|[gqHV.oyʱo!.`<2Q-iBQ(7wftt(+?,9sߟ 'a~bFJ\7u<|=T/C +Ja#F Br!t91 tA' +ҕ1ayYO@p`KmƳX~Ԋ8(-1C㔓K!-vնc ؿ qi#pCsKI7AXXb9~^P_{-_v3_K 7xoQQHc0?c S.ی_q<;!8e 0%2T7t|%PdA^L !"ܗ'})#ϴMrRW-3s̖isICW̉CnK4yͭںU[SAv͹k0oIlD,bTmMjZ܎O@z/D@:bfV+L /tF!y̓}_cMUm76 =H14uavȌ@d:0pRU3 ]a&rL C^ۜǺ1X4d"4J:iT9NSXQ[G渍t:O"wvOTUkXV'@ Tmũ0/۴q m/wgrSfL妃C Vp5oL>G㹇[(gˌ_9g)8uw_Icڬxhm `]^硦B鷯İ%\ 8#9"@e*#L4Ń8[ ŋrs͊n 1 hCdDx͸3orO;j[p]/Z oa %ad8̊fx+ t=9N]d ZZ;q s^Ce70l` PPeGf:hحɍ8!A"OI>)}B3# O.5{_DėvƓױp6,SqEiqrȉeH =(Ɨ pU_ sp B_P[|C|c-WO|֏9]fw>U'O՗a;X]uMh'v[AՎfejaaLAfB})ۀ@;هS_d+Ѓax.c|y{#$\zpdgEG{G}-_E!1Kۧ ?;*64CCڜx S4|(a A!@L"hmo7W#+VaVV1X=y!{z>T:gґ0s sMUd|+.SH"R?\34 @;Ƅ\[3Ak+/^t؎@\7Nl̅d۞Acs'r2#ODNvԘB YpB5J8AV s8P0fB{[;]]s 0&10pp`dRfc,yOeb_|@%XdIYn^?#iɰ׍e/]IX\WoDvVJ bSE#T7t` Ч0"+ɱS]f_okCn]C{8a  P;9H.-R˲vBL+>wqݵ/ @ :;w!@r 0}E }{ BcYQt'n钹e9xPo'kċ7oS5,)~}rhTbj IzB.BBwPLeۏ/GanaJm 3y>S=Y#(y*SHgE=PYs>TB`OgoD&%emKV@4̟6`MDLO{({0̯ݕ;P]ߎF)Fan&Vmރ-kǖcjӾ{!J2Q܉L<2m52g#lߞ0}f}Enhv8kzS";c m _~<e*x/5ip ԡ?7V !^( G9: =$9趛X,+W^|=ƔOLڜy+ 8Vmm. (@vVQ|b\9 cŠ_1,mj!tr#|ЈAMƕSpIaf~@fj\}+\m2<ېjLF@ F0p 21*ofd7"e~ 9օv쮮AWW-B4Q\$vWd#Ǽxk2O{0؞1<;? VƒZ_Uek&ࢉPhh-ܵ5֋,}Y-vA~X G;hq(=?ΝrI`{9 BpgS򳆡OQ u=؃ ICǸxsF<7m;u0~xDL @ 6dn_^W\hH!?/[o4rsb<{ҫs1-\sm4#=ar ]m/pBu #0GbtxHrΟ8ىC@SF2|L:?,ߎvY('pQދD<UK!L`{kPQ&oULGA ˯|WUtL %˖_3F󜡻:o? S @BOM_i ں~+Ox1O`)s[&eyg2u[sw۳8՟E3m9RGW )L8W_J֋gX> ٙzp_MR) QZ-xJa԰R A9?v0f[!Q IcX 9Y(--s ^ql cx+}/{#Oq}b3hkc= F)Ĝo@|ld;4-H1m~z 8Akөkg%(Ąaňx eyYcx<+axEͯ9Qԉ645w F+ˡsD2ܥ[`6c1Ndt"Fpz70fyaQ?@(AZOM CK5B(E0ƒ IDATxҐL?,BS{BVQBoʝȌ\D|"Y+v`q0)LuX_CqQ)B2By? ?^x3_^}@(cB̘5kd^A3Zsa8zi7va֪]N>SB?Ps|uH豰a2v:3:f; gא CBW(쏖\ lk 2gY6q}k:`3f:f (#RLkh϶iL$F SickOu(5SR0i ۺd@ &ai;Uh+Y(D 0t@!ލ6n@H$Bjn㵅B&8D$s"Wf^䢓Oxfٝ?B:TY9{7\狇rReqׯGgczpڞ 1:y2g0B~Fyҡh\է?0͌'q\ϊ`HQ2E\̂Bn8bQQϪ'lmyνBl'6״$kSø}з8C]ckmH򛄇y#*xj2Y[V|jT iYfv.ߛ sF\aVj i4*PP PL0-浂00"ծ>SF<|_u,?+'?=İR#H 2r'#ȃ00k]5~hl>^e@`Dعzd&u#ڱV2xO5N@O nW-.)1$I)@F 0%LEAJ+WIG 0godbߴi gC}v@̀ 7lX$)FڹL듣U]\N^2f|I=0}=mP89k 8చ9`޶&]CPx@|'D6T88F7DXӖl ,hxhlO`uj"[w 7߼j;I dgFiˣ%?́yj!Nmz2tݶ>.0\P/ePűX'l{x;?}|wvgKi+(mMgBqk;ysBrсˍ06ҬƁqPRMbކxD`S+3mwݹ'6 (5|p:31su5 cQ,7|'!Թ0^NtbgA WFKkw1dD<Tn|b^a p-5w`ӶjdgE1,zP^&3>,:;Ï:))>`{r~{=jVcS_2$s5=cƮ03s 1AHYX4`)' 4TN]4pkX 11} 9K ]9fD5jM<2,l)O^pUS4<}"H|g<d8͌~^BTZ݂~jZ)(TƝ>ɝZ8hZFQ0Tl09XB8o;]sN?DRОvAntLs 1=u^Osh/>B S (/\ ج1etcZrIY8I(ur3T8:ID Օ-'?  HM|?͞62sg,ך 4wʔ^V3e~~28BHl}m:p~]W:Y%g„0@YVsX_й)VDǜ<i"ՒU eO#zY>.<}.4'_sO_&A //DCSC.\1 rp`3/ {}dcޜ=H$25r@zGwfm'iAȁEfЍdz@yxaYgk&AN3;Z1?:?yW *Tʎg{v[{A0M!vه+5'dm׭CdK}2q@b oAۗrF\xdCdJw[G'}2\G|D+i2+daI.H_s}^|`#A>Gu] f~8#/St%9:jcې[8}B|@}*Gw@߳g}'`;qNmpZX uzұ|7HH ,)j{v2b|mف 1傁ww(TPNG(90GTjq(-YIGIַawm3[1c: (/F|\z8~P|v4 \ cr WV/[g~#H@fFC~/#عZ/QHRPh!OlƃO ^n80?sYg7ω۫VZs|[ےVù5[$W B$/!Ǎ`e 1f&$ n %#2H 2(͎3@x),%|8i Lp^G+F2ùgB`V T?d{*P1X&)`J(p D$A%!У/ڄx̣}uKmhOoŲ 5D\@o-0#Ϥgmu=O 0S~ m/0PDlr tq('4H3f;a4p]LAn}Uià2B&[ٴy '. LdEn4e:< B¤& =0CEB,*,Ӝ[s%vBte=٩_vIdܺ~>t@nNޣK iYU;o 5ذ[[ 3lŝ?mT*-VAi24CKF~ +?~ϑk ,6<@vC#% ;V?騉F"!sξA8ds3O}1ш~(E#J{wt/ ]S )ӒA}_Hq<#5v֨5M=T@&9ʰhP]G4gz>T7g\EɠO\ UYb>~ΰ1PЎA}WB&#z:4g~Ð:i3iog[30GpqҦ#lհdA[BkABOo =.b(:6r"$YG'6wӞȎDp 8c@y6a0|'Lj-t AtGT8#1㔉#0s2x .@č"KOv^aUprӼx[ز0jd#*dlWIK9Y Vތ',}1n:KP+mwt9V$Cyظa 5Lj4Ƣ );(12ut׸'+0 b ;t8FhAd&tcLc槃D!NO 8>rlW+xZ 7Nš=r}s9fܰHl1|~@'u ZӌзO!nE!ޕĪ՛0;p-?,rCW.џ1vsn%[G k=)JLPy~?~6~ A:TEif ,c(f6=FsUVk8Zd!2 J |,^^2P 4Y(!9dY~ơ[֩r}n US|;M15A /<9?<\c^9꡽CϿ?&~Wqa6JsÌ%?Tu&/ U=gΦk*k; }ݛFS[o-t 9NH1ӆ TL5 s:`>?yh^ϝb93.1s-0;^WQ1hA2B1BVv3C}G@h+T\_mv#*0bxP4A_& 8Ny2ƤU|Ӊւ%L B\F{24STkG`!?ʎ@kyL; 3j7(0QRE0vx5 !8Zɧ_>w`}oIߗX.t[*Ȝ76r K@?_inn CEصsmY`fk9$:z7$ߧ,'H:MDymśm&ga* O} c ۑ 6i-#. n aCL3L7 a7Dhw3\nvc!S\bR#%x '.gnr~])ԍp +Wu߭shxWOۭOH@D7f0yH$|]}Ý߼<):g/ėgSܣ{BYI%@Oީ? <Ǝn{bxkzpαR6RPhb^u(ue|=_#̓dLIanCn3=$ :XV?§¬_)RF3t5zw{dELq5";di!$pcwk9`:bi7_|dnn < K(hhڪ:>˗_}>v%?z^Ÿ3'"*۩ k dg} {»3ÃmKcTzk>93&E8F4YX 7{ ;¶֔ۻ4ٕs a4@nr: 冾)5Wl$zc ftO)8 /gt@W[`#+ךCn6Skrݻo}q"F*2{CEQ W57?&WZAveyT0,I IDAT&4s69La:`VhXOދe ]Kmj=yDcި_FmWr,Y }laϑZ1eFT{4S5usVՌmJ̩~"sp' wbǶGP+߻70tD@ũ9'ŹqCJEa_Ȟv2c\[)ش5f'|ZDOFV3 /yx|PXk~o%Qvcafi91sP=w[:8$0ZAgĥ7Ɨp)}5>X[Nx=!%g[-C$vyGŘ达i a !\~G3S~{n`o{QA"#$1Țٰ6N6Rә-H} Ej<[nPi9~c|a2lݲ|1@op+=w?:&@G+DKtE\~q L{ 4va[]z%ёRFs[d`#8 04TB25+Bu̻Ҋ:{m~g\SY-[9fK ޖ+Un<}uuro}$䣶v ~c|CÎu M.`$UCP%QKb`fJu7٘<Ɛ#1;;AÀx$@@kWu5m˲᎗am1Ǻ@wS{5|X}$ ohf%]=)[4Zwo^* B>RZ7Aiyhj(=*wό2q'fay$Es{~+F0p-y]m]OyL'?yJLҷ$[w ; q#sLYۆ {ƦfʆgC6ބ6bF4Ǔ2{'GA藗A1L蟏 wi X;Ӊ-z$!7SQp}۫]2?YYiڕaY綵#3/{QA-pH$Ah/h[=~^_ @Kݩ4`sp>𑕙c˖]:hðI҅ ^aE]ed"~{gfFpr\|p/뱽NFvDPy/ (]I)?3gZaLYvrbypW 7tms= S t?d]"TCr`Wcge9w9o@[p9=|p@D CLˋZ;y. V$@]]4kxLU@\e ðB 8qd&:ϿU)ge L r]㆗F-ۉ0Zs>(˄k]7?_.wvuL7ܤ7̽Y D\fߏ{gUא_fܐrI RpIJ{;W[ 'WRHYwC .k9GXj3 q4 k޹<zEb9<{df(/+:el?lӎ;UV@nf,قBkG]MZ~ cv`HQj tcn,ch0SJ{W8ƿQz_]Ҧ^ ?DL)HajG(hCVZzvkF ܘNkK;wAqYtB.NGdho| wjR,EDYw@Jm+9@Q%qmM誌XZt$taYc;2~XX[oO [Pa֞@g>*B/?jи%O78αGq?sޭcJ(U{k;KJRуqex an1@E裨bqDhYOPPn&iF>(=~@#ut߻^poʆCh,iAgl_ɝ8ÍN4&[%ڬHU zicfS 򑛛u[~f22b45=v &II9f/l.m)Mp~ˎ!b^)QG2ղ "ACBݙۇkrmڠ1wnJ}eԲnٔG:0{wj:zΖp<݉Z[!daF拐˝$^_-grp)#"] BlX븀Cvy枓;[:'>;g VW5b{;X$ER 3ѧceuhPnO!d!cuz{%EKM # zM u)BAj)kGq g'qqv }\H9Yhnj$e~aSxA{i([ y-qUB/VLш mOݛ[]au>XNc񇅃I%ѭ$pwn߆McflelQB/< ؀B5E#x(bIA*4H_~+G0_B h{rjYĻjOPїSyJRIs@]@Nh[o-e,tMy6HT\U<?z+W2~wci|o;~puFֿ5~n ݒvmf Z[=g/? U?Z|0gϬg:˃ ?0 k4TVLлMP6#E떗[fԺIQ:iM󴷼&3QW@QP{''t?o/[iQ R֛7Xc%&ǰsä=71V}˝SK8HA+#BqoSTPRM*'Bje;EI/QC#mƑ^pSJ֑ ȴX;K; #ϝ*7o[==8B'^ԩ?vN=;u Sx/EԨgOSCպ8xbm0{z$xiG͓cnzܘn8Cgz!~ *Ϗ>07$ZGJ,|w~Ql`1ޣ53/hT. `T+RXŢi>s80/?p,G]2t{2#Meg.FYbq%ʱ*'iBMcC#U_cl eq24}p1G`RZE\(3E-/rLNX^_ }nj~;֕e-H?{Z+ͯ)2ʶ:7l0tV)Z/0u7].0]2pͻ@ Kȥk3 Kr ߍoҊUop(%)T>o - p?0+(IٿBI_/|q?*Ú;[BiVW m jQh:x=p6W UYmq1G2Lw}Mv/rqׅ"CGB-I{bgce NQ'~a`kgz88Wfzmuw<>G}t!×"OS$:@SjЮD2~Wkzm k4jQ|N a#3cz·>po. l{zɔѳLA:QHXK k̙]{]MsΓ.Y{-Rߝzmpr>% Cg<uM0wF|ލn÷0 m~ q1N3NgkŃmIFAF(:ÏA04PP=>x #qb,/C|^`%p(;Вoff{̌_#S{F}7&%,y4561]2TcQ[QYB%B([FO *.D WK1eqNE~]NWsRq Xv1y@yڌo%xhm¾3_ڏ sN/qb1u R\ˇMx~=G²!5%D\UWЧ}4|  KXUn{́vzIogʳ3Xn$XļQC̘+mTucU#АT.&en(t =c,O-Qx2~/_/}ܹmw IYw4qػu .>wYa`N_%pJ'yvn,L܉lSee{Bo"e6z[hm@_Pd6rLNC@y3غ~¤'F1ujWZ{X| !%hӈ(ʀ}-t!o,8⢏5 { Ha#oACP4/1 sv23WfٗO N˧H5^?KOė;ylxvR003w~@fŸ! 0Tt.vcM1_bWDE}b@ݹi" vg_埫iܼn` &y}B3\CQ5STȩՎ*!3V,KDl9_'̭2X~?ڦÃY;#AO| Χ5Κ>Tu:\Gy4hs gNFz|)b])%{L?VѲD IDAT&lX7m&UH" F{RmP ,?p*8û\;+PSq@N/k~+J~=46O*/0 g~>1A f_򼎣{5_{HqZ;zo8%3Gws2SA2=^`9_>fho{Oֽj4O@ * O4U\^WI;f~VqViJd>˛(;rSS@k̒2!"ćR;o`m`&FfZ!_C+:~S2X_+-';ɵQIΠXF. '.9\{w @1'`tWǎUسc B NT!ĺ륿CZܻc+x ꄂs  eڕHzÈv/D%,S@a69C~J]?]Dr/=&ah nBי[SC? EbwnxfL {~8ky[Ii"Wdd7/ 5&o C!~ZDc2o.wEhD@!H)p4׏ܱ ?ͨ]4d{j7UwaRI]EM7;.OB>m٘?h3. ]zwԬ^j@(HІfNHuLeȤ N0NM'8uR{}hVlOj(s',O^Z MUYO|qCv1*E5k;osCKuBbvx0P@^ۦ"锰Eq]&~}`@컂i)05'z/$;ˮ Ei8r0L)0 M H]ϓL TG!g :r;6{cQO:gt6`q`Вt\E%LMMOF:A]hJ 3 + 3Tz*DŽ 6ɠ#AP^AM;';qC]4,# dk[_ ŭ񷌃2?iq*bUB5 Bp&L,J ?jJ{ 'J /$ЇrKVU?`kRY!t A UAYu{OM!d6l&Ϟ]I*'يČYTk{`6jZgǠSY W(0Bf~ć1EÜ b)䡤XԹ=M@j7 .付j*sބ=;lQM0ʵ>ٴ M)Rҥk<0!dSFv$yQhK~#W{4*] Jyu;*]P.r}[+‰ ¶_7ec} <3i@E./16t!*9C`a_H\3ҙ& @o CjS}L<'4uWBL L䥽K$s=Y͸Mķ8ckR2%h> 4]^FtG1Pg Ug=iT:q|2yL"?4~X|]FY8OpHTskԩ o2l9w~^ǭ'tA>lu}S f<𳙎` jŧ5 mC"aN HT44xlԙM:Τ>w2ZB, &IԍJ6% C+ :|B?1p|ۦ:;60>w|lê=yէ>ۢj-*emMXնda&-ט?% AAjfKfe8 J0Wj|ެ2]8} fSvN5QUUഖ@Qgf7 MZUs~`06j|$*a ˔a®-x][sUyy9QFqg Y|ޛ&*¯ܿ;ưq<]ۧ\/0o0HV;v;H 0Y"5JaY:7`ܻq23]bv6K=,:PZ \)w0l%h3+ʫӦ8h9Lv"xs;1T8hl:㓠806FIt$:䝭obBrNݜ]C춽w•q ^HmnĽ'k;0O]QB1ۙ(.bRV[#+ y|Z:Rt Jz1X"5ɵ`x@|ڦNj"J#0c=I#Kfo/1zpzW[^ VְzG3;[8-7="YKg_8øA0uۈ{b/o݄-n*SL >c_8MjLMwjWa}]2)mQ #xJ 垁;1 uPf|P4jU|J.hZW_8]0~o~wuVaq%ݑCGan7..%@Aoj$r  06LOΉ*d^3`6S>VS2HȒӉ6pޝޝ\yuUA,AJa1__@`<UOb{[ofH1 $ = |~0ɧ tGj R"d`N>]Pڶ8MJL=P}'!IbZ"H>\ҥo 6rUłNV" Iu#*V²Lf[cZk1ݽy[;M㘴bJ J Bl$R^ Íͬ[eG%E%u _j59U`@d\dHŏ#qiVt1a|> #-)M5D_rT\)3xAPsvPe֦ ~0sM>5 r RZ (qT1;[k1MnOܓ'g,;0'BсK_B*bT_k^v%هDԙߣ\hC5_%67d+;$75I@jٚ|-5RAjj07@a )%-v)ȯ@W`bsHXns ,6ΗԼ͉T2{ -$- U2|KǾ$LŠ/S{DU*SmrPyj<.%BdR.m~woބ^srj0S:2~T%I89& MEPnlPߘ)! Z&/f%on*t4G a`22v}+O׫#34pm|A#A`Pglqt@YRM`ݣ_@ 7051ېW&0Wۯx@^/Į@ `co]@p :6ghӿMJ!\_T'1&;8&h:}eF=f4 :@#9vE.~w7(fnEK?ɍ"j-j l; ԑN#|ؾu;`jJ|&/]4& $*hECm"t9S63CO0L&\0>gnY%Wŵϙ0g2lM]n*k-hϿk.|F0 Lh`5*F-+Bpk`Za:I]/TG]tӀT/z'0v`(@(f|FNKxؼ'N BZw@T) *^*]sQx/>W)!D]Nٸ+1!rGsjv1h| nћR2m39KHF& f& 8gfۮd)gJD-|Tb 1;-IS~ J\< =¥QK TLE# 0I'g؁6P7OP52 5k5zA&eѿ4Z~cN)I%u=5~< y1M<_fj p%˘3mJ#3ے_j EqMC`j | .P=۾&_ppIAH輿"H_F.f^qpBgvZgt2Z]3N9Ta jࠀAkx{*P0)h#?7 ~}kYJ7FYh`Qo4A `ҽaeB Ř??'`ad& S.\t?(l\Dr\Y.]o&ưk;1,| d ,+jA@Y=&uT,/_3#r;#Adޖ/JB8ulnjo`}[6aWjQAΤ0~vxXn.4y,-"}2cɳ6)Q(0Ϫ#6~sӖVo(^AgR`[6J`~|tp!iM TU$("D I6 سO| O;%U2H]ZZRA3}̩.VpbglJۥp}@%y U21CAZDڴER4@1XY+hteaFwA 6+2 sؾ>@9aMڦ oKq_xb6HStx/Hl HAX>CpN|+ڼI0|88=C̟$h ֢U@#um0Rpd_&K$v=vnnϿ_} p߀B53ϛ*82)<>&@k F׹tuZ:(ܰsC8)h"dZ{# ?~4^}Y5}*TcG :ϲzv˭`n>2Fn,ߗkTJ, 6nܳK򓟅(F3%Fq}BPȆhD*FV^<0G/`ve2\pYI~M(;7PWy|ײ޺}Ktu$ԺqZ͍r˦⚉>JffIa9}LW'Ք !Esn$+=tF/pLP{Pݼa8 w?CZW:h\=HwIA+`/D贄GY϶gW>*]ri-RYTitnRK:ϡZ~*fjg V< | D'پO%])A7m;qzf O=F DcpjZҸ_Rv--&LN^7S#&!' b t?v~"{P)( -q~U0no~L+"vqG֝|6¤ |d{,uX]},bz6@( @4ɱMp0@RE^S&0(R/K>wPY̭3t22عm$`}o؍ȕVKg{80L2Y UxO·J"ٍ,<՜-=3,`0'i㻓Che &B+ftZp\?֯-]h:0 E춂e3=ժ Ôpiigy ]Zs$1&!jg<"$$dž, gKA+N)"̤ZxpGBlj)uD9 8Qۀ_(A%&E~%,WZXBmao৯ e!*S:ms1ټb*ψ^L//A ɉni3 xE^VNGs0\.||q R0(;uwo@rN`lz .WUcOBLO`ڞlT $Y׽,0h+0mBc*~Be`UNBAUޕXt )yCb|!r du gF7=@ODlSRM$ `<W ;ۦk)46.h<%s^[_Y \YL`y<;hj٘Em;޳!<)qAm咬Hqug݄0| eG2.>+|@=s(%3.SZ;t:6.Blܑj@e͟T,3ȫ)P^n JT!۹U@%hVH*uqfH?b*]a"@' kB;e$^9Cf#\J ,J:S &Z"\%0g hz=4K6֑uq p,zx98l@ ~Z6 l Y-ыEocמ g@elݴn(&$攪'OM lڼAʨz\xi=d A0 IDAṰ qjDc('OP7?ؙZ ҁCTW0 @PaӃq7h1B$?u7lo*@#L,GhR6]䀆<ǂ_s2} X9_2 8fc|앺W1n4WWL~@+&+ܾ`~qDPzyA+&.j 7lĘ#0WKIܰZcsѿϤrO|%Z *ˤlwD&@?`*вxǏ䭈ol,_EI>4K:SK_*@Ur>z1(a)pVvQÁXg) @2yǤ w&3;hD ;̪Hy:w#‰T6Qmdڬ}-03]p]veo:\*J3%li%h~w JlfCO1bb5 g !1,w8iA 7c2&6Y?Vei$?Me?g#y70 % l9fep9/ j$_4J2 *KK]ާhì>Lk_1s-'w^iE, @vMxxs_CԘwA$X<{\8g?]3Ep"1Z)(J!Y9&_{wr(:.J̈*`LێV5п A ɟ3phIw>CvdIą;`WFg^^}Sh >a|_;#5> 2p'cߍũ%\)Xd5` '9F%I(22ǐ@:-2#P\;3PId'myƺ^ACYKN 9ʩ6l[R`p;RwNf щ˂+gfxvTCUzvzyʒHPW% R06 {sv!sulzaPwȃي޹SE)('I4!H|l@˰N, 'Z<D24Zi Lv݁J 씹*M?PHƗGnyga|?uK 3V*qf`chv0r6 &bI:Iֳ95n6ܴ{'ɓ3RK7WqeP_y˃-Btl|߭Sfgj: VuyNEbw g(MoI*/aN5w7n21el?6dyNZ[[:},6a*N|)BvM@c4FS2X-̯S}"qP$P Ma*󍸺  9 } seɟ#n )Ӫ;K5Atm^p &K/) W$ܹg+y 6X79VhIx+$Q0Xv%^=b( 'Vʳ~D<| M[&UYg*)snh\LX}X'S'^f`lh#(jъ}gX(iN.qI$ ;5/"hO¯1}^oI,-`M;uk;~}ބ: 3o $YB*?3ë ! Ƚro#5 _yɼF*wRu$٘RmPS3$ rG@~W/0>'/:e痫4۠`8WP~[W/~S_8+:6/Ffi>˧Z?5 C0(\~X1=ggc<y%,՜ߞsЯqin]ߘ.+x=@!?/ƆgELA4w! ߕHw'] JUS/Z*S.KtTΕl٤~8K0&% YH+CQ 8@'wtPwei?A2AV' ,3k@a8gqo3>ifQ/L*SU5}>IP_n@7jBqlvhS(_SxFyώBNcMh _\(HF@g2ɩH Mp>8=y%"l="Rfns@ޝff,p*`-Ɇ}<§:5\P!_t5 ̈́fXXcTޏ 66BWx-صc T9t脴Nj^ 3{}(J~]. 3?e`LDIRsH=9UeZFb*lP\:%a9.)V@R"/ I/#hF'@Ε^#"88<(32>[B0I_4ax?۴-4 g4wx׶zؚg`%`S{`}{.b:Aam}+BW=1˲רt9  J{%@PDWߩ#*Rh3j,ƣgo!K}q2Vs ?K'PwÜ{DUXj1X P骮-46B%ž'`@Vj- |{ @W V !9dLno~_DQ}h' jeH0sܸC/Q[Ʃ3W2E<>ޱOMM`yi9XLW061AT:#:&)t,i6Y$qAl2 Pd 肑'fp"XG 2xDD%9gAҐ(:حOs=3$RlAj7ab4,17=%4M±3[0'pVL+3sYοt  0]kp,r H%瓀s03/%(C=ew\c@W +!IZ^el7G54RdQM*Oٍ?1My[ W[mGu@9KK

0.mBC *MOT`2Ld> M<`LٜQ?| CM4^])C)iSB3R62 q&e_: U?Hxep:!e߃3 \6gJ2f߽dq%ɀnˆnGӯiƑQ,,,4}DKg=|WJW izb |p1_Wy˧`v"-rݙnĭ7njDA"/hDR~$$ɟ:qvɗst"ls ^wBDQ(:nupޔ_A^< m|F1~  -C GuOœdC`P07Ԡ8J; 2#yuܬٝ<2:^lc+ ?sݙCms+ujifT R5&ji"mUnDh' S2X@PU1*5Dj@FVP>yH%*[ z}mش,ȝ"Ei}as#G-1Z?HL-CmzO%.󕠀GɯUȻ')9QMiQnNMt̡Fg>gƱS[Xp6#z}*+_{xv5f vUW aɶt#PA,HNiͷ"8b#ɀ~޾@ELDUIH£r~Eàbbe]eq*?w/ך,CFJ9l9oࠉ {e;#qJ$25EM\q{qG2>Pp]4fhv1(R޶ 3FEݣ&P/Ws gees0p +8ؿZ ~Md#,OQgbذy0rt>QɁ$ `ye;.q@vQF6n<넆6? l߬\`"C5n8-t4QdȉJiq>Mwuoe;I6gTRz}@VQ/˘YaRD*Sšk0e06]7~̩ w!&g}I tf:7(St>^kgf٣->e}aF7b3 .c0,S~{>dZeaP?D8 8Vߚ5J!K..K  VLܥ:V%1PvmqjHu vG|&53&˕ï Z[}mӯv?[且_lJ22wX;șM1ph?&;4ѝ,@Y26@&'a;7MbjB6>&14[ ee3 a:DWaA3p͠[y;־k1fTc@˥-!%pS#5}p3M-SA\עʛMׁ # `; l\>K ޯ8g^O>W,]5>`(4@Xg$+˽$[ -k`–-1ccl8;:cGf6 I?YH@e܅. Spi]N#5x[pU9='RN@d4Qΐ)KnN%,U 8ޫ8cDOP an_`C :.^*ݯBWUX犦pH^B1!jc󺜝,S>HLա@"4b:=%7:R&^J)?4(E-쫟,'tTtaW"Cxj'Q//8cLN?ej{֥Og?5Vl|Lc`]c+/X4#Tx}qi';H/o:yf%2OξW_dZC*NRŸV@eBG`5 PWY$*<>k90׭8JYLQAwߒ w;8%kDK *%h~eӿz(u/*& _C\rz"輚'0?ʆA`4upc !GzRX,`ɤdI/mׅ<:^BT҆~j~=l%ڃ9-y5*[w!˯j_dB5 Mo7ɞѻ,ÿ4f溅tBҪT%;@+x*4O9^E_* q/JrCkn 5rNt2kZs.jo89~&DţqBEIJa]~f ^3XA<n-])~_H۰gAmL]$>TU#_\U"õ@Iszkc޲5LD O[GV4P  <54PZ9夳7)[bry ˃ۀ b|ٺZ8)68P+81GdAR ~c? 濩hmـXݸN,`%*?,)  YG"ĭf1a4xzi~lAA7'|%!(yW;[?fh@ e[ x(T# s!`@Hvx~d@+ P?h50F;$4138"}̘'I>kz~}?uvb}~D-ߴt-OoɄ:2ˊN+v -ȤVBfzrͫ +#׃`ۖuPc5!3<8~*LZeЩ*—vyuzMB* kǞg;b3h4&S:H\N!4:N} Qr~S#5zm|%5PI,hxj ZLM l,ehS@BT!(Ӟ/L}S\?;ذ͏Uqf j>>(ᙬ qm-%f=7;hPF}:QiK(E3H `FݝGӝd\!I]] dV^䍠dg|)ݳdb4_BZŴd>\ ~*STk[3m̃70>+G^TMׄ /40 TU ]RCAUba 䭻Wˉ!P^0T*Oy+&s1HSDXpF$xm<6*$zmAuoX[R2ۦ4q`:!Ҕ|_k3 1 jcpm*V&<"QUK}NAgXD}5w?sqWw.|OMהi|m5A>Nq:jş'aӦu].đcEoQXDrVt{Wu7؞Mױ6M)[U]ZJ+H,BB Jl?*([**bkC4]6)M$bDZ'^ƞ}\{ƅR7ߑoY}p|b.(}T1Ζ=G*)t q (IjÝr:-j'!WA9鵐@Dk*6R`-r3@CG(?r9x! ' gJM*b?9%H劢ȗ=?}W\q 5sOwy 痆93`U/",IJ-X~ =7S[30( _RNL! NGV, bL 3t+`{+i24i8ƌwLq핶a'4(?,\t M08ωȪI`BI],JU^? Qr;fFs.[u{Ccʆ &^T@m9g_ŊD 33x-QX\3'>_0y/q!OאwWS*ҏ4e#q_##Pi(IY/̨t^VzRD7g'0s'*~\׏6P}ڂglA}'/R%..[Aed0&@ *vP!M ,,!^,$*8IC`td Z]߭H[Ȼ+2a'IaJNI${R},S?-[Wm] BjM>)>Öר!?ѰO;c@d#^#L1 d&LFR0K~N^NedWH!`:+Kii̤؟T07k_;m,)E0Ts]""O[Az\iʂ,.( E@x7Բ_%eDGv PB QY+lӧpP}ܦjҐ(` ǨZy[Oaˆ %c8 Mi `5N)`u#Z?p?# aȐg/+Qev\Qhis<~z1>TDDܧ4y_ԧQ珑~>d] >E]B@3 |F)V؀ 0r^ dCd@V߲o#?$ GXZYSϜGn05Wd(#wfƥb̦RAOJ¯97D`ԇܟ +4LσPXIV]R*+I\/2+pks2`Ţ{'gbȆ dc_ד#R}HߌN=<4~G!PXx3 HyQ~meZ[F^hu:Xk$4B\4XL^Rز?}ƄIcTvtI p6Hz#\sTYga] :Lƾ/bGU 1 YBQƆ]]B ЁdVyv9?0REqM44]<IKHmT9*⮕}q=/UAC)~al $%Ua`3{g~&MEﶃcD0?LVS9Ui?ϐg]"Fkʯ,!} yKoUL]tʢCR\DBn_.upo4䈾`!VR @6v%'Xq9.KQ񹏠d ``y$ qg xuhOW=ӟߊM A\֚R28rDo 2Xur^/Y(X]( 13߉$GOs%i?s]+KXp;+)<'akj:QVM_ q{ ]I=*?+o X3pI`/ r|3;D6lfcvp"dh.(OǬVVZ8$FG12:8/ܺMvManvY|c%zJtH  'V޾Ƴk </R˾(+O{Lhc ҇N].]y"{'f?R6=@^IF D7#${eP6e9&NObqa#Į]pu;V+2>N93_ 9>}{aq_)uS(X ` 3F(J :cU- L `;c#uaXx `~黲~v6]Ukk :W\8yRUڈNULT䁰N0 ;3S^5/ ť:.ʧ ׏>}_/nGnnJl#sWlQRxQwc 6uGzp&S|2ꇈ? !RX 3)NtWy&WKc2c+I=gqВuG)m٦iW7rTor@֖s͎a:YV''#I*T/;b(_Rv}vy Q t\¬[#f .](6?}yGƑ9{6 dȄU n\蠼u`R@+:ȪFS7*¥3DȬwܞAœ8c/V́Lx1j% Ե@g˹K܋N羍G@ N?#,.?՝ gEr/.Ϣ*-GȘ=^A_ &R\^WJ{u[ 6mH+`i>rpUT`9G3kר ybX[VhlpݕssmTH$,$cv9V.nBr@LUx: m6|Qx- aՃXV@TxqGpP8zdV F.cCy>FX3$cD }u#>/~xu\r3-[o| D@J_!V$PmwYgGTdS JzJʫA\v[Q+w !r)_RvQ&d}:@(徐 _|M7kZK~q26\ @K^8mlN2 9 d\g"؀3 yy=!֙kV mէ҅ivP{+}΃!klp>,iK ~=4 <H<3m`@LeiXxzOfp*f-nlH> ̾=B ?#P:qߪ=>&)uTXAeM%B)0cs%|6 +( :$=BT_B5C8y_~~s??}l```-$^EqH,o:s70҄uV o/HbG/e(;L+KIfAy9~.a lPPOdS'~{ꣿ{-ޘ}d^Qz}4ʿ-}8{P YrVNƣDDb}dLWeLJ*=d" SIDATV]"6*|,@$6g-Og~߾øS{ߌMHsd@6c"kGS%8wĮC_0M , <=o(~,Fj)PO%o xc,ğb쟶wAjI24~6Kʾg_7/nW餢;wmM<&^ަ |K}0>>N7WpQR=(?%A^̃q\@ I6iD?>ov˾~#Zwk¬=f44F}bZyg4KȒN-;\sn{GcVL<\T 0r]Sמa@&edB~˻I0vP,U:?*A{M'Rjx905&α7=`<0'g)p` f'ap@l'h?wމR3${;:42k[^}Tȃ]h?Gh$H{9VVZس% }.\;mZ^G _ZS:SioE;" qnt גDauq $PwJ\}k_43 `,6a $cHZSis׹|;nr'9U9c?kw8P."1:ȋ>v3ˈ̀{]koI- !Xq!o1ص$ښ _M NM #4b>M/`|xt&@qQKvqNw6v?snF\\]k'녗H/Q|5~{woa)wOjjd_۟=_{KW˷,5\a7`stv;мZjhID籲Bj1:_78ns``­@&XGC": vkʋd?s/ 76GఛLr $]]غuMs<g;,!#K}EQdi"dYբם<{Z7e8bls;wqnfZ&˲du93kmc&(e8tqΝɋsʳcO8711ZjZjZjZjZjZjZjZjZjZjZjZjZjZjZjZj+:IENDB`(0` %* 6$^f^I[O9J2Z=$a9q`GtmXto[wkSsmXokWHfbQ & F8)`03A-YJ5TA*L2T3`7mAwE!O*K[*e,n.r@w28G6" ( ^, 1 6:!?%E*J-P2V6`:j?tC~GKQZfq{{o,& ") . 3;">"C&F(J+P/U4^=a<i@rEzIMP T [ c k pofU , P1 59]NkApEuH {L O!R"U"V"W"Y!Y XTL݆B(& + t3< D%P0}d:f8c7`6d8g:i;h<j>mAqDtG xJ!|M!P"R"T"U"U"T!QMH@x9*, b4=!E&N+veyQ7tA!zC!zC!q? m>p? r@ p@pArC uF wH!zK"}M"O"Q"R"S!R!O LG~Av:i4, :4=!F'P,X1L+H$K%M&J%xD"wC"yD"xD"vC vD!wG!yI"{K"|L"~N"O"O!O N~L!{FvAo;f4Y0+ 3="G'Q-Z2i?%N(P'Q)S)S)L%}G#~G$~H$}G#zF"yF"zH"zJ"{K"{L"{L!zK yJzT/zd}jPj?`4W/2: E&P-\3g9iRúƾtTT*V+W,X,X,S)K%J%J%J%I$|H#zG"yH"yI"xI!vH tH rJ$qXpwgOdH-P.G**2"7B%N+Z2g9s? zdƾ~^Y-Z-[-\-]-].Z,N'L%K%K%J%J$}I#zH"wF!sE pDmCntnpdMJ.@(5>"J)V0d7r>}D"Ǽyi7d/e0d0d0b0_/R)M&L&K%J%I$~I#|H#xF!sD mA{fxqyeVB*D08#458D&Q-`4n<{C"P-Žɹ|Fn3n3l2j2h1d0V*O'M&K%J%H$|G#yE"tC!qD#}yri[F[L6:$6\="J)Y1i9w@!I&Ӿy5w5v4s4p3m2h1X*P(M&K%I$~G$zE"uB!rE&|vlZJ5L;)48B%P-a5q=H'úȬ877}6y5u4q3k2X*P(M&K%H${F#vB!p? oxqvnZK:(1 @3 : G(W/h8w@ nSęL::98~7y6s4k1W)P(M&Y7}G#xC"q@ }ZAzsk@,/ |9 ="L*^2n;}C"ǜEÖ=<:98{6u4h0U)O'K%H$zE"s@ oA#}umTD4. 9 :@$Q,c4s>^AӬU͡Bɞ@Ś?=;9~7v5c.T(N'J%|F#tB!n?!vn^RA- ;!RC%U-g6x@ ؛߿ZݻTڹR׵OӱLϭJ˦HǟE×C?n4b/Z,T*M'eIxo]P?, H$^X*q6B!O'{ؙЁ`WVV߽TܺSضPԱMϬJ˥GƝE•Bd?5ē:͝>ԧBۯFILNOPQQPN߸LڲKլI۽uƾ|m}ra~kwrai7xfͮm,}26Ǖ:Ξ>էAۯEGOKMMML޶KڱI֬GѧGԱfȚúvew}k^,Úuq.26Ǖ9Ν=ӥ@ثCܰF޲GߴHߴH޳HܱGحFԩEФC˟Aƙ?=~}XAsVBp[Ju{xjvέr-15ƒ8˙<Р>Ӥ@էA֩BשC֩CԧCФBҩUɛ?Ŗ=;8P[9x@!a7[<(xgt$Эip-|036Ɠ9ʗ;͛<Ν=Ξ>͝>˛>͢OǙD‘;97Ըe7sRdEeNtZF~R ӷk*v.~1357ď8đ9Đ98876~4v2ּjbK9~r|xqּ_P\}6}12333}3y1t0m.f-h7̱·ǿy}vݯд̪nq;j+m,o-o-n-k,g,b+])nDqbxoc8W%Y&[&['['Y'V&R%N#en~A#^G{kݱ80×q_{iDFlJƦO'J!I!K&D!}?y<nYjZ LsB,R kVu=#q<$f0gV}bTx^N lXa/*]+z~VE_,l;&eU_,\+X)U)Y0P'XS*vB v$DVZXW*LX+8_1????( @ / . VA/aV@N5^7qV;wgN`?łkHmDD]ZK& + 6!7H2H.Q4]8nB}ELZk x&yb_, D- 6F.F(I*P/[:`;kBwHNS [!f mhŠ[ . n6A$}qaY<(Q-W1[4_8e=lBtH}M R!U"X"[![TG<. t8D%nXEg:j;f9h:j<k>oBtF yJ!~O"R"T"T!Q LBx9>/ V9F'T.w`F#J%G$vB!wC!uC!uD xH!{K"~M"O"P!O KyBo9a2"/ 7G'V0lC)jJQ(S*S)K%H$H$|G"zH"zI"zK"zK!xJ zX5wagA!X0H*6C%U/g9dKǿX,Z-[-[-S)K%K%J%~I#zH"vG!rF vV6t|iR7A(`4$W+w<S,֐YUݻSضPүL˥GŚDq2B!U)ógWYYXVݻS׵PЭKɡFAp4\.}_oN=-`*2p1A!V)и}VXYYZWT۷P԰MĆӴѷpźpYL;`,l/?xWƤќ޺URVVWXXXUܸQїpZN=i.T;ͼl/<Ǜ@өEݶLQTUUVWWTa¹ygM=-e,h2MѭhҨIܲIORSSTUUR߽_¹maOvr]Xa+xyD֯t6|04Î7Ɠ9Ɣ:Œ:’?75ͬǫsvvpܯW]|8{0}1|1x1q/h-i9Ȼúǿv߇Ѿ`/^'`(_)\(W'Z0{cJ񰮣T:ǭM+lMŦO*M*S4y=XBzlR)R?(j]; |rfOzumWbrmX2fbQ( 25G2N4Z6qBK [%m){ zo  / X2M9(F'M,U3a=mC|KR \!g gߟX8% / x; `H6}hT[2^5c8g<nCwI P!T"V"V M?N1 d=!O+|F$F#vC!tB!tC vF!{K"O"Q"O Iv>f481&"W/{F(ƽʠFÖ=:}7f/O'~G#qA!{ofT- `J&m7qTՏW۹RԱM˦G•Bo4Y,mNxq_1xi.? a5uXYYV۹QүLɡH@_.{k:#|h->sLpVXXYW߼SۼgõmM=-he,pH-{IΨ]լHOTUVWWRxp^<(>a**gU̥^ӨGKQSTTTRpfStp\f1X7g;˜>ڮFLPQTOڲL{k{hƿ<¹ô}@6Ξ>ڭDJJJٯHЦEΩ_tv}kЋ}=4Ȗ:Р>ң@Т@΢JÔânV7ævZO/y?!P9}&lX^,(m>*nwO#`4w^ɭn2f0R(J%xE"u^yeM;'9 I(}M0—N8q3S(~H%iQ{ZM<@"\0vnӭKƝD9W+x_`TD_)|:jԉXYUղNҳp[mdR`,z="mٿݺ`SVWVЏpgUzzWƛXԩFORURݱtkX>_Ɣ:ڭELLڰIճjv}k$׍X6̚<̜>ƗAMƳgT|lҵm0n-g,oDĻr@~mW=Ʈ|F+qA)}o0owTC_1?././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/HDFCompass.py0000666000000000000000000000226514544243330013420 0ustar00############################################################################## # Copyright by The HDF Group. # # All rights reserved. # # # # This file is part of the HDF Compass Viewer. The full HDF Compass # # copyright notice, including terms governing use, modification, and # # terms governing use, modification, and redistribution, is contained in # # the file COPYING, which can be found at the root of the source code # # distribution tree. If you do not have access to this file, you may # # request a copy from help@hdfgroup.org. # ############################################################################## import logging from hdf_compass import compass_viewer logging.basicConfig( level=logging.INFO, format='%(levelname)-7s %(name)s.%(funcName)s:%(lineno)d > %(message)s' ) logging.getLogger("hdf_compass").setLevel(logging.DEBUG) # INFO to minimize verbosity, DEBUG for higher verbosity compass_viewer.run() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/MANIFEST.in0000666000000000000000000000060514544243123012651 0ustar00# Include the license file include COPYING include additional_legal/*.* include README.txt include HDFCompass.desktop include HDFCompass.ico include HDFCompass.icns include HDFCompass.py recursive-include tests *.* recursive-include docs *.* prune docs/_build include data/asc/*.* include data/bag/*.* include data/hdf5/*.* include data/hdf5/Download/download_data.py././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1714825027.0269916 hdf_compass-0.7b15/PKG-INFO0000666000000000000000000001310014615423503012203 0ustar00Metadata-Version: 2.1 Name: hdf_compass Version: 0.7b15 Summary: An experimental viewer program for HDF5 and related formats. Home-page: https://github.com/HDFGroup/hdf-compass/ Author: HDFGroup Author-email: help@hdfgroup.org License: BSD-like license Keywords: data hdf bag ascii grid opendap Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Science/Research Classifier: Natural Language :: English Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Topic :: Scientific/Engineering :: Information Analysis Classifier: Topic :: Office/Business :: Office Suites Classifier: Topic :: Utilities License-File: COPYING Requires-Dist: numpy Requires-Dist: matplotlib Requires-Dist: h5py Requires-Dist: pypubsub Requires-Dist: wxPython Requires-Dist: requests Provides-Extra: geonodes Requires-Dist: cartopy[plotting]; extra == "geonodes" Provides-Extra: bag Requires-Dist: hyo2.bag>=1.2.4; extra == "bag" Provides-Extra: opendap Requires-Dist: pydap>=3.2; extra == "opendap" Provides-Extra: adios Requires-Dist: adios>=1.9.1b19; extra == "adios" HDF Compass =========== .. image:: https://badge.fury.io/py/hdf_compass.svg :target: https://badge.fury.io/py/hdf_compass :alt: PyPI Status .. image:: https://readthedocs.org/projects/hdf-compass/badge/?version=stable :target: http://hdf-compass.readthedocs.org/en/stable/?badge=stable :alt: Stable Documentation Status .. image:: https://readthedocs.org/projects/hdf-compass/badge/?version=latest :target: http://hdf-compass.readthedocs.org/en/latest/?badge=latest :alt: Latest Documentation Status .. image:: https://github.com/HDFGroup/hdf-compass/actions/workflows/hdf-compass_on_windows.yml/badge.svg?branch=py3 :target: https://github.com/HDFGroup/hdf-compassg/actions/workflows/hdf-compass_on_windows.yml :alt: Windows .. image:: https://github.com/HDFGroup/hdf-compass/actions/workflows/hdf-compass_on_linux.yml/badge.svg?branch=py3 :target: https://github.com/HDFGroup/hdf-compass/actions/workflows/hdf-compass_on_linux.yml :alt: Linux .. image:: https://coveralls.io/repos/github/HDFGroup/hdf-compass/badge.svg?branch=py3 :target: https://coveralls.io/github/HDFGroup/hdf-compass?branch=py3 :alt: coverall Welcome to the project! HDF Compass is an experimental viewer program for HDF5 and related formats, designed to complement other more complex applications like HDFView. Strong emphasis is placed on clean minimal design, and maximum extensibility through a plugin system for new formats. HDF Compass is written in Python, but ships as a native application on Windows, OS X, and Linux, by using PyInstaller to package the app. Binary executables are available for Windows (Windows 7 or later) and Mac OS X (Yosemite or later) at the Project Page listed below. Bug reports and pull requests are welcome! For non-trivial PRs please open an issue first, so the core developers can give feedback on your idea. Development Environment ----------------------- You will need: * `Python 3.6 `_ * `NumPy `_ * `Matplotlib `_ * `wxPython Phoenix 4.0.0+ `_ (`PyPI `_ and `extra wheels for Linux `_) * `Cartopy `_ * `h5py `_ *[HDF plugin]* * `hyo2.bag `_ *[BAG plugin]* * `Pydap `_ *[OPeNDAP plugin]* (>=3.3) * `Requests `_ *[HDF Rest API plugin]* * `adios `_ *[ADIOS Plugin]* (Linux/OSX only) For packaging the app: * `PyInstaller `_ *(>= 3.3 or `latest dev `_ )* Running the Program ------------------- ``$ python HDFCompass.py`` Note: If you are using the Anaconda distribution on the Mac, you will see the message: "This program needs access to the screen. Please run with a Framework build of python...". In this case use the pythonw command: ``$ pythonw HDFCompass.py`` Note: on Mac, HDF Compass doesn't create an initial window, use the system Application menu to open a file or remote resource. Note: If you are using conda and see debug message like "No module named 'h5py'" with the h5py package installed, install python.app: ``$ conda install python.app`` Packaging --------- Single-file: ``$ pyinstaller --clean -y HDFCompass.1file.spec`` Single-folder (useful for debugging the ``pyinstaller`` settings): ``$ pyinstaller --clean -y HDFCompass.1folder.spec`` Other info ---------- * Github: `http://github.com/HDFGroup/hdf-compass `_ * Project page: `https://www.hdfgroup.org/projects/compass/ `_ * License: BSD-like HDF Group license (See `COPYING `_) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1714822938.0 hdf_compass-0.7b15/README.rst0000666000000000000000000001017714615417432012614 0ustar00HDF Compass =========== .. image:: https://badge.fury.io/py/hdf_compass.svg :target: https://badge.fury.io/py/hdf_compass :alt: PyPI Status .. image:: https://readthedocs.org/projects/hdf-compass/badge/?version=stable :target: http://hdf-compass.readthedocs.org/en/stable/?badge=stable :alt: Stable Documentation Status .. image:: https://readthedocs.org/projects/hdf-compass/badge/?version=latest :target: http://hdf-compass.readthedocs.org/en/latest/?badge=latest :alt: Latest Documentation Status .. image:: https://github.com/HDFGroup/hdf-compass/actions/workflows/hdf-compass_on_windows.yml/badge.svg?branch=py3 :target: https://github.com/HDFGroup/hdf-compassg/actions/workflows/hdf-compass_on_windows.yml :alt: Windows .. image:: https://github.com/HDFGroup/hdf-compass/actions/workflows/hdf-compass_on_linux.yml/badge.svg?branch=py3 :target: https://github.com/HDFGroup/hdf-compass/actions/workflows/hdf-compass_on_linux.yml :alt: Linux .. image:: https://coveralls.io/repos/github/HDFGroup/hdf-compass/badge.svg?branch=py3 :target: https://coveralls.io/github/HDFGroup/hdf-compass?branch=py3 :alt: coverall Welcome to the project! HDF Compass is an experimental viewer program for HDF5 and related formats, designed to complement other more complex applications like HDFView. Strong emphasis is placed on clean minimal design, and maximum extensibility through a plugin system for new formats. HDF Compass is written in Python, but ships as a native application on Windows, OS X, and Linux, by using PyInstaller to package the app. Binary executables are available for Windows (Windows 7 or later) and Mac OS X (Yosemite or later) at the Project Page listed below. Bug reports and pull requests are welcome! For non-trivial PRs please open an issue first, so the core developers can give feedback on your idea. Development Environment ----------------------- You will need: * `Python 3.6 `_ * `NumPy `_ * `Matplotlib `_ * `wxPython Phoenix 4.0.0+ `_ (`PyPI `_ and `extra wheels for Linux `_) * `Cartopy `_ * `h5py `_ *[HDF plugin]* * `hyo2.bag `_ *[BAG plugin]* * `Pydap `_ *[OPeNDAP plugin]* (>=3.3) * `Requests `_ *[HDF Rest API plugin]* * `adios `_ *[ADIOS Plugin]* (Linux/OSX only) For packaging the app: * `PyInstaller `_ *(>= 3.3 or `latest dev `_ )* Running the Program ------------------- ``$ python HDFCompass.py`` Note: If you are using the Anaconda distribution on the Mac, you will see the message: "This program needs access to the screen. Please run with a Framework build of python...". In this case use the pythonw command: ``$ pythonw HDFCompass.py`` Note: on Mac, HDF Compass doesn't create an initial window, use the system Application menu to open a file or remote resource. Note: If you are using conda and see debug message like "No module named 'h5py'" with the h5py package installed, install python.app: ``$ conda install python.app`` Packaging --------- Single-file: ``$ pyinstaller --clean -y HDFCompass.1file.spec`` Single-folder (useful for debugging the ``pyinstaller`` settings): ``$ pyinstaller --clean -y HDFCompass.1folder.spec`` Other info ---------- * Github: `http://github.com/HDFGroup/hdf-compass `_ * Project page: `https://www.hdfgroup.org/projects/compass/ `_ * License: BSD-like HDF Group license (See `COPYING `_) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1714825026.7549908 hdf_compass-0.7b15/additional_legal/0000777000000000000000000000000014615423503014367 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/additional_legal/ADIOS_Copyrights_and_Licenses.txt0000666000000000000000000000642214544243123022654 0ustar00ADIOS is freely available under the terms of the BSD license given below. Copyright (c) 2008 - 2009. UT-BATTELLE, LLC. All rights reserved. Copyright (c) 2008 - 2009. Georgia Institute of Technology. All rights reserved. Produced at the National Center for Computational Sciences in Oak Ridge National Laboratory. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the disclaimer (as noted below) in the documentation and/or other materials provided with the distribution. * Neither the name of the UT-BATTELLE, LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Additional BSD Notice 1. This notice is required to be provided under our contract with the U.S. Department of Energy (DOE). This work was produced at Oak Ridge National Laboratory under Contract No. DEAC05-00OR22725 with the DOE. 2. Neither the United States Government nor UT-BATTELLE, LLC nor any of their employees, makes any warranty, express or implied, or assumes any liability or responsibility for the accuracy, completeness, or usefulness of any information, apparatus, product, or process disclosed, or represents that its use would not infringe privately-owned rights. 3. Also, reference herein to any specific commercial products, process, or services by trade name, trademark, manufacturer or otherwise does not necessarily constitute or imply its endorsement, recommendation, or favoring by the United States Government or UT-BATTELLE, LLC. The views and opinions of authors expressed herein do not necessarily state or reflect those of the United States Government or UT-BATTELLE, LLC, and shall not be used for advertising or product endorsement purposes. Note about Mini-XML =================== The mxml/mxml-2.9 subdirectory contains the unmodified code of Mini-XML 2.9 which is otherwise freely accessible from http://www.msweet.org/projects.php/Mini-XML The author of that open source software is Michael Sweet The mxml-2.9 library comes with the LGPL v2 license, see mxml/mxml-2.9/COPYING ADIOS can be linked with a pre-installed mxml library otherwise it will build the libmxml.a library from this included source. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/additional_legal/HydroPro_Icons_Terms.txt0000666000000000000000000000261314544243123021204 0ustar00 || || =================================================== ____ ____ ___ / / / / / / / /__/ / / / / / ___ ___ ____/ / ______ ______ / ___ / / / / / / __ / / ___/ / __ / / / / / / /_/ / / /_/ / / / / /_/ / /___/ /___/ /____ / /______/ /__/ /_____/ _________________/ / ____ ___ ____ _ / / / / / _/ / / /\/\|_) /___________________/ / __/ /_/ /___/ /_/ =================================================== || || || DOCK ICONS BY || || BEN FLEMING || || || || VISIT || || mediadesign.deviantart.com || || FOR MORE || || || =================================================== [[[[[[ www.opclans.com ]]]]]]] These icons are now available for commercial use! You don't have to ask permission to use them, however it would be nice if you could inform me that you're using them, and what you're using them for. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/additional_legal/KDE_Oxygen_Icon_Set_Copyright_and_License.txt0000666000000000000000000001745214544243123025173 0ustar00The following license applies to the icons used with HDF Compass, excluding the "globe" icon (which is copyright MediaDesign and has no restrictions on use). GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library.././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/additional_legal/NumPy_Copyright_and_License.txt0000666000000000000000000000313014544243123022510 0ustar00Numpy license Copyright © 2005-2013, NumPy Developers. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. o Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. o Neither the name of the NumPy Developers nor the names of any contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [Source: http://www.numpy.org/license.html] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/additional_legal/PyDAP_Copyright_and_License.txt0000666000000000000000000000220214544243123022354 0ustar00 PyDAP License Copyright (c) 2003–2010 Roberto De Almeida Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/additional_legal/PyInstaller_Copyrights_and_Licenses.txt0000666000000000000000000004434414544243123024270 0ustar00 PyInstaller licensing terms This page includes the following documents: The PyInstaller licensing terms GNU General Public License The PyInstaller licensing terms ================================ Copyright (c) 2010-2013, PyInstaller Development Team Copyright (c) 2005-2009, Giovanni Bajo Based on previous work under copyright (c) 2002 McMillan Enterprises, Inc. PyInstaller is licensed under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. Bootloader Exception -------------------- In addition to the permissions in the GNU General Public License, the authors give you unlimited permission to link or embed compiled bootloader and related files into combinations with other programs, and to distribute those combinations without any restriction coming from the use of those files. (The General Public License restrictions do apply in other respects; for example, they cover modification of the files, and distribution when not linked into a combine executable.) Bootloader and Related Files ---------------------------- Bootloader and related files are files which are embedded within the final executable. This includes files in directories: ./bootloader/ ./PyInstaller/loader About the PyInstaller Development Team -------------------------------------- The PyInstaller Development Team is the set of contributors to the PyInstaller project. A full list with details is kept in the documentation directory, in the file 'doc/credits.txt'. The core team that coordinates development on GitHub can be found here: https://github.com/pyinstaller/pyinstaller. As of 2013, it consists of: * Giovanni Bajo * Hartmut Goebel * Martin Zibricky Our Copyright Policy -------------------- PyInstaller uses a shared copyright model. Each contributor maintains copyright over their contributions to PyInstaller. But, it is important to note that these contributions are typically only changes to the repositories. Thus, the PyInstaller source code, in its entirety is not the copyright of any single person or institution. Instead, it is the collective copyright of the entire PyInstaller Development Team. If individual contributors want to maintain a record of what changes/contributions they have specific copyright on, they should indicate their copyright in the commit message of the change, when they commit the change to the PyInstaller repository. With this in mind, the following banner should be used in any source code file to indicate the copyright and license terms: #----------------------------------------------------------------------------- # Copyright (c) 2013, PyInstaller Development Team. # # Distributed under the terms of the GNU General Public License with exception # for distributing bootloader. # # The full license is in the file COPYING.txt, distributed with this software. #----------------------------------------------------------------------------- GNU General Public License -------------------------- GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Library General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/additional_legal/Python_Copyright_and_License.txt0000666000000000000000000000527314544243123022733 0ustar00Python 3.4.2 Terms and conditions for accessing or otherwise using Python Copyright © 2001-2014 Python Software Foundation; All Rights Reserved PSF LICENSE AGREEMENT FOR PYTHON 3.4.2 1. This LICENSE AGREEMENT is between the Python Software Foundation (“PSF”), and the Individual or Organization (“Licensee”) accessing and otherwise using Python 3.4.2 software in source or binary form and its associated documentation. 2. Subject to the terms and conditions of this License Agreement, PSF hereby grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, analyze, test, perform and/or display publicly, prepare derivative works, distribute, and otherwise use Python 3.4.2 alone or in any derivative version, provided, however, that PSF’s License Agreement and PSF’s notice of copyright, i.e., “Copyright © 2001-2014 Python Software Foundation; All Rights Reserved” are retained in Python 3.4.2 alone or in any derivative version prepared by Licensee. 3. In the event Licensee prepares a derivative work that is based on or incorporates Python 3.4.2 or any part thereof, and wants to make the derivative work available to others as provided herein, then Licensee hereby agrees to include in any such work a brief summary of the changes made to Python 3.4.2. 4. PSF is making Python 3.4.2 available to Licensee on an “AS IS” basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 3.4.2 WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON 3.4.2 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 3.4.2, OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. 6. This License Agreement will automatically terminate upon a material breach of its terms and conditions. 7. Nothing in this License Agreement shall be deemed to create any relationship of agency, partnership, or joint venture between PSF and Licensee. This License Agreement does not grant permission to use PSF trademarks or trade name in a trademark sense to endorse or promote products or services of Licensee, or any third party. 8. By copying, installing or otherwise using Python 3.4.2, Licensee agrees to be bound by the terms and conditions of this License Agreement. [Source: https://docs.python.org/3/license.html] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/additional_legal/cartopy_Copyrights_and_Licenses.txt0000666000000000000000000000416014544243123023473 0ustar00============================================= Cartopy copyright, licensing and contributors ============================================= .. |copy| unicode:: U+000A9 .. COPYRIGHT SIGN Cartopy code ------------ All Iris source code, unless explicitly stated, is |copy| ``British Crown copyright, 2014`` and is licensed under the **GNU Lesser General Public License** as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. You should find all source files with the following header: .. admonition:: Code License |copy| British Crown Copyright 2011 - 2014, Met Office This file is part of cartopy. Cartopy is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Cartopy is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with cartopy. If not, see ``_. Cartopy documentation and examples ---------------------------------- All documentation, examples and sample data found on this website and in source repository are licensed under the UK's Open Government Licence: .. admonition:: Documentation, example and data license |copy| British Crown copyright, 2014. You may use and re-use the information featured on this website (not including logos) free of charge in any format or medium, under the terms of the `Open Government Licence `_. We encourage users to establish hypertext links to this website. Any email enquiries regarding the use and re-use of this information resource should be sent to: psi@nationalarchives.gsi.gov.uk. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/additional_legal/h5py_Copyrights_and_Licenses.txt0000666000000000000000000002675214544243123022712 0ustar00 h5py Copyright and License Terms and Related Licenses and Legal Information This page includes the following documents: h5py Copyright and License Terms HDF5 Copyright Statement h5py Copyright and License Terms ================================ Copyright Notice and Statement for the h5py Project Copyright (c) 2008 Andrew Collette and contributors http://h5py.alfven.org All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: a. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. b. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. c. Neither the name of the author nor the names of contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. HDF5 Copyright Statement ======================== HDF5 (Hierarchical Data Format 5) Software Library and Utilities Copyright 2006-2007 by The HDF Group (THG). NCSA HDF5 (Hierarchical Data Format 5) Software Library and Utilities Copyright 1998-2006 by the Board of Trustees of the University of Illinois. All rights reserved. Contributors: National Center for Supercomputing Applications (NCSA) at the University of Illinois, Fortner Software, Unidata Program Center (netCDF), The Independent JPEG Group (JPEG), Jean-loup Gailly and Mark Adler (gzip), and Digital Equipment Corporation (DEC). Redistribution and use in source and binary forms, with or without modification, are permitted for any purpose (including commercial purposes) provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions, and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions, and the following disclaimer in the documentation and/or materials provided with the distribution. 3. In addition, redistributions of modified forms of the source or binary code must carry prominent notices stating that the original code was changed and the date of the change. 4. All publications or advertising materials mentioning features or use of this software are asked, but not required, to acknowledge that it was developed by The HDF Group and by the National Center for Supercomputing Applications at the University of Illinois at Urbana-Champaign and credit the contributors. 5. Neither the name of The HDF Group, the name of the University, nor the name of any Contributor may be used to endorse or promote products derived from this software without specific prior written permission from THG, the University, or the Contributor, respectively. DISCLAIMER: THIS SOFTWARE IS PROVIDED BY THE HDF GROUP (THG) AND THE CONTRIBUTORS "AS IS" WITH NO WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED. In no event shall THG or the Contributors be liable for any damages suffered by the users arising out of the use of this software, even if advised of the possibility of such damage. Portions of HDF5 were developed with support from the University of California, Lawrence Livermore National Laboratory (UC LLNL). The following statement applies to those portions of the product and must be retained in any redistribution of source code, binaries, documentation, and/or accompanying materials: This work was partially produced at the University of California, Lawrence Livermore National Laboratory (UC LLNL) under contract no. W-7405-ENG-48 (Contract 48) between the U.S. Department of Energy (DOE) and The Regents of the University of California (University) for the operation of UC LLNL. DISCLAIMER: This work was prepared as an account of work sponsored by an agency of the United States Government. Neither the United States Government nor the University of California nor any of their employees, makes any warranty, express or implied, or assumes any liability or responsibility for the accuracy, completeness, or usefulness of any information, apparatus, product, or process disclosed, or represents that its use would not infringe privately- owned rights. Reference herein to any specific commercial products, process, or service by trade name, trademark, manufacturer, or otherwise, does not necessarily constitute or imply its endorsement, recommendation, or favoring by the United States Government or the University of California. The views and opinions of authors expressed herein do not necessarily state or reflect those of the United States Government or the University of California, and shall not be used for advertising or product endorsement purposes. PyTables Copyright Statement ============================ Copyright Notice and Statement for PyTables Software Library and Utilities: Copyright (c) 2002, 2003, 2004 Francesc Altet Copyright (c) 2005, 2006, 2007 Carabos Coop. V. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: a. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. b. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. c. Neither the name of the Carabos Coop. V. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. stdint.h (Windows version) License ================================== Copyright (c) 2006-2008 Alexander Chemeris Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. The name of the author may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Python Copyright and License Terms ================================== Copyright 2001-2013 Python Software Foundation; All Rights Reserved. 1. This LICENSE AGREEMENT is between the Python Software Foundation ("PSF"), and the Individual or Organization ("Licensee") accessing and otherwise using Python Python 2.7.5 software in source or binary form and its associated documentation. 2. Subject to the terms and conditions of this License Agreement, PSF hereby grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, analyze, test, perform and/or display publicly, prepare derivative works, distribute, and otherwise use Python Python 2.7.5 alone or in any derivative version, provided, however, that PSF's License Agreement and PSF's notice of copyright, i.e., "Copyright 2001-2013 Python Software Foundation; All Rights Reserved" are retained in Python Python 2.7.5 alone or in any derivative version prepared by Licensee. 3. In the event Licensee prepares a derivative work that is based on or incorporates Python Python 2.7.5 or any part thereof, and wants to make the derivative work available to others as provided herein, then Licensee hereby agrees to include in any such work a brief summary of the changes made to Python Python 2.7.5. 4. PSF is making Python Python 2.7.5 available to Licensee on an "AS IS" basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON Python 2.7.5 WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON Python 2.7.5 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON Python 2.7.5, OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. 6. This License Agreement will automatically terminate upon a material breach of its terms and conditions. 7. Nothing in this License Agreement shall be deemed to create any relationship of agency, partnership, or joint venture between PSF and Licensee. This License Agreement does not grant permission to use PSF trademarks or trade name in a trademark sense to endorse or promote products or services of Licensee, or any third party. 8. By copying, installing or otherwise using Python Python 2.7.5, Licensee agrees to be bound by the terms and conditions of this License Agreement. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/additional_legal/hydroffice_bag_Copyrights_and_Licenses.txt0000666000000000000000000000344614544243330024753 0ustar00 Copyright Notice and License Terms for hyo2.bag - BAG package for HydrOffice ----------------------------------------------------------------------------- hyo2.bag Copyright 2015 - G.Masetti and B.R.Calder. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted for any purpose (including commercial purposes) provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions, and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions, and the following disclaimer in the documentation and/or materials provided with the distribution. 3. In addition, redistributions of modified forms of the source or binary code must carry prominent notices stating that the original code was changed and the date of the change. 4. All publications or advertising materials mentioning features or use of this software are asked, but not required, to acknowledge that it was developed by G.Masetti and B.R.Calder and credit the contributors. 5. Neither the name of G.Masetti and B.R.Calder, nor the name of any Contributor may be used to endorse or promote products derived from this software without specific prior written permission from G.Masetti and B.R. Calder or the Contributor, respectively. DISCLAIMER: THIS SOFTWARE IS PROVIDED BY THE CCOM AND THE CONTRIBUTORS "AS IS" WITH NO WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED. In no event shall G.Masetti and B.R.Calder or the Contributors be liable for any damages suffered by the users arising out of the use of this software, even if advised of the possibility of such damage. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/additional_legal/matplotlib_Copyright_and_License.txt0000666000000000000000000000531714544243123023620 0ustar00 License agreement for matplotlib 1.4.2 Copyright (c) 2012-2013 Matplotlib Development Team; All Rights Reserved Copyright (c) 2002-2012 John D. Hunter; All Rights Reserved 1. This LICENSE AGREEMENT is between the Matplotlib Development Team ("MDT"), and the Individual or Organization (“Licensee”) accessing and otherwise using matplotlib software in source or binary form and its associated documentation. 2. Subject to the terms and conditions of this License Agreement, MDT hereby grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, analyze, test, perform and/or display publicly, prepare derivative works, distribute, and otherwise use matplotlib 1.4.2 alone or in any derivative version, provided, however, that MDT’s License Agreement and MDT’s notice of copyright, i.e., “Copyright (c) 2012-2013 Matplotlib Development Team; All Rights Reserved” are retained in matplotlib 1.4.2 alone or in any derivative version prepared by Licensee. 3. In the event Licensee prepares a derivative work that is based on or incorporates matplotlib 1.4.2 or any part thereof, and wants to make the derivative work available to others as provided herein, then Licensee hereby agrees to include in any such work a brief summary of the changes made to matplotlib 1.4.2. 4. MDT is making matplotlib 1.4.2 available to Licensee on an “AS IS” basis. MDT MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, MDT MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF MATPLOTLIB 1.4.2 WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. 5. MDT SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF MATPLOTLIB 1.4.2 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING MATPLOTLIB 1.4.2, OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. 6. This License Agreement will automatically terminate upon a material breach of its terms and conditions. 7. Nothing in this License Agreement shall be deemed to create any relationship of agency, partnership, or joint venture between MDT and Licensee. This License Agreement does not grant permission to use MDT trademarks or trade name in a trademark sense to endorse or promote products or services of Licensee, or any third party. 8. By copying, installing or otherwise using matplotlib 1.4.2, Licensee agrees to be bound by the terms and conditions of this License Agreement. [Source: http://matplotlib.org/users/license.html] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/additional_legal/wxWidgets_Copyrights_and_Licenses.txt0000666000000000000000000010002214544243123023771 0ustar00 Copyrights and Licenses for wxWidgets This page includes the following documents: wxWidgets Copyrights and Licenses wxWindows Library Licence, Version 3.1 GNU Library General Public License The Open Group and DEC Licenses wxWidgets Copyrights and Licenses --------------------------------- Copyright (c) 1992-2013 Julian Smart, Vadim Zeitlin, Stefan Csomor, Robert Roebling, and other members of the wxWidgets team, please see the acknowledgements section below. Portions (c) 1996 Artificial Intelligence Applications Institute Please also see the wxWindows licence files (preamble.txt, lgpl.txt, gpl.txt, licence.txt, licendoc.txt) for conditions of software and documentation use. Note that we use the old name wxWindows in the licence, pending recognition of the new name by OSI. wxWindows Library Licence GNU Library General Public License The Open Group and DEC License Acknowledgements The following is the list of the core, active developers of wxWidgets which keep it running and have provided an invaluable, extensive and high-quality amount of changes over the many of years of wxWidgets' life: Julian Smart Vadim Zeitlin Robert Roebling Robin Dunn Stefan Csomor Vaclav Slavik Paul Cornett Wlodzimierz `ABX' Skiba Chris Elliott David Elliott Kevin Hock Stefan Neis Michael Wetherell We would particularly like to thank the following peoples for their contributions to wxWidgets, and the many others who have been involved in the project over the years. Apologies for any unintentional omissions from this alphabetic list: Yiorgos Adamopoulos, Jamshid Afshar, Alejandro Aguilar-Sierra, AIAI, Patrick Albert, Karsten Ballueder, Mattia Barbon, Michael Bedward, Kai Bendorf, Yura Bidus, Keith Gary Boyce, Chris Breeze, Pete Britton, Ian Brown, C. Buckley, Marco Cavallini, Dmitri Chubraev, Robin Corbet, Cecil Coupe, Andrew Davison, Gilles Depeyrot, Neil Dudman, Hermann Dunkel, Jos van Eijndhoven, Tom Felici, Thomas Fettig, Matthew Flatt, Pasquale Foggia, Josep Fortiana, Todd Fries, Dominic Gallagher, Guillermo Rodriguez Garcia, Wolfram Gloger, Norbert Grotz, Stefan Gunter, Bill Hale, Patrick Halke, Stefan Hammes, Guillaume Helle, Harco de Hilster, Cord Hockemeyer, Markus Holzem, Olaf Klein, Leif Jensen, Bart Jourquin, Guilhem Lavaux, Ron Lee, Jan Lessner, Nicholas Liebmann, Torsten Liermann, Per Lindqvist, Francesco Montorsi, Thomas Runge, Tatu Männistö, Scott Maxwell, Thomas Myers, Oliver Niedung, Ryan Norton, Hernan Otero, Ian Perrigo, Timothy Peters, Giordano Pezzoli, Harri Pasanen, Thomaso Paoletti, Garrett Potts, Marcel Rasche, Dino Scaringella, Jobst Schmalenbach, Arthur Seaton, Paul Shirley, Stein Somers, Petr Smilauer, Neil Smith, Kari Systä, George Tasker, Arthur Tetzlaff-Deas, Jonathan Tonberg, Jyrki Tuomi, Janos Vegh, Andrea Venturoli, David Webster, Otto Wyss, Xiaokun Zhu, Edward Zimmermann. Many thanks also to AIAI for being willing to release the original version of wxWidgets into the public domain, and to our patient partners. `Graphplace', the basis for the wxGraphLayout library, is copyright Dr. Jos T.J. van Eijndhoven of Eindhoven University of Technology. The code has been used in wxGraphLayout (not in wxWidgets anymore) with his permission. We also acknowledge the author of XFIG, the excellent Unix drawing tool, from the source of which we have borrowed some spline drawing code. His copyright is included below. XFig2.1 is copyright (c) 1985 by Supoj Sutanthavibul. Permission to use, copy, modify, distribute, and sell this software and its documentation for any purpose is hereby granted without fee, provided that the above copyright notice appear in all copies and that both that copyright notice and this permission notice appear in supporting documentation, and that the name of M.I.T. not be used in advertising or publicity pertaining to distribution of the software without specific, written prior permission. M.I.T. makes no representations about the suitability of this software for any purpose. It is provided "as is" without express or implied warranty. wxWindows Library Licence, Version 3.1 ====================================== Copyright (c) 1998-2005 Julian Smart, Robert Roebling et al Everyone is permitted to copy and distribute verbatim copies of this licence document, but changing it is not allowed. WXWINDOWS LIBRARY LICENCE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public Licence as published by the Free Software Foundation; either version 2 of the Licence, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public Licence for more details. You should have received a copy of the GNU Library General Public Licence along with this software, usually in a file named COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. EXCEPTION NOTICE 1. As a special exception, the copyright holders of this library give permission for additional uses of the text contained in this release of the library as licenced under the wxWindows Library Licence, applying either version 3.1 of the Licence, or (at your option) any later version of the Licence as published by the copyright holders of version 3.1 of the Licence document. 2. The exception is that you may use, copy, link, modify and distribute under your own terms, binary object code versions of works based on the Library. 3. If you copy code from files distributed under the terms of the GNU General Public Licence or the GNU Library General Public Licence into a copy of this library, as this licence permits, the exception does not apply to the code that you add in this way. To avoid misleading anyone as to the status of such modified files, you must delete this exception notice from such code and/or adjust the licensing conditions notice accordingly. 4. If you write modifications of your own for this library, it is your choice whether to permit this exception to apply to your modifications. If you do not wish that, you must delete the exception notice from such code and/or adjust the licensing conditions notice accordingly. GNU LIBRARY GENERAL PUBLIC LICENSE ================================== Version 2, June 1991 Copyright (C) 1991 Free Software Foundation, Inc. 675 Mass Ave, Cambridge, MA 02139, USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. [This is the first released version of the library GPL. It is numbered 2 because it goes with version 2 of the ordinary GPL.] Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This license, the Library General Public License, applies to some specially designated Free Software Foundation software, and to any other libraries whose authors decide to use it. You can use it for your libraries, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library, or if you modify it. For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link a program with the library, you must provide complete object files to the recipients so that they can relink them with the library, after making changes to the library and recompiling it. And you must show them these terms so they know their rights. Our method of protecting your rights has two steps: (1) copyright the library, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the library. Also, for each distributor's protection, we want to make certain that everyone understands that there is no warranty for this free library. If the library is modified by someone else and passed on, we want its recipients to know that what they have is not the original version, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that companies distributing free software will individually obtain patent licenses, thus in effect transforming the program into proprietary software. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. Most GNU software, including some libraries, is covered by the ordinary GNU General Public License, which was designed for utility programs. This license, the GNU Library General Public License, applies to certain designated libraries. This license is quite different from the ordinary one; be sure to read it in full, and don't assume that anything in it is the same as in the ordinary license. The reason we have a separate public license for some libraries is that they blur the distinction we usually make between modifying or adding to a program and simply using it. Linking a program with a library, without changing the library, is in some sense simply using the library, and is analogous to running a utility program or application program. However, in a textual and legal sense, the linked executable is a combined work, a derivative of the original library, and the ordinary General Public License treats it as such. Because of this blurred distinction, using the ordinary General Public License for libraries did not effectively promote software sharing, because most developers did not use the libraries. We concluded that weaker conditions might promote sharing better. However, unrestricted linking of non-free programs would deprive the users of those programs of all benefit from the free status of the libraries themselves. This Library General Public License is intended to permit developers of non-free programs to use free libraries, while preserving your freedom as a user of such programs to change the free libraries that are incorporated in them. (We have not seen how to achieve this as regards changes in header files, but we have achieved it as regards changes in the actual functions of the Library.) The hope is that this will lead to faster development of free libraries. The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, while the latter only works together with the library. Note that it is possible for a library to be covered by the ordinary General Public License rather than by this special one. GNU LIBRARY GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License Agreement applies to any software library which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Library General Public License (also called "this License"). Each licensee is addressed as "you". A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) "Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. 1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) The modified work must itself be a software library. b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. (For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. This option is useful when you wish to copy part of the code of the Library into a program that is not a library. 4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. 5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. 6. As an exception to the Sections above, you may also compile or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) b) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. c) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. d) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. 7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. 10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 13. The Free Software Foundation may publish revised and/or new versions of the Library General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. 14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS The Open Group and DEC Licenses ------------------------------- Copyright 1987, 1988, 1998 The Open Group Permission to use, copy, modify, distribute, and sell this software and its documentation for any purpose is hereby granted without fee, provided that the above copyright notice appear in all copies and that both that copyright notice and this permission notice appear in supporting documentation. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE OPEN GROUP BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Except as contained in this notice, the name of The Open Group shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Software without prior written authorization from The Open Group. Copyright 1987 by Digital Equipment Corporation, Maynard, Massachusetts. All Rights Reserved Permission to use, copy, modify, and distribute this software and its documentation for any purpose and without fee is hereby granted, provided that the above copyright notice appear in all copies and that both that copyright notice and this permission notice appear in supporting documentation, and that the name of Digital not be used in advertising or publicity pertaining to distribution of the software without specific, written prior permission. DIGITAL DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL DIGITAL BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1714825026.6789958 hdf_compass-0.7b15/data/0000777000000000000000000000000014615423503012024 5ustar00././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1714825026.7570188 hdf_compass-0.7b15/data/asc/0000777000000000000000000000000014615423503012572 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/data/asc/sample.asc0000666000000000000000000000030414544243123014537 0ustar00ncols 4 nrows 6 xllcorner 0.0 yllcorner 0.0 cellsize 50.0 NODATA_value -9999 -9999 -9999 5 2 -9999 20 100 36 3 8 35 10 32 42 50 6 88 75 27 9 13 5 1 -9999 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1714825026.7599912 hdf_compass-0.7b15/data/bag/0000777000000000000000000000000014615423503012555 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/data/bag/bdb_00.bag0000666000000000000000000006674014544243123014272 0ustar00HDF  m`TREEHEAPXBAG_root@(hTREE8HEAPXHmetadatatracking_listelevationuncertaintySNOD Hh H Bag Version 1.5.3$tIbad allocationERRO4EVSNOD(  8X hrow col depth  uncertainty   track_codelist_series EV @ Tracking List Length 8$tI$tI   EV HMinimum Elevation Value  L HMaximum Elevation Value  oH$tI$tI   `&EV PMinimum Uncertainty Value  R^A PMaximum Uncertainty Value  ATREE59= AEIMQ U$Y(],a0e4i4$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI᲍ōlč7V)ɏ'd$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tIHǍŎٍs 7ڍf=_;ŬoŠ٦ђŕ7꓍!ړʼnłk̍Ň$tI$tIRKŻ"nMFҍťJۍŘɍčŻSٍA܍్r뗍gż쾍H᠍Í)$tI$tI Ů(~gń^΍ōڍ(׍zҍKŢňŊ؍ōȍzŞ ՍsŪڍ ?$tI$tI/#.~ŚRxQCmY ū4Z$B (Ş]9FOn}K$tI$tIN@ʼnŶoŔfŞj4Ŭ=2OŸ* % %Ł\8VŴ=xqńŚ3ŀʌ{馎ݎ$tI$tI-uuqcņhůz^s\ŪehŅ\IŶ,uäuǎ!wŋ ŷ(ŖŎK$tI$tIu}ŖnŔu8ſ*`zŜ։Tb˧Ł/ێp3K VŨn…ۏ֏@'Q$tI$tIojŨ~Ŋq֎FƎŨŤŎَnݎ{׎JP̎_'XjʼndT {ŲQܲʑ"$tI}z“E>ş͎=؎<Ď ȎŶŔłގńEMœťMKŖőE(=-LZeŎR$tIwË顎ŽŪyŠώňŅő$ Ť.3ūŖŨJUŦ>mFteL홒$tI*;źŵŐłŲVY;ŔW| y@Ŧ1**O;OJŮXr$tIWzsǎńŅh ŁŧW!z{G'k>qW2ſFE2ŰYŒ/D3ʼn $tIMdk;NŻ#pu#`;ŠŦ\OŠJN]E.D<zZO$Ł$tIGeŇ Ŭ̎Î'U#)SS@łBŮ1?u6*"(Ўfŷ.iΎ$tIȾl܎׎cS-S"œx'ŰşśrߎQǎZ'Ŗů3&'n$tImĎn29ÎŅż"Ř#rŢoێŢx ǎv͎ⴎKƎů JҎŷ^$tIµ6_nlœ֎ lŢ ŌŎjˎ|܎ŘũЎŅˎmێxō|Ŵ/ڎőCЎťW$tIõţŧŌߎ$ ŷ 5؎ţˎ/ҎsɎń œĎŊώeĎώŜ؎Lю[ŝD$tIʎŐĎ1KՎ᭎Ł^ň܎׎֎1ޕGk *0WƻiT̎ǎAӎżňJů3$tIǎbŭ֎ ŧśю׎lێZK]ſr^y`#oɖœHĎŐŖi;ž?:$tI[ŮЎȎSWŃ:Ǝŝņ_҈ЈũY:\*vSvŻk\[)?k $tI.[ltʼnfJŃſi셎Ōuw0:ŗnfFjŧ}|+}\gŀJ0Ũ$tIsxŏŇԭR՚ufdbų|iŁoa $ˆzcAj_pӕkpŇivNB5$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tIaŎBŇ$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tIAoArAAAAAվAAvAAFAfAϚAAAֵA2ATALAA/$A)A{MAdAA[A|A窕A^AASAWAiAm^AAعAdAmAdNAۗAopAA䡼AZ#AϿA]A叫AkAHAײAVAŚAAߧAAӎA$tIqAZaAEھA4ArA``A_AFA!#ASAAA/FAA\AүAA7AvBAAzAAaAA 2A0A aA%A ALA$tIKA.AALAZAAwA|AALAA)AAvASZAA(A}dAkAԷAAAhkAACADA AA`]AAgA$tIAA½A_DA-A6 A~`A% Ae[AJA*AռA AA"APAA]AWA)AAA AAe˾AA0AT(A>AMA$tI6AZAսAGAA'AA#AAN AAA̸AL\A$AAAoA2Ab*AA鉨AA8A5AЗA$tIAAA]AA'A믽AQAA6A\A)þACAAA)AAA{AlSA+AwAhA3AuAKAAvAA2A$tIAeA#AA̼A鍼A AuqA(APA7AKUA0AAxEApA1A 7AAA?ǽAQAEAA0AAA>A1AuA$tIXAC:ALAAAAmA`AAVgAپAAPAg:A"_AAAAlAA;A AAҾAASAAAAaAA$tI8A0AvAmAA A uA)#A1AAؾAAqA7AEվA/%AIA3AxAAfA̗AAzAAEAAAFAaA$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI$tI4A!AA 86edd853-2b98-4a9f-8618-11e578c43f51engutf8datasetGiuseppe Masetti (gmasetti@ccom.nh)ccom-jhcpostdocprocessor2015-10-05ISO 191152003/Cor.1:2006geometryOnlysurface12row32100column31100point10423000.000000000000,3956300.000000000000 426100.000000000000,3959500.000000000000centerPROJCS["EPSG:32619",GEOGCS["unnamed",DATUM["WGS_1984",SPHEROID["WGS_1984",6378137,298.2572201434276],TOWGS84[0,0,0,0,0,0,0]],PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433],EXTENSION["Scaler","0,0,0,0.01,0.01,0.0001"],EXTENSION["Source","CARIS"]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",-69],PARAMETER["scale_factor",0.9996],PARAMETER["false_easting",500000],PARAMETER["false_northing",0],UNIT["Meter",1]]WKTVERT_CS["MLW", VERT_DATUM["MLW", 2000]]WKTcaris_00.csar2015-08-15creationGiuseppe Masetti (gmasetti@ccom.nh)ccom-jhcpostdocoriginatorTest abstractunderDevelopmentgrid100engutf8elevationCARIS BASE Editor 4.1.14-69.8516-69.817635.747735.77682015-08-01T14:30:472015-08-15T22:14:53-4692.037109-4525.958984productUncertCSAR5,632datasetMethod: Extract2015-10-05T19:24:36Giuseppe Masetti (gmasetti@ccom.nh)processorSourceSource = K:\Processing\Products\hips\south_area\south_area_cube_100m.csarcreationDesignated soundings applied by automated procedure.0otherRestrictionsFreeunclassifiedTest noteLangsethPlatform Nameinstrument unknowninstrument type unknown ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1714825026.761993 hdf_compass-0.7b15/data/hdf5/0000777000000000000000000000000014615423503012652 5ustar00././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1714825026.7649927 hdf_compass-0.7b15/data/hdf5/Download/0000777000000000000000000000000014615423503014421 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/data/hdf5/Download/download_data.py0000666000000000000000000000215514544243330017575 0ustar00""" Download additional data files from AWS """ import os.path try: import wget except ImportError as e: print("missing wget, try to install it: pip install wget: %s" % e) import sys sys.exit(1) files = [ 'ami_hdf.h5', 'attrfile.h5', 'craterlake.h5', 'compound.h5', 'comp_complex.h5', 'countries.h5', 'hapmap_compressed.h5', 'hdf5_test.h5', 'iso++.h5', 'sequences.h5', 'snps1000_chr22_CEU.h5', 'LD_22_5Kx5K_matrix.h5', # 5 MB 'shuttle_hdf.h5', # 8 MB 'full8_400um.mnc.h5', # 10MB 'DOQ.h5', # 45 MB 'Sample_Urban_Data.h5', # 145 MB 'h5pf_md_manygroupnew2.h5', # 155 MB 'gz6_SCRIS_npp_d20140522_t0754579_e0802557_b13293__noaa_pop.h5', # 187 MB # 'NARA_TWR.h5', # 3 GB! Uncomment if you really want this. ] for filename in files: uri = 'https://s3.amazonaws.com/hdfgroup/data/hdf5test/' + filename print( uri) if os.path.isfile(filename): print(filename, "already downloaded - skipping") else: wget.download(uri, bar=wget.bar_thermometer) print("done!") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/data/hdf5/tall.h50000666000000000000000000002014414544243123014044 0ustar00HDF  d `HEAP0TREEp HEAP0TREE   SNOD@  HEAP0TREE0 HEAP0TREE x @ SNOD x @ hH  HEAP04TREE  xHEAP0 dTREE SNOD   20000323162808 x  P attr1 1st attribute of dset1.1.1X     $ #(- $*06#*18? (08@H $-6?HQSNODhP  20000323162808` SNOD!  20000323162808 ????ff?33???ٙ?ff?33SNODx! 20000323162808 x=>L>>>L>??L>??fff?g1g2` @ attr1  abcdefghi H attr2  g1.1g1.2dset2.1dset2.2dset1.1.1dset1.1.2g1.2.1 somevalueslink P attr2 2nd attribute of dset1.1.1 g1.2.1 @extlinksomefilesomepath././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1714825026.790996 hdf_compass-0.7b15/docs/0000777000000000000000000000000014615423503012043 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/docs/conf.py0000666000000000000000000002226514544243330013350 0ustar00# -*- coding: utf-8 -*- # # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys import os import shlex # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # source_suffix = ['.rst', '.md'] source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'HDF Compass' copyright = u'2017, The HDF Group' author = u'The HDF Group' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = '0.7' # The full version, including alpha/beta/rc tags. release = '0.7.b5' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all # documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. #keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'default' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. #html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' #html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # Now only 'ja' uses this config value #html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. #html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. htmlhelp_basename = 'HDFCompassdoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', # Latex figure (float) alignment #'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'HDFCompass.tex', u'HDF Compass Documentation', u'The HDF Group', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, 'hdfcompass', u'HDF Compass Documentation', [author], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'HDFCompass', u'HDF Compass Documentation', author, 'HDFCompass', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. #texinfo_no_detailmenu = False ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/docs/data_model.rst0000666000000000000000000002515714544243123014677 0ustar00Data Model `[developer]` ======================== Introduction ------------ This document describes the publically accessible data model package which is used by HDFCompass to display objects in a file, OpenDAP server or other resource. The data model is implemented as a collection of classes in a top-level Python package, ``compass_model``, which is completely independent of the GUI code. It has no dependencies beyond the Python standard library. This makes it possible to develop and test new plugins independently of GUI development; in particular, the automated Python unit-testing framework can be used, which is impossible for code that depends on the GUI. The classes in ``compass_model`` are abstract, and define a standard interface for objects like containers, regular multidimensional arrays, images, and key/value stores. "Plug-ins" consist of concrete implementations which satisfy this interface. For example, the built-in HDF5 plugin which ships with HDFCompass implements a ``Group`` class which inherits from ``compass_model.Container``, and a Dataset class which inherits from ``compass_model.Array``. The GUI has a collection of viewers which can display any object following the interfaces defined in ``compass_model``. For example, ``compass_model.Container`` implementations are displayed in a browser-style view, with list, icon, and tree displays possible. Likewise, ``compass_model.Array`` implementations are displayed in a spreadsheet-like view, with facilities for plotting data. Multiple concrete classes can handle the same object in a file. For example, an HDF5 image is implemented as a dataset with special attributes. Three classes in the HDF5 plugin are capable of handling such an object, which inherit respectively from ``compass_model.Image``, ``compass_model.Array``, and ``compass_model.KeyValue``; the last represents HDF5 attributes. When an icon in the GUI is double-clicked, the default (last-registered) class is used to open to object in the file. The other classes are made available in a context menu, for example, if the user wants to open an image with the Array viewer or see the HDF5 attributes. Numeric types (integers, floats, multidimensional data) are handled with the NumPy type model, which can represent nearly all formats. Python strings (byte and Unicode) are also supported. Data stores ----------- .. class:: Store Represents a file or remote resource which contains objects. Objects within the store can be retrieved using *keys*, which may be any hashable Python object. For example, an HDF5 store generally uses string keys representing paths in the file, although object and region reference objects are also valid. Objects may be retrieved using the __getitem__ syntax (``obj = store[key]``). The retrieved object is a Node instance. The exact class depends on the order in which Node handlers were registered; see the `push` method below. Methods related to the plugin system ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Typically, a model will implement several classes based on the ``compass_model`` abstract classes such as ``Container`` or ``Array``. This rasises the question: when an object is retrieved from the data store, which class should be used? The answer is that each ``Node`` subclass you write should be "registered" with your ``Store`` subclass, and have a static method called ``canhandle``. When an object is opened, by default the most recently registered class which reports it can understand the object is used. All registered subclasses may be retrieved via the ``gethandlers`` function, which an optionally request that only subclasses capable of handling *key* be returned. This is the basis for the right-click "Open As" menu in the GUI. .. classmethod:: Store.push(nodeclass) Register a Node subclass. These are kept in a list inside the class; when an object is retrieved from the store, the first class for which `nodeclass.canhandle(store, key)` returns True is instantiated and returned. .. method:: Store.__getitem__(key) Open an object in the store, using the last registered class which reports it can open the key. .. method:: Store.gethandlers(key=None) Retrieve all registered Node subclasses, optionally filtering by those which report they can handle *key*. .. method:: Store.__contains__(key) **(Abstract)** True if *key* is exists in the store, False otherwise. Other methods & properties ~~~~~~~~~~~~~~~~~~~~~~~~~~ .. staticmethod:: Store.canhandle(url) **(Abstract)** Return True if this class can make sense of *url*, False otherwise. This method is used by the GUI when determining which Store implementation to use when opening a file. For example, the HDF5 plugin uses ``h5py.is_hdf5(filename)``. .. method:: Store.__init__(url) **(Abstract)** Create a new store instance from the data at *url*. URLs are given in the ``scheme://locator`` fashion. For example, an HDF5 file might be located by ``file:///path/to/file.hdf5``. .. method:: Store.close(): **(Abstract)** Discontinue access to the data store. .. method:: Store.get_parent(key) **(Abstract)** Return the object which contains *key*, or None if no such object exists. .. attribute:: Store.url **(Abstract)** The URL used to open the store .. attribute:: Store.displayname **(Abstract)** A short name used for the store (e.g. ``basename(filepath)``). .. attribute:: Store.root **(Abstract)** A Node instance representing the starting point in the file. For hierarchical formats, this would be the root container. For scalar formats (FITS, for example), this could be e.g. an Array or Image instance. .. attribute:: Store.file_extensions For plugins that support local file access, this is a dictionary mapping file kinds to lists of extensions in "glob" format, e.g. ``{'HDF5 File': ['.h5', '.hdf5']}``. This will be used to populate the filter in the file-open dialog, among other things. Nodes ----- A "node" is any object which lives in the data store. The Node class defined below is the base class for more interesting abstract classes like containers and arrays. It defines much of the interface. .. class:: Node Base class for all objects which live in a data store. You generally shouldn't inherit from Node directly, but from one of the more useful Node subclasses in this file. Direct Node subclasses can't do anything interesting in the GUI; all they do is show up in the browser. .. attribute:: Node.icons Class attribute containing a dict for icon support. Keys should be integers giving icon size; values are a callable returning a byte string with PNG image data. Example: ``icons = {16: get_png_16, 32: get_png_32}``. Since icons are a pain to handle, default icons are provided by ``compass_model`` and this attribute is optional. .. attribute:: Node.classkind **(Abstract)** A short string (2 or 3 words) describing what the class represents. This will show up in e.g. the "Open As" context menu. Example: "HDF5 Image" or "Swath". .. staticmethod:: Node.canhandle(store, key) **(Abstract)** Determine whether this class can usefully represent the object. Keep in mind that keys are not technically required to be strings. .. method:: Node.__init__(store, key): **(Abstract)** Create a new instance representing the object pointed to by *key* in *store*. .. attribute:: Node.key **(Abstract)** Unique key which identifies this object in the store. Keys may be any hashable object, although strings are the most common. .. attribute:: Node.store **(Abstract)** The data store to which the object belongs. .. attribute:: Node.displayname **(Abstract)** A short name for display purposes (16 chars or so; more will be ellipsized). .. attribute:: Node.description **(Abstract)** Descriptive string (possibly multi-line). Containers ---------- .. class:: Container(Node) Represents an object which holds other objects, like an HDF5 group or a filesystem directory. Implementations will be displayed using the browser view. .. method:: Container.__len__() **(Abstract)** Get the number of objects directly attached to the container. .. method:: Container.__getitem__(index) **(Abstract)** Retrieve the node at *index*. Note this returns a Node instance, not a key. Arrays ------ .. class:: Array(Node) The array type represents a multidimensional array, using an interface inspired by Numpy arrays. Implementations will be displayed in a spreadsheet-style viewer with controls for subsetting and plotting. .. attribute:: Array.shape Shape of the array, as a Python tuple. .. attribute:: Array.dtype NumPy data type object representing the type of the array. .. method:: Array.__getitem__(indices) Retrieve data from the array, using the standard array-slicing syntax ``data = array[idx1, idx2, idx3]``. *indices* are the slicing arguments. Only integer indexes and slice objects (representing ranges) are supported. Key-Value lists --------------- .. class:: KeyValue(Node) Represents an object which contains a sequence of key: value attributes. Keys must be strings. Values may be Python or NumPy objects. Implementations will be displayed using a list-like control. .. attribute:: KeyValue.keys **(Abstract)** A list containing all (string) keys contained in the object. .. method:: KeyValue.__getitem__(name) **(Abstract)** Retrieve the value associated with string *name*. Images ------ .. class:: Image(Node) Represents an image. The current interface supports only true-color RGB images with the origin at upper left, although this could easily be extended to more complex formats including RGBA or palette-based images. Implementations are displayed in an image viewer. .. attribute:: Image.width Image width in pixels .. attribute:: Image.height Image height in pixels .. attribute:: Image.data Image data. Currently RGB, pixel-interlaced. Top-level functions ------------------- One public function is defined in ``compass_model``: .. function:: push(storeclass) Register a new Store subclass with HDFCompass. When a URL is being opened, the class will be queried (via ``storeclass.canhandle``) to see if it can make sense of the URL. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/docs/how_to_contribute.rst0000666000000000000000000000010714544243123016327 0ustar00How to contribute `[developer]` =============================== TBD././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/docs/how_to_freeze.rst0000666000000000000000000000007714544243123015437 0ustar00How to freeze `[developer]` =========================== TBD././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/docs/how_to_install.rst0000666000000000000000000000004514544243123015620 0ustar00How to install ============== TBD././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/docs/how_to_release.rst0000666000000000000000000000123314544243330015572 0ustar00How to release `[developer]` ============================ Versioning ---------- The following files need to be update for each new release: - HDFCompass.1file.spec - HDFCompass.1folder.spec - setup.cfg - setup.py - spec.json - docs/conf.py - hdf_compass/utils/__init__.py PyInstaller ----------- For the `HDFCompass.1file.spec` file, you need to verify that the following parameters are passed to the ``EXE()`` function: * ``console=False``: to avoid that a console window is opened at run-time for standard I/O * ``debug=False``: to avoid that the boot-loader issues progress messages while initializing and starting the bundled app ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/docs/how_to_use.rst0000666000000000000000000000003514544243123014745 0ustar00How to use ========== TBD././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/docs/index.rst0000666000000000000000000000245214544243123013706 0ustar00.. Copyright by The HDF Group. All rights reserved. This file is part of the HDF Compass Viewer. The full HDF Compass copyright notice, including terms governing use, modification, and terms governing use, modification, and redistribution, is contained in the file COPYING, which can be found at the root of the source code distribution tree. If you do not have access to this file, you may request a copy from help@hdfgroup.org. HDF Compass's documentation =========================== This document provide information about the HDF Compass application. In Brief -------- HDF Compass is an experimental viewer program for HDF5 and related formats, designed to complement other more complex applications like HDFView. Strong emphasis is placed on clean minimal design, and maximum extensibility through a plugin system for new formats. HDF Compass is written in Python, but ships as a native application on Windows, OS X, and Linux, by using PyInstaller and Py2App to package the app. Contents -------- .. toctree:: :maxdepth: 2 requirements license how_to_install how_to_use how_to_contribute data_model how_to_release how_to_freeze Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/docs/license.rst0000666000000000000000000001314214544243330014217 0ustar00License ======= Application License ------------------- Copyright Notice and License Terms for HDF Compass - Viewer for HDF5 and other file formats ----------------------------------------------------------------------------- HDF Compass Copyright 2014-2016 by The HDF Group. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted for any purpose (including commercial purposes) provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions, and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions, and the following disclaimer in the documentation and/or materials provided with the distribution. 3. In addition, redistributions of modified forms of the source or binary code must carry prominent notices stating that the original code was changed and the date of the change. 4. All publications or advertising materials mentioning features or use of this software are asked, but not required, to acknowledge that it was developed by The HDF Group and credit the contributors. 5. Neither the name of The HDF Group, nor the name of any Contributor may be used to endorse or promote products derived from this software without specific prior written permission from The HDF Group or the Contributor, respectively. DISCLAIMER: THIS SOFTWARE IS PROVIDED BY THE HDF GROUP AND THE CONTRIBUTORS "AS IS" WITH NO WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED. In no event shall The HDF Group or the Contributors be liable for any damages suffered by the users arising out of the use of this software, even if advised of the possibility of such damage. Additional Copyright and License Information -------------------------------------------- Copyright and license terms for the following software can be found in the adjacent subdirectory Additional_Legal/. This information was obtained from sources provided by each software package provider at the location specified under "Original source:" and was current as of 19 December 2014. HDF Compass uses selected icons from the following icon sets: KDE Oxygen icon set Provided by: KDE Community Copyright and license information: additional_legal/KDE_Oxygen_Icon_Set_Copyright_and_License.txt Original source: http://www.gnu.org/copyleft/lesser.html (via link from https://techbase.kde.org/Projects/Oxygen/Licensing) License type: GNU LGPL version 3 license HydroPro icon set Provided by: Ben Fleming via MediaDesign Copyright and license information: additional_legal/HydroPro_Icons_Terms.txt Original source: http://www.iconarchive.com/icons/media-design/hydropro/readme.txt License type: Freeware Pre-built HDF Compass binaries include the following software. None of this software is present in whole or in part in the HDF Compass source code. Python interpreter Provided by: Python Software Foundation Copyright and license information: additional_legal/Python_Copyright_and_License.txt Original source: https://docs.python.org/3/license.html License type: BSD-style license wxPython GUI layer Provided by: Julian Smart, Vadim Zeitlin, Stefan Csomor, Robert Roebling, and other members of the wxWidgets team Copyright and license information: additional_legal/wxWidgets_Copyrights_and_Licenses.txt Original source: http://docs.wxwidgets.org/3.0/page_copyright.html License type: GNU LGPL version 2 license with exception PyInstaller runtime support code (Windows & Linux only) Provided by: PyInstaller Development Team Copyright and license information: additional_legal/PyInstaller_Copyrights_and_Licenses.txt Original source: https://github.com/pyinstaller/pyinstaller/blob/develop/COPYING.txt License type: GNU GPL version 2 license with exception HDF5 for Python (h5py) Provided by: Andrew Collette and contributors Copyright and license information: additional_legal/h5py_Copyrights_and_Licenses.txt Original source: http://docs.h5py.org/en/latest/licenses.html License type: BSD-style license HydrOffice BAG (hyo2.bag) Provided by: G.Masetti, B.R.Calder, and contributors Copyright and license information: additional_legal/hydroffice_bag_Copyrights_and_Licenses.txt Original source: https://bitbucket.org/ccomjhc/hyo_bag/raw/tip/COPYING.txt License type: BSD-style license NumPy Provided by: NumPy Developers Copyright and license information: additional_legal/NumPy_Copyright_and_License.txt Original source: http://www.numpy.org/license.html License type: BSD-style license matplotlib Provided by: Matplotlib Development Team Copyright and license information: additional_legal/matplotlib_Copyright_and_License.txt Original source: http://matplotlib.org/users/license.html License type: BSD-style license PyDAP Provided by: Roberto De Almeida Copyright and license information: additional_legal/PyDAP_Copyright_and_License.txt Original source: http://www.pydap.org/license.html License type: BSD-style license ADIOS Provided by: ADIOS Developers (ORNL), and contributors Copyright and license information: additional_legal/ADIOS_Copyrights_and_Licenses.txt Original source: https://www.olcf.ornl.gov/center-projects/adios/ License type: BSD-style license ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/docs/make.bat0000777000000000000000000001612414544243123013456 0ustar00@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. xml to make Docutils-native XML files echo. pseudoxml to make pseudoxml-XML files for display purposes echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled echo. coverage to run coverage check of the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) REM Check if sphinx-build is available and fallback to Python version if any %SPHINXBUILD% 2> nul if errorlevel 9009 goto sphinx_python goto sphinx_ok :sphinx_python set SPHINXBUILD=python -m sphinx.__init__ %SPHINXBUILD% 2> nul if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) :sphinx_ok if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\HDFCompass.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\HDFCompass.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdf" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf cd %~dp0 echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdfja" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf-ja cd %~dp0 echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) if "%1" == "coverage" ( %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage if errorlevel 1 exit /b 1 echo. echo.Testing of coverage in the sources finished, look at the ^ results in %BUILDDIR%/coverage/python.txt. goto end ) if "%1" == "xml" ( %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml if errorlevel 1 exit /b 1 echo. echo.Build finished. The XML files are in %BUILDDIR%/xml. goto end ) if "%1" == "pseudoxml" ( %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml if errorlevel 1 exit /b 1 echo. echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. goto end ) :end ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/docs/requirements.rst0000666000000000000000000000004114544243123015312 0ustar00Requirements ============ TBD././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1714825026.7929926 hdf_compass-0.7b15/hdf_compass/0000777000000000000000000000000014615423503013401 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/hdf_compass/__init__.py0000666000000000000000000000007114544243330015507 0ustar00__import__('pkg_resources').declare_namespace(__name__) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1714825026.8159935 hdf_compass-0.7b15/hdf_compass/adios_model/0000777000000000000000000000000014615423503015660 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/hdf_compass/adios_model/__init__.py0000666000000000000000000000231314544243330017767 0ustar00############################################################################## # Copyright by The HDF Group. # # Helmholtz-Zentrum Dresden - Rossendorf, # # Computational Radiation Physics # # All rights reserved. # # # # This file is part of the HDF Compass Viewer. The full HDF Compass # # copyright notice, including terms governing use, modification, and # # terms governing use, modification, and redistribution, is contained in # # the file COPYING, which can be found at the root of the source code # # distribution tree. If you do not have access to this file, you may # # request a copy from help@hdfgroup.org. # ############################################################################## from hdf_compass.adios_model.model import ADIOSStore, ADIOSGroup, ADIOSDataset, ADIOSKV import logging logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/hdf_compass/adios_model/model.py0000666000000000000000000002233614544243330017337 0ustar00############################################################################## # Copyright by The HDF Group. # # Helmholtz-Zentrum Dresden - Rossendorf, # # Computational Radiation Physics # # All rights reserved. # # # # This file is part of the HDF Compass Viewer. The full HDF Compass # # copyright notice, including terms governing use, modification, and # # terms governing use, modification, and redistribution, is contained in # # the file COPYING, which can be found at the root of the source code # # distribution tree. If you do not have access to this file, you may # # request a copy from help@hdfgroup.org. # ############################################################################## """ Implementation of compass_model classes for ADIOS files. """ from itertools import groupby import sys import os.path as op import posixpath as pp import adios import logging logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) # Py2App can't successfully import otherwise from hdf_compass import compass_model from hdf_compass.utils import url2path class ADIOSStore(compass_model.Store): """ Data store implementation using an ADIOS file. Keys are the full names of objects in the file. """ @staticmethod def plugin_name(): return "ADIOS" @staticmethod def plugin_description(): return "A plugin used to browse ADIOS files." file_extensions = {'ADIOS File': ['*.bp']} def __contains__(self, key): if(self.valid): if(not key.startswith("/") and key != ""): key = "/%s" % key keylist = self.f.var.keys() for k in keylist: if(not k.startswith("/") and k != ""): k = "/%s" % k if(k.startswith(key)): return True return False @property def url(self): return self._url @property def display_name(self): return op.basename(self.f.name) @property def root(self): return self["/"] @property def valid(self): return self._valid @staticmethod def can_handle(url): if(not url.startswith('file://')): logger.debug("able to handle %s? no, not starting with file://" % url) return False if(not url.endswith('.bp')): logger.debug("able to handle %s? no, missing .bp ending" % url) return False logger.debug("able to handle %s? yes" % url) return True def __init__(self, url): self._url = url path = url2path(url) try: self.f = adios.file(path) self._valid = True except: log.debug("ADIOSStore: Init failed") self._valid = False self.f = None def close(self): if(self.valid): self.f.close() self._valid = False else: print("ADIOS: can't close invalid file") def get_parent(self, key): # HDFCompass requires the parent of the root container be None if key == "" or key == "/": return None pkey = pp.dirname(key) if pkey == "": pkey = "/" return self[pkey] class ADIOSGroup(compass_model.Container): """ Represents an ADIOS group, to be displayed in the browser view. """ class_kind = "ADIOS Group" @staticmethod def can_handle(store, key): return (key in store and isinstance(store.f[key], adios.group)) @property def _names(self): # Lazily build the list of names; this helps when browsing big files if self._xnames is None: self._xnames = [] c = self._key.count('/') if self._key != "/": c = c + 1 keylist = self._store.f.var.keys() for k in keylist: if not k.startswith("/"): k = "/%s" % k while k != self._key and k != "/": if k.startswith(self._key) and k.count('/') == c and not k in self._xnames: self._xnames.append(k) k = pp.dirname(k) # Natural sort is expensive if len(self._xnames) < 1000: self._xnames.sort() return self._xnames def __init__(self, store, key): self._store = store self._xnames = None if (not key.startswith("/")) and key != "": key = "/%s" % key self._key = key @property def key(self): return self._key @property def store(self): return self._store @property def display_name(self): name = pp.basename(self.key) if name == "": name = '/' return name @property def display_title(self): return "%s %s" % (self.store.display_name, self._key) @property def description(self): return 'Group "%s" (%d members)' % (self.display_name, len(self)) def __len__(self): return len(self._names) def __iter__(self): for name in self._names: yield self.store[pp.join(self._key, name)] def __getitem__(self, idx): name = self._names[idx] key = op.join(self._key, name) return self._store[key] class ADIOSDataset(compass_model.Array): """ Represents an ADIOS dataset. """ class_kind = "ADIOS Dataset" @staticmethod def can_handle(store, key): return key in store and isinstance(store.f[key], adios.var) def __init__(self, store, key): self._store = store self._key = key self._dset = store.f[key] @property def key(self): return self._key @property def store(self): return self._store @property def display_name(self): return pp.basename(self._key) @property def description(self): return 'Dataset "%s"' % (self.display_name,) @property def shape(self): return self._dset.shape @property def dtype(self): return self._dset.dtype def __getitem__(self, args): return self._dset[args] def is_plottable(self): if self.dtype.kind == 'S': logger.debug("Not plottable since ASCII String (characters: %d)" % self.dtype.itemsize) return False if self.dtype.kind == 'U': logger.debug("Not plottable since Unicode String (characters: %d)" % self.dtype.itemsize) return False return True class ADIOSText(compass_model.Text): """ Represents a text array (both ASCII and UNICODE). """ class_kind = "ADIOS Dataset[text]" @staticmethod def can_handle(store, key): if key in store and isinstance(store.f[key], adios.var): if store.f[key].dtype.kind == 'S': logger.debug("ASCII String (characters: %d)" % DATA[key].dtype.itemsize) return True if store.f[key].dtype.kind == 'U': logger.debug("Unicode String (characters: %d)" % DATA[key].dtype.itemsize) return True return False def __init__(self, store, key): self._store = store self._key = key self.data = store.f[key] @property def key(self): return self._key @property def store(self): return self._store @property def display_name(self): return "displayname" @property def description(self): return 'Text "%s"' % (self.display_name,) @property def text(self): return self.data[()] class ADIOSKV(compass_model.KeyValue): """ A KeyValue node used for ADIOS attributes. """ class_kind = "ADIOS Attributes" @staticmethod def can_handle(store, key): return (key in store) def __init__(self, store, key): self._store = store self._obj = store.f[key] if (not key.startswith("/")) and key != "": key = "/%s" % key self._key = key self._names = list(filter(lambda k: '/' not in k, self._obj.attrs.keys())) @property def key(self): return self._key @property def store(self): return self._store @property def display_name(self): n = pp.basename(self._key) return n if n != '' else '/' @property def description(self): return self.display_name @property def keys(self): return self._names def __getitem__(self, name): a = self._obj.attrs[name] return a.value # Register handlers ADIOSStore.push(ADIOSKV) ADIOSStore.push(ADIOSGroup) ADIOSStore.push(ADIOSDataset) ADIOSStore.push(ADIOSText) compass_model.push(ADIOSStore) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/hdf_compass/adios_model/test.py0000666000000000000000000000251214544243330017210 0ustar00############################################################################## # Copyright by The HDF Group. # # Helmholtz-Zentrum Dresden - Rossendorf, # # Computational Radiation Physics # # All rights reserved. # # # # This file is part of the HDF Compass Viewer. The full HDF Compass # # copyright notice, including terms governing use, modification, and # # terms governing use, modification, and redistribution, is contained in # # the file COPYING, which can be found at the root of the source code # # distribution tree. If you do not have access to this file, you may # # request a copy from help@hdfgroup.org. # ############################################################################## from hdf_compass.compass_model.test import container, store from hdf_compass.adios_model import ADIOSGroup, ADIOSStore from hdf_compass.utils import data_url import os url = os.path.join(data_url(), "adios", "adios_test.bp") s = store(ADIOSStore, url) c = container(ADIOSStore, url, ADIOSGroup, "") ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1714825026.821992 hdf_compass-0.7b15/hdf_compass/array_model/0000777000000000000000000000000014615423503015677 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/hdf_compass/array_model/__init__.py0000666000000000000000000000205014544243330020004 0ustar00############################################################################## # Copyright by The HDF Group. # # All rights reserved. # # # # This file is part of the HDF Compass Viewer. The full HDF Compass # # copyright notice, including terms governing use, modification, and # # terms governing use, modification, and redistribution, is contained in # # the file COPYING, which can be found at the root of the source code # # distribution tree. If you do not have access to this file, you may # # request a copy from help@hdfgroup.org. # ############################################################################## from hdf_compass.array_model.model import ArrayStore, ArrayContainer, ArrayKV, Array import logging logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/hdf_compass/array_model/model.py0000666000000000000000000002124714544243330017356 0ustar00############################################################################## # Copyright by The HDF Group. # # All rights reserved. # # # # This file is part of the HDF Compass Viewer. The full HDF Compass # # copyright notice, including terms governing use, modification, and # # terms governing use, modification, and redistribution, is contained in # # the file COPYING, which can be found at the root of the source code # # distribution tree. If you do not have access to this file, you may # # request a copy from help@hdfgroup.org. # ############################################################################## """ Testing model for array types. """ import numpy as np import os.path as op import logging logger = logging.getLogger(__name__) from hdf_compass import compass_model DT_CMP = np.dtype([('a', 'i'), ('b', 'f')]) DATA = {'array://localhost/a_0d': np.array(1), 'array://localhost/a_1d': np.arange(10), 'array://localhost/a_2d': np.arange(10 * 10).reshape((10, 10)), 'array://localhost/a_3d': np.arange(10 * 10 * 10).reshape((10, 10, 10)), 'array://localhost/a_4d': np.arange(10 * 10 * 10 * 10).reshape((10, 10, 10, 10)), 'array://localhost/cmp_0d': np.array((1, 2), dtype=DT_CMP), 'array://localhost/cmp_1d': np.ones((10,), dtype=DT_CMP), 'array://localhost/cmp_2d': np.ones((10, 10), dtype=DT_CMP), 'array://localhost/cmp_3d': np.ones((10, 10, 10), dtype=DT_CMP), 'array://localhost/cmp_4d': np.ones((10, 10, 10, 10), dtype=DT_CMP), 'array://localhost/S_0d': np.array(b"Hello"), 'array://localhost/S_1d': np.array([b"Hello", b"Ciao"]), 'array://localhost/S_2d': np.array([[b"Hello", b"Ciao"], [b"Hello", b"Ciao"]]), 'array://localhost/S_3d': np.array([[[b"Hello", b"Ciao"], [b"Hello", b"Ciao"]], [[b"Hello", b"Ciao"], [b"Hello", b"Ciao"]]]), 'array://localhost/U_1d': np.array(["Hello", "Ciao"]), 'array://localhost/U_2d': np.array([["Hello", "Ciao"], ["Hello", "Ciao"]]), 'array://localhost/U_3d': np.array([[["Hello", "Ciao"], ["Hello", "Ciao"]], [["Hello", "Ciao"], ["Hello", "Ciao"]]]), 'array://localhost/v_0d': np.array(b'\x01', dtype='|V1'), 'array://localhost/non_square': np.arange(5 * 10).reshape((5, 10)), } class ArrayStore(compass_model.Store): """ A "data store" represented by a set of arrays in memory. Keys are array names as reported in DATA. """ @staticmethod def plugin_name(): return "Array" @staticmethod def plugin_description(): return "A testing plugin used to evaluate the array visualization." def __contains__(self, key): if (key == '/') or (key is None): logger.debug("is root: %s" % key) return True return key in DATA @property def url(self): return self._url @property def display_name(self): return "Testing arrays" @property def root(self): return self['/'] @property def valid(self): return self._valid @staticmethod def can_handle(url): if url == "array://localhost": logger.debug("able to handle %s? yes" % url) return True logger.debug("able to handle %s? no" % url) return False def __init__(self, url): if not self.can_handle(url): raise ValueError(url) self._url = url self._valid = True def close(self): self._valid = False def get_parent(self, key): if key is None: return None if key == "/": return None return self[op.dirname(key)] class ArrayContainer(compass_model.Container): """ Represents a container of in-memory array. """ class_kind = "Array Container" @staticmethod def can_handle(store, key): return (key == '/') or (key is None) def __init__(self, store, key): self._store = store self._key = key self._names = sorted(DATA.keys()) @property def key(self): return None @property def store(self): return self._store @property def display_name(self): return "/" @property def description(self): return self.display_name def __len__(self): return len(DATA) def __iter__(self): for name in self._names: yield self._store[name] def __getitem__(self, idx): key = self._names[idx] return self._store[key] class Array(compass_model.Array): """ An N-D array """ class_kind = "TestArray" @staticmethod def can_handle(store, key): return key in DATA def __init__(self, store, key): self._store = store self._key = key self.data = DATA[key] @property def key(self): return self._key @property def store(self): return self._store @property def display_name(self): return self.key.rsplit('/', 1)[-1] @property def description(self): return self.display_name @property def shape(self): return self.data.shape @property def dtype(self): return self.data.dtype def __getitem__(self, args): return self.data[args] def is_plottable(self): if self.dtype.kind == 'S': logger.debug("Not plottable since ASCII String (characters: %d)" % self.dtype.itemsize) return False if self.dtype.kind == 'U': logger.debug("Not plottable since Unicode String (characters: %d)" % self.dtype.itemsize) return False return True class ArrayText(compass_model.Text): """ Represents a text array (both ASCII and UNICODE). """ class_kind = "TestArray [text]" @staticmethod def can_handle(store, key): if key not in DATA: return False if DATA[key].dtype.kind == 'S': # log.debug("ASCII String (characters: %d)" % DATA[key].dtype.itemsize) return True if DATA[key].dtype.kind == 'U': # log.debug("Unicode String (characters: %d)" % DATA[key].dtype.itemsize) return True return False def __init__(self, store, key): self._store = store self._key = key self.data = DATA[key] @property def key(self): return self._key @property def store(self): return self._store @property def display_name(self): return self.key.rsplit('/', 1)[-1] @property def description(self): return 'Text "%s"' % (self.display_name,) @property def shape(self): return self.data.shape @property def text(self): txt = str() if len(self.shape) == 0: print(self.data) txt += str(self.data) elif len(self.shape) == 1: for el in self.data: txt += el + ",\n" elif len(self.shape) == 2: for i in range(self.shape[0]): for j in range(self.shape[1]): txt += self.data[i, j] + ", " txt += "\n" else: txt = ">> display of more than 2D string array not implemented <<" return txt class ArrayKV(compass_model.KeyValue): class_kind = "Array Key/Value Attrs" @staticmethod def can_handle(store, key): return key in DATA def __init__(self, store, key): self._store = store self._key = key self.data = {'a': np.array((1, 2)), 'b': np.array("Hello"), 'c': np.array('\x01', dtype='|V1')} @property def key(self): return self._key @property def store(self): return self._store @property def display_name(self): return self.key.rsplit('/', 1)[-1] @property def description(self): return self.display_name @property def keys(self): return self.data.keys() def __getitem__(self, args): return self.data[args] ArrayStore.push(ArrayKV) ArrayStore.push(ArrayContainer) ArrayStore.push(Array) ArrayStore.push(ArrayText) compass_model.push(ArrayStore) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/hdf_compass/array_model/test.py0000666000000000000000000000213414544243330017227 0ustar00############################################################################## # Copyright by The HDF Group. # # All rights reserved. # # # # This file is part of the HDF Compass Viewer. The full HDF Compass # # copyright notice, including terms governing use, modification, and # # terms governing use, modification, and redistribution, is contained in # # the file COPYING, which can be found at the root of the source code # # distribution tree. If you do not have access to this file, you may # # request a copy from help@hdfgroup.org. # ############################################################################## from hdf_compass.compass_model.test import container, store from hdf_compass.array_model import ArrayStore, ArrayContainer url = "array://localhost" s = store(ArrayStore, url) c = container(ArrayStore, url, ArrayContainer, None)././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1714825026.8279915 hdf_compass-0.7b15/hdf_compass/asc_model/0000777000000000000000000000000014615423503015327 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/hdf_compass/asc_model/__init__.py0000666000000000000000000000203014544243330017432 0ustar00############################################################################## # Copyright by The HDF Group. # # All rights reserved. # # # # This file is part of the HDF Compass Viewer. The full HDF Compass # # copyright notice, including terms governing use, modification, and # # terms governing use, modification, and redistribution, is contained in # # the file COPYING, which can be found at the root of the source code # # distribution tree. If you do not have access to this file, you may # # request a copy from help@hdfgroup.org. # ############################################################################## from hdf_compass.asc_model.model import AsciiGrid, ASCFile, Attributes import logging logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler())././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/hdf_compass/asc_model/model.py0000666000000000000000000001276514544243330017013 0ustar00############################################################################## # Copyright by The HDF Group. # # All rights reserved. # # # # This file is part of the HDF Compass Viewer. The full HDF Compass # # copyright notice, including terms governing use, modification, and # # terms governing use, modification, and redistribution, is contained in # # the file COPYING, which can be found at the root of the source code # # distribution tree. If you do not have access to this file, you may # # request a copy from help@hdfgroup.org. # ############################################################################## """ HDF Compass "pilot" plugin for viewing ASCII Grid Data. Subclasses consist of a Container and an Array, representing directories and the ASCII grid data respectively. See: http://en.wikipedia.org/wiki/Esri_grid for a description of the file format """ import os.path as op import linecache import numpy as np import logging logger = logging.getLogger(__name__) from hdf_compass import compass_model from hdf_compass.utils import url2path class AsciiGrid(compass_model.Store): """ A "data store" represented by an ascii grid file. """ @staticmethod def plugin_name(): return "Ascii Grid" @staticmethod def plugin_description(): return "A plugin used to browse Ascii Grid." file_extensions = {'ASC File': ['*.asc']} def __contains__(self, key): if key == '/': return True return False @property def url(self): return self._url @property def display_name(self): return op.basename(self._url) @property def root(self): return self['/'] @property def valid(self): return self._valid @staticmethod def can_handle(url): if not url.startswith('file://'): logger.debug("able to handle %s? no, not starting with file://" % url) return False if not url.endswith('.asc'): logger.debug("able to handle %s? no, missing .asc extension" % url) return False first_line = open(url2path(url)).readline() if first_line.split()[0].upper() != "NCOLS": logger.debug("able to handle %s? no, invalid first line" % url) return False logger.debug("able to handle %s? yes" % url) return True def __init__(self, url): if not self.can_handle(url): raise ValueError(url) self._url = url self._valid = True def close(self): self._valid = False def get_parent(self, key): return None def getFilePath(self): return url2path(self._url) class ASCFile(compass_model.Array): """ Represents a .asc grid file. """ class_kind = "ASCII Grid File" @staticmethod def can_handle(store, key): if key == '/': return True return False def __init__(self, store, key): self._store = store self._key = key file_path = self._store.getFilePath() self._nrows = int(linecache.getline(file_path, 1).lstrip("ncols")) self._ncols = int(linecache.getline(file_path, 2).lstrip("nrows")) self._data = None @property def key(self): return self._key @property def store(self): return self._store @property def display_name(self): return self._store.display_name @property def description(self): return 'File "%s", size %d bytes' % (self.display_name, op.getsize(self.key)) @property def shape(self): return self._nrows, self._ncols @property def dtype(self): return np.dtype('float') def __getitem__(self, args): if self._data is None: self._data = np.loadtxt(self._store.getFilePath(), skiprows=6, unpack=True) return self._data[args] class Attributes(compass_model.KeyValue): class_kind = "Attributes of ASC Grid File" @staticmethod def can_handle(store, key): if key == '/': return True return False def __init__(self, store, key): self._store = store self._key = key file_path = self._store.getFilePath() self.data = {'NODATA Value': float(linecache.getline(file_path, 6).lstrip("NODATA_value")), 'cellsize': float(linecache.getline(file_path, 5).lstrip("cellsize")), 'yllcorner': float(linecache.getline(file_path, 4).lstrip("yllcorner")), 'xllcorner': float(linecache.getline(file_path, 3).lstrip("xllcorner"))} @property def key(self): return self._key @property def store(self): return self._store @property def display_name(self): return self.key @property def description(self): return self.display_name def close(self): self._valid = False @property def keys(self): return self.data.keys() def __getitem__(self, args): return self.data[args] AsciiGrid.push(Attributes) # attribute data AsciiGrid.push(ASCFile) # array compass_model.push(AsciiGrid) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/hdf_compass/asc_model/test.py0000666000000000000000000000213014544243330016653 0ustar00############################################################################## # Copyright by The HDF Group. # # All rights reserved. # # # # This file is part of the HDF Compass Viewer. The full HDF Compass # # copyright notice, including terms governing use, modification, and # # terms governing use, modification, and redistribution, is contained in # # the file COPYING, which can be found at the root of the source code # # distribution tree. If you do not have access to this file, you may # # request a copy from help@hdfgroup.org. # ############################################################################## import os from hdf_compass.compass_model.test import store from hdf_compass.asc_model import AsciiGrid from hdf_compass.utils import data_url url = os.path.join(data_url(), "asc", "sample.asc") s = store(AsciiGrid, url) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1714825026.8349903 hdf_compass-0.7b15/hdf_compass/bag_model/0000777000000000000000000000000014615423503015312 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/hdf_compass/bag_model/__init__.py0000666000000000000000000000235014544243330017422 0ustar00############################################################################## # Copyright by The HDF Group. # # All rights reserved. # # # # This file is part of the HDF Compass Viewer. The full HDF Compass # # copyright notice, including terms governing use, modification, and # # terms governing use, modification, and redistribution, is contained in # # the file COPYING, which can be found at the root of the source code # # distribution tree. If you do not have access to this file, you may # # request a copy from help@hdfgroup.org. # # # # author: gmasetti@ccom.unh.edu # ############################################################################## __version__ = "0.1.6" from hdf_compass.bag_model.model import BAGStore, BAGDataset, BAGGroup, BAGImage, BAGKV import logging logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/hdf_compass/bag_model/model.py0000666000000000000000000005452414544243330016775 0ustar00############################################################################## # Copyright by The HDF Group. # # All rights reserved. # # # # This file is part of the HDF Compass Viewer. The full HDF Compass # # copyright notice, including terms governing use, modification, and # # terms governing use, modification, and redistribution, is contained in # # the file COPYING, which can be found at the root of the source code # # distribution tree. If you do not have access to this file, you may # # request a copy from help@hdfgroup.org. # # # # author: gmasetti@ccom.unh.edu # ############################################################################## """ Implementation of compass_model classes for BAG files. """ from itertools import groupby import os.path as op import posixpath as pp import h5py from hyo2.bag.bag import BAGFile from hyo2.bag.bag import BAGError from hdf_compass import compass_model from hdf_compass.utils import url2path import logging logger = logging.getLogger(__name__) def sort_key(name): """ Sorting key for names in an BAG group. We provide "natural" sort order; e.g. "7" comes before "12". """ return [(int(''.join(g)) if k else ''.join(g)) for k, g in groupby(name, key=str.isdigit)] class BAGStore(compass_model.Store): """ Data store implementation using a BAG file (closely mimicking HDF5Store). Keys are the full names of objects in the file. """ @staticmethod def plugin_name(): return "BAG" @staticmethod def plugin_description(): return """A plugin used to browse Open Navigation Surface BAG files. It provides additional features that are not available with the general HDF5 plugin: - View of metadata information as XML (+ content validation). - Plot of elevation and uncertainty using their geographic extent/ The plugin is developed and maintained by G.Masetti [gmasetti@ccom.unh.edu]. """ file_extensions = {'BAG File': ['*.bag']} def __contains__(self, key): return key in self.f @property def url(self): return self._url @property def display_name(self): return op.basename(self.f.filename) @property def root(self): return self['/'] @property def valid(self): return bool(self.f) @staticmethod def can_handle(url): if not url.startswith('file://'): logger.debug("able to handle %s? no, invalid url" % url) return False path = url2path(url) if not BAGFile.is_bag(path): logger.debug("able to handle %s? no, not a BAG" % url) return False logger.debug("able to handle %s? yes" % url) return True def __init__(self, url): super().__init__(url=url) if not self.can_handle(url): raise ValueError(url) self._url = url path = url2path(url) self.f = BAGFile(path, 'r') def close(self): self.f.close() def get_parent(self, key): # HDFCompass requires the parent of the root container be None if key == "" or key == "/": return None p_key = pp.dirname(key) if p_key == "": p_key = "/" return self[p_key] class BAGGroup(compass_model.Container): """ Represents an BAG group (closely mimicking HDF5Group). """ class_kind = "BAG Group" @staticmethod def can_handle(store, key): return (key in store) and (isinstance(store.f[key], h5py.Group)) @property def _names(self): # Lazily build the list of names; this helps when browsing big files if self._xnames is None: self._xnames = list(self._group) # Natural sort is expensive # noinspection PyTypeChecker if len(self._xnames) < 1000: self._xnames.sort(key=sort_key) return self._xnames def __init__(self, store, key): super().__init__(store=store, key=key) self._store = store self._key = key self._group = store.f[key] self._xnames = None @property def key(self): return self._key @property def store(self): return self._store @property def display_name(self): name = pp.basename(self.key) if name == "": name = '/' return name @property def display_title(self): return "%s %s" % (self.store.display_name, self.key) @property def description(self): return 'Group "%s" (%d members)' % (self.display_name, len(self)) def __len__(self): return len(self._group) def __iter__(self): # noinspection PyTypeChecker for name in self._names: yield self.store[pp.join(self.key, name)] def __getitem__(self, idx): name = self._names[idx] return self.store[pp.join(self.key, name)] class BAGRoot(compass_model.Container): """ Represents the BAG root. """ class_kind = "BAG Root" @staticmethod def can_handle(store, key): return (key == "/BAG_root") and (key in store) and (isinstance(store.f[key], h5py.Group)) @property def _names(self): # Lazily build the list of names; this helps when browsing big files if self._xnames is None: self._xnames = list(self._group) # Natural sort is expensive # noinspection PyTypeChecker if len(self._xnames) < 1000: self._xnames.sort(key=sort_key) return self._xnames def __init__(self, store, key): super().__init__(store=store, key=key) self._store = store self._key = key self._group = store.f[key] self._xnames = None @property def key(self): return self._key @property def store(self): return self._store @property def display_name(self): name = pp.basename(self.key) return name @property def display_title(self): return "%s %s" % (self.store.display_name, self.key) @property def description(self): return 'Root Group "%s" (%d members)' % (self.display_name, len(self)) def __len__(self): return len(self._group) def __iter__(self): # noinspection PyTypeChecker for name in self._names: yield self.store[pp.join(self.key, name)] def __getitem__(self, idx): name = self._names[idx] return self.store[pp.join(self.key, name)] class BAGDataset(compass_model.Array): """ Represents a BAG dataset (closely mimicking HDF5Dataset). """ class_kind = "BAG Dataset" @staticmethod def can_handle(store, key): return key in store and isinstance(store.f[key], h5py.Dataset) def __init__(self, store, key): super().__init__(store=store, key=key) self._store = store self._key = key self._dset = store.f[key] @property def key(self): return self._key @property def store(self): return self._store @property def display_name(self): return pp.basename(self.key) @property def description(self): return 'Dataset "%s"' % (self.display_name,) @property def shape(self): return self._dset.shape @property def dtype(self): return self._dset.dtype def __getitem__(self, args): return self._dset[args] def is_plottable(self): if self.dtype.kind == 'S': logger.debug("Not plottable since ASCII String (characters: %d)" % self.dtype.itemsize) return False if self.dtype.kind == 'U': logger.debug("Not plottable since Unicode String (characters: %d)" % self.dtype.itemsize) return False return True class BAGElevationArray(compass_model.Array): """ Represents a BAG elevation. """ class_kind = "BAG Elevation [array]" @staticmethod def can_handle(store, key): return (key == "/BAG_root/elevation") and (key in store) and (isinstance(store.f[key], h5py.Dataset)) def __init__(self, store, key): super().__init__(store=store, key=key) self._store = store self._key = key self._dset = store.f.elevation(mask_nan=True) @property def key(self): return self._key @property def store(self): return self._store @property def display_name(self): return pp.basename(self.key) @property def description(self): return 'Dataset "%s"' % (self.display_name,) @property def shape(self): return self._dset.shape @property def dtype(self): return self._dset.dtype def __getitem__(self, args): return self._dset[args] class BAGElevationGeoArray(compass_model.GeoArray): """ Represents a BAG elevation. """ class_kind = "BAG Elevation [geo array]" @staticmethod def can_handle(store, key): # for GeoSurface we use cartopy that can be challenging to freeze on OSX to dependencies (i.e. geos) try: import cartopy.crs as ccrs except (ImportError, OSError): return False return (key == "/BAG_root/elevation") and (key in store) and (isinstance(store.f[key], h5py.Dataset)) def __init__(self, store, key): super().__init__(store=store, key=key) self._store = store self._key = key self._dset = store.f.elevation(mask_nan=True) self._meta = store.f.populate_metadata() @property def key(self): return self._key @property def store(self): return self._store @property def display_name(self): return pp.basename(self.key) @property def description(self): return 'Dataset "%s"' % (self.display_name,) @property def shape(self): return self._dset.shape @property def dtype(self): return self._dset.dtype @property def extent(self): """ Geographic extent as a tuple: (lon_min, lon_max, lon_min, lon_max) """ return self._meta.geo_extent() def __getitem__(self, args): return self._dset[args] class BAGElevation(compass_model.GeoSurface): """ Represents a BAG elevation. """ class_kind = "BAG Elevation" @staticmethod def can_handle(store, key): # for GeoSurface we use cartopy that can be challenging to freeze on OSX to dependencies (i.e. geos) try: import cartopy.crs as ccrs except (ImportError, OSError): return False # for GeoSurface we are using a matplotlib function present after 1.5.x import matplotlib plt_maj, plt_min, _ = matplotlib.__version__.split('.') if (int(plt_maj) == 1) and (int(plt_min) < 5): return False return (key == "/BAG_root/elevation") and (key in store) and (isinstance(store.f[key], h5py.Dataset)) def __init__(self, store, key): super().__init__(store=store, key=key) self._store = store self._key = key self._dset = store.f.elevation(mask_nan=True) self._meta = store.f.populate_metadata() @property def key(self): return self._key @property def store(self): return self._store @property def display_name(self): return pp.basename(self.key) @property def description(self): return 'Dataset "%s"' % (self.display_name,) @property def shape(self): return self._dset.shape @property def dtype(self): return self._dset.dtype @property def extent(self): """ Geographic extent as a tuple: (x_min, x_max, y_min, y_max) """ return self._meta.geo_extent() def __getitem__(self, args): return self._dset[args] class BAGTrackinList(compass_model.Array): """ Represents a BAG tracking list. """ class_kind = "BAG Tracking List" @staticmethod def can_handle(store, key): return (key == "/BAG_root/tracking_list") and (key in store) and (isinstance(store.f[key], h5py.Dataset)) def __init__(self, store, key): super().__init__(store=store, key=key) self._store = store self._key = key self._dset = store.f.tracking_list() @property def key(self): return self._key @property def store(self): return self._store @property def display_name(self): return pp.basename(self.key) @property def description(self): return 'Dataset "%s"' % (self.display_name,) @property def shape(self): return self._dset.shape @property def dtype(self): return self._dset.dtype def __getitem__(self, args): return self._dset[args] class BAGMetadataRaw(compass_model.Array): """ Represents a raw BAG metadata. """ class_kind = "BAG Metadata [raw]" @staticmethod def can_handle(store, key): return (key == "/BAG_root/metadata") and (key in store) and (isinstance(store.f[key], h5py.Dataset)) def __init__(self, store, key): super().__init__(store=store, key=key) self._store = store self._key = key self._dset = store.f.metadata(as_string=False, as_pretty_xml=False) @property def key(self): return self._key @property def store(self): return self._store @property def display_name(self): return pp.basename(self.key) @property def description(self): return 'Dataset "%s"' % (self.display_name,) @property def shape(self): return self._dset.shape @property def dtype(self): return self._dset.dtype def __getitem__(self, args): return self._dset[args] def is_plottable(self): if self.dtype.kind == 'S': logger.debug("Not plottable since ASCII String (characters: %d)" % self.dtype.itemsize) return False if self.dtype.kind == 'U': logger.debug("Not plottable since Unicode String (characters: %d)" % self.dtype.itemsize) return False return True class BAGMetadataText(compass_model.Text): """ Represents a text BAG metadata. """ class_kind = "BAG Metadata [text]" @staticmethod def can_handle(store, key): return (key == "/BAG_root/metadata") and (key in store) and (isinstance(store.f[key], h5py.Dataset)) def __init__(self, store, key): super().__init__(store=store, key=key) self._store = store self._key = key try: self._dset = store.f.metadata(as_string=True, as_pretty_xml=True) except BAGError as e: logger.warning("unable to retrieve metadata as xml (%s)" % e) self._dset = "" @property def key(self): return self._key @property def store(self): return self._store @property def display_name(self): return pp.basename(self.key) @property def description(self): return 'Dataset "%s"' % (self.display_name,) @property def text(self): return self._dset class BAGMetadataXml(compass_model.Xml): """ Represents a text BAG metadata. """ class_kind = "BAG Metadata [xml]" @staticmethod def can_handle(store, key): return (key == "/BAG_root/metadata") and (key in store) and (isinstance(store.f[key], h5py.Dataset)) def has_validation(self): """For BAG data there is a known validation mechanism based on XSD and Schematron""" return True def __init__(self, store, key): super().__init__(store=store, key=key) self._store = store self._key = key try: self._dset = store.f.metadata(as_string=True, as_pretty_xml=True) except BAGError as e: logger.warning("unable to retrieve metadata as xml (%s)" % e) self._dset = "" @property def key(self): return self._key @property def store(self): return self._store @property def display_name(self): return pp.basename(self.key) @property def description(self): return 'Dataset "%s"' % (self.display_name,) @property def text(self): return self._dset @property def validation(self): """ Collect a message string with the result of the validation """ return self.store.f.validation_info() class BAGUncertaintyArray(compass_model.Array): """ Represents an uncertainty array. """ class_kind = "BAG Uncertainty [array]" @staticmethod def can_handle(store, key): return (key == "/BAG_root/uncertainty") and (key in store) and (isinstance(store.f[key], h5py.Dataset)) def __init__(self, store, key): super().__init__(store=store, key=key) self._store = store self._key = key self._dset = store.f.uncertainty(mask_nan=True) @property def key(self): return self._key @property def store(self): return self._store @property def display_name(self): return pp.basename(self.key) @property def description(self): return 'Dataset "%s"' % (self.display_name,) @property def shape(self): return self._dset.shape @property def dtype(self): return self._dset.dtype def __getitem__(self, args): return self._dset[args] class BAGUncertainty(compass_model.GeoArray): """ Represents a BAG uncertainty. """ class_kind = "BAG Uncertainty" @staticmethod def can_handle(store, key): # for GeoSurface we use cartopy that can be challenging to freeze on OSX to dependencies (i.e. geos) try: import cartopy.crs as ccrs except (ImportError, OSError): return False return (key == "/BAG_root/uncertainty") and (key in store) and (isinstance(store.f[key], h5py.Dataset)) def __init__(self, store, key): super().__init__(store=store, key=key) self._store = store self._key = key self._dset = store.f.uncertainty(mask_nan=True) self._meta = store.f.populate_metadata() @property def key(self): return self._key @property def store(self): return self._store @property def display_name(self): return pp.basename(self.key) @property def description(self): return 'Dataset "%s"' % (self.display_name,) @property def shape(self): return self._dset.shape @property def dtype(self): return self._dset.dtype @property def extent(self): """ Geographic extent as a tuple: (x_min, x_max, y_min, y_max) """ return self._meta.geo_extent() def __getitem__(self, args): return self._dset[args] class BAGKV(compass_model.KeyValue): """ A KeyValue node used for BAG attributes (closely mimicking HDF5KV). """ class_kind = "BAG Attributes" @staticmethod def can_handle(store, key): return key in store.f def __init__(self, store, key): super().__init__(store=store, key=key) # logger.debug("init") self._store = store self._key = key self._obj = store.f[key] self._names = self._obj.attrs.keys() @property def key(self): return self._key @property def store(self): return self._store @property def display_name(self): n = pp.basename(self.key) return n if n != '' else '/' @property def description(self): return self.display_name @property def keys(self): return self._names def __getitem__(self, name): return self._obj.attrs[name] class BAGImage(compass_model.Image): """ BAG true-color images (closely mimicking HDF5Image). """ class_kind = "BAG Truecolor Image" @staticmethod def can_handle(store, key): if key not in store: return False obj = store.f[key] if obj.attrs.get('CLASS') != 'IMAGE': return False if obj.attrs.get('IMAGE_SUBCLASS') != 'IMAGE_TRUECOLOR': return False if obj.attrs.get('INTERLACE_MODE') != 'INTERLACE_PIXEL': return False return True def __init__(self, store, key): super().__init__(store=store, key=key) logger.debug("init") self._store = store self._key = key self._obj = store.f[key] @property def key(self): return self._key @property def store(self): return self._store @property def display_name(self): n = pp.basename(self.key) return n if n != '' else '/' @property def description(self): return self.display_name @property def width(self): return self._obj.shape[1] @property def height(self): return self._obj.shape[0] @property def palette(self): return None @property def data(self): return self._obj[:] # Register handlers BAGStore.push(BAGKV) BAGStore.push(BAGDataset) BAGStore.push(BAGElevationArray) BAGStore.push(BAGElevationGeoArray) BAGStore.push(BAGElevation) BAGStore.push(BAGUncertaintyArray) BAGStore.push(BAGUncertainty) BAGStore.push(BAGTrackinList) BAGStore.push(BAGMetadataRaw) BAGStore.push(BAGMetadataText) BAGStore.push(BAGMetadataXml) BAGStore.push(BAGGroup) BAGStore.push(BAGRoot) BAGStore.push(BAGImage) compass_model.push(BAGStore) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/hdf_compass/bag_model/test.py0000666000000000000000000000246614544243330016652 0ustar00############################################################################## # Copyright by The HDF Group. # # All rights reserved. # # # # This file is part of the HDF Compass Viewer. The full HDF Compass # # copyright notice, including terms governing use, modification, and # # terms governing use, modification, and redistribution, is contained in # # the file COPYING, which can be found at the root of the source code # # distribution tree. If you do not have access to this file, you may # # request a copy from help@hdfgroup.org. # # # # author: gmasetti@ccom.unh.edu # ############################################################################## import os from hdf_compass.compass_model.test import store, container from hdf_compass.bag_model import BAGGroup, BAGStore from hdf_compass.utils import data_url url = os.path.join(data_url(), "bag", "bdb_00.bag") s = store(BAGStore, url) c = container(BAGStore, url, BAGGroup, "/")././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1714825026.8409922 hdf_compass-0.7b15/hdf_compass/compass_model/0000777000000000000000000000000014615423503016226 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/hdf_compass/compass_model/__init__.py0000666000000000000000000000216314544243330020340 0ustar00############################################################################## # Copyright by The HDF Group. # # All rights reserved. # # # # This file is part of the HDF Compass Viewer. The full HDF Compass # # copyright notice, including terms governing use, modification, and # # terms governing use, modification, and redistribution, is contained in # # the file COPYING, which can be found at the root of the source code # # distribution tree. If you do not have access to this file, you may # # request a copy from help@hdfgroup.org. # ############################################################################## from hdf_compass.compass_model.model import get_stores, push, Store, Node, \ Container, KeyValue, GeoArray, GeoSurface, Array, Text, Xml, Image, Unknown import logging logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1714825026.8779905 hdf_compass-0.7b15/hdf_compass/compass_model/icons/0000777000000000000000000000000014615423503017341 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/hdf_compass/compass_model/icons/array_16.png0000666000000000000000000000144014544243123021471 0ustar00PNG  IHDRaIDATxuSKHQ>)ZeI Em 6BЪiD"hVAVE!(h.d`(g9/50.ʽ^\д[kkk-z{|| |;<444‚ ;8du|f#'h4@4rAhs\ DBҦ&vtts8;;+lllBOH(Ihe&tv}}3ѿo`+ H[K]"IENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/hdf_compass/compass_model/icons/array_64.png0000666000000000000000000001231014544243123021472 0ustar00PNG  IHDR@@iqIDATx UՖϭj YAEe6Nև& qHO C4qF(VhP@A(Pd(((jZp@Ivg{kS?jŋGnݚ#111#rsSSSQGsssC>}/IyĢ˟)))}333vܹ{=:|:A8k?عs]UUU[u+'OtJhѢȺuu\Ю] =|Aiii) ~FGmmm5k6nٲˊj__y啿)' ??)]Z|;x㏗q}U!̙3'M6O4鯹ÜVh,9 Ոc~+D̙3;̼kEc9ĉ"o悃}^y啄UVԩS=//[ sdv  _hu ~ïc1$(8W%=nh駟N֭[=kjj:xbĆl{:@UE+//$=9}:u]Ekɼ w ,ul]OYjη*RҸ@U({nE?k:8奤;ZyRTJ G DǁbWʗ8rN iii^Ȣ:p >`7|c>|w_rX>iҤ+駟Iy,`zOjTccdymڴGc=%\YQn}vcǎУGy 7tÀvpZIB1aΝzŹß~i+V @+,fL*d>}z䑊+,ЋvynRMs˾ŋBhܸq/ү:" nprځ600Qb48#~ a[d4"OC.A ґ@T ߺ{/Sm;oٲeCPw ɰT g5HQAt$! m@zjg$ c  ,,< #Fp,|0`.!=(Z9tZ>lO(ӧORnC1Ca a0 2dka (a87ڛPsg 3ƥ?L#@0qa> 6D.?|jk{͚5t(QDkШ630 <"`lFq%qi7Κð~aц]f1i*]0eʔZPa8)P@d0*4 1c#"Jcc"Ù]pq9j4u k0  dsSOWSOEફJ7tvE,[eéboB2([>[N[| XB-@#oժUh91m 0ϟ?Zlڨ)2:e[nurC1f̷B4r !4cVA!3.DsPUjB %5gB'n9(ƅɀ YkT@EdX@(6m2Q N( ʀyЩX`p„Aa/[o . AZ}& LRH3 ՙ r>"R` Is31 "RLY` ![M41ZG  $3\`QcBSbPe(BLj7M a528mBȚM89d0 Yz|RD{dX-4RصU}ϝ;P,cza$+rH  ^J:a,bhB-䷩9܏-4!6p\l1I9sH.a -V]pJ |Uh&{X\YI[ۿ\1M`&a!-$~1yL06g[\mSSd(UjH y r4C)(HΑ4E$BPpD :14A0"&7Cq CdL1gΜt@SqG; #B\3!DGyƢm /HB6m;pEZtfW # Κ5 :J9Ȳ%q A!3mGȿ1Jx!8noK̔ ,co*8b`<`,RO/㐿rδ>d!!ZDX"k 8F8= rQ icBM lj9@ROB8Gas? 0Ŵ ӯ֮];Gʑ+!E]tlEF@qxAx")@<{HłB#Da16ndOM` )U"ESٿdɒ9={@}bame@9=s„ Y2,?Is !>MaNAy`j 'aA ۶BJQgl0ϚO! hXtc٤k5=A5Qjh}L$tPh@Mfa)Et ̰ ư:&l}a,m0T0e<2jJ)wݿ\}!odS: Ei@U8 AYk}|'~jΐ$Vm 6Cw|Sy|gς`l,(*5(hT5\슆R HTYSQҳ%2ҸY_*;{1#00ph7762T'8o/F݀1cqơKZiU*/+hsΨ?0LF\/Kd7C~pfFiЄP3 "uIگs)ߑ(V s>&Y!2j/98AQW %a !3 rvoI< LZ]_,EDQAže2(l96@NUǫVA I* BM`: FqDxT7^MEb2\@mV?k&A6(@9JP7-5g' /\D p7C~oC"r<*`߇٭g !+ @HZf~*43ĖDU( Y(Nk&jVTZM*hOioifҨZˮV9~BtٲLRt9NQ?Vɹ:]Ȱvr:C-U鑬$F'E;*}_W-+29K HźDT*ԠOq~RuD"Zu=S-KfPK &tӤ369FEgF; oTk_XM1XeIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/hdf_compass/compass_model/icons/folder_16.png0000666000000000000000000000055514544243123021634 0ustar00PNG  IHDRa4IDATxcd0E61g|'g6 o,L`!) J5 |'?%Rte:zG b@ ?ex(?FI4'{EoOW0dcdĨYqߏ?"EM_vFXw2O_x#F_lk˯7ƴ5^ `#>ѩH'6Zh$}h@f |T[1B ju0ZxIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/hdf_compass/compass_model/icons/folder_64.png0000666000000000000000000000322414544243123021633 0ustar00PNG  IHDR@@iq[IDATx{lEfvw}^[Ji %TQ T%!FE@ŀ"#T)P4&#( ؖ((Ƅ #Emz^{w{;3x%֖s˖wv 0;]#ks >yR>#g |[ 7(ֻC¥Bܴr@i=Y>G<z"6ew#ҫ<Y4cIG`+nbҐb,ڷ Zx`կ $ f|i*4(U*U?QBWC J^)9YZ dl{4: @֛ Ek0vMeNj'P+NZ v=-Ar##qj?l੡+Y@ eg`z=qoSj'^(Ċa un:tHZ8@Z<{&RP +E |lvtk+XpIM͵m`Yh-LdE3+ j! S uiuP- r@퓚=̧ &!ǹGE !K"-VA$ΆݞN]tyQ׵2>Î$1 'v{dΡǹjM0 %D<3t66zxpp@FA<sXNG5foC$6&="z0U7ωjvh#'Oc \hdF&K0ҁ`{XtE M@AR5b#(Mbs"Scn4)?U]l$Yh.\5xzڅ&) ?_4PCƪg:з+ WZafFQ-oH0D{s<XXQf܊19 @'2ab#„x|T"ڳsXR['v MTK0H!KRg*|Z*4^=E_pF=@5Hh#v=J[$E_n@@MAFN"+n=7nANf]}A8.6!>"GL|,D yeq8|Vj>J,VWPuBgH M9Mr4`vf 0;]1ߒ6{2IENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/hdf_compass/compass_model/icons/image_16.png0000666000000000000000000000126114544243123021436 0ustar00PNG  IHDRaxIDATxڭ_HSQm:7]zU:dH FA 2%Bf/Y͈JE61,Mۮs{o^z p~]U6ME߮z*2nvtVkA+~M7Z1 WF.ڪ1kAJdHtTM[5O^A|W Mn3&va(PdHAƎ| g5x e 'jK\0ZAUS&CcC]u-ɵѹ#U@Yk)La E8~20alQ2D |L`c!!XpfpֳMMWDBL@%H)pJp9XOgADl.t=3 5HV FR( ؘo͊ |U`"E1pq{Q$ WM{y{zhC]E+=ը)iЅ{GX{e*ڍ}ʩC(KvYV B4yU3 hnB",*IENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/hdf_compass/compass_model/icons/image_64.png0000666000000000000000000001316514544243123021447 0ustar00PNG  IHDR@@iq ֭[eXLa2fYK͈yLNNJ2o_XXhWMam_k;pQ`MMMLKKK]8D"d2scយ33ζ+CX  8xP hsG8YJ NB6nZBN B.j+0!f3?CƞO0<0e1c@hk#Sՙ\P-g?wbb‘ Lss'|qo'&x,8zi&)^ZaZ÷(tAC7@ρ#GnhͫFGa%X^K-)sOs orM>+m6mABmrq;.MCl8- ȁh䈳%՗Ȱ 6jOoxڨ8,GL` W@ą'6^#9uHd#:sbp(!Xq_ZK JoB<! {rsZZ_Љrr8NP"/ 8QsUhj1"_C_A9z-5`_+f l;:;JnACuɶ*pQZ ll@hk=8&j5 r`[0b a0tV6aY%v;\ "= [ /Yʔa?mHsd.łLw"0q˺ܲt+0hM!L+8NBk>3z =x>BJr:nXjF$wAn2:[11m`62Vv1L^Nh[(Y9ܶuˇ`8E9/G_܂I70b4Ā| `*t[8ߎh+WMD1t6Z= Ǜ&L*4if5#rӀbqRb,e{Gź͸ON@ (q6.Ø/@J2d2X`oHh誚Bգ(ZZ{H݁ x&WOZ  zaR Ϥ|\CU,ɃYaa%>u ,<8(7cƋ01y4C)+`Xg̅HXG-xXhZ8YЋk*T~*bY`$Lr=,Dh0fKbI-vH< %%3O,$S2*4sP.&FvtF nCw|oU^S)Z!$G4ZP*M0%0rhM 0"qwNbU E p)2]@t B2B @k*Th}WuW.usG0/C|*1W.u6c*9XaћLȠaL5"th̅gD=!+5fE.=N0!s,#+!qN'>lRl^U\9 7CAكDiqx5,y9ȽRJ'>=3hU" "\\)Ʀex>X4juCF{;F)U̠w!U:4s$㝨XD^3?zWjprvh/̊_oBy{7y)P(ux[ MgVBy5 ӧE[=|Xɀ[Dq ~ѧ v-n]_C/=? ݐ fOݺ*w J]O!'2O=ԎJp"Y"NB}H_co@߆ȯ5[S "/BFR Ȅ c: Qx!//ZAЋ5Ppb6Ct-XVwZ@8EP+8߇T,_:!F*IDL[ I7Hdv`2c*Ǧ@2Q\|zCe"}g-oJl2,Y [Y=:=4!6$ R/Pˌ8҂? F:ҏ 1@.0 B+P2X42ܵ>TL E<4߿nւP;8M>dwut,S8u_`0Ġm,ŗ7ג۞{Rp QrO?p\!4X3oA: RБ?gS}<)BH1/񸢔Glj#pcr12L*ډV'@QmWc0E\-:j Ir9E=aXr,.dD&Pb-~؏w?3s )=Ƅi~K,PЙterVx}S:;3P'3w솢髸a-Lh,䄊M¡Y"~jChIeh1((*G2VWy-$ Ʌ#uԙD4j;w7ANdRG${C[!3bi fwH&xS=n:){,*?j!"TW1p ls())DM{>9wTN1ru At:ꖯnv(hI5Ā$9 $v9Lw8Ӻ525g6tC_ ;P&8&<m0`oF32y򩚜}^*v ([2|~ 3_v'z} ܴHKb)]Czd9=q] n>dݾBG1/v^(q ť($x_ԔJHuka?>T]S`) N:J&}mYCbJBk\/}t)rh83c,js2r5VYfCjCAy%xwwwW2!`R ӴL>},UoJ̦eeve3d`6a@EADdZ$Xq1ʱ~!1* {e*/PLj 28sz۶>zsfǒB(e<'za :NG HL9k73Irt[eS;8*f+~ J-2. /\>@ز‚\4'z_Xr>}*,0+݀L~枓  L7.tW(hpĂŤ,H .\# rbIF,#y-_Uݾ}jjϩӧC. K\ HaH42Sp 7UKrEQzNƧ@tC=0یxu2X^mBGF&4J %$LZm@_fls6ff Cis~=M&$()>sċDR)%>iy'X PUGO((x.ohjXJtͳ1DB!ѡm1:>a.$f[`(IiOw`E8>v)*%$$HyIiL0B.FZ Ta:28%d޳}/ xqmV\63_f{JgN&'! bM!Jz>c2VpMMbbbYrBdjy}#\`| zwcx4/R`d$i+u~(2h5i!'G7nu-#EҞpͤ>Qc}U]BEEL|S S?r2W?m]Sϱ(qv &C}RAtr=m'ៜDW!G227o.ml|Y<BO0LzS=LPjcg;3 !=3Wk 6ibA'aG0~n緿ý{>8|Yg#جO,31EO*ˤ,fIKxS nd+2%]øo! wk\V<d2-A8)WHt14 қS%"=pp#^2#rycø{Hx~70 s" 3gh Lå Й!$ 2uY L` gK2|nSTUWs'xLJ5~V=`g3{IRBt pĜrkU=`(ND&B~?v=zƥK]gje1Y;7#sbj)PJ/ J /5›7͟H" S=]=/x_d|?E?+SpT@x E`|r~44>=>zw/+:SNDѳTY)2\ %zzx\9vXۮ]  ozcggg!?Jly Çs/X. __4SO1п 1=>`'0{ulO K bIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/hdf_compass/compass_model/icons/kv_16.png0000666000000000000000000000160714544243123021000 0ustar00PNG  IHDRaNIDATxuSmHSQ>wu3?Nӹjvҕ%ĢUSAJc?1R'%Y0X:#% i--Im:]ֲ_clwvnjz>}9PEHaӧOLP4 HGzz>^o4 FujbLOqf]]Mp 9wndĤV-p?6 ^$1==}x.׷5p:_J平L&GQtL3KK{(Xw= ې\v\n\gg盪*e! =j1Kټ)L~xvrbIiZZrH`||sz Bhփ`܋H=ݽ`nΪ5ݏ䧦VHt[_U*a\Rt 6CCC'j$JJT*U+2 0̐nmmmiA"KH05Bwܴ @V'^Tf'5oy,Wiv@/flkyDrď4'acV~Bk b|~ d=t @&;^*f^?ZsFi =r$EŦN ,,,1~"|;`2+$&&Bqp!9hGG3>FAt4Pb|~- _Z]]&e |xܿ(j3ǚJ #/\Ps\y )RnSsp|@9W  \$h*k+ eTHZ%%%z:֦Kzt_?fώ/IENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/hdf_compass/compass_model/icons/kv_64.png0000666000000000000000000001443514544243123021006 0ustar00PNG  IHDR@@iqIDATx[ XSW> @E*ZۊU"XܪZmcK_EUV\TG[mb`ƺMUQKH DT9oyseb{X,( nKK I#N,kᾮJzg BJd< 4UR'U]]Fo@<hp6vޡ_AAfȀ>ƻu^n+++V[[SO`ggt:蠑;,uvvy5+Q/yy%$$$0\B֚>wvr9VGuZ\1| V mm$&MGRA0 Gdg_aV^a]QQv.+ \~SS̙oNJMMTgcc p_G@^\ح-V"1)}iHkܷb%R A2YAv3%+\Oo2rmT!jY;M9|Jjݺ?*@ hdOЍpPa4T_yѣX[_7.hX\ZC` =?-3߉.fW߿s ,5RkZ=P֠oF~57VZ?B]!-O$ O T΁8,Xm۶ lm|T6lI;vᶁ:c~"33s{x8+UA[? VPqkׯ_;S(5~ӵ}ٲ%|ž=?xZƞ9u`/VjǏ};\j 1xa/0$ 07ӎ0][n\ͽGNN{>?gd${zp;u! 3@Zj5 RO@+,׿Ƭ۽{k?Ztߣf耤vPvMFc*ij Yx忿b&eg G(;u*sȑ/i4ZzPEGju(<^'/PONlL{ ?!򰨨|ճJ{oqfJʡpb: @p(ef /Gm'[^Om)7}0=3XQūȆpTSϐ72rZƍ#1ɴ֭:|ıeFJEy{{{xOKRd]ԣ J@<0/q=/V!XFQe)&&6f<ϠʘTM ܽ{.2Xl-_;6,`׮_ "Nmi0KÁvZANC8O~(w?oڴqoEEy,Yd5kF=76*7PVꙍ 89I;@@,zrZq߮]{2wݺϚք:kƌ7_v5NPsG`tLJE%`cزeBBB_ٹKD"G;E̙<`<dFH>A¹ O""I>Ah,_r{EOYԢ;%O[@}wQ*@ Ҵ,hrSD.HNgo/0mkӳynu( Gr&! `x5Q1}z$%I44(4,VF7Mr.L?{L Y8;{%goȚ5q9rh/a@IK:KY`R{G[z܆ 0vry`i=<-O \|6Th񻜜 ~{jd(uJA///)[|ʕ+k44@^QQTo-?y&Mժ]]}\[PPRK/YQ9.4HN5H;QsxpϟhnQ).CVvť;Gc?N]ǎn߾#c}=Mv8$u,V v0,(-Hxy@>,5J&+!%%2 )t|u3?3 {mm~Vֹw!h f`rR"qTb L)ID 5μt'eeU`WUw֮;,ظqx'?M][Yq!>.8SҋNj%tu)YYW{vz`*//)0>ZXjy㏓|_xzQwwgDK\ !w,g~󉖨R)v^5q? IG0 0c3ن kACQT4:WTtgzO @y{}|딱K;]K fqp06u,# H$ɇA,H&+ b\I}%N}Cǎ)~~n8^H\GuI}S@,;ohJ #ψIG,MT4yD||v^htн(&/|dE'H4u„*NM yaA7]oC'7]4۹% ːH=Gč5důDv0jp?,dp?*|yy =}l#.RZ%) /q7$5e]`Y,ӪzjK yȡ3MO( Fu` AJ#Xp!:)VLXl ^i) apqmAjꉯB7>laD ŭ[tuLg`xgh)ϣ:^Whf:P^^Vr_2DhLo%o8R8CgڊR[[Xaw3{]4-Dbi񫪪Ձbi911nիB:\~ ÇOwmG~s-%[Æ䙔#D* hsQDU=bhP6JZw= @K3]]6ml U;}r9.IQg71HAvZ;}2Q+,H3^>AȢEO(`Gd ; x!E0]39RV_pĉq,%Bp~f7aBd~d;dŧB0 qn7,lBdnUUU M'QNM{sv\`& CXF2c}g zjuwuHo`'%m<}Q;DPqg^} +#g}Zh;DLl>(1\cj̈́&0Xxؘ1Q[(:rqdq;u#HqsՌ:w\Z\ҮXԹs98?e1h7@r\%4 bv'j O-z=--kXlG (]Em& Λ.300Q 5""t\.?cF96_C,l|N湙6&Y fe@7A 㒘g$53=! r:9 BxGL<'b2JNneg?Xw%Ml Mѱ:9KLt fӴ{ա痗kijjz;E-%Vӝ[Ə<鼩dל= *jk`Hgg4vŋ;V;((ة3v*=66fU| l 3/,55kh EyM4`III>DeĈC `˗/7\xQvڵb8/X嗝w:$@uo1 k;O\>mmX{'fʵ+p;% )_vH41%{[ x-''gS9pZB5}`@cowW cFwnݚ/v=?Fǧ~b}Vǃ_8_fc҅ ,=jԨ[nDh΁ 70;fdd%@ ?;' hxlUɁ!!c7nޯ_?ɴiSAx7u24۽m ~嗗߹s'p ]E922%xݻw7l? oCmB1n~z`bFi3.xxgZD1X*tȐ! KKKqט xXOqY a* ?:}QhxO (]̺Ri!Kt̫"$ThUiXtep ` XYYyRA...[[[ kCgz;vS^C(3cPcy8wWnŸ@s=.U)ܓ%KIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/hdf_compass/compass_model/icons/license.txt0000666000000000000000000001720614544243123021531 0ustar00 GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library.././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/hdf_compass/compass_model/icons/readme.txt0000666000000000000000000000041114544243123021332 0ustar00This folder contains icons from the KDE Oxygen project, used as the default icons for various Node classes. They are packaged up into a .py file by the "package_icons.py" script which comes with HDFCompass. Refer to "license.txt" for the icon licensing terms.././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/hdf_compass/compass_model/icons/text_16.png0000666000000000000000000000124414544243123021341 0ustar00PNG  IHDRasRGBbKGD pHYs^tIME;S$IDAT8ˍSKkSA=ssM4m`\$B$kۊuSM?@D\*΅D *ťk+BJ,$&77y{3s\&mD  sw|3jzC/|mm0'\PG_YZIP[!Έׯ¶;A @411ȬdK tpzUΑ|']݁^! q \ Uwu;"qXpXt]KUjSP:6{vNnɶ𦰰/ֱ^@Erd͜88>[7AITOfgH)A 3^U*,qeQudv<fz.N=˖W$I0V6D"q7<ʣk6Qi"[-6`F".ʥҽ|>/>Kbbȕ+˲83;D"Dρ\.!tЧATjX(1 +  MbjF BiotuIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/hdf_compass/compass_model/icons/text_64.png0000666000000000000000000000744514544243123021355 0ustar00PNG  IHDR@@iqsRGBbKGD pHYs7]7]F]tIME  6C(OIDATxmlWzgfpaM0!llUuUvU-!Rд5* Ҕ@X\lhM~u<0/;ڞ wG9gfΜ9Emh~fԔixL:5 755MLDŽPϿ"= PtZYX pkl roZ' Hw@|<2qpcEo$,'N΍"NyĊ`, ;W Q-{ o\Bb,>;֢.*FA:mr CyoUKmhDcuMQNau ˁDJD M^9Ö-[@/'%T#!Bb_L\'sNeF_g tا,kThgh^W%L)8, +C%G}i眍9,M LP , +5?TRxm_p`Yݬ\zU-WFkяDZGբ&@2dٲe T]_q%r;)YZLi_j)mc!FDJnnd'$ ZzZD8e4g#>7o Q 凙Ѡc DӱĘQQ3Z IOOh^cB9 ozBkam:U[+"Hܜ%Yʈ>V#۶ٵk{9DA6J踄+.>R|+Ps [% hKJG"j4^Ziq{[xإqwfTK|irq.af3#y|KDo^jRA:dbTX,T zdI(?"6 x"(m!P W) pm>@+VrO=\C^D~9D'@i'%uB,Z᧙B ef‡ߟe`p }}XDIqT&6"~1:tHHlO\΍+2!!"ŋ/ȱcǨcҥ#->IZ 'E% ӵ D477cz( (eހ8*CCC'[Z\&^yϙ">_r%4@6㸵̚(444P__O2b,SSS/U/,!`k2@6b*M5L:TC˒nI;YAP 4&m Av85OޘeYAY]k]ȧR)R A6״,q ]"..m۞גǶmqg .B-ld2R,%K>xhۖS~%qımٻ{z1)RED{Syرc""b B!/۷?,kV~_<)r%rϮHŋؘ[{wg h6R\V'˲C}~y"5U~k58PW'X6D|?W'ZSHsqI^-w8 Y08uDz9]wqy&SSl{<*pk! 0ִ*2@)_?CzzyrSp~[Xٔ@p_\WG";A[oh!smް6D?9ѾQr7pEv'L2==3088XfMZ ˗//'_f2222ȣLEr~m_/~ ٕ+ܺ5]MM^`SSAff ЎM6ƍ/2u Ξ94<4\m۶0'>ϓd}h˖y`ޮɗyOf#GݸY#NqsU+7u"wqOf泯)kot^c,= {]P ?>>>C><ǧ&-tw˅ K>-9 ٬_N:;7Ȏ͛?@Ο;'+WKC׵GDD~s9rM ˕K}r[|djrRFe_+`7fIDTPtvvzժOS"Y+`vv~ʒ%KX(ң=*""H$5~=--dff d2~'djjL6m㷻d,͵ ѯ4éJѥ*"n d8C7oacccoڼW17?"7:CERwP(PW_ms9n }i;%*| ^HxE v?LC`xc~-"847335E'[n!pSwuNmrRw㈮N*])p-؎F6E"}.(lyN^zN3$S)VS {)W|hm;8.¤y-|ɹ+ `VDl&iT*Vww@߾- tQ?r_@\hR5˭ZJ*Ը M^<^>Fy6uV{MH' {T~~[nayIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/hdf_compass/compass_model/icons/unknown_16.png0000666000000000000000000000160414544243123022054 0ustar00PNG  IHDRaKIDATx]mL[U]]o[If$̀j2X#>0iL5>"`Ȃ6dCc@: [A6Xn 0${rO=yWCACѤX֪BZz~x1 "*qdԽ4:;;;d@IyjkMbBYZ3b]ĢzK,X 0lnnާkOfc٫f<>;+9J.z{{e3336ͅΟp=#&F@1?r(6b QtUG.x6 ]N]ޝUcxD׋q[X; oEO0f|lb%eEj۔?%OI.gMe[A|u6twww}rЉ}/b42 i0up\5^P7 RF&EoZ4>>~=c$)U[ZZZJ3Z*zGd?~ߠ}H;7dc{)7GIiroeyIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/hdf_compass/compass_model/icons/unknown_64.png0000666000000000000000000001253614544243123022065 0ustar00PNG  IHDR@@iq%IDATx[ tTUkRJXB EۡA]eEEYqpT@[60WFl#2*aIIR{իz5R U3Ww_{KG?a4DB/˲_ X,[[Oȸ'n0ϒ$ҿQ`ӡ M .Q  = Ř wȸL&=:kf μG|Ȑ!qt޽{inVfh4 㨮n<|]vUWUUv3[,EH$"C7CX nw׉'^7y4ȿ[RK"h;"v^zڵk?lll</^q Β|&goMΝ;w޽{"h E^fڠBaO>yߌ3&.k>Yڌ,[l… _ ` gџ 2:_6d͚5 ***zH ^KB&[&+ЂC84iҤ"pVz@eYѰ2eʔ^|l@\nкx6 ^aiӦ~ժUB_SSyIȊ<2u}'xꩧ9!!Zm> B ^'NO?+-z 0{,ktҥ~;N ^K iqS/J3u3QNSV@#z]d62ƍtB`_|Yf- '} R lDzϟ?UǙgֺ0WqSu_J ԹBn6Yp1a@'o J 0՜ 1wYMYBK)wgn_~N+a +Џ cp١U_y 8Wtd財Mhʐ j:UP(PSgF2!K$=JԶ/xt,<pLybrb67 (N4@LD)8:Sl6Bb)A&4h۞﹌.*Xb[!CCwNb%@xx+E8ʝ ? wՆicԯzviV J{ < 'oLԭ%ح&}(ўMtr#Y"]1)F`oS$w`M$kK @#ft#cǎ7r.Zuip"R\@Vt:i0Qh#E|S1;SQmEuVr;Ap(B5ҫi$" iXt#!*"?K:Eo=<YS$h Nfz@>W_-&,FQw5>G}A Au%Qzu& k^zs^[D~ԕH6;AU -4˪%x>Z7sE£a)<^~{-r< 3,li&FݺP6aQ )PGkg^ -^m(%2 T}wwҼ[{%eÆ vm3yfC<\v/?~x%~XAG֝Ku"`DOenЖgϜgq ؍!0AGT}G璮ŝ*> 7S~"2p`M1;>ޘ6yB^*GHeeewZֳpx:fry$#  Zyп&:CRh!-O5lIH3vRt7@ ,X<5Zk׾4~k2?'i>]WXRcH P(g5O۞ ,-:+f'EIwӼß!{FwٷS\ Sxĉ1A_~嚾}N'@CN_ً:`J ';"'_kfr[E!G~}r D: `boSmx: xhlw 1ct*gϞ}ICZx^p2b-"e~8\P nhU9x3>GgM(]A]?cZPՀmsR5Dp(RcAzq~@s^(?X6rC C0[wf 8?g_Qܬ*<ę?Y#gh~" Ci|-^Lx 4s) 0跟7#@>5ҖCo]G'>tF+%2'| @1UAv{Oʈ>281=1*~z q\h.t5-}Ct+;ps!.Ap@H{[0_"=#4 ~- 3jgS;@Z9M KJZYurn͊9I@0S"֮8Y%7L"f(*oA@C>]r%m'~MiΉY7t!Ůc8YQBlpoU#ԛOΉEb׃ە g] ]0Emn298A1M@*=:-h͙`B%ZeQ]%QWZ#@ W6b1]OSAY nhA3:e8w=4Adx+ϥ͈PV69ÊifU㷦vOdr'+PLzʙ`)HƖЍwӊOGCCZ`n/WG&݉0a Fd2 l`V] 0≙a~d ő;I+PQ5.d>꧍zRv-Ύ˷!ŰM61AVHlJ KɩKsB7)֎9}'Oɲ] ͇>Q(\AEB3$iqq_"ʐ ?d0rןaLyd :d&CuN"R<}U |E[i<RT;oxA/Q8")^FzwΈFRqä́난 yҘ# % )mQuaJ&#Id#_P"7DazwPe^Sk9-N]]M9 f\ W]@xkO+w}KZ߬N{ H/ޝ B !~ClocJF]Y:SaH&Sx4 2CüեfIN) щ鶑%49oZ#*pH9Ӏk" *((N4ÇPڞ1۷Czݺu;<)܆d@stt5 "ӀTPAp5r eEQyyy>h3;[X;E^ :uփoدնҾ[ ^s:Y⯲VgJtoah$JDžN#zY"0Z`Z \N=]Y֑Yq9"IENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/hdf_compass/compass_model/icons/xml_16.png0000666000000000000000000000113614544243123021155 0ustar00PNG  IHDRabKGD pHYs^tIME -`@IDAT8˕;hSQ&6PjŊ:E(.-J\| :HEਓRХhUK|H܄<=܂8|9z,d(v"[@{U0 {],s=*O Cϧq~'xRs@B' @̂TDfRcsδ_{Ϭkö“al{ۋ<.)N }qE܂sR Ku([;kpcgY ׀Rz[_gZh6^cV4`+xeN UYD"}ǀmAفl2+4U Ǜ4LHȐ"ir"t躥%)RnQĬyS5kaOz+xVFha+#e`kEUgImCtEl0EZ>@ĻW#(* pc 7 i$-q0b>6zWkpjŽ#qoL^hN( ٨VPp UmTݐԵVPv* WX2ŠC^`EGT35Lĭp$aq΁ "aAU+l8E p ,続i(") %,Yѐ#Jvd:9f-y҄S7ǿֿG_k Pt$ɢk-Pja=W >ש:6E*#P1L"0H44_V0lpX\l%8UxD]C_r5Db*oiVN BCzثvnȈe]XݖVj!hFp"k{ :EP;b?8W*M!Fˊ pU|1lY0\0XXjS$"0  Ul@B\W5R9!E4q`8%-hQ..y_r?~*-ihŐ(b0Q36QE*")ìⵁUX֒Wqv;Q3dlpInU6mHD\E@)dMjRPU`}RTݐRT RG"iA6fHEbmB* uZ(UjzsQD(.\M}{lSkcnH᠁D,upt, hN M=+,|)Ә@mW& (ؕ[ n_ž0ܳ<_Z [s]5J1YUQՐܟ,n(ve|Iͭ7 TKP)KZAWNg ϾmFaU0ԥhz@1W%² ckB{VIvϣ1xm4ϜA ~Pvakbw@ =[l]?Co SK鎇@ؽ|1T $ 8c܂/ " s#UtVn 9!F*ˆ)1 k2j?r >z/NTNba>s{9_4e45`: If$Ft*k`}Bo_oN=u``8u>,GB_}VHspqxSuOxI1)Vү( CkZ.+ hMե~?^0=fAp>Oԋҳm۰)7>s^wA ؼWK񤁭ݟ}<sߵ\pu5Rxb ]٠7H)YG`G+i=.jjwq茂XJrz 3yo#x6)+VafIՌ 5ïA"OzNa`46M0+9c#M1E-O{rPˎ1{ j.T|٠1dF9~{(Pp Is5tƤv1sÃ'`k8Hr4 i[4n{A7;$V\cC wu-z󋻭S@iem l -Hۊ-ݐ}CuocU&*_h{l˧~#Wjr*nPiTPp?T[Nzv-tT]ӴX^0,LǏyS2"-gJWU\ ,|a(8˛W"|GzP6*N[Η2 )+MD #e]&l o(^6b}^ZftAP zfpb@1Q5Uܹ^~GF\]3jCl'E&>su/*=Am}OS'ܬQ͘Q:A%\#ls0􊤈Ghַ =/K]ǜʲϭ`0::x ̩N1E&q VjSW7%ݷQ ޹ch4Vb RD(a `m0giۄB|!Ofup|1Q6uu`;类904fe5φgmDY:xJIO(\2V^^»F=&mr*bp8jv}1\P+P/NL\P$8xPdতm#^_٭M3ϸRuoNU^"0,!^$A1uve!>>wfڼR!7տګx(vJ(@Ϻ]`*dܐB%i˜TIݯC1%#tKœHG1:y꛷GK<6W%}GB?\>*5@K~ 9߀tN~<?i4ͭTݕz/cUtIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/hdf_compass/compass_model/model.py0000666000000000000000000003665214544243330017713 0ustar00############################################################################## # Copyright by The HDF Group. # # All rights reserved. # # # # This file is part of the HDF Compass Viewer. The full HDF Compass # # copyright notice, including terms governing use, modification, and # # terms governing use, modification, and redistribution, is contained in # # the file COPYING, which can be found at the root of the source code # # distribution tree. If you do not have access to this file, you may # # request a copy from help@hdfgroup.org. # ############################################################################## """ The following class definitions represent the pluggable data model. By subclassing various things in this module, and implementing the missing (virtual) methods and properties, you can have the GUI infrastructure display your own objects without writing a single line of wxPython code. GUI support exists for generic kinds of objects including browsable containers, array-like datasets, and images. More importantly, you can add support for entirely new file formats. To implement support for a new kind of "datastore" (HDF5, HDF4, NetCDF, etc.), start by subclassing Store and overriding the appropriate methods and properties. Then, subclass the various Node classes this module provides (Container, Array, Image, etc.) and implement the various virtual bits of those classes as well: class FooStore(compass_model.Store): ... class FooGroup(compass_model.Container): ... class FooDataset(compass_model.Array): ... class FooPicture(compass_model.Image): ... You're not required to do all of them (and of course not all formats provide something like an Image), but the more you implement the more capable the interface will be. So that your Store class knows which kinds of nodes are available to open, manually register your Node subclasses: FooStore.push(FooGroup) FooStore.push(FooDataset) FooStore.push(FooPicture) Finally, let the rest of the world know about your new data store support by calling this module's register() function: compass_model.push(FooStore) You can also extend other peoples' stores. Suppose there's a module for reading HDF5 files, with a store class called foohdf5.HDF5Store, but the author didn't support the HDF5 Image standard. Just write a subclass of Image, and register it with the other person's class: class MyHDF5Image(Image): ... foohdf5.HDF5Store.push(MyHDF5Image) Of course, this assumes you know enough about the internals of the other person's Store to make your new class useful. """ from abc import ABCMeta, abstractmethod, abstractproperty import os import logging logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) _stores = [] def push(store): """ Register a new data store class """ _stores.insert(0, store) def get_stores(): """ Get a list containing known data store classes """ return _stores[:] icon_folder = os.path.abspath(os.path.join(os.path.dirname(__file__), 'icons')) class Store(object, metaclass=ABCMeta): """ Represents a data store (i.e. a file or remote resource). """ # ------------------------------------------------------------------------- # Plugin support __nodeclasses = None @staticmethod def plugin_name(): """ Short name for the plugin. """ raise NotImplementedError @staticmethod def plugin_description(): """ Plugin description. Return useful info about the plugin as the main functionalities, the author, the support email (if any) """ raise NotImplementedError @classmethod def push(cls, nodeclass): """ Register a Node subclass. When a key is being opened, each subclass is queried in turn. The first one which reports it can handle the key is used. """ if cls.__nodeclasses is None: cls.__nodeclasses = [Unknown] cls.__nodeclasses.insert(0, nodeclass) @abstractmethod def __contains__(self, key): """ Check if a key is valid. """ logger.error("to be overloaded") def __getitem__(self, key): """ Return a Node instance for *key*. Figures out the appropriate Node subclass for the object identified by "key", creates an instance and returns it. """ return self.gethandlers(key)[0](self, key) def gethandlers(self, key=None): """ Rather than picking a handler and returning the Node, return a list of all handlers which can do something useful with *key*. If *key* is not specified, returns all handlers """ if self.__nodeclasses is None: self.__nodeclasses = [Unknown] if key is None: return self.__nodeclasses if key not in self: raise KeyError(key) return [nc for nc in self.__nodeclasses if nc.can_handle(self, key)] # End plugin support # ------------------------------------------------------------------------- # For plugins which support local files, this is a dictionary mapping # file kinds to lists of extensions, e.g. {'HDF5 File': ['*.hdf5', '*.h5']} file_extensions = {} @property @abstractmethod def url(self): """ Identifies the file or Web resource (string). Examples might be "file:///path/to/foo.hdf5" or "http://www.example.com/web/resource" """ raise NotImplementedError @property @abstractmethod def display_name(self): """ Short name for display purposes. For example, for a file-based store you could implement this with os.path.basename(self.path). """ raise NotImplementedError @property @abstractmethod def root(self): """ The root node. Serves as the entry point into the resource. Every Store must implement this, and is required to return a Container instance. """ raise NotImplementedError @staticmethod # Python 2.x does not have abstractstatic def can_handle(url): """ Test if this class can open the resource. Returns True or False. Note this may have side effects, but the resource must not be modified. """ raise NotImplementedError @property @abstractmethod def valid(self): """ True if the store is open and ready for use, False otherwise. """ raise NotImplementedError @abstractmethod def __init__(self, url): """ Open the resource. """ pass def close(self): """ Discontinue access to the resource. Any further use of this object, or retrieved nodes, is undefined. """ pass def get_parent(self, key): """ Return the parent node of the object identified by *key*. If an object has no parent, or contains itself, this should return None. That way, the "up" arrow on the Container view will be grayed out. """ pass class Node(object, metaclass=ABCMeta): """ Base class for all objects which live in a data store. You generally shouldn't inherit from Node directly, but from one of the more useful Node subclasses in this file. Direct Node subclasses can't do anything interesting in the GUI; all they do is show up in the browser. """ # Class attribute containing a dict for icon support. # Keys should be paths to icon files. # Example: icons = {16: png_16, 32: png_32} icons = NotImplemented # A short string (2 or 3 words) describing what the class represents. # This will show up in e.g. the "Open As" context menu. # Example: "HDF5 Image" or "Swath" class_kind = NotImplemented @staticmethod def can_handle(store, key): """ Determine whether this class can usefully represent the object. Keep in mind that keys are not technically required to be strings. """ raise NotImplementedError @abstractmethod def __init__(self, store, key): """ Create an instance of this class. Subclasses must not modify the signature. """ pass @property def key(self): """ Unique key which identifies this object in the store. Keys may be any hashable object, although strings are the most common. """ raise NotImplementedError @property def store(self): """ The data store to which the object belongs. """ raise NotImplementedError @property def display_name(self): """ A short name for display purposes """ raise NotImplementedError @property def display_title(self): """ A longer name appropriate for display in a window title. Defaults to *display_name*. """ return self.display_name @property def description(self): """ Descriptive string (possibly multi-line) """ raise NotImplementedError def preview(self, w, h): """ [Optional] PNG image preview """ return None class Container(Node, metaclass=ABCMeta): """ Represents an object which holds other objects (like an HDF5 group). Subclasses will be displayed using the browser view. """ icons = {16: os.path.join(icon_folder, "folder_16.png"), 64: os.path.join(icon_folder, "folder_64.png")} def __len__(self): """ Number of child objects """ raise NotImplementedError def __iter__(self): """ Iterator over child objects. Should yield Nodes, not keys (use your Store.open method). """ raise NotImplementedError def __getitem__(self, idx): """ Open an item by index (necessary to support ListCtrl). Should return a Node, not a key (use your Store.open method). """ raise NotImplementedError class KeyValue(Node, metaclass=ABCMeta): """ Represents an object which contains a sequence of key: value attributes. Keys must be strings. Subclasses will be displayed using a list-like control. """ icons = {16: os.path.join(icon_folder, "kv_16.png"), 64: os.path.join(icon_folder, "kv_64.png")} @property def keys(self): """ Return a list of attribute keys. """ raise NotImplementedError def __getitem__(self, name): """ Return the raw attribute value """ raise NotImplementedError class Array(Node, metaclass=ABCMeta): """ Represents a NumPy-style regular, rectangular array. Subclasses will be displayed in a spreadsheet-style viewer. """ icons = {16: os.path.join(icon_folder, "array_16.png"), 64: os.path.join(icon_folder, "array_64.png")} @property def shape(self): """ Shape tuple """ raise NotImplementedError @property def dtype(self): """ Data type """ raise NotImplementedError def __getitem__(self, args): """ Retrieve data elements """ raise NotImplementedError def is_plottable(self): """ To be overriden in case that there are cases in which the array is not plottable """ return True class GeoArray(Node, metaclass=ABCMeta): """ Represents a NumPy-style regular, rectangular array with a known geographic extent. """ icons = {16: os.path.join(icon_folder, "array_16.png"), 64: os.path.join(icon_folder, "array_64.png")} @property def shape(self): """ Shape tuple """ raise NotImplementedError @property def dtype(self): """ Data type """ raise NotImplementedError @property def extent(self): """ Geographic extent as a tuple: (x_min, x_max, y_min, y_max) """ raise NotImplementedError def __getitem__(self, args): """ Retrieve data elements """ raise NotImplementedError def is_plottable(self): """ To be overriden in case that there are cases in which the array is not plottable """ return True class GeoSurface(Node, metaclass=ABCMeta): """ Represents a NumPy-style regular, rectangular surface with a known geographic extent. """ icons = {16: os.path.join(icon_folder, "array_16.png"), 64: os.path.join(icon_folder, "array_64.png")} @property def shape(self): """ Shape tuple """ raise NotImplementedError @property def dtype(self): """ Data type """ raise NotImplementedError def __getitem__(self, args): """ Retrieve data elements """ raise NotImplementedError def is_plottable(self): """ To be overriden in case that there are cases in which the array is not plottable """ return True class Image(Node, metaclass=ABCMeta): """ A single raster image. """ icons = {16: os.path.join(icon_folder, "image_16.png"), 64: os.path.join(icon_folder, "image_64.png")} @property def width(self): """ Image width in pixels """ pass @property def height(self): """ Image height in pixels """ pass @property def palette(self): """ Palette array, or None. """ raise NotImplementedError @property def data(self): """ Image data """ class Text(Node, metaclass=ABCMeta): """ A text. """ icons = {16: os.path.join(icon_folder, "text_16.png"), 64: os.path.join(icon_folder, "text_64.png")} @property def text(self): """ Text data """ class Xml(Text, metaclass=ABCMeta): """ A XML text. """ icons = {16: os.path.join(icon_folder, "xml_16.png"), 64: os.path.join(icon_folder, "xml_64.png")} def has_validation(self): """To be overriden in case that the xml has a known mechanism to be validated""" return False @property def validation(self): """ Validation info """ class Unknown(Node): """ "Last resort" node (and the only concrete class in this module). These can always be created, but do nothing useful. """ icons = {16: os.path.join(icon_folder, "unknown_16.png"), 64: os.path.join(icon_folder, "unknown_64.png")} class_kind = "Unknown" @staticmethod def can_handle(store, key): return True def __init__(self, store, key): self._key = key self._store = store @property def key(self): return self._key @property def store(self): return self._store @property def display_name(self): import os.path try: return os.path.basename(str(self.key)) except Exception: return "Unknown" @property def description(self): return "Unknown object" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/hdf_compass/compass_model/test.py0000666000000000000000000001647514544243330017573 0ustar00############################################################################## # Copyright by The HDF Group. # # All rights reserved. # # # # This file is part of the HDF Compass Viewer. The full HDF Compass # # copyright notice, including terms governing use, modification, and # # terms governing use, modification, and redistribution, is contained in # # the file COPYING, which can be found at the root of the source code # # distribution tree. If you do not have access to this file, you may # # request a copy from help@hdfgroup.org. # ############################################################################## """ Provides support for testing basic properties of data model implementations. Checks to make sure all required properties and methods are implemented, and does basic sanity/consistency checking. More specific tests must be written by plugin authors. The public interface consists of the following functions, each of which provides a TestCase subclass which may be used with unittest: - store - container - array [not implemented] - key-value [not implemented] - image [not implemented] Example, in my_model.test: from hdf_compass.compass_model.test import store, container from hdf_compass.my_model import MyStore, MyContainer URL = "file:///path/to/myfile.ext" store_tests = store(MyStore, URL) container_tests = container(MyStore, URL, MyContainer, "some-key") To run unittest, which discovers classes "store_tests" and "container_tests": $ python -m unittest hdf_compass.my_model.test """ import unittest as ut from hdf_compass.compass_model import Node, Store # --- Public API -------------------------------------------------------------- def store(store_cls_, url_): """ Construct a TestCase appropriate for a Store subclass. store_cls_: Your compass_model.Store implementation. url_: A URL representing a valid data-store to test against. """ class TestStore(_TestStore): store_cls = store_cls_ url = url_ return TestStore def container(store_cls_, url_, node_cls_, key_): """ Construct a TestCase class appropriate for a Container subclass. store_cls_: Your compass_model.Store implementation. url_: A URL representing a valid data-store to test against. node_cls_: Your compass_model.Container implementation. key_: A valid key which points to a container. """ class TestContainer(_TestContainer): store_cls = store_cls_ url = url_ node_cls = node_cls_ key = key_ return TestContainer # --- End public API ---------------------------------------------------------- class _TestStore(ut.TestCase): """ Base class for testing Stores. """ store_cls = None url = None def setUp(self): self.store = self.store_cls(self.url) def tearDown(self): if self.store.valid: self.store.close() def test_class(self): """ Verify the thing we get from store_cls is actually a Store """ self.assertIsInstance(self.store, Store) def test_url(self): """ Verify store.url produces a string """ self.assertIsInstance(self.store.url, str) def test_display_name(self): """ Verify store.display_name produces a string. """ self.assertIsInstance(self.store.display_name, str) def test_root(self): """ Verify store.root exists and has no parent """ self.assertIsInstance(self.store.root, Node) self.assertIs(self.store.get_parent(self.store.root.key), None) def test_close_valid(self): """ Verify that store.close() works and is reflected by store.valid """ self.assertTrue(self.store.valid) self.store.close() self.assertFalse(self.store.valid) def test_can_handle(self): """ Verify can_handle() works properly """ self.assertTrue(self.store_cls.can_handle(self.url)) self.assertFalse(self.store_cls.can_handle("file:///no/such/path")) def test_handlers(self): """ The implementation has at least one Node handler registered """ h = self.store.gethandlers() # Test for N > 1 because compass_model.Unknown is always present self.assertGreater(len(h), 1) class _TestNode(ut.TestCase): """ Base class for testing Node objects. """ store_cls = None node_cls = None url = None key = None def setUp(self): self.store = self.store_cls(self.url) self.node = self.node_cls(self.store, self.key) def tearDown(self): if self.store.valid: self.store.close() def test_contains(self): """ Consistency check for store __contains___ """ self.assertTrue(self.key in self.store) self.assertFalse("key_not_in_store" in self.store) def test_icons(self): """ Icon dict is present and contains valid icon paths: keys: int values: icon paths Required sizes: 16x16 and 64x64 """ import os for key, val in self.node_cls.icons.items(): self.assertIsInstance(key, int) self.assertIsInstance(val, str) self.assertTrue(os.path.exists(val)) # required resolutions self.assertIn(16, self.node_cls.icons) self.assertIn(64, self.node_cls.icons) def test_class_kind(self): """ class_kind is present, and a string """ self.assertIsInstance(self.node_cls.class_kind, str) def test_can_handle(self): """ can_handle() consistency check """ self.assertTrue(self.node_cls.can_handle(self.store, self.key)) self.assertFalse(self.node_cls.can_handle(self.store, "/some/random/key")) def test_key(self): """ Node.key returns a hashable object. We also require that the key be unchanged. """ out = self.node.key hash(out) self.assertEqual(self.node.key, self.key) def test_store(self): """ Node.store returns a data store of the same class and URL. """ self.assertIsInstance(self.node.store, self.store_cls) self.assertEqual(self.node.store.url, self.store.url) def test_display_name(self): """ display_name exists and is a string """ self.assertIsInstance(self.node.display_name, str) class _TestContainer(_TestNode): """ Class for testing compass_model.Container implementations. """ def test_len(self): """ Object length is consistent with iter() result """ self.assertGreaterEqual(len(self.node), 0) self.assertEqual(len(self.node), len(list(self.node))) def test_getitem(self): """ __getitem__ works properly """ for idx in range(len(self.node)): out = self.node[idx] self.assertIsInstance(out, Node) def test_getitem_exception(self): """ out-of range indices raise IndexError """ with self.assertRaises(IndexError): self.node[len(self.node)] ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1714825026.889992 hdf_compass-0.7b15/hdf_compass/compass_viewer/0000777000000000000000000000000014615423503016427 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/hdf_compass/compass_viewer/__init__.py0000666000000000000000000000205514544243330020541 0ustar00############################################################################## # Copyright by The HDF Group. # # All rights reserved. # # # # This file is part of the HDF Compass Viewer. The full HDF Compass # # copyright notice, including terms governing use, modification, and # # terms governing use, modification, and redistribution, is contained in # # the file COPYING, which can be found at the root of the source code # # distribution tree. If you do not have access to this file, you may # # request a copy from help@hdfgroup.org. # ############################################################################## from hdf_compass.compass_viewer.viewer import run, can_open_store, open_store, CompassApp import logging logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/hdf_compass/compass_viewer/__main__.py0000666000000000000000000000326314544243330020524 0ustar00############################################################################## # Copyright by The HDF Group. # # All rights reserved. # # # # This file is part of the HDF Compass Viewer. The full HDF Compass # # copyright notice, including terms governing use, modification, and # # terms governing use, modification, and redistribution, is contained in # # the file COPYING, which can be found at the root of the source code # # distribution tree. If you do not have access to this file, you may # # request a copy from help@hdfgroup.org. # ############################################################################## import logging class LoggingFilter(logging.Filter): """ An example of logging filter that disables the logging from a specific module """ def filter(self, record): # print(record.name) if record.name.startswith('hdf_compass.compass_viewer.info'): return False return True # logging settings logger = logging.getLogger() logger.setLevel(logging.NOTSET) ch = logging.StreamHandler() ch.setLevel(logging.DEBUG) # change to WARNING to minimize verbosity, DEBUG for high verbosity ch_formatter = logging.Formatter('%(levelname)-7s %(name)s.%(funcName)s:%(lineno)d > %(message)s') ch.setFormatter(ch_formatter) # ch.addFilter(LoggingFilter()) # uncomment to activate the logging filter logger.addHandler(ch) from hdf_compass.compass_viewer.viewer import run run() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1714825026.8959925 hdf_compass-0.7b15/hdf_compass/compass_viewer/array/0000777000000000000000000000000014615423503017545 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/hdf_compass/compass_viewer/array/__init__.py0000666000000000000000000000202114544243330021650 0ustar00############################################################################## # Copyright by The HDF Group. # # All rights reserved. # # # # This file is part of the HDF Compass Viewer. The full HDF Compass # # copyright notice, including terms governing use, modification, and # # terms governing use, modification, and redistribution, is contained in # # the file COPYING, which can be found at the root of the source code # # distribution tree. If you do not have access to this file, you may # # request a copy from help@hdfgroup.org. # ############################################################################## from hdf_compass.compass_viewer.array.frame import ArrayFrame import logging logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler())././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/hdf_compass/compass_viewer/array/frame.py0000666000000000000000000006241514544243330021220 0ustar00############################################################################## # Copyright by The HDF Group. # # All rights reserved. # # # # This file is part of the HDF Compass Viewer. The full HDF Compass # # copyright notice, including terms governing use, modification, and # # terms governing use, modification, and redistribution, is contained in # # the file COPYING, which can be found at the root of the source code # # distribution tree. If you do not have access to this file, you may # # request a copy from help@hdfgroup.org. # ############################################################################## """ Implements a viewer frame for compass_model.Array. """ import wx import wx.grid from wx.lib.newevent import NewCommandEvent import numpy as np import os import logging import numpy logger = logging.getLogger(__name__) from hdf_compass.compass_viewer.frame import NodeFrame from hdf_compass.compass_viewer.array.plot import LinePlotFrame, LineXYPlotFrame, ContourPlotFrame, HistogramPlotFrame # Indicates that the slicing selection may have changed. # These events are emitted by the SlicerPanel. ArraySlicedEvent, EVT_ARRAY_SLICED = NewCommandEvent() ArraySelectionEvent, EVT_ARRAY_SELECTED = NewCommandEvent() # Menu and button IDs ID_VIS_MENU_PLOT = wx.NewId() ID_VIS_MENU_PLOTXY = wx.NewId() ID_VIS_MENU_HIST = wx.NewId() ID_VIS_MENU_COPY = wx.NewId() ID_VIS_MENU_EXPORT = wx.NewId() def gen_csv(data, delimiters): """ converts any N-dimensional array to a CSV-string """ if(type(data) == numpy.ndarray or type(data) == list): return delimiters[0].join(map(lambda x: gen_csv(x,delimiters[1:]), data)) else: return str(data) class ArrayFrame(NodeFrame): """ Top-level frame displaying objects of type compass_model.Array. From top to bottom, has: 1. Toolbar (see ArrayFrame.init_toolbar) 2. SlicerPanel, with controls for changing what's displayed. 3. An ArrayGrid, which displays the data in a spreadsheet-like view. """ last_open_csv = os.getcwd() csv_delimiters_copy = ['\n', '\t'] csv_delimiters_export = ['\n', ','] def __init__(self, node, pos=None): """ Create a new array viewer to display the node. """ NodeFrame.__init__(self, node, size=(800, 400), title=node.display_name, pos=pos) self.node = node # Update the menu vis_menu = wx.Menu() if self.node.is_plottable(): vis_menu.Append(ID_VIS_MENU_PLOT, "Plot Data\tCtrl-D") vis_menu.Append(ID_VIS_MENU_HIST, "Histogram\tCtrl-H") vis_menu.Append(ID_VIS_MENU_PLOTXY, "Plot XY\tCtrl-T") self.add_menu(vis_menu, "Visualize") # Initialize the toolbar self.init_toolbar() # The Slicer is the panel with indexing controls self.slicer = SlicerPanel(self, node.shape, node.dtype.fields is not None) # Create the grid array self.grid = ArrayGrid(self, node, self.slicer) # Sizer for slicer and grid gridsizer = wx.BoxSizer(wx.VERTICAL) if len(node.shape) > 2 or node.dtype.fields is not None: gridsizer.Add(self.slicer, 0, wx.EXPAND) gridsizer.Add(self.grid, 1, wx.EXPAND) self.view = gridsizer self.Bind(EVT_ARRAY_SLICED, self.on_sliced) self.Bind(EVT_ARRAY_SELECTED, self.on_selected) if self.node.is_plottable(): self.Bind(wx.EVT_MENU, self.on_plot, id=ID_VIS_MENU_PLOT) self.Bind(wx.EVT_MENU, self.on_hist, id=ID_VIS_MENU_HIST) self.Bind(wx.EVT_MENU, self.on_plotxy, id=ID_VIS_MENU_PLOTXY) self.Bind(wx.EVT_MENU, self.on_copy, id=ID_VIS_MENU_COPY) self.Bind(wx.EVT_MENU, self.on_export, id=ID_VIS_MENU_EXPORT) # Workaround for wxPython bug (see SlicerPanel.enable_spinctrls) ID_WORKAROUND_TIMER = wx.NewId() self.Bind(wx.EVT_TIMER, self.on_workaround_timer, id=ID_WORKAROUND_TIMER) self.timer = wx.Timer(self, ID_WORKAROUND_TIMER) self.timer.Start(100) def init_toolbar(self): """ Set up the toolbar at the top of the window. """ t_size = (24, 24) plot_bmp = wx.Bitmap(os.path.join(self.icon_folder, "viz_plot_24.png"), wx.BITMAP_TYPE_ANY) hist_bmp = wx.Bitmap(os.path.join(self.icon_folder, "viz_hist_24.png"), wx.BITMAP_TYPE_ANY) plot_xy_bmp = wx.Bitmap(os.path.join(self.icon_folder, "viz_plot_xy_24.png"), wx.BITMAP_TYPE_ANY) copy_bmp = wx.Bitmap(os.path.join(self.icon_folder, "viz_copy_24.png"), wx.BITMAP_TYPE_ANY) export_bmp = wx.Bitmap(os.path.join(self.icon_folder, "save_24.png"), wx.BITMAP_TYPE_ANY) self.toolbar = self.CreateToolBar(wx.TB_HORIZONTAL | wx.NO_BORDER | wx.TB_FLAT | wx.TB_TEXT) self.toolbar.SetToolBitmapSize(t_size) # Rank of the underlying array rank = len(self.node.shape) if rank > 1 and self.node.dtype.fields is None: self.toolbar.AddControl(wx.StaticText(self.toolbar, wx.ID_ANY, "Row Dim:")) self.rowSpin = wx.SpinCtrl(self.toolbar, max=rank - 1, size=(55, 25), value=str(0), min=0, name="rowSpin") self.toolbar.AddControl(self.rowSpin) self.toolbar.AddControl(wx.StaticText(self.toolbar, wx.ID_ANY, "Col Dim:")) self.colSpin = wx.SpinCtrl(self.toolbar, max=rank - 1, size=(55, 25), value=str(1), min=0, name="colSpin") self.toolbar.AddControl(self.colSpin) self.Bind(wx.EVT_SPINCTRL, self.on_dimSpin) self.toolbar.AddStretchableSpace() self.toolbar.AddTool(ID_VIS_MENU_COPY, "Copy", copy_bmp) self.toolbar.AddTool(ID_VIS_MENU_EXPORT, "Export", export_bmp) if self.node.is_plottable(): self.toolbar.AddTool(ID_VIS_MENU_PLOT, "Plot Data", plot_bmp) self.toolbar.AddTool(ID_VIS_MENU_HIST, "Histogram", hist_bmp) self.toolbar.AddTool(ID_VIS_MENU_PLOTXY, "Plot XY", plot_xy_bmp, shortHelp="Plot data against first row") self.toolbar.Realize() def on_selected(self, evt): """ User has chosen to display a different part of the dataset. """ idx = 0 for x in self.indices: self.slicer.set_spin_max(idx, self.node.shape[x]-1) idx = idx + 1 self.grid.ResetView() def get_selected_data(self): """ function to get the selected data in an array returns (data, names, line) data: array of sliced data names: name array for plots line: bool-value, True if 1D-Line, False if 2D """ cols = self.grid.GetSelectedCols() rows = self.grid.GetSelectedRows() rank = len(self.node.shape) # Scalar data can't be line-plotted. if rank == 0: return None, None, True # Get data currently in the grid if rank > 1 and self.node.dtype.names is None: args = [] for x in range(rank): if x == self.row: args.append(slice(None, None, None)) elif x == self.col: args.append(slice(None, None, None)) else: idx = 0 for y in self.indices: if y == x: args.append(self.slicer.indices[idx]) break idx = idx + 1 data = self.node[tuple(args)] if self.row > self.col: data = np.transpose(data) else: data = self.node[self.slicer.indices] # Columns in the view are selected if len(cols) != 0: # The data is compound if self.node.dtype.names is not None: names = [self.grid.GetColLabelValue(x) for x in cols] data = [data[n] for n in names] return data, names, True # Plot multiple columns independently else: if rank > 1: data = [data[(slice(None, None, None),c)] for c in cols] names = ["Col %d" % c for c in cols] if len(data) > 1 else None return data, names, True # Rows in view are selected elif len(rows) != 0: data = [data[(r,)] for r in rows] names = ["Row %d" % r for r in rows] if len(data) > 1 else None return data, names, True # No row or column selection. Plot everything else: # The data is compound if self.node.dtype.names is not None: names = [self.grid.GetColLabelValue(x) for x in range(self.grid.GetNumberCols())] data = [data[n] for n in names] return data, names, True # Plot 1D elif rank == 1: return [data], [], True # Plot 2D else: return data, [], False def on_sliced(self, evt): """ User has chosen to display a different part of the dataset. """ self.grid.Refresh() def on_plot(self, evt): """ User has chosen to plot the current selection """ data, names, line = self.get_selected_data() if data is None: logger.info("unable to retrieve data") return if line: f = LinePlotFrame(data, names) f.Show() else: if isinstance(data, np.ndarray): if (data.shape[0] < 2) or (data.shape[1] < 2): logger.info("unable to contour data for shape: %s" % (data.shape, )) return f = ContourPlotFrame(data) f.Show() def on_hist(self, evt): """ User has chosen to plot the current selection """ data, names, line = self.get_selected_data() if data is None: logger.info("unable to retrieve data") return if len(data) < 1: logger.info("first select data") return if np.isnan(np.nanmin(data)): logger.info("all nan values") return logger.debug("%s; names: %s; line: %s" % (data, names, line)) f = HistogramPlotFrame(data, names) f.Show() def on_plotxy(self, evt): """ User has chosen to plot the current selection against first selected row""" data, names, line = self.get_selected_data() if data is None: logger.info("unable to retrieve data") return if len(data) == 1: logger.info("select two or more columns") return if line: f = LineXYPlotFrame(data, names) f.Show() def on_copy(self, evt): """ User has chosen to copy the current selection to the clipboard """ # Calculate number of selected cells cols = self.grid.GetSelectedCols() rows = self.grid.GetSelectedRows() if len(self.node.shape) > 1: nr = self.node.shape[self.row] nc = self.node.shape[self.col] else: nr = self.node.shape[0] nc = 1 l = len(cols) * nr if l == 0: l = len(rows) * nc if l == 0: l = nc * nr # Display warning if too much if l > 20000: dlg = wx.MessageDialog(self, "Do you really want to copy {} cells to the clipboard?\n" "This operation could take a while.".format(l), 'Copy', wx.YES_NO | wx.ICON_WARNING) result = dlg.ShowModal() if result == wx.ID_NO: return data, names, line = self.get_selected_data() string = gen_csv(data, ArrayFrame.csv_delimiters_copy) clipdata = wx.TextDataObject() clipdata.SetText(string) wx.TheClipboard.Open() wx.TheClipboard.SetData(clipdata) wx.TheClipboard.Close() def on_export(self, evt): """ User has chosen to export the current selection to a CSV-File """ wc_string = "CSV files (*.csv)|*.csv" dlg = wx.FileDialog(self, "Export", wildcard=wc_string, defaultDir=ArrayFrame.last_open_csv, style=wx.FD_SAVE|wx.FD_OVERWRITE_PROMPT) if dlg.ShowModal() != wx.ID_OK: return path = dlg.GetPath() ArrayFrame.last_open_csv = os.path.dirname(path) try: f = open(path, "w") data, names, line = self.get_selected_data() string = gen_csv(data, ArrayFrame.csv_delimiters_export) f.write(string) f.close() except: dlg = wx.MessageDialog(self, "Unable to write file %s" % path, "Error", wx.OK | wx.ICON_WARNING) dlg.ShowModal() dlg.Destroy() def on_workaround_timer(self, evt): """ See slicer.enable_spinctrls docs """ self.timer.Destroy() self.slicer.enable_spinctrls() @property def indices(self): """ A tuple of integer indices appropriate for dim selection. """ l = [] for x in range(len(self.node.shape)): if x == self.row or x == self.col: continue l.append(x) return tuple(l) @property def row(self): """ The dimension selected for the row """ return self.rowSpin.GetValue() @property def col(self): """ The dimension selected for the column """ return self.colSpin.GetValue() def on_dimSpin(self, evt): """ Dimmension Spinbox value changed; notify parent to refresh the grid. """ pos = evt.GetPosition() otherSpinner = self.rowSpin if evt.GetEventObject() == self.rowSpin : otherSpinner = self.colSpin if pos == otherSpinner.GetValue(): if (pos > 0) : pos = pos - 1 else: pos = pos + 1 otherSpinner.SetValue(pos) wx.PostEvent(self, ArraySelectionEvent(self.GetId())) class SlicerPanel(wx.Panel): """ Holds controls for data access. Consult the "indices" property, which returns a tuple of indices that prefix the array. This will be RANK-2 elements long, unless hasfields is true, in which case it will be RANK-1 elements long. """ @property def indices(self): """ A tuple of integer indices appropriate for slicing. Will be RANK-2 elements long, RANK-1 if compound data is in use (hasfields == True). """ return tuple([x.GetValue() for x in self.spincontrols]) def __init__(self, parent, shape, hasfields): """ Create a new slicer panel. parent: The wxPython parent window shape: Shape of the data to visualize hasfields: If True, the data is compound and the grid can only display one axis. So, we should display an extra spinbox. """ wx.Panel.__init__(self, parent) self.parent = parent self.shape = shape self.hasfields = hasfields self.spincontrols = [] # Rank of the underlying array rank = len(shape) # Rank displayable in the grid. If fields are present, they occupy # the columns, so the data displayed is actually 1-D. visible_rank = 1 if hasfields else 2 sizer = wx.BoxSizer(wx.HORIZONTAL) # Will arrange the SpinCtrls if rank > visible_rank: infotext = wx.StaticText(self, wx.ID_ANY, "Array Indexing: ") sizer.Add(infotext, 0, flag=wx.EXPAND | wx.ALL, border=10) for idx in range(rank - visible_rank): maxVal = shape[idx] - 1 if not hasfields: maxVal = shape[self.parent.indices[idx]] - 1 sc = wx.SpinCtrl(self, max=maxVal, value="0", min=0) sizer.Add(sc, 0, flag=wx.EXPAND | wx.ALL, border=10) sc.Disable() self.spincontrols.append(sc) self.SetSizer(sizer) self.Bind(wx.EVT_SPINCTRL, self.on_spin) def enable_spinctrls(self): """ Unlock the spin controls. Because of a bug in wxPython on Mac, by default the first spin control has bizarre contents (and control focus) when the panel starts up. Call this after a short delay (e.g. 100 ms) to enable indexing. """ for sc in self.spincontrols: sc.Enable() def set_spin_max(self, idx, max): self.spincontrols[idx].SetRange(0, max) def on_spin(self, evt): """ Spinbox value changed; notify parent to refresh the grid. """ wx.PostEvent(self, ArraySlicedEvent(self.GetId())) class ArrayGrid(wx.grid.Grid): """ Grid class to display the Array. Cell contents and appearance are handled by the table model in ArrayTable. """ def __init__(self, parent, node, slicer): wx.grid.Grid.__init__(self, parent) table = ArrayTable(parent) self.SetTable(table, True) # Column selection is always allowed selmode = 2 # wx.grid.Grid.SelectColumns # Row selection is forbidden for compound types, and for # scalar/1-D datasets if node.dtype.names is None and len(node.shape) > 1: selmode |= 1 # wx.grid.Grid.SelectRows self.SetSelectionMode(selmode) def ResetView(self): """Trim/extend the grid if needed""" rowChange = self.GetTable().GetRowsCount() - self.GetNumberRows() colChange = self.GetTable().GetColsCount() - self.GetNumberCols() if rowChange != 0 or colChange != 0: self.ClearGrid() locker = wx.grid.GridUpdateLocker(self) if rowChange > 0: msg = wx.grid.GridTableMessage( self.GetTable(), wx.grid.GRIDTABLE_NOTIFY_ROWS_APPENDED, rowChange ) self.ProcessTableMessage(msg) elif rowChange < 0: msg = wx.grid.GridTableMessage( self.GetTable(), wx.grid.GRIDTABLE_NOTIFY_ROWS_DELETED, 0, -rowChange ) self.ProcessTableMessage(msg) if colChange > 0: msg = wx.grid.GridTableMessage( self.GetTable(), wx.grid.GRIDTABLE_NOTIFY_COLS_APPENDED, colChange ) self.ProcessTableMessage(msg) elif colChange < 0: msg = wx.grid.GridTableMessage( self.GetTable(), wx.grid.GRIDTABLE_NOTIFY_COLS_DELETED, 0, -colChange ) self.ProcessTableMessage(msg) # The scroll bars aren't resized (at least on windows) # Jiggling the size of the window rescales the scrollbars # h,w = self.GetSize() # self.SetSize((h+1, w)) # self.SetSize((h, w)) self.ForceRefresh() class LRUTileCache(object): """ Simple tile-based LRU cache which goes between the Grid and the Array object. Caches tiles along the last 1 or 2 dimensions of a dataset. Access is via __getitem__. Because this class exists specifically to support point-based callbacks for the Grid, arguments may only be indices, not slices. """ TILESIZE = 100 # Tiles will have shape (100,) or (100, 100) MAXTILES = 50 # Max number of tiles to retain in the cache def __init__(self, arr): """ *arr* is anything implementing compass_model.Array """ import collections self.cache = collections.OrderedDict() self.arr = arr def __getitem__(self, args): """ Restricted to an index or tuple of indices. """ if not isinstance(args, tuple): args = (args,) # Split off the last 1 or 2 dimensions coarse_position, fine_position = args[0:-2], args[-2:] def clip(x): """ Round down to nearest TILESIZE; takes e.g. 181 -> 100 """ return (x // self.TILESIZE) * self.TILESIZE # Tuple with index of tile corner tile_key = coarse_position + tuple(clip(x) for x in fine_position) # Slice which will be applied to dataset to retrieve tile tile_slice = coarse_position + tuple(slice(clip(x), clip(x) + self.TILESIZE) for x in fine_position) # Index applied to tile to retrieve the desired data point tile_data_index = tuple(x % self.TILESIZE for x in fine_position) # Case 1: Add tile to cache, ejecting oldest tile if needed if not tile_key in self.cache: if len(self.cache) >= self.MAXTILES: self.cache.popitem(last=False) tile = self.arr[tile_slice] self.cache[tile_key] = tile # Case 2: Mark the tile as recently accessed else: tile = self.cache.pop(tile_key) self.cache[tile_key] = tile return tile[tile_data_index] class ArrayTable(wx.grid.GridTableBase): """ "Table" class which provides data and metadata for the grid to display. The methods defined here define the contents of the table, as well as the number of rows, columns and their values. """ def __init__(self, parent): """ Create a new Table instance for use with a grid control. node: An compass_model.Array implementation instance. slicer: An instance of SlicerPanel, so we can see what indices the user has requested. """ wx.grid.GridTableBase.__init__(self) self.node = parent.node self.selecter = parent self.slicer = parent.slicer self.rank = len(self.node.shape) self.names = self.node.dtype.names self.cache = LRUTileCache(self.node) def GetNumberRows(self): """ Callback for number of rows displayed by the grid control """ if self.rank == 0: return 1 elif self.rank == 1: return self.node.shape[0] elif self.names is not None: return self.node.shape[-1] return self.node.shape[self.selecter.row] def GetNumberCols(self): """ Callback for number of columns displayed by the grid control. Note that if compound data is in use, columns display the field names. """ if self.names is not None: return len(self.names) if self.rank < 2: return 1 return self.node.shape[self.selecter.col] def GetValue(self, row, col): """ Callback which provides data to the Grid. row, col: Integers giving row and column position (0-based). """ # Scalar case if self.rank == 0: data = self.node[()] if self.names is None: return "%s" % data return "%s" % data[col] # 1D case if self.rank == 1: data = self.cache[row] if self.names is None: return "%s" % data return "%s" % data[self.names[col]] # ND case. Watch out for compound mode! if self.names is not None: args = self.slicer.indices + (row,) else: l = [] for x in range(self.rank): if x == self.selecter.row: l.append(row) elif x == self.selecter.col: l.append(col) else: idx = 0 for y in self.selecter.indices: if y == x: l.append(self.slicer.indices[idx]) break idx = idx + 1 args = tuple(l) data = self.cache[args] if self.names is None: return "%s" % data return "%s" % data[self.names[col]] def GetRowLabelValue(self, row): """ Callback for row labels. Row number is used unless the data is scalar. """ if self.rank == 0: return "Value" return str(row) def GetColLabelValue(self, col): """ Callback for column labels. Column number is used, except for scalar or 1D data, or if we're displaying field names in the columns. """ if self.names is not None: return self.names[col] if self.rank == 0 or self.rank == 1: return "Value" return str(col) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1714753362.0 hdf_compass-0.7b15/hdf_compass/compass_viewer/array/plot.py0000666000000000000000000003041414615207522021100 0ustar00############################################################################## # Copyright by The HDF Group. # # All rights reserved. # # # # This file is part of the HDF Compass Viewer. The full HDF Compass # # copyright notice, including terms governing use, modification, and # # terms governing use, modification, and redistribution, is contained in # # the file COPYING, which can be found at the root of the source code # # distribution tree. If you do not have access to this file, you may # # request a copy from help@hdfgroup.org. # ############################################################################## """ Matplotlib window with toolbar. """ from math import ceil import numpy as np import wx import matplotlib.pyplot as plt from matplotlib.figure import Figure from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigCanvas from matplotlib.backends.backend_wx import NavigationToolbar2Wx as NavigationToolbar import logging logger = logging.getLogger(__name__) from hdf_compass.compass_viewer.frame import BaseFrame ID_VIEW_INCREASE_BINS = wx.NewId() ID_VIEW_DECREASE_BINS = wx.NewId() ID_VIEW_INCREASE_OPACITY = wx.NewId() ID_VIEW_DECREASE_OPACITY = wx.NewId() ID_VIEW_CMAP_JET = wx.NewId() # default ID_VIEW_CMAP_BONE = wx.NewId() ID_VIEW_CMAP_GIST_EARTH = wx.NewId() ID_VIEW_CMAP_OCEAN = wx.NewId() ID_VIEW_CMAP_RAINBOW = wx.NewId() ID_VIEW_CMAP_RDYLGN = wx.NewId() ID_VIEW_CMAP_WINTER = wx.NewId() class PlotFrame(BaseFrame): """ Base class for Matplotlib plot windows. Override draw_figure() to plot your figure on the provided axes. """ def __init__(self, data, title="a title"): """ Create a new Matplotlib plotting window for a 1D line plot """ logger.debug(self.__class__.__name__) BaseFrame.__init__(self, id=wx.ID_ANY, title=title, size=(800, 400)) self.data = data self.panel = wx.Panel(self) self.dpi = 100 self.fig = Figure((6.0, 4.0), dpi=self.dpi) self.canvas = FigCanvas(self.panel, -1, self.fig) self.axes = self.fig.add_subplot(111) self.axes.set_xlabel('') self.axes.set_ylabel('') self.toolbar = NavigationToolbar(self.canvas) self.vbox = wx.BoxSizer(wx.VERTICAL) self.vbox.Add(self.canvas, 1, wx.LEFT | wx.TOP | wx.GROW) self.vbox.Add(self.toolbar, 0, wx.EXPAND) self.panel.SetSizer(self.vbox) self.vbox.Fit(self) self.draw_figure() def draw_figure(self): raise NotImplementedError class LinePlotFrame(PlotFrame): def __init__(self, data, names=None, title="Line Plot"): self.names = names PlotFrame.__init__(self, data, title) def draw_figure(self): lines = [self.axes.plot(d)[0] for d in self.data] if self.names is not None: for n in self.names: self.axes.legend(tuple(lines), tuple(self.names)) class HistogramPlotFrame(PlotFrame): def __init__(self, data, names=None, title="Line Plot"): self.names = names self.bins = [] self.opacity = 1.0 PlotFrame.__init__(self, data, title) self.hist_menu = wx.Menu() self.hist_menu.Append(ID_VIEW_INCREASE_BINS, "Increase bins x10\tCtrl++") self.hist_menu.Append(ID_VIEW_DECREASE_BINS, "Decrease bins x10\tCtrl+-") self.hist_menu.Append(ID_VIEW_INCREASE_OPACITY, "Increase opacity\tCtrl+0") self.hist_menu.Append(ID_VIEW_DECREASE_OPACITY, "Decrease opacity\tCtrl+9") self.add_menu(self.hist_menu, "Bins") self.Bind(wx.EVT_MENU, self.on_increase_bins, id=ID_VIEW_INCREASE_BINS) self.Bind(wx.EVT_MENU, self.on_decrease_bins, id=ID_VIEW_DECREASE_BINS) self.Bind(wx.EVT_MENU, self.on_increase_opacity, id=ID_VIEW_INCREASE_OPACITY) self.Bind(wx.EVT_MENU, self.on_decrease_opacity, id=ID_VIEW_DECREASE_OPACITY) def draw_figure(self): with_labels = True if self.names is None: with_labels = False else: if len(self.names) == 0: with_labels = False for _i, d in enumerate(self.data): # color = matplotlib.colors.to_rgb( # self.axes._get_patches_for_fill.prop_cycler) self.bins.append(ceil(np.sqrt(d.size))) if with_labels: _, bins, _ = self.axes.hist(d, bins=ceil(self.bins[_i]), label=self.names[_i], alpha=self.opacity) else: _, bins, _ = self.axes.hist(d, bins=ceil(self.bins[_i]), alpha=self.opacity) if with_labels: self.axes.legend() def on_increase_bins(self, evt): logger.debug("increasing bins") self.axes.clear() for _i, d in enumerate(self.data): self.bins[_i] *= 10.0 if self.names is not None: self.axes.hist(d, bins=ceil(self.bins[_i]), label=self.names[_i], alpha=self.opacity) else: self.axes.hist(d, bins=ceil(self.bins[_i]), alpha=self.opacity) if self.names is not None: self.axes.legend() self._refresh_plot() def on_decrease_bins(self, evt): logger.debug("decreasing bins") self.axes.clear() for _i, d in enumerate(self.data): self.bins[_i] /= 10.0 if self.names is not None: self.axes.hist(d, bins=ceil(self.bins[_i]), label=self.names[_i], alpha=self.opacity) else: self.axes.hist(d, bins=ceil(self.bins[_i]), alpha=self.opacity) if self.names is not None: self.axes.legend() self._refresh_plot() def on_decrease_opacity(self, evt): if self.opacity >= 0.2: logger.debug("decreasing opacity") self.opacity -= 0.1 self.axes.clear() else: logger.debug("opacity is at minimum") return 0 for _i, d in enumerate(self.data): if self.names is not None: self.axes.hist(d, bins=ceil(self.bins[_i]), label=self.names[_i], alpha=self.opacity) else: self.axes.hist(d, bins=ceil(self.bins[_i]), alpha=self.opacity) if self.names is not None: self.axes.legend() self._refresh_plot() def on_increase_opacity(self, evt): if self.opacity <= 0.9: logger.debug("increasing opacity") self.opacity += 0.1 self.axes.clear() else: logger.debug("opacity is at maximum") return 0 for _i, d in enumerate(self.data): if self.names is not None: self.axes.hist(d, bins=ceil(self.bins[_i]), label=self.names[_i], alpha=self.opacity) else: self.axes.hist(d, bins=ceil(self.bins[_i]), alpha=self.opacity) if self.names is not None: self.axes.legend() self._refresh_plot() def _refresh_plot(self): self.axes.relim() # make sure all the data fits self.axes.autoscale() # auto-scale self.canvas.draw() class LineXYPlotFrame(PlotFrame): def __init__(self, data, names=None, title="Line XY Plot"): self.names = names PlotFrame.__init__(self, data, title) def draw_figure(self): self.axes.set_xlabel(self.names[0]) if len(self.data)==2: # a simple X-Y plot using 2 columns self.axes.set_ylabel(self.names[1]) lines = [self.axes.plot(self.data[0], d)[0] for d in self.data[1::]] if self.names is not None: for n in self.names: self.axes.legend(tuple(lines), tuple(self.names[1::])) class ContourPlotFrame(PlotFrame): def __init__(self, data, names=None, title="Contour Plot"): # need to be set before calling the parent (need for plotting) self.colormap = "jet" self.cb = None # matplotlib color-bar PlotFrame.__init__(self, data, title) self.cmap_menu = wx.Menu() self.cmap_menu.Append(ID_VIEW_CMAP_JET, "Jet", kind=wx.ITEM_RADIO) self.cmap_menu.Append(ID_VIEW_CMAP_BONE, "Bone", kind=wx.ITEM_RADIO) self.cmap_menu.Append(ID_VIEW_CMAP_GIST_EARTH, "Gist Earth", kind=wx.ITEM_RADIO) self.cmap_menu.Append(ID_VIEW_CMAP_OCEAN, "Ocean", kind=wx.ITEM_RADIO) self.cmap_menu.Append(ID_VIEW_CMAP_RAINBOW, "Rainbow", kind=wx.ITEM_RADIO) self.cmap_menu.Append(ID_VIEW_CMAP_RDYLGN, "Red-Yellow-Green", kind=wx.ITEM_RADIO) self.cmap_menu.Append(ID_VIEW_CMAP_WINTER, "Winter", kind=wx.ITEM_RADIO) self.add_menu(self.cmap_menu, "Colormap") self.Bind(wx.EVT_MENU, self.on_cmap_jet, id=ID_VIEW_CMAP_JET) self.Bind(wx.EVT_MENU, self.on_cmap_bone, id=ID_VIEW_CMAP_BONE) self.Bind(wx.EVT_MENU, self.on_cmap_gist_earth, id=ID_VIEW_CMAP_GIST_EARTH) self.Bind(wx.EVT_MENU, self.on_cmap_ocean, id=ID_VIEW_CMAP_OCEAN) self.Bind(wx.EVT_MENU, self.on_cmap_rainbow, id=ID_VIEW_CMAP_RAINBOW) self.Bind(wx.EVT_MENU, self.on_cmap_rdylgn, id=ID_VIEW_CMAP_RDYLGN) self.Bind(wx.EVT_MENU, self.on_cmap_winter, id=ID_VIEW_CMAP_WINTER) self.status_bar = wx.StatusBar(self, -1) self.status_bar.SetFieldsCount(2) self.SetStatusBar(self.status_bar) self.canvas.mpl_connect('motion_notify_event', self.update_status_bar) self.canvas.Bind(wx.EVT_ENTER_WINDOW, self.change_cursor) def on_cmap_jet(self, evt): logger.debug("cmap: jet") self.colormap = "jet" self._refresh_plot() def on_cmap_bone(self, evt): logger.debug("cmap: bone") self.colormap = "bone" self._refresh_plot() def on_cmap_gist_earth(self, evt): logger.debug("cmap: gist_earth") self.colormap = "gist_earth" self._refresh_plot() def on_cmap_ocean(self, evt): logger.debug("cmap: ocean") self.colormap = "ocean" self._refresh_plot() def on_cmap_rainbow(self, evt): logger.debug("cmap: rainbow") self.colormap = "rainbow" self._refresh_plot() def on_cmap_rdylgn(self, evt): logger.debug("cmap: RdYlGn") self.colormap = "RdYlGn" self._refresh_plot() def on_cmap_winter(self, evt): logger.debug("cmap: winter") self.colormap = "winter" self._refresh_plot() def _refresh_plot(self): self.draw_figure() self.canvas.draw() def draw_figure(self): max_elements = 500 # don't attempt plot more than 500x500 elements rows = self.data.shape[0] cols = self.data.shape[1] row_stride = rows // max_elements + 1 col_stride = cols // max_elements + 1 data = self.data[::row_stride, ::col_stride] xx = np.arange(0, self.data.shape[1], col_stride) yy = np.arange(0, self.data.shape[0], row_stride) img = self.axes.contourf(xx, yy, data, 25, cmap=plt.cm.get_cmap(self.colormap)) self.axes.set_aspect('equal') if self.cb: self.cb.update_normal(img) else: self.cb = plt.colorbar(img, ax=self.axes) self.cb.ax.tick_params(labelsize=8) def change_cursor(self, event): self.canvas.SetCursor(wx.StockCursor(wx.CURSOR_CROSS)) def update_status_bar(self, event): msg = str() if event.inaxes: x, y = int(event.xdata), int(event.ydata) z = self.data[y, x] msg = "x= %d, y= %d, z= %f" % (x, y, z) self.status_bar.SetStatusText(msg, 1) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1714825026.9019928 hdf_compass-0.7b15/hdf_compass/compass_viewer/container/0000777000000000000000000000000014615423503020411 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/hdf_compass/compass_viewer/container/__init__.py0000666000000000000000000000203114544243330022515 0ustar00############################################################################## # Copyright by The HDF Group. # # All rights reserved. # # # # This file is part of the HDF Compass Viewer. The full HDF Compass # # copyright notice, including terms governing use, modification, and # # terms governing use, modification, and redistribution, is contained in # # the file COPYING, which can be found at the root of the source code # # distribution tree. If you do not have access to this file, you may # # request a copy from help@hdfgroup.org. # ############################################################################## from hdf_compass.compass_viewer.container.frame import ContainerFrame import logging logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler())././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/hdf_compass/compass_viewer/container/frame.py0000666000000000000000000002070614544243330022061 0ustar00############################################################################## # Copyright by The HDF Group. # # All rights reserved. # # # # This file is part of the HDF Compass Viewer. The full HDF Compass # # copyright notice, including terms governing use, modification, and # # terms governing use, modification, and redistribution, is contained in # # the file COPYING, which can be found at the root of the source code # # distribution tree. If you do not have access to this file, you may # # request a copy from help@hdfgroup.org. # ############################################################################## """ Implements a viewer for compass_model.Container instances. This frame is a simple browser view with back/forward/up controls. Currently list and icon views are supported. """ import wx import os import logging logger = logging.getLogger(__name__) from hdf_compass import compass_model from hdf_compass.compass_viewer.frame import NodeFrame from hdf_compass.compass_viewer.events import ID_COMPASS_OPEN from hdf_compass.compass_viewer.events import EVT_CONTAINER_SELECTION from hdf_compass.compass_viewer.container.list import ContainerReportList, ContainerIconList ID_GO_MENU_BACK = wx.NewId() ID_GO_MENU_NEXT = wx.NewId() ID_GO_MENU_UP = wx.NewId() ID_GO_MENU_TOP = wx.NewId() ID_VIEW_MENU_LIST = wx.NewId() ID_VIEW_MENU_ICON = wx.NewId() class ContainerFrame(NodeFrame): """ A frame to display a Container class, and browse its contents. """ def __init__(self, node, pos=None): """ Create a new frame. node: Container instance to display. pos: Screen position at which to display the window. """ NodeFrame.__init__(self, node, size=(800, 400), title=node.display_title, pos=pos) view_menu = wx.Menu() view_menu.Append(ID_VIEW_MENU_LIST, "List view") view_menu.Append(ID_VIEW_MENU_ICON, "Icon view") self.add_menu(view_menu, "View") self.view_menu = view_menu go_menu = wx.Menu() go_menu.Append(ID_GO_MENU_BACK, "Back") go_menu.Append(ID_GO_MENU_NEXT, "Next") go_menu.Append(ID_GO_MENU_UP, "Up") go_menu.Append(ID_GO_MENU_TOP, "Top") self.add_menu(go_menu, "Go") self.go_menu = go_menu self.acc_tbl = wx.AcceleratorTable([ (wx.ACCEL_CMD, wx.WXK_LEFT, ID_GO_MENU_BACK), (wx.ACCEL_CMD, wx.WXK_RIGHT, ID_GO_MENU_NEXT), (wx.ACCEL_CMD, wx.WXK_UP, ID_GO_MENU_UP), (wx.ACCEL_CMD | wx.ACCEL_SHIFT, wx.WXK_UP, ID_GO_MENU_TOP), ]) self.SetAcceleratorTable(self.acc_tbl) self.Bind(wx.EVT_MENU, self.on_open, id=ID_COMPASS_OPEN) self.Bind(EVT_CONTAINER_SELECTION, lambda evt: self.update_info()) self.Bind(wx.EVT_MENU, lambda evt: self.go_back(), id=ID_GO_MENU_BACK) self.Bind(wx.EVT_MENU, lambda evt: self.go_next(), id=ID_GO_MENU_NEXT) self.Bind(wx.EVT_MENU, lambda evt: self.go_up(), id=ID_GO_MENU_UP) self.Bind(wx.EVT_MENU, lambda evt: self.go_top(), id=ID_GO_MENU_TOP) self.Bind(wx.EVT_MENU, lambda evt: self.list_view(), id=ID_VIEW_MENU_LIST) self.Bind(wx.EVT_MENU, lambda evt: self.icon_view(), id=ID_VIEW_MENU_ICON) self.toolbar = self.CreateToolBar(wx.TB_HORIZONTAL | wx.NO_BORDER | wx.TB_FLAT | wx.TB_TEXT) tsize = (24, 24) back_bmp = wx.Bitmap(os.path.join(self.icon_folder, "go_back_24.png"), wx.BITMAP_TYPE_ANY) next_bmp = wx.Bitmap(os.path.join(self.icon_folder, "go_next_24.png"), wx.BITMAP_TYPE_ANY) up_bmp = wx.Bitmap(os.path.join(self.icon_folder, "go_up_24.png"), wx.BITMAP_TYPE_ANY) top_bmp = wx.Bitmap(os.path.join(self.icon_folder, "go_top_24.png"), wx.BITMAP_TYPE_ANY) icon_bmp = wx.Bitmap(os.path.join(self.icon_folder, "view_icon_24.png"), wx.BITMAP_TYPE_ANY) list_bmp = wx.Bitmap(os.path.join(self.icon_folder, "view_list_24.png"), wx.BITMAP_TYPE_ANY) self.toolbar.SetToolBitmapSize(tsize) self.toolbar.AddTool(ID_GO_MENU_BACK, "back", back_bmp) self.toolbar.AddTool(ID_GO_MENU_NEXT, "next", next_bmp) self.toolbar.AddSeparator() self.toolbar.AddTool(ID_GO_MENU_UP, "up", up_bmp) self.toolbar.AddTool(ID_GO_MENU_TOP, "top", top_bmp) self.toolbar.AddStretchableSpace() self.toolbar.AddTool(ID_VIEW_MENU_LIST, "list", list_bmp) self.toolbar.AddTool(ID_VIEW_MENU_ICON, "icon", icon_bmp) self.toolbar.Realize() self.view = ContainerReportList(self, node) self.history = [node] self.history_ptr = 0 self.update_view() def list_view(self): """ Switch to list view """ if not isinstance(self.view, ContainerReportList): self.view = ContainerReportList(self, self.history[self.history_ptr]) def icon_view(self): """ Switch to icon view """ if not isinstance(self.view, ContainerIconList): self.view = ContainerIconList(self, self.history[self.history_ptr]) # --- Begin history support functions ------------------------------------- @property def node(self): return self.history[self.history_ptr] def go_back(self): """ Go to the previously displayed item """ if self.history_ptr > 0: self.history_ptr -= 1 self.update_view() def go_next(self): """ Go to the next displayed item """ if self.history_ptr < (len(self.history) - 1): self.history_ptr += 1 self.update_view() def go_up(self): """ Go to the enclosing container """ node = self.history[self.history_ptr] parent = node.store.get_parent(node.key) if parent.key != node.key: # at the root item self.go(parent) def go_top(self): """ Go to the root node """ node = self.history[self.history_ptr] parent = node.store.root if parent.key != node.key: # at the root item self.go(parent) def go(self, newnode): """ Go to a particular node. Adds the node to the history. """ del self.history[self.history_ptr + 1:] self.history.append(newnode) self.history_ptr += 1 self.update_view() # --- End history support functions --------------------------------------- def update_view(self): """ Refresh the entire contents of the frame according to self.node. """ self.SetTitle(self.node.display_title) self.view = type(self.view)(self, self.node) self.update_info() can_go_back = self.history_ptr > 0 can_go_next = self.history_ptr < (len(self.history) - 1) can_go_up = self.node.store.get_parent(self.node.key) is not None can_go_top = self.node.key != self.node.store.root.key self.go_menu.Enable(ID_GO_MENU_BACK, can_go_back) self.go_menu.Enable(ID_GO_MENU_NEXT, can_go_next) self.go_menu.Enable(ID_GO_MENU_UP, can_go_up) self.go_menu.Enable(ID_GO_MENU_TOP, can_go_top) self.toolbar.EnableTool(ID_GO_MENU_BACK, can_go_back) self.toolbar.EnableTool(ID_GO_MENU_NEXT, can_go_next) self.toolbar.EnableTool(ID_GO_MENU_UP, can_go_up) self.toolbar.EnableTool(ID_GO_MENU_TOP, can_go_top) icon_view_allowed = len(self.node) <= 1000 self.view_menu.Enable(ID_VIEW_MENU_ICON, icon_view_allowed) self.toolbar.EnableTool(ID_VIEW_MENU_ICON, icon_view_allowed) def update_info(self): """ Refresh the left-hand side information panel, according to the current selection in the view. """ node = self.view.selection if node is None: node = self.node # Nothing selected; show this node's info self.info.display(node) def on_open(self, evt): """ Trap Container open events, so we can browse instead of launching new windows. """ new_node = evt.node logger.debug("Got request to open node: %s" % new_node.key) if isinstance(new_node, compass_model.Container): self.go(new_node) else: evt.Skip() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/hdf_compass/compass_viewer/container/list.py0000666000000000000000000002036614544243330021744 0ustar00############################################################################## # Copyright by The HDF Group. # # All rights reserved. # # # # This file is part of the HDF Compass Viewer. The full HDF Compass # # copyright notice, including terms governing use, modification, and # # terms governing use, modification, and redistribution, is contained in # # the file COPYING, which can be found at the root of the source code # # distribution tree. If you do not have access to this file, you may # # request a copy from help@hdfgroup.org. # ############################################################################## """ Handles list and icon view for Container display. """ import wx import logging logger = logging.getLogger(__name__) from hdf_compass import compass_model from hdf_compass.compass_viewer.events import CompassOpenEvent from hdf_compass.compass_viewer.events import ContainerSelectionEvent ID_CONTEXT_MENU_OPEN = wx.NewId() ID_CONTEXT_MENU_OPENWINDOW = wx.NewId() class ContainerList(wx.ListCtrl): """ Base class for list and icons views, both of which use wx.ListCtrl. Defines the current selection (via .selection property) as well as handling item activation (double-click, Enter) and right-click context menu. """ def __init__(self, parent, node, **kwds): wx.ListCtrl.__init__(self, parent, **kwds) self.Bind(wx.EVT_LIST_ITEM_RIGHT_CLICK, self.on_rclick) self.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.on_activate) self.Bind(wx.EVT_MENU, self.on_context_open, id=ID_CONTEXT_MENU_OPEN) self.Bind(wx.EVT_MENU, self.on_context_openwindow, id=ID_CONTEXT_MENU_OPENWINDOW) self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.hint_select) self.Bind(wx.EVT_LIST_ITEM_DESELECTED, self.hint_select) self.Bind(wx.EVT_LIST_ITEM_FOCUSED, self.hint_select) self.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.hint_select) @property def selection(self): """ The currently selected item, or None. """ idx = self.GetFirstSelected() if idx < 0: return None return self.node[idx] def hint_select(self, evt): """ Fire off a ContainerSelectionEvent """ wx.PostEvent(self, ContainerSelectionEvent(self.GetId())) evt.Skip() def on_activate(self, evt): """ Emit a CompassOpenEvent when an item is double-clicked. """ # wxPython Mac incorrectly treats a rapid left-right click as activate ms = wx.GetMouseState() if ms.RightIsDown(): return idx = evt.GetIndex() newnode = self.node[idx] pos = wx.GetTopLevelParent(self).GetPosition() evt = CompassOpenEvent(newnode, pos=pos) wx.PostEvent(self, evt) # --------------------------------------------------------- # Context menu support def on_rclick(self, evt): """ Pop up a context menu appropriate for the item """ # Click didn't land on an item idx = evt.GetIndex() if idx < 0: return # The node which was right-clicked in the view. node = self.node[idx] self._menu_node = node # Determine a list of handlers which can understand this object. # We exclude the default handler, "Unknown", as it can't do anything. handlers = [x for x in node.store.gethandlers(node.key) if x != compass_model.Unknown] # This will map menu IDs -> Node subclass handlers self._menu_handlers = {} menu = wx.Menu() menu.Append(ID_CONTEXT_MENU_OPEN, "Open") menu.Append(ID_CONTEXT_MENU_OPENWINDOW, "Open in New Window") if len(handlers) > 0: # Populate the "Open As" submenu, and record what # menu IDs map to what Node subclasses submenu = wx.Menu() for h in handlers: id_ = wx.NewId() self._menu_handlers[id_] = h submenu.Append(id_, h.class_kind) self.Bind(wx.EVT_MENU, self.on_context_openas, id=id_) menu.AppendSubMenu(submenu, "Open As") else: # No handlers were available, so gray out the "Open" entries id1 = wx.NewId() menu.Append(id1, "Open As") menu.Enable(ID_CONTEXT_MENU_OPEN, 0) menu.Enable(ID_CONTEXT_MENU_OPENWINDOW, 0) menu.Enable(id1, 0) self.PopupMenu(menu) del self._menu_node del self._menu_handlers menu.Destroy() def on_context_open(self, evt): """ Called in response to a plain "Open" in the context menu. Posts an event requesting that the node be opened as-is. """ pos = wx.GetTopLevelParent(self).GetPosition() wx.PostEvent(self, CompassOpenEvent(self._menu_node, pos=pos)) def on_context_openwindow(self, evt): """ Called in response to a plain "Open in New Window" in the context menu. Posts an event directly to the app object. """ pos = wx.GetTopLevelParent(self).GetPosition() wx.PostEvent(wx.GetApp(), CompassOpenEvent(self._menu_node, pos=pos)) def on_context_openas(self, evt): """ Called in response to one of the "Open As" context menu items. """ # The "Open As" submenu ID id_ = evt.GetId() # Node which was right-clicked in the view node_being_opened = self._menu_node # The requested Node subclass to instantiate h = self._menu_handlers[id_] # Brand new Node instance of the requested type node_new = h(node_being_opened.store, node_being_opened.key) # Send off a request for it to be opened in the appropriate viewer pos = wx.GetTopLevelParent(self).GetPosition() wx.PostEvent(self, CompassOpenEvent(node_new, pos=pos)) # End context menu support # ------------------------------------------------------------------- class ContainerIconList(ContainerList): """ Icon view of nodes in a Container. """ def __init__(self, parent, node): """ New icon list view """ ContainerList.__init__(self, parent, node, style=wx.LC_ICON | wx.LC_AUTOARRANGE | wx.BORDER_NONE | 0x400) self.node = node self.il = wx.GetApp().imagelists[64] self.SetImageList(self.il, wx.IMAGE_LIST_NORMAL) for item in range(len(self.node)): subnode = self.node[item] image_index = self.il.get_index(type(subnode)) self.InsertImageStringItem(item, subnode.display_name, image_index) class ContainerReportList(ContainerList): """ List view of the container's contents. Uses a wxPython virtual list, allowing millions of items in a container without any slowdowns. """ def __init__(self, parent, node): """ Create a new list view. parent: wxPython parent object node: Container instance to be displayed """ ContainerList.__init__(self, parent, node, style=wx.LC_REPORT | wx.LC_VIRTUAL | wx.LC_SINGLE_SEL | wx.BORDER_NONE) self.node = node self.InsertColumn(0, "Name") self.InsertColumn(1, "Kind") self.SetColumnWidth(0, 300) self.SetColumnWidth(1, 200) self.il = wx.GetApp().imagelists[16] self.SetImageList(self.il, wx.IMAGE_LIST_SMALL) self.SetItemCount(len(node)) self.Refresh() def OnGetItemText(self, item, col): """ Callback method to support virtual list ctrl """ if col == 0: return self.node[item].display_name elif col == 1: return type(self.node[item]).class_kind return "" def OnGetItemImage(self, item): """ Callback method to support virtual list ctrl """ subnode = self.node[item] return self.il.get_index(type(subnode)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/hdf_compass/compass_viewer/events.py0000666000000000000000000000375414544243330020315 0ustar00############################################################################## # Copyright by The HDF Group. # # All rights reserved. # # # # This file is part of the HDF Compass Viewer. The full HDF Compass # # copyright notice, including terms governing use, modification, and # # terms governing use, modification, and redistribution, is contained in # # the file COPYING, which can be found at the root of the source code # # distribution tree. If you do not have access to this file, you may # # request a copy from help@hdfgroup.org. # ############################################################################## """ Defines a small number of custom events, which are useful for the GUI. """ import wx from wx.lib.newevent import NewCommandEvent import logging logger = logging.getLogger(__name__) ID_COMPASS_OPEN = wx.NewId() class CompassOpenEvent(wx.PyCommandEvent): """ Event posted when a request has been made to "open" a object in the container. The source should always be ID_COMPASS_OPEN. The type of event is EVT_MENU, because wxPython doesn't like it if we use anything else. When binding handlers, make sure to explicitly specify the source ID (or check it in the callback) to avoid catching these events by mistake. """ def __init__(self, newnode, **kwds): """ Request that *newnode* be displayed by the GUI """ wx.PyCommandEvent.__init__(self, wx.EVT_MENU.typeId, ID_COMPASS_OPEN) self.node = newnode self.kwds = kwds # Hints that the selected item in the view may have changed. # Interested parties should inspect the view to figure out the new selection. ContainerSelectionEvent, EVT_CONTAINER_SELECTION = NewCommandEvent() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/hdf_compass/compass_viewer/frame.py0000666000000000000000000004512414544243330020100 0ustar00############################################################################## # Copyright by The HDF Group. # # All rights reserved. # # # # This file is part of the HDF Compass Viewer. The full HDF Compass # # copyright notice, including terms governing use, modification, and # # terms governing use, modification, and redistribution, is contained in # # the file COPYING, which can be found at the root of the source code # # distribution tree. If you do not have access to this file, you may # # request a copy from help@hdfgroup.org. # ############################################################################## """ Defines wx.Frame subclasses which are the foundation of the various windows displayed by HDFCompass. Much of the common functionality (e.g. "Open File..." menu item) is implemented here. """ import logging import os from datetime import date import wx import wx.richtext as rtc from pubsub import pub logger = logging.getLogger(__name__) from hdf_compass.compass_viewer.info import InfoPanel ID_OPEN_RESOURCE = wx.NewId() ID_CLOSE_FILE = wx.NewId() ID_PLUGIN_INFO = wx.NewId() MAX_RECENT_FILES = 8 from hdf_compass import compass_model from hdf_compass.utils import __version__, is_darwin, path2url from .events import CompassOpenEvent open_frames = 0 # count the open frames class BaseFrame(wx.Frame): """ Base class for all frames used in HDF Compass. Implements common menus including File and Help, and handles their events. When implementing a new viewer window, you should instead inherit from BaseFrame (below), which adds a left-hand side information panel, and participates in the reference counting that automatically shows the initial window when all other frames are closed. """ icon_folder = os.path.abspath(os.path.join(os.path.dirname(__file__), 'icons')) open_frames = 0 # count the number of frames last_open_path = os.getcwd() def __init__(self, **kwds): """ Constructor; any keywords are passed on to wx.Frame. """ wx.Frame.__init__(self, None, **kwds) BaseFrame.open_frames += 1 logger.debug("new frame -> open frames: %s" % BaseFrame.open_frames) # Frame icon ib = wx.IconBundle() icon_32 = wx.Icon() icon_32.CopyFromBitmap(wx.Bitmap(os.path.join(self.icon_folder, "favicon_32.png"), wx.BITMAP_TYPE_ANY)) ib.AddIcon(icon_32) icon_48 = wx.Icon() icon_48.CopyFromBitmap(wx.Bitmap(os.path.join(self.icon_folder, "favicon_48.png"), wx.BITMAP_TYPE_ANY)) ib.AddIcon(icon_48) self.SetIcons(ib) # This is needed to display the app icon on the taskbar on Windows 7 if os.name == 'nt': import ctypes ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID('HDFCompass') self.urlhistory = wx.FileHistory(MAX_RECENT_FILES) self.config = wx.Config("HDFCompass") self.urlhistory.Load(self.config) menubar = wx.MenuBar() # File menu fm = wx.Menu() # Open Recent Menu recent = wx.Menu() self.urlhistory.UseMenu(recent) self.urlhistory.AddFilesToMenu() fm.Append(wx.ID_OPEN, "&Open...\tCtrl-O") fm.Append(ID_OPEN_RESOURCE, "Open &Resource...\tCtrl-R") fm.Append(wx.ID_ANY, "O&pen Recent", recent) fm.AppendSeparator() fm.Append(wx.ID_CLOSE, "&Close Window\tCtrl-W") fm.Append(ID_CLOSE_FILE, "Close &File\tShift-Ctrl-W") fm.Enable(ID_CLOSE_FILE, False) fm.Append(wx.ID_EXIT, "E&xit", " Terminate the program") menubar.Append(fm, "&File") # Help menu; note that on the Mac, the About entry is automatically # moved to the main application menu by wxPython. help_menu = wx.Menu() help_menu.Append(wx.ID_HELP, "Online &Manual", "Open online documentation") help_menu.Append(ID_PLUGIN_INFO, "&Plugin Info", "Information about the available plugins") help_menu.Append(wx.ID_ABOUT, "&About HDFCompass", "Information about this program") menubar.Append(help_menu, "&Help") self.SetMenuBar(menubar) self.Bind(wx.EVT_MENU, self.on_file_open, id=wx.ID_OPEN) self.Bind(wx.EVT_MENU, self.on_resource_open, id=ID_OPEN_RESOURCE) self.Bind(wx.EVT_MENU, self.on_manual, id=wx.ID_HELP) self.Bind(wx.EVT_MENU, self.on_plugin_info, id=ID_PLUGIN_INFO) self.Bind(wx.EVT_MENU, self.on_about, id=wx.ID_ABOUT) self.Bind(wx.EVT_MENU, self.on_exit, id=wx.ID_EXIT) self.Bind(wx.EVT_MENU, self.on_close, id=wx.ID_CLOSE) self.Bind(wx.EVT_CLOSE, self.on_close) self.Bind(wx.EVT_MENU_RANGE, self.on_url_history, id=wx.ID_FILE1, id2=wx.ID_FILE9) def on_close(self, evt): """ Called on frame closing """ BaseFrame.open_frames -= 1 logger.debug("close frame -> open frames: %s" % BaseFrame.open_frames) self.Destroy() if isinstance(self, InitFrame): self.on_exit(evt) def on_exit(self, evt): """ Called on "exit" event from the menu """ logger.debug("exit app -> closing all open frames: %s" % BaseFrame.open_frames) wx.Exit() def on_manual(self, evt): """ Open the url with the online documentation """ import webbrowser webbrowser.open('http://hdf-compass.readthedocs.org/en/stable/') def on_plugin_info(self, evt): """ Open a tabs frame with info about the available plugins """ plug_info = PluginInfoFrame(self) plug_info.Show() def on_about(self, evt): """ Display an "About" dialog """ from wx import adv info = adv.AboutDialogInfo() info.Name = "HDF Compass" info.Version = __version__ info.Copyright = "(c) 2014-%d The HDF Group" % date.today().year icon_48 = wx.Icon() icon_48.CopyFromBitmap(wx.Bitmap(os.path.join(self.icon_folder, "favicon_48.png"), wx.BITMAP_TYPE_ANY)) info.SetIcon(icon_48) adv.AboutBox(info) def on_file_open(self, evt): """ Request to open a file via the Open entry in the File menu """ def make_filter_string(): """ Make a wxPython dialog filter string segment from dict """ filter_string = [] hdf_filter_string = [] # put HDF filters in the front for store in compass_model.get_stores(): if len(store.file_extensions) == 0: continue for key in store.file_extensions: s = "{name} ({pattern_c})|{pattern_sc}".format( name=key, pattern_c=",".join(store.file_extensions[key]), pattern_sc=";".join(store.file_extensions[key])) if s.startswith("HDF"): hdf_filter_string.append(s) else: filter_string.append(s) filter_string = hdf_filter_string + filter_string filter_string.append('All Files (*.*)|*.*') pipe = "|" return pipe.join(filter_string) wc_string = make_filter_string() dlg = wx.FileDialog(self, "Open Local File", wildcard=wc_string, defaultDir=BaseFrame.last_open_path, style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST) if dlg.ShowModal() != wx.ID_OK: return path = dlg.GetPath() BaseFrame.last_open_path = os.path.dirname(path) url = path2url(path) self.open_url(url) def on_url_history(self, evt): """ Opens url from history """ fileNum = evt.GetId() - wx.ID_FILE1 url = self.urlhistory.GetHistoryFile(fileNum) self.open_url(url, fileNum) def on_resource_open(self, evt): """ Request to open a URL via the File menu """ dlg = wx.TextEntryDialog(self, 'Enter resource URL:') if dlg.ShowModal() != wx.ID_OK or dlg.GetValue() == "": dlg.Destroy() return url = dlg.GetValue() url = url.strip() # remove any new lines dlg.Destroy() self.open_url(url) def open_url(self, url, file_num=-1): """ Opens url and saves it to history """ from . import can_open_store, open_store if can_open_store(url): self.urlhistory.AddFileToHistory(url) # add url to top of list self.urlhistory.Save(self.config) self.config.Flush() open_store(url) else: if (file_num >= 0) and (file_num < MAX_RECENT_FILES): self.urlhistory.RemoveFileFromHistory(file_num) self.urlhistory.Save(self.config) self.config.Flush() dlg = wx.MessageDialog(self, 'The following url could not be opened:\n\n%s' % url, 'No handler for url', wx.OK | wx.ICON_INFORMATION) dlg.ShowModal() dlg.Destroy() def add_menu(self, menu, title): """ Add a menu at the appropriate place in the menubar """ mb = self.GetMenuBar() mb.Insert(1, menu, title) class InitFrame(BaseFrame): """ Frame displayed when the application starts up. This includes the menu bar provided by TopFrame. On the Mac, although it still exists (to prevent the application from exiting), the frame is typically not shown. """ def __init__(self): style = wx.DEFAULT_FRAME_STYLE & (~wx.RESIZE_BORDER) & (~wx.MAXIMIZE_BOX) title = "HDF Compass" super(InitFrame, self).__init__(size=(552, 247), title=title, style=style) data = wx.Bitmap(os.path.join(self.icon_folder, "logo.png"), wx.BITMAP_TYPE_ANY) bmp = wx.StaticBitmap(self, wx.ID_ANY, data) # The init frame isn't visible on Mac, so there shouldn't be an # option to close it. "Quit" does the same thing. if is_darwin: mb = self.GetMenuBar() mu = mb.GetMenu(0) mu.Enable(wx.ID_CLOSE, False) self.Center() class NodeFrame(BaseFrame): """ Base class for any frame which displays a Node instance. Provides a "Close file" menu item and manages open data stores. Has three attributes of note: .node: Settable Node instance to display .info: Read-only InfoPanel instance (left-hand sidebar) .view: Settable wx.Panel instance for the right-hand view. In order to coordinate file-close events across multiple frames, a reference-counting system is used. When a new frame that uses a store is created, that store's reference count (in cls._stores) is incremented. When the frame is closed, the store's count is decremented. When the reference count reaches 0 or the "Close File" is selected from the menu, the store is closed and a pubsub notification is sent out to all other frames. They check to see if their .node.store's are valid, and if not, close themselves. """ # --- Store reference-counting methods ------------------------------------ _stores = {} @classmethod def _incref(cls, store): """ Record that a client is using the specified store. """ try: cls._stores[store] += 1 except KeyError: cls._stores[store] = 1 @classmethod def _decref(cls, store): """ Record that a client is finished using the specified store. """ try: val = cls._stores[store] if val == 1: cls._close(store) del cls._stores[store] else: cls._stores[store] = val - 1 except KeyError: pass @classmethod def _close(cls, store): """ Manually close the store, and broadcast a pubsub notification. """ cls._stores.pop(store, None) store.close() pub.sendMessage('store.close') # --- End store reference-counting ---------------------------------------- @property def info(self): """ The InfoPanel object used for the left-hand sidebar. """ return self.__info @property def node(self): """ Node instance displayed by the frame. """ return self.__node @node.setter def node(self, newnode): self.__node = newnode @property def view(self): """ Right-hand view """ return self.__view @view.setter def view(self, window): if self.__view is None: self.__sizer.Add(window, 1, wx.EXPAND) else: self.__sizer.Replace(self.__view, window) self.__view.Destroy() self.__view = window self.Layout() def __init__(self, node, **kwds): """ Constructor. Keywords are passed on to wx.Frame. node: The compass_model.Node instance to display. """ super(NodeFrame, self).__init__(**kwds) # Enable the "Close File" menu entry fm = self.GetMenuBar().GetMenu(0) fm.Enable(ID_CLOSE_FILE, True) # Create the "window" menu to hold "Reopen As" items. wm = wx.Menu() # Determine a list of handlers which can understand this object. # We exclude the default handler, "Unknown", as it can't do anything. # See also container/list.py. handlers = [x for x in node.store.gethandlers(node.key) if x != compass_model.Unknown] # This will map menu IDs -> Node subclass handlers self._menu_handlers = {} # Note there's guaranteed to be at least one entry: the class # being used for the current frame! for h in handlers: id_ = wx.NewId() self._menu_handlers[id_] = h wm.Append(id_, "Reopen as " + h.class_kind) self.Bind(wx.EVT_MENU, self.on_menu_reopen, id=id_) self.GetMenuBar().Insert(1, wm, "&Window") self.__node = node self.__view = None self.__info = InfoPanel(self) self.__sizer = wx.BoxSizer(wx.HORIZONTAL) self.__sizer.Add(self.__info, 0, wx.EXPAND) self.SetSizer(self.__sizer) self.info.display(node) self.Bind(wx.EVT_CLOSE, self.on_close_evt) self.Bind(wx.EVT_MENU, self.on_menu_closefile, id=ID_CLOSE_FILE) self._incref(node.store) pub.subscribe(self.on_notification_closefile, 'store.close') def on_notification_closefile(self): """ Pubsub notification that a file (any file) has been closed """ # if not self.node.store.valid: # self.Destroy() pass def on_close_evt(self, evt): """ Window is about to be closed """ self._decref(self.node.store) evt.Skip() def on_menu_closefile(self, evt): """ "Close File" menu item activated. Note we rely on the pubsub message (above) to actually close the frame. """ self._close(self.node.store) def on_menu_reopen(self, evt): """ Called when one of the "Reopen As" menu items is clicked """ # The "Reopen As" submenu ID id_ = evt.GetId() # Present node node_being_opened = self.node # The requested Node subclass to instantiate. h = self._menu_handlers[id_] logger.debug('opening: %s %s' % (node_being_opened.store, node_being_opened.key)) # Brand new Node instance of the requested type node_new = h(node_being_opened.store, node_being_opened.key) # Send off a request for it to be opened in the appropriate viewer # Post it directly to the App, or Container will intercept it! pos = wx.GetTopLevelParent(self).GetPosition() wx.PostEvent(wx.GetApp(), CompassOpenEvent(node_new, pos=pos)) class PluginInfoFrame(wx.Frame): icon_folder = os.path.abspath(os.path.join(os.path.dirname(__file__), 'icons')) def __init__(self, parent): # make that the plugin info is displayed in the middle of the screen frame_w = 320 frame_h = 250 x = wx.SystemSettings.GetMetric(wx.SYS_SCREEN_X) // 2 - frame_w // 2 y = wx.SystemSettings.GetMetric(wx.SYS_SCREEN_Y) // 2 - frame_h // 2 wx.Frame.__init__(self, parent, title="Plugin Info", pos=(x, y), size=(frame_w, frame_h)) # Frame icon ib = wx.IconBundle() icon_32 = wx.Icon() icon_32.CopyFromBitmap(wx.Bitmap(os.path.join(self.icon_folder, "favicon_32.png"), wx.BITMAP_TYPE_ANY)) ib.AddIcon(icon_32) icon_48 = wx.Icon() icon_48.CopyFromBitmap(wx.Bitmap(os.path.join(self.icon_folder, "favicon_48.png"), wx.BITMAP_TYPE_ANY)) ib.AddIcon(icon_48) self.SetIcons(ib) p = wx.Panel(self) nb = wx.Notebook(p) for store in compass_model.get_stores(): try: # log.debug(store.plugin_name()) # log.debug(store.plugin_description()) pnl = wx.Panel(nb) t = rtc.RichTextCtrl(pnl, -1, style=wx.TE_READONLY) t.BeginFontSize(9) t.BeginAlignment(wx.TEXT_ALIGNMENT_CENTRE) t.BeginBold() t.WriteText("Name: ") t.EndBold() t.BeginItalic() t.WriteText(store.plugin_name()) t.EndItalic() t.Newline() t.Newline() t.BeginBold() t.WriteText("Description") t.EndBold() t.Newline() t.BeginItalic() t.WriteText(store.plugin_description()) t.EndItalic() t.Newline() # store.plugin_description(), style=wx.TE_MULTILINE|wx.TE_READONLY|wx.TE_CENTER) szr = wx.BoxSizer() szr.Add(t, 1, wx.ALL | wx.EXPAND, 5) pnl.SetSizer(szr) nb.AddPage(pnl, store.plugin_name()) except NotImplementedError: # skip not implemented plugin name/description logger.debug("Not implemented name/description for %s" % store) sizer = wx.BoxSizer() sizer.Add(nb, 1, wx.ALL | wx.EXPAND, 3) p.SetSizer(sizer) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1714825026.9079914 hdf_compass-0.7b15/hdf_compass/compass_viewer/geo_array/0000777000000000000000000000000014615423503020377 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/hdf_compass/compass_viewer/geo_array/__init__.py0000666000000000000000000000227014544243330022510 0ustar00############################################################################## # Copyright by The HDF Group. # # All rights reserved. # # # # This file is part of the HDF Compass Viewer. The full HDF Compass # # copyright notice, including terms governing use, modification, and # # terms governing use, modification, and redistribution, is contained in # # the file COPYING, which can be found at the root of the source code # # distribution tree. If you do not have access to this file, you may # # request a copy from help@hdfgroup.org. # # # # author: gmasetti@ccom.unh.edu # ############################################################################## from hdf_compass.compass_viewer.geo_array.frame import GeoArrayFrame import logging logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/hdf_compass/compass_viewer/geo_array/frame.py0000666000000000000000000003447714544243330022061 0ustar00############################################################################## # Copyright by The HDF Group. # # All rights reserved. # # # # This file is part of the HDF Compass Viewer. The full HDF Compass # # copyright notice, including terms governing use, modification, and # # terms governing use, modification, and redistribution, is contained in # # the file COPYING, which can be found at the root of the source code # # distribution tree. If you do not have access to this file, you may # # request a copy from help@hdfgroup.org. # # # # author: gmasetti@ccom.unh.edu # ############################################################################## """ Implements a viewer frame for compass_model.Array. """ import wx import wx.grid from wx.lib.newevent import NewCommandEvent import os import logging logger = logging.getLogger(__name__) from hdf_compass.compass_viewer.frame import NodeFrame from hdf_compass.compass_viewer.geo_array.plot import LinePlotFrame, ContourPlotFrame # Indicates that the slicing selection may have changed. # These events are emitted by the SlicerPanel. ArraySlicedEvent, EVT_ARRAY_SLICED = NewCommandEvent() # Menu and button IDs ID_VIS_MENU_PLOT = wx.NewId() class GeoArrayFrame(NodeFrame): """ Top-level frame displaying objects of type compass_model.Array. From top to bottom, has: 1. Toolbar (see ArrayFrame.init_toolbar) 2. SlicerPanel, with controls for changing what's displayed. 3. An ArrayGrid, which displays the data in a spreadsheet-like view. """ def __init__(self, node, pos=None): """ Create a new array viewer to display the node. """ NodeFrame.__init__(self, node, size=(800, 400), title=node.display_name, pos=pos) self.node = node # Update the menu vis_menu = wx.Menu() if self.node.is_plottable(): vis_menu.Append(ID_VIS_MENU_PLOT, "Map Data\tCtrl-D") self.add_menu(vis_menu, "Visualize") # Initialize the toolbar self.init_toolbar() # The Slicer is the panel with indexing controls self.slicer = SlicerPanel(self, node.shape, node.dtype.fields is not None) # Create the grid array self.grid = ArrayGrid(self, node, self.slicer) # Sizer for slicer and grid gridsizer = wx.BoxSizer(wx.VERTICAL) gridsizer.Add(self.slicer, 0, wx.EXPAND) gridsizer.Add(self.grid, 1, wx.EXPAND) self.view = gridsizer self.Bind(EVT_ARRAY_SLICED, self.on_sliced) if self.node.is_plottable(): self.Bind(wx.EVT_MENU, self.on_plot, id=ID_VIS_MENU_PLOT) # Workaround for wxPython bug (see SlicerPanel.enable_spinctrls) ID_WORKAROUND_TIMER = wx.NewId() self.Bind(wx.EVT_TIMER, self.on_workaround_timer, id=ID_WORKAROUND_TIMER) self.timer = wx.Timer(self, ID_WORKAROUND_TIMER) self.timer.Start(100) def init_toolbar(self): """ Set up the toolbar at the top of the window. """ t_size = (24, 24) plot_bmp = wx.Bitmap(os.path.join(self.icon_folder, "viz_plot_24.png"), wx.BITMAP_TYPE_ANY) self.toolbar = self.CreateToolBar(wx.TB_HORIZONTAL | wx.NO_BORDER | wx.TB_FLAT | wx.TB_TEXT) self.toolbar.SetToolBitmapSize(t_size) self.toolbar.AddStretchableSpace() if self.node.is_plottable(): self.toolbar.AddTool(ID_VIS_MENU_PLOT, "Map Data", plot_bmp) self.toolbar.Realize() def on_sliced(self, evt): """ User has chosen to display a different part of the dataset. """ self.grid.Refresh() def on_plot(self, evt): """ User has chosen to plot the current selection """ cols = self.grid.GetSelectedCols() rows = self.grid.GetSelectedRows() # Scalar data can't be line-plotted. if len(self.node.shape) == 0: return # Columns in the view are selected if len(cols) != 0: # The data is compound if self.node.dtype.names is not None: names = [self.grid.GetColLabelValue(x) for x in cols] data = self.node[self.slicer.indices] # -> 1D compound array data = [data[n] for n in names] f = LinePlotFrame(data, names) f.Show() # Plot multiple columns independently else: if len(self.node.shape) == 1: data = [self.node[self.slicer.indices]] else: data = [self.node[self.slicer.indices + (c,)] for c in cols] names = ["Col %d" % c for c in cols] if len(data) > 1 else None f = LinePlotFrame(data, names) f.Show() # Rows in view are selected elif len(rows) != 0: data = [self.node[self.slicer.indices + (slice(None, None, None), r)] for r in rows] names = ["Row %d" % r for r in rows] if len(data) > 1 else None f = LinePlotFrame(data, names) f.Show() # No row or column selection. Plot everything else: data = self.node[self.slicer.indices] # The data is compound if self.node.dtype.names is not None: names = [self.grid.GetColLabelValue(x) for x in range(self.grid.GetNumberCols())] data = [data[n] for n in names] f = LinePlotFrame(data, names) f.Show() # Plot 1D elif len(self.node.shape) == 1: f = LinePlotFrame([data]) f.Show() # Plot 2D else: f = ContourPlotFrame(data, extent=self.node.extent) f.Show() def on_workaround_timer(self, evt): """ See slicer.enable_spinctrls docs """ self.timer.Destroy() self.slicer.enable_spinctrls() class SlicerPanel(wx.Panel): """ Holds controls for data access. Consult the "indices" property, which returns a tuple of indices that prefix the array. This will be RANK-2 elements long, unless hasfields is true, in which case it will be RANK-1 elements long. """ @property def indices(self): """ A tuple of integer indices appropriate for slicing. Will be RANK-2 elements long, RANK-1 if compound data is in use (hasfields == True). """ return tuple([x.GetValue() for x in self.spincontrols]) def __init__(self, parent, shape, hasfields): """ Create a new slicer panel. parent: The wxPython parent window shape: Shape of the data to visualize hasfields: If True, the data is compound and the grid can only display one axis. So, we should display an extra spinbox. """ wx.Panel.__init__(self, parent) self.shape = shape self.hasfields = hasfields self.spincontrols = [] # Rank of the underlying array rank = len(shape) # Rank displayable in the grid. If fields are present, they occupy # the columns, so the data displayed is actually 1-D. visible_rank = 1 if hasfields else 2 sizer = wx.BoxSizer(wx.HORIZONTAL) # Will arrange the SpinCtrls if rank > visible_rank: infotext = wx.StaticText(self, wx.ID_ANY, "Array Indexing: ") sizer.Add(infotext, 0, flag=wx.EXPAND | wx.ALL, border=10) for idx in range(rank - visible_rank): sc = wx.SpinCtrl(self, max=shape[idx] - 1, value="0", min=0) sizer.Add(sc, 0, flag=wx.EXPAND | wx.ALL, border=10) sc.Disable() self.spincontrols.append(sc) self.SetSizer(sizer) self.Bind(wx.EVT_SPINCTRL, self.on_spin) def enable_spinctrls(self): """ Unlock the spin controls. Because of a bug in wxPython on Mac, by default the first spin control has bizarre contents (and control focus) when the panel starts up. Call this after a short delay (e.g. 100 ms) to enable indexing. """ for sc in self.spincontrols: sc.Enable() def on_spin(self, evt): """ Spinbox value changed; notify parent to refresh the grid. """ wx.PostEvent(self, ArraySlicedEvent(self.GetId())) class ArrayGrid(wx.grid.Grid): """ Grid class to display the Array. Cell contents and appearance are handled by the table model in ArrayTable. """ def __init__(self, parent, node, slicer): wx.grid.Grid.__init__(self, parent) table = ArrayTable(node, slicer) self.SetTable(table, True) # Column selection is always allowed selmode = 2 # wx.grid.Grid.SelectColumns # Row selection is forbidden for compound types, and for # scalar/1-D datasets if node.dtype.names is None and len(node.shape) > 1: selmode |= 1 # wx.grid.Grid.SelectRows self.SetSelectionMode(selmode) class LRUTileCache(object): """ Simple tile-based LRU cache which goes between the Grid and the Array object. Caches tiles along the last 1 or 2 dimensions of a dataset. Access is via __getitem__. Because this class exists specifically to support point-based callbacks for the Grid, arguments may only be indices, not slices. """ TILESIZE = 100 # Tiles will have shape (100,) or (100, 100) MAXTILES = 50 # Max number of tiles to retain in the cache def __init__(self, arr): """ *arr* is anything implementing compass_model.Array """ import collections self.cache = collections.OrderedDict() self.arr = arr def __getitem__(self, args): """ Restricted to an index or tuple of indices. """ if not isinstance(args, tuple): args = (args,) # Split off the last 1 or 2 dimensions coarse_position, fine_position = args[0:-2], args[-2:] def clip(x): """ Round down to nearest TILESIZE; takes e.g. 181 -> 100 """ return (x // self.TILESIZE) * self.TILESIZE # Tuple with index of tile corner tile_key = coarse_position + tuple(clip(x) for x in fine_position) # Slice which will be applied to dataset to retrieve tile tile_slice = coarse_position + tuple(slice(clip(x), clip(x) + self.TILESIZE) for x in fine_position) # Index applied to tile to retrieve the desired data point tile_data_index = tuple(x % self.TILESIZE for x in fine_position) # Case 1: Add tile to cache, ejecting oldest tile if needed if not tile_key in self.cache: if len(self.cache) >= self.MAXTILES: self.cache.popitem(last=False) tile = self.arr[tile_slice] self.cache[tile_key] = tile # Case 2: Mark the tile as recently accessed else: tile = self.cache.pop(tile_key) self.cache[tile_key] = tile return tile[tile_data_index] class ArrayTable(wx.grid.GridTableBase): """ "Table" class which provides data and metadata for the grid to display. The methods defined here define the contents of the table, as well as the number of rows, columns and their values. """ def __init__(self, node, slicer): """ Create a new Table instance for use with a grid control. node: An compass_model.Array implementation instance. slicer: An instance of SlicerPanel, so we can see what indices the user has requested. """ wx.grid.GridTableBase.__init__(self) self.node = node self.slicer = slicer self.rank = len(node.shape) self.names = node.dtype.names self.cache = LRUTileCache(self.node) def GetNumberRows(self): """ Callback for number of rows displayed by the grid control """ if self.rank == 0: return 1 return self.node.shape[-1] def GetNumberCols(self): """ Callback for number of columns displayed by the grid control. Note that if compound data is in use, columns display the field names. """ if self.names is not None: return len(self.names) if self.rank < 2: return 1 return self.node.shape[-2] def GetValue(self, row, col): """ Callback which provides data to the Grid. row, col: Integers giving row and column position (0-based). """ # Scalar case if self.rank == 0: data = self.node[()] if self.names is None: return "%s" % data return "%s" % data[col] # 1D case if self.rank == 1: data = self.cache[row] if self.names is None: return "%s" % data return "%s" % data[self.names[col]] # ND case. Watch out for compound mode! if self.names is None: args = self.slicer.indices + (col, row) else: args = self.slicer.indices + (row,) data = self.cache[args] if self.names is None: return "%s" % data return "%s" % data[self.names[col]] def GetRowLabelValue(self, row): """ Callback for row labels. Row number is used unless the data is scalar. """ if self.rank == 0: return "Value" return str(row) def GetColLabelValue(self, col): """ Callback for column labels. Column number is used, except for scalar or 1D data, or if we're displaying field names in the columns. """ if self.names is not None: return self.names[col] if self.rank == 0 or self.rank == 1: return "Value" return str(col) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1714753362.0 hdf_compass-0.7b15/hdf_compass/compass_viewer/geo_array/plot.py0000666000000000000000000002223414615207522021733 0ustar00############################################################################## # Copyright by The HDF Group. # # All rights reserved. # # # # This file is part of the HDF Compass Viewer. The full HDF Compass # # copyright notice, including terms governing use, modification, and # # terms governing use, modification, and redistribution, is contained in # # the file COPYING, which can be found at the root of the source code # # distribution tree. If you do not have access to this file, you may # # request a copy from help@hdfgroup.org. # # # # author: gmasetti@ccom.unh.edu # ############################################################################## """ Matplotlib window with toolbar. """ import numpy as np import wx try: # for GeoArray we use cartopy that can be challenging to freeze on OSX to dependencies (i.e. geos) import cartopy.crs as ccrs from cartopy.mpl.gridliner import LONGITUDE_FORMATTER, LATITUDE_FORMATTER except (ImportError, OSError): pass import matplotlib.pyplot as plt from matplotlib.figure import Figure from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigCanvas from matplotlib.backends.backend_wx import NavigationToolbar2Wx as NavigationToolbar import logging logger = logging.getLogger(__name__) from hdf_compass.compass_viewer.frame import BaseFrame ID_VIEW_CMAP_JET = wx.NewId() # default ID_VIEW_CMAP_BONE = wx.NewId() ID_VIEW_CMAP_GIST_EARTH = wx.NewId() ID_VIEW_CMAP_OCEAN = wx.NewId() ID_VIEW_CMAP_RAINBOW = wx.NewId() ID_VIEW_CMAP_RDYLGN = wx.NewId() ID_VIEW_CMAP_WINTER = wx.NewId() class PlotFrame(BaseFrame): """ Base class for Matplotlib plot windows. Override draw_figure() to plot your figure on the provided axes. """ def __init__(self, data, title="a title"): """ Create a new Matplotlib plotting window for a 1D line plot """ logger.debug(self.__class__.__name__) BaseFrame.__init__(self, id=wx.ID_ANY, title=title, size=(800, 400)) self.data = data self.panel = wx.Panel(self) self.dpi = 100 self.fig = Figure((6.0, 4.0), dpi=self.dpi) self.canvas = FigCanvas(self.panel, -1, self.fig) self.axes = self.fig.add_subplot(111, projection=ccrs.PlateCarree()) self.toolbar = NavigationToolbar(self.canvas) self.vbox = wx.BoxSizer(wx.VERTICAL) self.vbox.Add(self.canvas, 1, wx.LEFT | wx.TOP | wx.GROW) self.vbox.Add(self.toolbar, 0, wx.EXPAND) self.panel.SetSizer(self.vbox) self.vbox.Fit(self) self.draw_figure() def draw_figure(self): raise NotImplementedError class LinePlotFrame(PlotFrame): def __init__(self, data, names=None, title="Line Plot"): self.names = names PlotFrame.__init__(self, data, title) def draw_figure(self): lines = [self.axes.plot(d)[0] for d in self.data] if self.names is not None: for n in self.names: self.axes.legend(tuple(lines), tuple(self.names)) class ContourPlotFrame(PlotFrame): def __init__(self, data, extent, names=None, title="Contour Map"): self.geo_extent = extent logger.debug("Extent: %f, %f, %f, %f" % self.geo_extent) if (self.geo_extent[0] > self.geo_extent[1]) or (self.geo_extent[2] > self.geo_extent[3]): msg = "Invalid geographic extent! Check values:\n" \ "- West: %f, East: %f\n" \ "- South: %f, North: %f" % self.geo_extent dlg = wx.MessageDialog(None, msg, "Geographic Bounding Box", wx.OK | wx.ICON_WARNING) dlg.ShowModal() # need to be set before calling the parent (need for plotting) self.colormap = "jet" self.cb = None # matplotlib color-bar self.surf = None self.xx = None self.yy = None PlotFrame.__init__(self, data, title) self.cmap_menu = wx.Menu() self.cmap_menu.Append(ID_VIEW_CMAP_JET, "Jet", kind=wx.ITEM_RADIO) self.cmap_menu.Append(ID_VIEW_CMAP_BONE, "Bone", kind=wx.ITEM_RADIO) self.cmap_menu.Append(ID_VIEW_CMAP_GIST_EARTH, "Gist Earth", kind=wx.ITEM_RADIO) self.cmap_menu.Append(ID_VIEW_CMAP_OCEAN, "Ocean", kind=wx.ITEM_RADIO) self.cmap_menu.Append(ID_VIEW_CMAP_RAINBOW, "Rainbow", kind=wx.ITEM_RADIO) self.cmap_menu.Append(ID_VIEW_CMAP_RDYLGN, "Red-Yellow-Green", kind=wx.ITEM_RADIO) self.cmap_menu.Append(ID_VIEW_CMAP_WINTER, "Winter", kind=wx.ITEM_RADIO) self.add_menu(self.cmap_menu, "Colormap") self.Bind(wx.EVT_MENU, self.on_cmap_jet, id=ID_VIEW_CMAP_JET) self.Bind(wx.EVT_MENU, self.on_cmap_bone, id=ID_VIEW_CMAP_BONE) self.Bind(wx.EVT_MENU, self.on_cmap_gist_earth, id=ID_VIEW_CMAP_GIST_EARTH) self.Bind(wx.EVT_MENU, self.on_cmap_ocean, id=ID_VIEW_CMAP_OCEAN) self.Bind(wx.EVT_MENU, self.on_cmap_rainbow, id=ID_VIEW_CMAP_RAINBOW) self.Bind(wx.EVT_MENU, self.on_cmap_rdylgn, id=ID_VIEW_CMAP_RDYLGN) self.Bind(wx.EVT_MENU, self.on_cmap_winter, id=ID_VIEW_CMAP_WINTER) self.status_bar = wx.StatusBar(self, -1) self.status_bar.SetFieldsCount(2) self.SetStatusBar(self.status_bar) self.canvas.mpl_connect('motion_notify_event', self.update_status_bar) self.canvas.Bind(wx.EVT_ENTER_WINDOW, self.change_cursor) def on_cmap_jet(self, evt): logger.debug("cmap: jet") self.colormap = "jet" self._refresh_plot() def on_cmap_bone(self, evt): logger.debug("cmap: bone") self.colormap = "bone" self._refresh_plot() def on_cmap_gist_earth(self, evt): logger.debug("cmap: gist_earth") self.colormap = "gist_earth" self._refresh_plot() def on_cmap_ocean(self, evt): logger.debug("cmap: ocean") self.colormap = "ocean" self._refresh_plot() def on_cmap_rainbow(self, evt): logger.debug("cmap: rainbow") self.colormap = "rainbow" self._refresh_plot() def on_cmap_rdylgn(self, evt): logger.debug("cmap: RdYlGn") self.colormap = "RdYlGn" self._refresh_plot() def on_cmap_winter(self, evt): logger.debug("cmap: winter") self.colormap = "winter" self._refresh_plot() def _refresh_plot(self): self.draw_figure() self.canvas.draw() def draw_figure(self): max_elements = 500 # don't attempt plot more than 500x500 elements rows = self.data.shape[0] cols = self.data.shape[1] row_stride = rows // max_elements + 1 col_stride = cols // max_elements + 1 self.surf = self.data[::row_stride, ::col_stride] is_empty = np.isnan(self.surf).all() logger.debug("empty: %s" % is_empty) if is_empty: wx.MessageBox("Nothing to plot!", "Geo Array", wx.OK, self) return self.xx = np.linspace(self.geo_extent[0], self.geo_extent[1], self.surf.shape[1]) self.yy = np.linspace(self.geo_extent[2], self.geo_extent[3], self.surf.shape[0]) img = self.axes.contourf(self.xx, self.yy, self.surf, 25, cmap=plt.cm.get_cmap(self.colormap), transform=ccrs.PlateCarree()) # TODO: cartopy bug -> https://github.com/SciTools/cartopy/issues/1348 # self.axes.coastlines(resolution='50m', color='gray', linewidth=1) # add gridlines with labels only on the left and on the bottom grl = self.axes.gridlines(crs=ccrs.PlateCarree(), color='gray', draw_labels=True) grl.xformatter = LONGITUDE_FORMATTER grl.yformatter = LATITUDE_FORMATTER grl.xlabel_style = {'size': 8} grl.ylabel_style = {'size': 8} grl.right_labels = False grl.top_labels = False self.axes.set_extent(self.geo_extent, crs=ccrs.PlateCarree()) if self.cb: self.cb.update_normal(img) else: self.cb = plt.colorbar(img, ax=self.axes) self.cb.ax.tick_params(labelsize=8) def change_cursor(self, event): self.canvas.SetCursor(wx.Cursor(wx.CURSOR_CROSS)) @staticmethod def _find_nearest(arr, value): """ Helper function to find the nearest value in an array """ return (np.abs(arr - value)).argmin() def update_status_bar(self, event): msg = str() is_empty = np.isnan(self.surf).all() if event.inaxes and not is_empty: x, y = event.xdata, event.ydata id_y, id_x = self._find_nearest(self.yy, y), self._find_nearest(self.xx, x) # log.debug("id: %f %f" % (id_y, id_x)) z = self.surf[id_y, id_x] msg = "x= %f, y= %f, z= %f" % (x, y, z) self.status_bar.SetStatusText(msg, 1) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1714825026.9159935 hdf_compass-0.7b15/hdf_compass/compass_viewer/geo_surface/0000777000000000000000000000000014615423503020711 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/hdf_compass/compass_viewer/geo_surface/__init__.py0000666000000000000000000000227414544243330023026 0ustar00############################################################################## # Copyright by The HDF Group. # # All rights reserved. # # # # This file is part of the HDF Compass Viewer. The full HDF Compass # # copyright notice, including terms governing use, modification, and # # terms governing use, modification, and redistribution, is contained in # # the file COPYING, which can be found at the root of the source code # # distribution tree. If you do not have access to this file, you may # # request a copy from help@hdfgroup.org. # # # # author: gmasetti@ccom.unh.edu # ############################################################################## from hdf_compass.compass_viewer.geo_surface.frame import GeoSurfaceFrame import logging logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler())././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/hdf_compass/compass_viewer/geo_surface/frame.py0000666000000000000000000003451114544243330022360 0ustar00############################################################################## # Copyright by The HDF Group. # # All rights reserved. # # # # This file is part of the HDF Compass Viewer. The full HDF Compass # # copyright notice, including terms governing use, modification, and # # terms governing use, modification, and redistribution, is contained in # # the file COPYING, which can be found at the root of the source code # # distribution tree. If you do not have access to this file, you may # # request a copy from help@hdfgroup.org. # # # # author: gmasetti@ccom.unh.edu # ############################################################################## """ Implements a viewer frame for compass_model.Array. """ import wx import wx.grid from wx.lib.newevent import NewCommandEvent import os import logging logger = logging.getLogger(__name__) from hdf_compass.compass_viewer.frame import NodeFrame from hdf_compass.compass_viewer.geo_surface.plot import LinePlotFrame, ContourPlotFrame # Indicates that the slicing selection may have changed. # These events are emitted by the SlicerPanel. ArraySlicedEvent, EVT_ARRAY_SLICED = NewCommandEvent() # Menu and button IDs ID_VIS_MENU_PLOT = wx.NewId() class GeoSurfaceFrame(NodeFrame): """ Top-level frame displaying objects of type compass_model.Array. From top to bottom, has: 1. Toolbar (see ArrayFrame.init_toolbar) 2. SlicerPanel, with controls for changing what's displayed. 3. An ArrayGrid, which displays the data in a spreadsheet-like view. """ def __init__(self, node, pos=None): """ Create a new array viewer to display the node. """ NodeFrame.__init__(self, node, size=(800, 400), title=node.display_name, pos=pos) self.node = node # Update the menu vis_menu = wx.Menu() if self.node.is_plottable(): vis_menu.Append(ID_VIS_MENU_PLOT, "Map Surface\tCtrl-D") self.add_menu(vis_menu, "Visualize") # Initialize the toolbar self.init_toolbar() # The Slicer is the panel with indexing controls self.slicer = SlicerPanel(self, node.shape, node.dtype.fields is not None) # Create the grid array self.grid = ArrayGrid(self, node, self.slicer) # Sizer for slicer and grid gridsizer = wx.BoxSizer(wx.VERTICAL) gridsizer.Add(self.slicer, 0, wx.EXPAND) gridsizer.Add(self.grid, 1, wx.EXPAND) self.view = gridsizer self.Bind(EVT_ARRAY_SLICED, self.on_sliced) if self.node.is_plottable(): self.Bind(wx.EVT_MENU, self.on_plot, id=ID_VIS_MENU_PLOT) # Workaround for wxPython bug (see SlicerPanel.enable_spinctrls) ID_WORKAROUND_TIMER = wx.NewId() self.Bind(wx.EVT_TIMER, self.on_workaround_timer, id=ID_WORKAROUND_TIMER) self.timer = wx.Timer(self, ID_WORKAROUND_TIMER) self.timer.Start(100) def init_toolbar(self): """ Set up the toolbar at the top of the window. """ t_size = (24, 24) plot_bmp = wx.Bitmap(os.path.join(self.icon_folder, "viz_plot_24.png"), wx.BITMAP_TYPE_ANY) self.toolbar = self.CreateToolBar(wx.TB_HORIZONTAL | wx.NO_BORDER | wx.TB_FLAT | wx.TB_TEXT) self.toolbar.SetToolBitmapSize(t_size) self.toolbar.AddStretchableSpace() if self.node.is_plottable(): self.toolbar.AddTool(ID_VIS_MENU_PLOT, "Map Surface", plot_bmp) self.toolbar.Realize() def on_sliced(self, evt): """ User has chosen to display a different part of the dataset. """ self.grid.Refresh() def on_plot(self, evt): """ User has chosen to plot the current selection """ cols = self.grid.GetSelectedCols() rows = self.grid.GetSelectedRows() # Scalar data can't be line-plotted. if len(self.node.shape) == 0: return # Columns in the view are selected if len(cols) != 0: # The data is compound if self.node.dtype.names is not None: names = [self.grid.GetColLabelValue(x) for x in cols] data = self.node[self.slicer.indices] # -> 1D compound array data = [data[n] for n in names] f = LinePlotFrame(data, names) f.Show() # Plot multiple columns independently else: if len(self.node.shape) == 1: data = [self.node[self.slicer.indices]] else: data = [self.node[self.slicer.indices + (c,)] for c in cols] names = ["Col %d" % c for c in cols] if len(data) > 1 else None f = LinePlotFrame(data, names) f.Show() # Rows in view are selected elif len(rows) != 0: data = [self.node[self.slicer.indices + (slice(None, None, None), r)] for r in rows] names = ["Row %d" % r for r in rows] if len(data) > 1 else None f = LinePlotFrame(data, names) f.Show() # No row or column selection. Plot everything else: data = self.node[self.slicer.indices] # The data is compound if self.node.dtype.names is not None: names = [self.grid.GetColLabelValue(x) for x in range(self.grid.GetNumberCols())] data = [data[n] for n in names] f = LinePlotFrame(data, names) f.Show() # Plot 1D elif len(self.node.shape) == 1: f = LinePlotFrame([data]) f.Show() # Plot 2D else: f = ContourPlotFrame(data, extent=self.node.extent) f.Show() def on_workaround_timer(self, evt): """ See slicer.enable_spinctrls docs """ self.timer.Destroy() self.slicer.enable_spinctrls() class SlicerPanel(wx.Panel): """ Holds controls for data access. Consult the "indices" property, which returns a tuple of indices that prefix the array. This will be RANK-2 elements long, unless hasfields is true, in which case it will be RANK-1 elements long. """ @property def indices(self): """ A tuple of integer indices appropriate for slicing. Will be RANK-2 elements long, RANK-1 if compound data is in use (hasfields == True). """ return tuple([x.GetValue() for x in self.spincontrols]) def __init__(self, parent, shape, hasfields): """ Create a new slicer panel. parent: The wxPython parent window shape: Shape of the data to visualize hasfields: If True, the data is compound and the grid can only display one axis. So, we should display an extra spinbox. """ wx.Panel.__init__(self, parent) self.shape = shape self.hasfields = hasfields self.spincontrols = [] # Rank of the underlying array rank = len(shape) # Rank displayable in the grid. If fields are present, they occupy # the columns, so the data displayed is actually 1-D. visible_rank = 1 if hasfields else 2 sizer = wx.BoxSizer(wx.HORIZONTAL) # Will arrange the SpinCtrls if rank > visible_rank: infotext = wx.StaticText(self, wx.ID_ANY, "Array Indexing: ") sizer.Add(infotext, 0, flag=wx.EXPAND | wx.ALL, border=10) for idx in range(rank - visible_rank): sc = wx.SpinCtrl(self, max=shape[idx] - 1, value="0", min=0) sizer.Add(sc, 0, flag=wx.EXPAND | wx.ALL, border=10) sc.Disable() self.spincontrols.append(sc) self.SetSizer(sizer) self.Bind(wx.EVT_SPINCTRL, self.on_spin) def enable_spinctrls(self): """ Unlock the spin controls. Because of a bug in wxPython on Mac, by default the first spin control has bizarre contents (and control focus) when the panel starts up. Call this after a short delay (e.g. 100 ms) to enable indexing. """ for sc in self.spincontrols: sc.Enable() def on_spin(self, evt): """ Spinbox value changed; notify parent to refresh the grid. """ wx.PostEvent(self, ArraySlicedEvent(self.GetId())) class ArrayGrid(wx.grid.Grid): """ Grid class to display the Array. Cell contents and appearance are handled by the table model in ArrayTable. """ def __init__(self, parent, node, slicer): wx.grid.Grid.__init__(self, parent) table = ArrayTable(node, slicer) self.SetTable(table, True) # Column selection is always allowed selmode = 2 # wx.grid.Grid.SelectColumns # Row selection is forbidden for compound types, and for # scalar/1-D datasets if node.dtype.names is None and len(node.shape) > 1: selmode |= 1 # wx.grid.Grid.SelectRows self.SetSelectionMode(selmode) class LRUTileCache(object): """ Simple tile-based LRU cache which goes between the Grid and the Array object. Caches tiles along the last 1 or 2 dimensions of a dataset. Access is via __getitem__. Because this class exists specifically to support point-based callbacks for the Grid, arguments may only be indices, not slices. """ TILESIZE = 100 # Tiles will have shape (100,) or (100, 100) MAXTILES = 50 # Max number of tiles to retain in the cache def __init__(self, arr): """ *arr* is anything implementing compass_model.Array """ import collections self.cache = collections.OrderedDict() self.arr = arr def __getitem__(self, args): """ Restricted to an index or tuple of indices. """ if not isinstance(args, tuple): args = (args,) # Split off the last 1 or 2 dimensions coarse_position, fine_position = args[0:-2], args[-2:] def clip(x): """ Round down to nearest TILESIZE; takes e.g. 181 -> 100 """ return (x // self.TILESIZE) * self.TILESIZE # Tuple with index of tile corner tile_key = coarse_position + tuple(clip(x) for x in fine_position) # Slice which will be applied to dataset to retrieve tile tile_slice = coarse_position + tuple(slice(clip(x), clip(x) + self.TILESIZE) for x in fine_position) # Index applied to tile to retrieve the desired data point tile_data_index = tuple(x % self.TILESIZE for x in fine_position) # Case 1: Add tile to cache, ejecting oldest tile if needed if not tile_key in self.cache: if len(self.cache) >= self.MAXTILES: self.cache.popitem(last=False) tile = self.arr[tile_slice] self.cache[tile_key] = tile # Case 2: Mark the tile as recently accessed else: tile = self.cache.pop(tile_key) self.cache[tile_key] = tile return tile[tile_data_index] class ArrayTable(wx.grid.GridTableBase): """ "Table" class which provides data and metadata for the grid to display. The methods defined here define the contents of the table, as well as the number of rows, columns and their values. """ def __init__(self, node, slicer): """ Create a new Table instance for use with a grid control. node: An compass_model.Array implementation instance. slicer: An instance of SlicerPanel, so we can see what indices the user has requested. """ wx.grid.GridTableBase.__init__(self) self.node = node self.slicer = slicer self.rank = len(node.shape) self.names = node.dtype.names self.cache = LRUTileCache(self.node) def GetNumberRows(self): """ Callback for number of rows displayed by the grid control """ if self.rank == 0: return 1 return self.node.shape[-1] def GetNumberCols(self): """ Callback for number of columns displayed by the grid control. Note that if compound data is in use, columns display the field names. """ if self.names is not None: return len(self.names) if self.rank < 2: return 1 return self.node.shape[-2] def GetValue(self, row, col): """ Callback which provides data to the Grid. row, col: Integers giving row and column position (0-based). """ # Scalar case if self.rank == 0: data = self.node[()] if self.names is None: return "%s" % data return "%s" %data[col] # 1D case if self.rank == 1: data = self.cache[row] if self.names is None: return "%s" % data return "%s" % data[self.names[col]] # ND case. Watch out for compound mode! if self.names is None: args = self.slicer.indices + (col, row) else: args = self.slicer.indices + (row,) data = self.cache[args] if self.names is None: return "%s" % data return "%s" % data[self.names[col]] def GetRowLabelValue(self, row): """ Callback for row labels. Row number is used unless the data is scalar. """ if self.rank == 0: return "Value" return str(row) def GetColLabelValue(self, col): """ Callback for column labels. Column number is used, except for scalar or 1D data, or if we're displaying field names in the columns. """ if self.names is not None: return self.names[col] if self.rank == 0 or self.rank == 1: return "Value" return str(col) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1714753362.0 hdf_compass-0.7b15/hdf_compass/compass_viewer/geo_surface/plot.py0000666000000000000000000002507014615207522022246 0ustar00############################################################################## # Copyright by The HDF Group. # # All rights reserved. # # # # This file is part of the HDF Compass Viewer. The full HDF Compass # # copyright notice, including terms governing use, modification, and # # terms governing use, modification, and redistribution, is contained in # # the file COPYING, which can be found at the root of the source code # # distribution tree. If you do not have access to this file, you may # # request a copy from help@hdfgroup.org. # # # # author: gmasetti@ccom.unh.edu # ############################################################################## """ Matplotlib window with toolbar. """ import numpy as np import wx try: # for GeoSurface we use cartopy that can be challenging to freeze on OSX to dependencies (i.e. geos) import cartopy.crs as ccrs from cartopy.mpl.gridliner import LONGITUDE_FORMATTER, LATITUDE_FORMATTER except (ImportError, OSError): pass import matplotlib.pyplot as plt from matplotlib.figure import Figure from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigCanvas from matplotlib.backends.backend_wx import NavigationToolbar2Wx as NavigationToolbar from matplotlib.colors import LinearSegmentedColormap from matplotlib import cm from matplotlib.colors import LightSource import logging logger = logging.getLogger(__name__) from hdf_compass.compass_viewer.frame import BaseFrame np.seterr(divide='ignore', invalid='ignore') ID_VIEW_CMAP_BAG = wx.NewId() # default ID_VIEW_CMAP_JET = wx.NewId() ID_VIEW_CMAP_BONE = wx.NewId() ID_VIEW_CMAP_GIST_EARTH = wx.NewId() ID_VIEW_CMAP_OCEAN = wx.NewId() ID_VIEW_CMAP_RAINBOW = wx.NewId() ID_VIEW_CMAP_RDYLGN = wx.NewId() ID_VIEW_CMAP_WINTER = wx.NewId() class PlotFrame(BaseFrame): """ Base class for Matplotlib plot windows. Override draw_figure() to plot your figure on the provided axes. """ def __init__(self, data, title="a title"): """ Create a new Matplotlib plotting window for a 1D line plot """ logger.debug(self.__class__.__name__) BaseFrame.__init__(self, id=wx.ID_ANY, title=title, size=(800, 400)) self.data = data self.panel = wx.Panel(self) self.dpi = 100 self.fig = Figure((6.0, 4.0), dpi=self.dpi) self.canvas = FigCanvas(self.panel, -1, self.fig) self.axes = self.fig.add_subplot(111, projection=ccrs.PlateCarree()) self.toolbar = NavigationToolbar(self.canvas) self.vbox = wx.BoxSizer(wx.VERTICAL) self.vbox.Add(self.canvas, 1, wx.LEFT | wx.TOP | wx.GROW) self.vbox.Add(self.toolbar, 0, wx.EXPAND) self.panel.SetSizer(self.vbox) self.vbox.Fit(self) self.draw_figure() def draw_figure(self): raise NotImplementedError class LinePlotFrame(PlotFrame): def __init__(self, data, names=None, title="Line Plot"): self.names = names PlotFrame.__init__(self, data, title) def draw_figure(self): lines = [self.axes.plot(d)[0] for d in self.data] if self.names is not None: for n in self.names: self.axes.legend(tuple(lines), tuple(self.names)) class ContourPlotFrame(PlotFrame): def __init__(self, data, extent, names=None, title="Surface Map"): self.geo_extent = extent logger.debug("Extent: %f, %f, %f, %f" % self.geo_extent) if (self.geo_extent[0] > self.geo_extent[1]) or (self.geo_extent[2] > self.geo_extent[3]): msg = "Invalid geographic extent! Check values:\n" \ "- West: %f, East: %f\n" \ "- South: %f, North: %f" % self.geo_extent dlg = wx.MessageDialog(None, msg, "Geographic Bounding Box", wx.OK | wx.ICON_WARNING) dlg.ShowModal() # need to be set before calling the parent (need for plotting) self.colormap = LinearSegmentedColormap.from_list("BAG", ["#63006c", "#2b4ef4", "#2f73ff", "#4b8af4", "#bee2bf"]) # register a new colormap cm.register_cmap(cmap=self.colormap) self.cb = None # matplotlib color-bar self.xx = None self.yy = None self.surf = None PlotFrame.__init__(self, data, title) self.cmap_menu = wx.Menu() self.cmap_menu.Append(ID_VIEW_CMAP_BAG, "BAG", kind=wx.ITEM_RADIO) self.cmap_menu.Append(ID_VIEW_CMAP_JET, "Jet", kind=wx.ITEM_RADIO) self.cmap_menu.Append(ID_VIEW_CMAP_BONE, "Bone", kind=wx.ITEM_RADIO) self.cmap_menu.Append(ID_VIEW_CMAP_GIST_EARTH, "Gist Earth", kind=wx.ITEM_RADIO) self.cmap_menu.Append(ID_VIEW_CMAP_OCEAN, "Ocean", kind=wx.ITEM_RADIO) self.cmap_menu.Append(ID_VIEW_CMAP_RAINBOW, "Rainbow", kind=wx.ITEM_RADIO) self.cmap_menu.Append(ID_VIEW_CMAP_RDYLGN, "Red-Yellow-Green", kind=wx.ITEM_RADIO) self.cmap_menu.Append(ID_VIEW_CMAP_WINTER, "Winter", kind=wx.ITEM_RADIO) self.add_menu(self.cmap_menu, "Colormap") self.Bind(wx.EVT_MENU, self.on_cmap_bag, id=ID_VIEW_CMAP_BAG) self.Bind(wx.EVT_MENU, self.on_cmap_jet, id=ID_VIEW_CMAP_JET) self.Bind(wx.EVT_MENU, self.on_cmap_bone, id=ID_VIEW_CMAP_BONE) self.Bind(wx.EVT_MENU, self.on_cmap_gist_earth, id=ID_VIEW_CMAP_GIST_EARTH) self.Bind(wx.EVT_MENU, self.on_cmap_ocean, id=ID_VIEW_CMAP_OCEAN) self.Bind(wx.EVT_MENU, self.on_cmap_rainbow, id=ID_VIEW_CMAP_RAINBOW) self.Bind(wx.EVT_MENU, self.on_cmap_rdylgn, id=ID_VIEW_CMAP_RDYLGN) self.Bind(wx.EVT_MENU, self.on_cmap_winter, id=ID_VIEW_CMAP_WINTER) self.status_bar = wx.StatusBar(self, -1) self.status_bar.SetFieldsCount(2) self.SetStatusBar(self.status_bar) self.canvas.mpl_connect('motion_notify_event', self.update_status_bar) self.canvas.Bind(wx.EVT_ENTER_WINDOW, self.change_cursor) def on_cmap_bag(self, evt): logger.debug("cmap: bag") self.colormap = cm.get_cmap("BAG") self._refresh_plot() def on_cmap_jet(self, evt): logger.debug("cmap: jet") self.colormap = cm.get_cmap("jet") self._refresh_plot() def on_cmap_bone(self, evt): logger.debug("cmap: bone") self.colormap = cm.get_cmap("bone") self._refresh_plot() def on_cmap_gist_earth(self, evt): logger.debug("cmap: gist_earth") self.colormap = cm.get_cmap("gist_earth") self._refresh_plot() def on_cmap_ocean(self, evt): logger.debug("cmap: ocean") self.colormap = cm.get_cmap("ocean") self._refresh_plot() def on_cmap_rainbow(self, evt): logger.debug("cmap: rainbow") self.colormap = cm.get_cmap("rainbow") self._refresh_plot() def on_cmap_rdylgn(self, evt): logger.debug("cmap: RdYlGn") self.colormap = cm.get_cmap("RdYlGn") self._refresh_plot() def on_cmap_winter(self, evt): logger.debug("cmap: winter") self.colormap = cm.get_cmap("winter") self._refresh_plot() def _refresh_plot(self): self.draw_figure() self.canvas.draw() def draw_figure(self): ls = LightSource(azdeg=315, altdeg=45) self.surf = self.data # try to plot the whole array try: blended_surface = ls.shade(self.surf, cmap=self.colormap, vert_exag=5, blend_mode="overlay", vmin=np.nanmin(self.surf), vmax=np.nanmax(self.surf)) # too big, two attempts for sub-sampling except MemoryError: rows = self.data.shape[0] cols = self.data.shape[1] # 1st attempt: <= 1024x1024 try: max_elements = 1024 row_stride = rows // max_elements + 1 col_stride = cols // max_elements + 1 self.surf = self.data[::row_stride, ::col_stride] blended_surface = ls.shade(self.surf, cmap=self.colormap, vert_exag=5, blend_mode="overlay", vmin=np.nanmin(self.surf), vmax=np.nanmax(self.surf)) except MemoryError: # 2st attempt: <= 512x512 max_elements = 512 row_stride = rows // max_elements + 1 col_stride = cols // max_elements + 1 self.surf = self.data[::row_stride, ::col_stride] blended_surface = ls.shade(self.surf, cmap=self.colormap, vert_exag=5, blend_mode="overlay", vmin=np.nanmin(self.surf), vmax=np.nanmax(self.surf)) logger.debug("too big: %s x %s > subsampled to %s x %s" % (self.data.shape[0], self.data.shape[1], self.surf.shape[0], self.surf.shape[1])) img = self.axes.imshow(blended_surface, origin='lower', cmap=self.colormap, extent=self.geo_extent, transform=ccrs.PlateCarree()) img.set_clim(vmin=np.nanmin(self.surf), vmax=np.nanmax(self.surf)) # TODO: cartopy bug -> https://github.com/SciTools/cartopy/issues/1348 # self.axes.coastlines(resolution='50m', color='gray', linewidth=1) # add gridlines with labels only on the left and on the bottom grl = self.axes.gridlines(crs=ccrs.PlateCarree(), color='gray', draw_labels=True) grl.xformatter = LONGITUDE_FORMATTER grl.yformatter = LATITUDE_FORMATTER grl.xlabel_style = {'size': 8} grl.ylabel_style = {'size': 8} grl.right_labels = False grl.top_labels = False self.axes.set_extent(self.geo_extent, crs=ccrs.PlateCarree()) if self.cb: self.cb.update_normal(img) else: self.cb = plt.colorbar(img, ax=self.axes) self.cb.ax.tick_params(labelsize=8) def change_cursor(self, event): self.canvas.SetCursor(wx.Cursor(wx.CURSOR_CROSS)) def update_status_bar(self, event): msg = str() if event.inaxes: x, y = event.xdata, event.ydata msg = self.axes.format_coord(x, y) self.status_bar.SetStatusText(msg, 1) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1714825026.9689915 hdf_compass-0.7b15/hdf_compass/compass_viewer/icons/0000777000000000000000000000000014615423503017542 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/hdf_compass/compass_viewer/icons/favicon_32.png0000666000000000000000000000514214544243123022202 0ustar00PNG  IHDR szzbKGD pHYs  tIME riTXtCommentCreated with GIMPd.e IDATXí{\}?q>{m76M NJJ:ڨPQRTEU#%R(@ *XA`D4&Cf5y9JBtutνq=ڿǣ)M/ ['?}Z&w[|(9vչsۧKvo K^>4:s@{opgv7?|l,?y`퓌(.|{M_d0߻'u<K *&ܵT* 9>mLq֯]Éo߻DvduMs"SC`qqc۴)p5`EjAgod[wN`16_vrҍV?~FAT!X\c^pA €Vˌ²[/o;xCl\ѣGH{F½#(4rY t }ݛRk?ݏ`"t sz3x4^J{5V $0-+{e0 zsUˆz\kտ}׾ߩ=ϽdvTߍ}9lc?XAWJBZ]Y ӕ9RDRbEÜ:]\ǿUh40֋9^l#S(C%J)V8i*=)pl:_%%RI24+j_;Dmtu\{J"!90YT=$%J*Ꜩ%@)V@vZnU+6\@ώ<[;8cdeb O3Z_1 l`{ʟ:aEZ 0Ivrmf2(D.II I"CRoa#C;H(UG11aH%z1d"nI2 RH)bi"7]1w&QC.~o24kGHSRTcx&'9 K\7X4*80JEGNQZ=$K{ ]Zs ) BHTVrsMM=xi60&U3,q.`fWGaؾ#(Ns V6)81v|=/BXl7JX"'R X,,4V2u@S:<\c1SqIj'v\k#D-f`wN( ZaFkIqfUR9RTzgCg:-s8X` y&6&i0AgcKp$AkBy1Sr4ҹ=iϾN,nR?Jm(OYWS-͓q;d:: *[|+O 4BeҽL7F17}gvz~7 SgSX%-qBT BvCWۏ폍ޗ3q[~5 fؗ(חčNXVȢeB2M:Q ejψL0?:'.H[o/~к?ۼKo,C+Wɑ Z/|Џ>C_F{>{b("\IENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/hdf_compass/compass_viewer/icons/favicon_48.png0000666000000000000000000001146314544243123022214 0ustar00PNG  IHDR00WbKGD pHYs  tIME 5HViTXtCommentCreated with GIMPd.eIDAThyW}?>gfgid,:|c.ۀ $+PS1*$b\X&[–juk=fߑ?zveE6G8IWof{e5\;s5=羑n:|֗`˙+V>6ƍLnŮbKa$X>8_|+M߹_r.˗u6Q\Zȇ/28<:::fmV|WNt]-^{?6\yf롃mP4 F̲RK/9Mg-gxLT>}ߪNسo#AmVAx31G2=yQuP(ҷ*;9<<ˏ>Eqƞ(rQ/T+?w6uݿ-7d9\#rgDatLURʳu~o4)Hu8mAktR%*ѷz`W5Gk}_⮟wp `ra(u䣐B!b2m/ 0m`@QT<ċd4 <|SԷZ#qxhj=! wpgWW/ϲw|"---ڳzA[!?}Qj(+F|*DA@PE:+["]x;)Y^)7}ky>-prx]/\ԓrtپ{/uc )~KD.Q9?`Q<' EB6jp Wu}'OMK1GFbhhRԕ g{#xq}Ozv3{ZRGňR!2\M KyE?}B D378gqƐ#y0da5V-qsFwS~OA* G_g?sOF >{v RӜ=ʱJBК?xfAJ6gq+%J rEvaeIqhch- Xכgtb 3s [Z¿~?wr 8{?iw`B{[j!ha&) gpxHqa_Ӊ1@, )-#BD.)cL%=+fֶĝ\{kN!D?xm]M*%I} 3Q$R5œ)rQcU[E+KBcB H!BùLA>1)뮿{z&~}/k(r@[M̧VqetC(dL$R dH'T<8=bCG6!)5ucfm-ZtQ)W=Ҋ/2NtlI5_OKWV Lr9l8s g)*l_xqn9.k.P KpXk)8cu#S]ӋNq;o>HTnA{tJCWrVJO8Ҍuǖ~|)tb84;c{C5ִSBRc9}uM-Xigq6p쟩sMFt-呃eV*,am]&8G( =A!+!3LK!*< R6wD(lKDdJK)%sj1`,$J U6՘rGvq[bke։Ji-YxɌBm9c )I-tg7W-9_dtp578kZ1gєxA:>O}# ycL`P6J0pX g)E c H)@ qc}9̥L$ymm% z"k228mQ&;%HQ85ĉXDZk4VOB"CJ>3$izJ(.-p^fn-1* 4SZ5k4^m:Ng,jk`hZcZdykp&K'W`)/vᄠ5PଖZ3v`C `hlCcb4&qIbl3=_gClBkMj4fdV5Ar. 4,J8dEfj'dk<ܒ5()XQi7)zJ<=iԱ:bMBי5bz)9Bn ԝMs&`S:w5T5ukEIY󦤤gDZItr6]&1`5H+$F }EC'8PJR ')ژcrr !Omk.fnk831H4Vjh)Iq҈䤤iL[w cL5ԒL)t~SE8']f}k }ɺmZ:K Z )vlk]!/SRHPa@{)" ftKXbE(yTulM-&NЕ RITx+1F XD3x|Z^{gNF?tV'N'XJ||~&f*iJ#I$N& 1=UESly[O ǤsyXgp8 8M1֢ffK1nҊW!UJBOTذQ",0[y =8sHkqiS%)zBZZ l1={'9Cj 7Os#. 9 us#-+ )#(DYiw!CUnhooȉ 9s&)i"56  J9Vv' 33;=wBdvύ;Yj7;0MشM8Scs$϶YI&!ɮtQq0 \FOrb˵F qni[?Dy똤3qs(47:;%JREG'yXS^_} ^~ۙu~ Y(SDﰗk+8\781<:ͪU˩TC#s!sIƼ\TGHPdE8yUu,L^= ɇ?H y{0Z՛PXX& B): ._!T֥f{8g+d"󸰀 윻io{kI%|߇;4L?aEm%nl0:2%84:͹T' o DMs\lO׏`2~?Fr2w'6imNm9g%4 Ps dpXB!r8GyDTbQl62|E3_~W^ )QZV񭗼Con0wX-\NNd-A8U ȵ aUeZ}G%A65moykĉϾOA~zȪ2Gm-ϋ%ZdQVuYİ6 2)t[奍~]x!ŠWo\틌+IENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1713620956.0 hdf_compass-0.7b15/hdf_compass/compass_viewer/icons/find_24.png0000666000000000000000000001235114610743734021505 0ustar00PNG  IHDRw=zTXtRaw profile type exifxmP[ ;y@8enϐjEI@B~5IsDh Em"Eh͡ѝY-NK Uͅ.Tu0dQnTHrfߊ{;a֧w$ 0w G4 AZi-ߞ?QY<iCCPICC profilex};HP8A!Cu/K`Zu0 $).kbYWWA|8;8)H&1^8܏sUenD\W+YOf\_.ʳjdO"aonZu"+*9A ?r]qsagF6=O,KVz x8j: 9U[ju/  שFHA*Bvi:{G\ *`X@ dgk&ݤP{1 }l \]}j#`pjp ?eCv$?P,gMy`Xs9Y-x=;tfrYJ viTXtXML:com.adobe.xmp VfbKGD pHYsodtIME 1 IDATHǭ{lSU߽w޾vmvl ۊsc6`b@Ԙ` .%*KQD PpAQ<& [޻s`[9YZeY2{â0CEąA!K!͞ ':괽mf7ݼA9o`W]ξXYCRz<%ǥ?&$BӸ9m7zK`8|9z Y6^lm4\N$I2W&˄DꩾШi]]lyUsVR0"j8Wl\;j7U`ӎ-3:ȗ~br#֐}_uɁ, )5d,|q͖֜@XXJNӞ*z+zĐcR/&@!Ckf Q7p̺.4@YP2]S,C]jsf#3!<5Z}+20_{0 z6{"'1BGFƘ| D2U#Hqɝ_eEI^YM}9 R&s5pC;dsmhGб bՏ7m~Hd/bøjqϜb!t ?⒱PR.f*W\͍6]_/>;BpAR/22]== AZVʢqKJ&Rb/)O]з3ܽsk{9]C؂lde^20`eh,teGLwF#S"=vV~2kfM6Ҳ}-1MKLa l],lG~|VE`J䁛U!|v4ͭ !ow{bѣK͘sZtg 3+V:.nV<6qkkk7Pql MEQjdYx3!t@ 㴢(n1 $Lx<ǵ? #jv{8'0_IENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/hdf_compass/compass_viewer/icons/go_back_24.png0000666000000000000000000000217214544243123022143 0ustar00PNG  IHDRw=AIDATxڵolSUy{__Cdž01N-f.1hd4 t$:P BŤs`H !QC2 f@ۭk{V@0Fw"f59=Oтm@h`gwSn[0@EPB65[#3iDkR%mڕZтaKinwymij*NNf&3;N.P2?9vm`;˟.x/~:~3:]-(CZ; ͽo<A|qMANCp" "CH@$? d:t]G3o F#1spv%XN,+^˃\))*;y@>PVgMI! 'Aǁs^4HB,\^$fel22۝x%M7Q~PXºa2H*#**?H%83 92lϲGTer!=<r9yvmJgy!L7 i:#%e)0t|fn"r1JAETbY5`39hSU,YbXhJ"[:|7ϭQ'xK4svރ5*F/k5E>4HH_YK hBמ@t21Ϣ%b&PTd腾&bW ,]%fLSah?{<<צ]M ̿c%.%rROF<)ͬrc)>pҀUUmZdo_CpҀ{]ŻtkWmIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/hdf_compass/compass_viewer/icons/go_next_32.png0000666000000000000000000000273114544243123022221 0ustar00PNG  IHDR szzIDATxՕklU F>`!5VDA1`4ZZ"FRQ@% Rn+APh}PZf罞;;ŔbZd9wW?Եf̳w f:5 2d﯁wEˡ*Ri`oƉ%j %:'|3fKt!))p# 1 kh5XH,:PB 6JDG[SƝh^Ș/;)THJN 0z@^w8FoyxT8oM$ͼ >:)ɏ`Cr&8tb=ϊ;9 ]\4QXB)v֥(tQ籒aE>9:aL, g4.0h *8r0Q իo(ˮ87ۆ9sP؉`m5Vw!Ŏu`U@<z0 Y3I#atu}=; ZSgy>(hEe (E"c{IP$9Uy˷IDATHǭMh\UwLfLMu&&M$VHkM %PHEAW袋.DtbFn]l*1H3)m2̽s=sQ?|ٜhx/ӾZs (YSfCsw߯H;0d0S`Bs !PcY؛]̎\Esn9g!-T|aRqHPFRq  Dk^ 7G)jm/% _ҭ"nVŚg5CQ0F5(Q eP ,H`кW KK39 ZCOhE.DZATU"ܘ W>ZƜs⁊4#2oNQΞB, Ѻ:() tW}rS ԏ.OWg ` DC5$ZDIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/hdf_compass/compass_viewer/icons/go_top_32.png0000666000000000000000000000301314544243123022037 0ustar00PNG  IHDR szz pHYs^tIME "WbKGDIDATxڽYlTUƿsf2]^[)%P(E@[J /&<b>FA6 Thܹ`V/=9;K0*Yv*y&xMe:0"ǵraQ>Wā`5)DdehY mNs bӄӛ6aFNWm>{Vw@p:UQ7nx"9/Ѱ^C.~YR|xqw9=P^{uQia80-YP`l]9)STg}0w,21uy;~8. =lcƜ@4M?^Rl19bLAEP~ďfΘ $M|*UT}H8 'f B91WFk@o.(]WXG<$&Ġ`$D ,n"#EFV[嚑2IoOY !&A20$>Fe3NW #ޢ*1sPġ $&8 3$P@D.+#9<B L4QhJH![/5[Fd O>SR5n[OCl:H{x ܔ⃤!?/)?;Y<V0pf߶+~>"e.\#%)paU&drql!.l0ˎ1ud~FƓ0J=ԖP% Z̀aG cَwvҪdp'۞{g{`xCė@F&&-_sCwTV{%1DIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/hdf_compass/compass_viewer/icons/go_up_24.png0000666000000000000000000000214714544243123021671 0ustar00PNG  IHDRw=.IDATxڭlEݻI[*"(J ?LD0WGL`bbS "b"T*JKK{ {FmϾdvfgg73Me0 ߬_4݅,-X3m*{~H~o^;^cٷ=Ss<r`uol{S;Y o?P72Sd@u%sFEǂKR2px ښWe{[|7m$@w [{|]vs1G|iIBiYY i\5VwzzU+ Ec gD=?)J1uѹCzŊ-.\B5[r96sX@TAC| йc+k. 4`E`BaHti*RUtMc:/*~;+|n›,lQ,9qqH(PQ&]`Wٿ_)[݅K*lv' xx̰,"YИ69vmD)¢h{[}jkhvd"qnX̋"F݇>hbOjo^XYo1**- L kn/5=vDhD#fb M V/ܶ2`6w (Ȭ^IENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/hdf_compass/compass_viewer/icons/go_up_32.png0000666000000000000000000000300114544243123021656 0ustar00PNG  IHDR szzIDATxŖ{lEݽ^-@RyJD^Z(1&$  il1 򪍁E%X£Hb *ZS{ήV*(^q_fs3Yuਧ9* s@+ȹoo]݂=5/lo#rL3xR %W=?]u$\v[fa B}XIƥRo[$M/ڳrYn=~olnң.>o ٫eOsGicÅW֔-q^Jpfy(nE&㠴wgd[1K(}X  xӄ}'Z| F cqYk<_4q)gk;C 763$asfwZ=<_]["spbɲ|~YU'5p:J9-}prvւġ=MzEhpP]= ((J @Af՝060tu = p@О _oG-e[))E)#@BqIY/Jٰ3΀ @@ NE 4J+7-<ʑxEq2Fo$ ma=` p~210) pxu@%-M5wjPT* A(dĄ` a A}:vۓҒRi0m^CU>n4ZjNB!'6!{!!T0T Uܓ ?`RaqVpڥ KKעAgK*[M?!, p|;|: ѱ0|t2<;&w5at `۲pS؄GdWֽ Mש 1 B[{ "y/o3@P|XpluܽP&ZL4Yj ?`@B#!8}R!rCsxm@,/PiU(vho ,)1 @t^#7b(Xvѕ7>|hwDxm dp854Me~&%ׁ}n6Y5>,X<]eOY@8b=r#<^ސd2` (ੈ&DvFo= A xtxtb^s `aq 7Vg|<>bc )\.uʮE9j .cY qCw)'h?*IENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/hdf_compass/compass_viewer/icons/license.txt0000666000000000000000000001720614544243123021732 0ustar00 GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library.././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/hdf_compass/compass_viewer/icons/logo.png0000666000000000000000000023137414544243123021221 0ustar00PNG  IHDR(sRGB pHYs  YiTXtXML:com.adobe.xmp 1 L'Y@IDATx콍ȭ+X(nZ =>cLA!R_o>}Ql>K|= [9b(e1Lԏ`XơQ)N -8qc:@ x_%}12=X3|yz_m?U̴PB_۷U/\ szJ[!iTu Nց 2 . UywG|N_pڥ&?eG|sɅ]Z1ztnuO? iE\d]f>'xHF|vP^ #C:3õS[Q/T mn`#8Zu򯷯/!=_hC+,,A8s/14UA3}+Dhpbg'̟~QԐxMb 9/;\>~' ٟo|nT>+|ƻ,QRKOQ_9~zPsXzJiŵ#g L :]:$=}p̱[ EuK7iw=c{Q C\׫~J%މ3 Xl ܇t&m=]B?¤qRtyޱ.au0DY,MT$h+:/]$^i_pZ3hQ|2!>qRQ6Kz/rt$EN_RF/ QT%+tN :`i{]w['oHq|ן܌VxGOL _Rhzn6܋;ۉ?M ?a?;)I,~+ vo%w[ՙZ݆smA`&irAߑc8yN0)q丆xɒ'2wq\+ŹFF[I"ȷ6C7:XV]]܏x|ZGM+"Pj9ͨyq)Ll~1?yCy<[WGkaQt\@.| |(HG[j}B~5.DC1CKDB,23~T + sc&\#A<*yݟZtP`~ݟAG_Cfg<>`).}U :Dk%p߀8}bo8 [{pPOx[wTpcwR~1M侮wKr<StحʈSnv}.b7mEx>ڼi?"Ro04--wHQC>w R]F$}\:GnZ$Mg"2&@u> $`|F!;Bi|r.Xǒ){zSooC/Ci: `ǁQ ;kQB0E<8Z+p_5lTjHo^ {'![ZGc\ngPxzmKy\]vgD7ϣq eGmE-N/܈#F7s?e Z][s|ۆ= sFF;Rs&N80UwT.,k9sUt6p7>m ^ -y.^x LqC!Ɲ(1eÖ ?eQ"nƹ>ySmg_;iIA€3Qg"0u~K_)j?}zuN]iށ>ʼf&i@vSv"aGVV=Nr.zv9Lɔ2Px>Bg/?z)?j!a(ag$y*/kk| I@Bϗ+ 4Bl\aSe4T,uԨܭ ޘ)_RϨHƷT*2]B Ӿܸ6X\ce[ ,uW=^E*5FL e}x0S|Bl`0:=$#Mf_NҁAd  Nng;)C2wRZfTO<]3 l~/;;״S3OSzj3'@U?IdR#"J uؿ/~_~郵:*'dB:/!GULxTneLLyq=EǠy=(lbNvDǺS~ʵ~^|ta~NA{ڒn@. rK Xp򅏘ZuxS|:",[MM.pLY[pZnpwLkFB 9RrHfZՂpIB96讑rf 1w[#',XX^L%sm_xg*YQl<2莟N[ _zw5'Ǫ܌p*q 3:y:sNɓ@r>$x8ԽK ޤ&?H>=Eė n Uwd!EH2E, hbGr k-?U*Q9vqvfNDh>\5}.T(>2bֹ=K?@],PClV`h^u.g>{H;OVN40"p Pnf zO8Nkd\7v6 ثFSIt̓⒗ 0/#$nñ-]S7WEޟ Dʶ~ǕQB s6nMz71ey%M|J ,6zİI0m2'a"Kҁ: {@cHI~!}%t!.L~~F5ٜ 0wy7zPΑ'圸PirpIԸ*p_&O|>kdxGߝo|M^<|zcj ?Z+qΒXp3?'x (5oS;s GgO~.[g\wuvvp p-7?'֏QRAUԙ8Nl6amhx^ThKD/9LRGe>}Q(}: cc(d0'f҅cͤ ()1 D&@v3xNq!T)f R "n8k} VHQgS*FbMXvp9H !rK Fe^'P?3y wcAsRT;:l׶<-tX71'( yƵZ݅ @ɪghAl>,6y.]d6ujC7ˣ@Ѓ`r ply,O.us憗X!*|~5Lj.zglu\KtVe$:fq܌j7]W:C&paS@KX: _آXɂ/qט\k3bSR= l&N&N@V: |Mu~QM3NY %xt*DY2~G?Йpx/)PuMXNyGHF|70 $DZw80C=0>tDO7p nU̽/{_2lׯ`-ۗoxy?ȹ[QuHo RH*w?&G7vjeF ۃK*Ro9I ^5 ㋶cWd."A0]tw toM:%Ⴉ!Xyuop#yr*U|no/7PCxUGn " Ƕ Y/˜8N͐齵9,M~ePz[@ b0ARJ_w^I-Rqg{UT)j^l':#wOPc$P^C݇%1tU. 璙ry ]*RnFuJ=}3&uxp6 TbFЩ,I7$ {qM}~>uNwT޾j|YׅI {1rȎiJ{^pHIC5{ZZ:7n( VnAқFyF٫WT2ԾYY౿ꎄwŽ&+9`3Tv`EHUJDufaY8  ͧ/2)%_ n=NWkUGG/HV-E?(~Ds|(a(B^4'iZ65(::Š;v9r|RQ*mRƐg!14n v$y?6]:l&@Kn?΃ze-8t2p:@b?)<1IOܶ͡ߒbg3$M`O 78ZwZGHmO"t>;hկ|@Wτ|Uߗsiiǽ:=<)# 齓q`pk1p th C&?kw'ߐdϨ p3<XW ~)oEnt%TZO^iBm:Mެoh o}ɭ780^Hٔ}q˜70~MgR|zKIícJhY)]5%;+81HG^ԥRE\-F)NTrl s&" :;_8z Ptvtӵ/)Df;S5 كz!dfyiַ!g`-o@UAE_{ Rkj!\Td?4Ź|~-K޾w|һ-yI7kT*d]ڄphy!1S^073Z,O y%Nc6@yb'SXI#տg8TؚD-7 ^( Tl;^>4͒I!]UJ@}V&OoU- -@u)Ξ*LVg=K8m7H]&6Bưq<*SHg˝*5:&ftop5k](j%lT9İRԄRgW'#' Ya I:1$OY5_NŊƟ*4x}"n}QW@7#W,>ĝ0}o= 3O~4•5FCr6@9-X~痱2~R^&7;_V%>;*?޾C*_7|F'aBnf)I,3 _"JV*BntLI?[Hv+&^l2'=ns^ >iнG9\T>˺&8:?,Lm/ ]`)?6g~Ĺ֯=HnZ_ΙgSs`6q̅0yJy&F._I{Xyc?ݬ8So P7| t-&OM\ǙQ(TDG$Hkk΁*=([2<6;1:7:I*-G+0T0pGŗE ؄šmm-)܆*8*1;fpb݌Sz5$?'#Dn.[9 HtZ:9c^Ђtc4<^U#0wc?d>} ?W½:"dþrqfQ~!0WBu-RjC_ʌ7sEq VTg_^c?$M@SřX˅JU9/){Cnm"]nZg_ r[;_MVuׇITҴqxMէqiA{P2O8>ZLTd{=ՒM͵Q[ gf \n:Xr}Flxx@ÍG I%  רt/5@S+pXHKf7q@&]4 XcD[>)HNbI)cq: b7B-sHϜUL fpWʖc>'ȕy+*/A` 遷m |}KoI՜>l!tWtRg] SԌn@lnblËx7) Xܺr `~x{u)йo_hZ1?R?6Qjf^xϟut&ªm5 EV%%p:ͷ<isc 6o:Mp QG^a_ ζ ׹2`bcluy7"DXfL&;\_Fìk_䴐 _su. >ď8r2PC8Lp`u6C{:8D@dB 'r3N8ԙYNP)I~!{:"lH}Nm_:0HE>;M I|_/J[jq\n$sk1¥-Bc|58 9QD*V:H҅C-ikݮk5E*Y9!ś2nOM>B;*g`T]Te=bbL\aZ%Mh]#xBLcV>8b v!U 28#Ơ`3#bȄ#5k*8 WfT`\' ʚ2 k)„-N_- RּDol}*Ͻ(v&&`S8$ɱE[yKi0D\D6; wPr=>yLc30s 87kؾxJEo,S^Rt9-q7ȼB hC7v Z7췛i yBK%ބC ŝnL)w~| oT}{JWZj |23'58!hzU&h`KO#~Vv&M}! '"xd!]8Up'w;XحT#6OmIzgq ͽILTa~Qj&&#&D_xgFҿvw>]t-0,QXZ fxXzbSq@ (_&pk!"RK@XAOwe ǞasM蠀gfRaY~OJ.4 w͡pSSH`K__󹫩w[ Z- 7֝X!ϾùԧZ F}?:,ޤݔ7|]n֦ K;0C_`Y_k*DiH!sڌO!TVX%P7r5S~׷K^yqV)!wߪеj5" O^VOlzm6XuX:uq!pfiG[CkVRKfSq'K˼SjVIONr4@ [֪ea>ƺ:PnB#W~tIf>A>dNq7H*ČuIƚgqkn*{`N!Ua;G~>6N0;q! YISv=#47{½nJxT::Y'RCFĖ^^YKG/6O &x?B{'.,>;On?~~ϩ8?֎ۓ[i?k ?:.+Ȟn y֍`7F/l[`}QN"&޸ `«#`wcP]'%XT۲C=B!64B'= Q6qإ : BGX nRnRRaK>P5֟&3&1uUYub؉?9'}^FD_Bpm gI)U:ÅqkpM +J#0)l+vP {ι|$WߙCdJœ-*H䆫^hrD7iÆ'[ fo U9B" T'`[q+^%g|6),A \ܛܯnRF;&%[q|? 4|>P}Үr*U.LOPǨG'gcnɗ74iu?|b*>,it2GP5hŃW=sa@ehbJ"v.(Ex6qlPOú F6nHO=廞m׿qO`7v'>D9?~}_|_ -k֐::œ," 1LJ": Iq{|J86ċ5fnW gl)p ObYERZ7pmj~mOΫ񕠥/h?OVWe,7-c]s "EcBxR1ݮ5pA]YsgASOCw[=H>6 7x9>Q[5K1|lbcƉMƯw$dS+Fg.zA|ǜ !ZH#vZ#sݑ(5 ͗:Hۺx]CKl[xKkh:#i}'s̼`o"Yf:?aզ#\_Â1ߟo՟%WH^;\훔㡥(> nPkx^h=mJ ŪӃϠN_@t#gprE߱xH_ lZQ:{aF865h~dtĩ8Jۓ/>u;l ҵQS3Ԡb'!ujΧ1@/LkE_*`XuNbZG]>v+ Rfp3Y(Aư4/+M:sOSZo_;hP+u5/vFq p/ްJ=[Rs'?f 7:G+r:UjmxBo'mf2Nҵ u]Iղ]j #3EùkB =c߇O( CMJW1̋ b:Xt4T.b ҍ.э.M W1{C5# *UvǾ#zH"\ie!eݧ9lZ S5 8Bq'djbLrp $]$2TH0I)Mt(eb8lj>TRKTLWd R oܴ\O15Ա2]uU+ Mq6suwDLyV徳Gn\Aa,qB1YZ7unG^+zFݲE.`0F;\pֽcBl'RruoS/)Ujf #`FÈppNCH|ǵOߟ$_|zubɧ<ք~cA/:x©1` I`\^%4s;jn-`y.J% O:eWOb]g r)t$rio6I^FN±>t5]-!2mD71O < y2c͡vиZ.2jG%mHRAPԉw#1;Ye@ ~ځy`]ع&ЍlW+C1o IpRɛ¬7hd xO(~l?;攝S8w³1tCna+F =0qƘ Bq93^لԑ5L+Anq>y#s)^ߝ)ߡ_KU7wA/Kq. ȳ*]%CZ,m:SG@nRZcK}ūu55 u^*mIhX5Ї509^N6Oǘ:Vb' mfQ#ݤ`.@/̟87kP4oHP a1q\Xxm(ch6)סV~Wg$WBsY& *nI 'wJ -Rqn#ȶAr-ckh7@&,p-4 :E *]d@$v{ aq:'N3]O|Y-qo'Zb6P&ydB7NZ!!`\px)L )?_~s .ӹtuz=ơ~_oGى =fMAj~'xt:DiƦFf㳎S([w+q6󀏆apALe{/#:+:)1vmB>JHĊSa:OFAx J4'd7g|Z9 }]$<S\<[_xyYV8]P ]qQٟVgQ>B3+}r#ѓ`N(S:Zp*@r|B_nq|zb;d2:V8'@]-0|7dG.o ~Fln8AHpЭBZ5)5Z3 lϯǟ&ſÿnTPM{V7v lno纱 71Vt QVkx]A5u (1Q|QHj1Ej/L8zh'&~鎐E*0*~,|_+_u3]gYTK>WbG:vrw |=8݄>uc[ao"A=[zd}.F{l@Z+Sw/"c)Q̱ⱋ^X6jR٨33JL)3l4ST]<i_|mg)NF ;Sw. 4wä&h?ť-9,`?}7Ckw|1$6^lIOh$pc'_!-n 8ɏBO6`NX k?>a67>o!ھޮuAv I)Zdmtq{zwOz5/6"u;>x Ra艭58szZ~F Hيo/J zc-NZo:pɠ%Ʈ"S],k]37ssҼzfNaiMAJEs|`z ZnL^ggv[̺kh3ȨftL*wONP7(/`qH;4-žu1_.65ٕl$)K7^M;xW<"&[δnC(L`rh{'<+Fxw^.??;T롎ŽZfDO_J(|7.w\?-2[[)ȆTyOll]|I^rt:d rR=i u. ~WԱQFMs}IA]Xq[ 98TR\p xC3;]ź(C`#)WȀ0,ιA\|rrs7Tl߼ >ӂё9f=?ĥ?{{?8Y>Cs!c&xqx[|GߚRf_&ZY27˔sg~uJ ubz@ 9L*9)0`n|>J 2RErM^uPM/z]/R(6tS^tCc2987?D0;L~QݶhVHI{&sfVgJ8ˁM |<Ѳ._kUn n|Y xqȐz' qъ NF/D4H4=Bӎf 5]eF5NV#١k\k8 ~.8֡)X~ Oy .0J $.'')i7z&xD|;d|ƱJ'b*QU(jC?bhZ 7C)}xw,?DMѯ~A'Gwj;K$Ŝz1$' aEE̾>XW8p UW&+U"-UtWζLY~_5-# 6Y2tQ_%zH :PFȮcou6 M~d&> K\:wh܅+M%xbeR1'~Y訐OkC6: =i4 {㊏$!f8il@9n(6^?C7'XM5ĝ~W ^񧄲/vM';?1'S7煍7)}/%x\53|:JPYvs֎|od@/&?9u~JuJк;0)kFc̖F"hoc>l[ ,7r\1K:<řg0ZOnRJF錫MeItMKQ';ګ|d>Ó1z :uSϥ)&Zܨ]>GT~Jp'p졫|eGͤd")l'5\:g7q%:;)O TeH~Kj\^g!C;'Y[VݏyϣsĽ^I9 o97uYAyu8ѐN`~cBkY2+L/BW9>Fc:!jN(ZChq-"CjJ|kL c ̓e#[3La0֥c`W)5d?iRzp< AT&~Lrn?R> x4xbGpx1v r2Lp1|D.}w_w3Uվs=X`?@IDATXfbwPAZ~+-W \sTfp|XZyw%WYmR|́gec,Ddu'uY6K=qK'K [{j@nԳ;ɗXmS8޼ uRlikJ0λb=6sJU"{շ:!1lM(X (#Dš4*؁d(Y>ÍE JV/ WǨspPvH;J-MdnHX\U ꕌQpR;N!wӼ UucZ鳎xɫ"ADE8vRℎI>ƙmSrqO=qCʘ řOX#x#ǜW9\AZ;&Z||ſϧ)G#dR|Geܩ \|1 u1B+ՌfmXh֕al2Z,8:52 'p?Ѫ,9,o$y>F`XeP ޵24&炬ѡDԜ]ǴRF~4CmuMݳ_D{}kAUU8+T6"o7Ho`BÚ p ΒPw[2AP&R7/ iKnƉ qXx Y-'p$v06<ۜzJ;uOҳH}ȵ\D CIG?:ғ^./\"Q7(T7F[ߟv[H.x X4yt"uB&ߺIqm}v/.}b[^;>:NjZJfsg,'jO&X@2u 5:3o~1s>;Uu>d67;bu+5rz il|75JYxb^Pws%颦9L%ziβדcrݑPn w/nw2:p7s>$qPW3i˂L"#Kn[dl:xZUtD(NWyԺ!zP3 䣞>ǹ\ۓ&-Sn\Eo|77*^k>*K~zf 4|^8h0YtDd';_\)TNJg[뻾ȫd u7)K0E}HS;!=c40~Lk]1 :rn ԤW=&gmew}"mj>.[r>_qJl# ]7Ma- onO-* lDҟK>z^ŷJ5(;.3=jQFnx0F˗  |d~±l6Бf^rFFC0L#Lw+aܩ?6_C^D%&;z;sR<Ѓ^%B?J3 w_]K/Vۃ=ĩt -6=yZEB<,IuNS'Bk cg1򴾶ϾL ?/O8gKڍ8t8%>c 5\qiZ(oqۤqS7)XHDw2gt56|N7\4bã?cszATJ] w&:sf oMK?Ջ-qƔnlx#aĎ 5!ѫnkKXhTm\6H9:ȕGB/b|>e-Vg!p#qWku+ 蚻)}AfT^pH$j WhZo=9h >->+ Ӿq:N3ؗ$5@."& )F(uTd e5 T GJZ#BD_sED}5\; }?IWKW(&69&%T=GU^a[ `GېO`#׹ H 8$7VǮ52a9Λ .՚ƀh\saUD['!3 xC 5ƲF$4m CmP40mŖG5 IgV@\BTB&6J&&c9۲~]/ؔ? \8iH`s[C.wzo%z$н9qӇ5bk?ヨ_ߟ_ᯁsGpun䅆EG8y྆gXJۨuג/X^++؜k݊i8Omթy?l :ߨ"%l"x C8#,tNF*9;,u6;Fgu5ctG-2#'P8wQ+ Vzf%Qw:.mGYi.ᤙdnҖ_WHo[34/U 9׸"UN و \gZSWs^szDGaGrD](V+~;߯S|ReݐVd]7=I'H'a A֐YƬ/7Q@ï}0M"7dW(0MJ]G'ȳ\8'ۦ#գGwRzݻ:GYe}'hc%)3jYˋ m?pRn}q\Աgr+WsLB[o# fٸG=["x2MH tH9#'KLBˁhaiM.: R='E>vk覅T0MOq,Ǘ\fU{I}ʄPlZ.ZpZcIb>@,ԻԐ S"ub)@Uh7/řd;#VOȟc>Oubyoĸ79 Ea_-n0w /jreP=)x7oXxqq!5`mQ2PVMG7*|&%rgs!Qo2oN0}Wt< YQ70EJЉݶI:70]S Ļu|k~ԏVtۑz syW!0$=N 6~xƲB_˥^1r FwsA4{ļQfFUj7C6O\Pģ-;w%q&0s["5r {\A DhĶL݇M9r$/Gϐ{W {r!rqpEOqVw"׋H<7|+A7*? FX/$^]+H2'y?Rd]"̬b /B%BgM 9{H4c;'usmuE|HY? a(;k0{u37qkAzwq >v/3ԕ萱\-Gk=!g;-l$ek\Ʊk؆ IF zjܡ?N<"Wi,W<k66i38! xni ȋrNL*@Xxj'jw4p.u}ޔl `tvcsA0Q <7󉟵.eFz\#t1gSpa7hTd)Y,1̧n>~}UI9 w'q//Gl߬1U-6FMk΋I|u2m hc& L!b ;Xπv׶aHz4_^#1R 9&?XWv8i̹l`-ֹ Da'>۔:FnP$yE`钲Xkt'+@58qЌǎ-8b|]%2L- 3WbVb'~XhPOaH*Z1+ 8#uǞ)ͅo-'ęܒ >眀~08;gmyqO<$* Fmt>l nX~|忔\ߟϨ{!%2IvzH%)XPQ)*YPdfAN Prr ޺Vv55:F[P`"۹nUdlsvX/t# !e3_uCqbu"n_xaEVl&vUu0߂Bʧw<r9AJ/ #"pRm6}?WiE6o%웙*f ߪۢ$\) 4f b^UD/pwts*8| +ѤA@!-Ll(eٜ '/wsc·"<:TZ*7[zpu4Eh 6`~։LM^4cT;!wb^/YC ȇ|\i{*AUk\p)|B u>*1s1`_I=yݿ* wOs+[+'#N #UBIAvQikehJ86@[  vW4u<.`Y+dl ^D?-㒾tA;jyj]bbBT$X\'ʪN ]-a,Euo@l;0{anrꦊ 'J*7(LնF5Sv9cc98[nH<+dQ7+t͖Dj-mēWߴڂ>rR!zKzσg}ץ-.圙Υbm̠2+%) 'zþ_mμ~}pYOh}_msm Un*/}pA_%ӚC+DZ>c3!Abȯ ˤC1hկ]bS3 6=ٗݤt6c uebl:?YsrQB~4!-}ݚJ奬;}yx3.|$I@a|Ji[};@@0$8s"ip;2Gs ~stZż S(c\nN1bH59`!DwP]. 3LLDllT|7tjЍ%.yDmKb:]~^g蟙wۛ4P/@ {h)l&lD+"Lxp'fBIO/RII۴l #7, ɯ_n._.?:!Vzqx%y͹~!/hcHu,ď'xrn ysCmpL\EtREyu_)g"~&u c, 0G5=YPt&a™ݗb1=AfN0n6+<5q=@TҘrU󑸋u^Di-L۶~[[qAcl-^bz n]z3(=CC`{XZNuM ʼڍcǫG%'ߗ;/}/%[ho|?>hmɂ^ң/>L_0rϘmXooRBW) ԧR^jA6/d3|`(/~\i! Gl> YY Q=Gz]F& fw|Ķ _g[%_G* 6qǡ%4!=ZҌ'F@@~BQ6T&::jE8[^gǰI۫YAa6R4Ljcȍv4m(;Rq;BTEs;SkCW:^0]N||JI.?ui) kl!=ϼ]Aݙو37™W7z(_1iEu,o|dBI]jor KM %7ݽ0 _qGenLl@Ou&%#v{గBuBWli.n׳`(z4IxFm N^I@b&o\(nD0l`UpV|ԫ(,C}ṉ~jl`Vtm7 +gRzhs}~(xPߩHafL`:̣KA_J<{d2tM@lc|=A >I x3tWy2vY#7nrFlc4d8 i"LMos-!}>;(#o0̜ŭ댗w8Ǹ_^}3"ʣ;i7(7Oݤl"ISfQ([M k-Kp=UWs2Eej(-RC,@>ުto* x(t7}7ަK6g |s ~p , 46p)$6HX!oCj͢zǏZU*uG!ƄSn2`'j%zPSӖ5~uP q#п[h v:F |_wEůx>bYɜJe҉؀6c`q(|ƦD@lsT 7a2nl5KGwg]~c-x>OSI"U6hHr #k/vxx lm<1c F%ޘ_K#}T~wuQC{+^.95=~qJ4z p$s n&1Y2XfW$]A{!#wjݤs$ܤU)bՏc0 #1=o'6 tR9 =8IJkD xDd`[7@>f@ijɅ hYˏ֘$N8ToN"w/HlJEV&W s [ R8h/Q- >N#" 2nY/ ghͳxmYI?T4r2)/BSsoK:_"s?摐#5nAmp;jmzq|?}npmUħ~<`|졷8i'f|7QuևliЋ$ /T={${HT8dFXK0N=cL7P prI;EukDcHI#@0' !]ʉw7P,?h55vϙKι%|n}z¼\ǨsLu)HYݴ&Z>dh3OLߪ[P݇f n/eƠ N~ޡ p ?!;3qL9oz_6QcثyhgPToϣhîGN%w]e 3pmU$J-6TWT( ϳ7(aFkPY ȣ.'gc9CFD'Z´m!<փ@t+M;f@qۋEhE%pR#L?؎/>}u;z~^|fI=?<[O8κC #akkSO|ȭԕ%v˞* ۵M߰KQ#a[9$5nlkWKf*B:u;vW᥏QAC-)nc!rDʼnKuzKk`f<2un3AD$bG€fNe.qpߡhٹ^ݠ̊LKf3WuJu#RC|l1px){G5^tl e"tmF7w+#:[6~bCV.::s/hR9TyQF23)?$A?>NgSZ973.0^H8}tCI]W@mpb`L~*:D|9缌򄈣;zS}̡'0C#2!cC%])Z{$U 1m# ƞE7X57Je-|ۜ P*= Dv59TO.#tbs\*?TuѤўTHEĚ<{3HDO&CN]U*!uM ¨-byM1EKҠu\GzEB&0ҵZxv};.VtaEߊjԲoK%AKYۥlqm s]-5rc7]l2(MNd L.f ,})RɻS)roYWRgyL({GZţ5jPjO"*@eEblTxBEOMcGlfns (݀fW7?z#0>à RS!2J%La\YnFKC^pUhj`R)Ek/q9A':d\mI[ayKMx[~/?wZ`8уMu[۳AK Mr=`j'ܕ/_qӕDo2!~ OT>{.y|aWw3~FnoP+}-)g'*H-jHRu;ST+ Ji:VW\Qf𰪭q:;_mg|{PtBi&utqb]dXK|kV 6s 6 1|A G[Y)2On6P4L~̚YG5L?Ceڅ/nS?M]ÍWl2(q%eLK=)kL1?e\ژ5?ccU(%`@`.;%ʎWqŜ[4d<:-ku5 #؅3b20'#P$x53N6t820!*sscTJA`#q$x㧺JI(SF ]摵 SSM԰`jvX;Z! w<7hG:nM~̮dW2י N: fj91QEN'2ˑद V 8}گc`pkr=xĥ\vJG_&5N=udİ9f3-iļs?;0H]'Wݪ'iNRkz7DڦBW1`$kSIyvQP-c K` vT6`%S'*c\RTn0qʹ 4m + +Z.'ocǸAusk7NhS<10#Td)S>ԣރUfq\wtL9@`~P.FzAx2YnC>{Nq , 'x@{@k8Dm*~ z+x=!o[a)/\!VNP|`lAEiʀJ1aSu!L@h>lҌzH ^2000yjꡬ /h0R,PIAn*FG2ur=xp=0ILM͒ ᅱ|d!N6f9 030k%B U."0YpD4G6Ph2e H,4HpXE$Af'#P()ADù(cFԐGBoG,a#s\eV1k;p%b Zn+"ӈmFZXȇG[o@ZM'PAdhP5v ŎLk(^jS,^q0c S 7q& h  ?edMK xqm9詆(\OjuC`15҄zomM/[7eENt9Df8p5q=p9<#aCk-[jFX,6R#0k'U%Ek%YG[l#?Gk.u$IV ].~Fm3{D~Uj>k'䰛^i8.` mP ;A .g%0, Ѭut$a`s{P r9VXi8QFov[HU5?D zoPhneW3WD+UM)K\ur)u(t: =t̋fչ(6M#0h5֣:fIyR&)SSSi^/ZMM˞Ji|=4ȂC;xƅUM~IBPt~DvG4 NJSUKEmL9~ zj(7fL|+gn+Ҽzc\[kiM B6^0.)R\8N`L8vd #s1Y v 4KM\ 1vPzRjK͊rE]EWW۲PA0ԳwO;6Z`b`8ݭ7{>Sh#44m I?"Oa7#@6#&*71!>ҶN\Mq(Pf: [tL m)x(*;T(C%7\'[WsjA<9߀WG}N4L] W0)~΢4| 4(_fzP^A6eޏB ,?@{mDGYiE0F) #ӮjP&&RgWB :Kh^j.\AȜӪS~Z)| ! _PZҟ5ѵL}0rj~, w{ᖅc]zs8Oo <97566#T=곺k=01{QBqo n?`áӐ.QA_S8Xgmކ m#SD4GG=K@kB.Q@gaNt[Q"rM^u"W@NKufwг5U:"`;ҭ H '뉁כ3^C<Y+Q ]1ș7luiEe8бθQzT\+lFڥy:[j0?MS|:Ԯ;>mGSzx4)Hm0Wx YܘjƔюx8Op&>6P?u_`Mc+)27'~1W ý.4,PD!Jg!#7 Љ*Z N{($XD.s(C<8= ih- 1kÔt\\튎TG^@^@p;\8:~ہ8vl5(X.SO AȫQy Nh =PǕ/[^Kq:텟}=b?l''PlWP;Z&E]gaeGot) Zp\dj=g]_ã\U!X2@* Tїn9Q2*Fhz@Xrm  {\ׅ<7E:+୳2o|3&NX%@~֕YH,[?p)pJeYi@t$%U( *Ex(ZZ80[ 0N3)UA^SPaQi:Ѝ5,ItͅLB-4Icu~Rr"s*b~(5'`d w+]Z*7f7t-,j#KfvC)k&# Z Fxi gYe [H2QD}pZoF1e2(DvhoEMH[#&nyz*z>9͂}#GEŘ|P럹s񴏼;IK>_X##Z""o_AɠBvrCij4͵XǵxSxv Fw\sEGGP2c#m,ĺ}:rE><63!*GVJ#i>l-G6_< J.?-,%6ͫ(X Dd+(4Pu\@N? hfuO`]Fq$aѦ+EG.G} ҄dPM#06%nArde}3p9˨:U}9kU7!4J)Yn_be0Sb;'&y}E:OfϹdC<)vG :`j< l $ֹ88Qӟ䐓U5Oun.WS6PPb|ҵKлAm"$toJ/h0pku=F\hP؃y 4pc'I:k. y,y|3OB;ssgVNZqiIX P g4uKMg[t<ՈO$:ջZ*󭇃|;ƃ7tqHi93JZ= ~ &T9 H{vcX_0rC,*J4{ٙm|gZWNH" PXjpTl"ebuJi`lf*܇oy03JueRq9 cdV3AP++\g`MM#07޺yȑ: ن6 g^MUy vLVJ|۫?bA8J=h]ȋG'y{R <#9FEm[y4SDG[soHLc}>)n9IvmRQ*18z֛("B(\>h ؀ۀrHkt6<Ն8xPTmHu >b_GNy܏+)9/mTpt<G: r7 0CUDq Є#~Cʖ^M~3^?9j5uӦ qvȯ?NEƣwW[+4= ?9:s1-}+$'`Z@LhO>C征v閇u5ѹ&ܬY](:8ĬP;jhn@cWxȠBQY 5G6 W;f)6*mL(H?p2>&WSWq\ZG)$;hR@ Su3rhW$nHɇtkXC''z:KG'tXGAQ'$2V,K6#34W9@G$|(- BLxT-Gvc[ptS.S-6BQ`]F,5J 6\owNra!dԜdKbqGٌc9NEEO9y hm]F:vt)!GF$5#Pb _6&ܤbJtudI<nIOEEnp`EBW,Hz1gb 3 /E8 <;Ň!D8z m& $j`0<+wP➄2 -Hsk 4l8|hꄢ j 'Wl Mi8h[^_GZ5+/ރ'ؘ.}YŇ1Rm&r-ߋj븄oSlEHѷKxdCzhɸ (BWH),Hަ+v{{ JJ-:5oKQkS B mSc<#M8!-14gKurþgԳv"cZ+gϱ_Gp˘YCVy l։rE'fleu,- XGV?[z姓Jp:!F'+@76shņ Vx\6 k ]aby6|6"0+5ѯ`m/Ĩ(m0ƓmhoAq}&k+T [ֶFu@vQ-DQL*W27OțTEnq׶g1#v8=͞qx"S8D@;o4#P;d{q /NqԎBs݄{p_ʔD;)}v\Q?JwKM/W'/ya37xt CCZ)kPc**vQtl[ ~svurxp9/]'w:s6 4fc͐nU*ƀ@ IMP99hH%ͦDeB5e|r?*={P *=fKmEmJYHQègͼXS_x*qu&&I+R9'`h:Դ/J5] ϰ|7+JD!D7I.ٿ8l+MNl4iHEAa Pb;\3\@` jak~(P.EGhܝb|.:K+OƩb Ƶ!)! pu0 @`G^j!2i0 U&GY^i#,Z.54y8?6GJ=%WQpoed۴%8٩iw!1GHVNNؿ Yxi ^%Jq1U7[EJǤ=:VRA,wp@@ re )tGt7#{Fg.eRBP7=iWAQnw+̟XTBaWP@(ԃMڦAܠ!Ts+ǷzaJ a;ҨI4ȶ9 * yohtzZmA6^~v pX+>_%0;1.WRnZܛ'|p_\ye>6ߣNAMN<'>vFvBcѠRAoMvff(1X2hBsl ?luƚ Z6+٪^q'Jғ(=6 p L,no>D#W4 ځQïy C ؿם%5QnBL6t.%Ÿ$^: 6579ئA> ʚt|j_ \r^AGy ]+ɗ|pܰFv3u8ʜ$7 ]o= 5t "7M Lq?V8.ZڙRԌbXBGUg</R >kӮicif:ts5]2tlA FS}X٢qP(΍nwPmPL`bW@Ad/ac_xxœcp%f;d&Zύr'X#D\OQ6ޛŞ^~Ӹ7-7#P:&$엑) ޔyxm8rg<'OZyGȲ tu?{hY `2Ƶ&85tZgoq;K_zma8q3 q 1@x&kh&痖LDZ$n# CEggmSf).+):DIU+Bځ^\ ]}_Zyz%l.Inia,Z6[IΞK[=,ۑD|}X" Sd [Yf9[,JXJhC䆇8,%pbtﵗ< $ 27lCҼE2(r|ŲtO%iڕ.f-&miM w'mi#~k}p;nH$҆ z&^uX7xni`IXF!]t@}q3(2`>w$PHKċxb]nbmSo܋ՕGۋC|M8LP@t#y"yX 17T@tNU=20k֩19V^x~ny{×]9fެ2,ii9q_HOX\}A/Lrh:eoI;<ЛLF# v.5Rҩ)v^IZAMc9 ͻ'yjpߧ`q`Xy{R=!=jG_y2V]c \ ^;E>l-Co؄(ǂ.J'@55B!Vl;҂4 n#SَiI7\Ǔ/̴kIl@Y\ύ!=hn!O894Cf<<ܿb5tgf>lHV'h5}8|h<&]|ʴ nvѣ#9%ZW1AP.T E _d.:gއW,r#M NQ"XC1so3ÚȺAj}B@em,_^h 5YРqěX]n֍Olَ< 7{tk>fmM5W#BWh.v3b\w8~;ҏ5gn:lnbط#dOkt:PYQh/rW䯑(ՔIQdžH" [3C!!dxǀUPxg,W彦>6D`001vDa2{i⦫ }FZ>n&;3K&VL[kW|M44GOQx]&R=7Lwi]qx[CXui7KoK&m&w%Yy +6}튴@e6;-v;|$~[4q|gޢ|,vu[mOʋWyKO^8|Yն^> Uý(B9Ij@Mo#yŏEˮ g@x{m-Y"JnoY<3wܛ{SH2&3UE>DYnD2uEI 70 f(Q̛,8",HDwu0ڜy\ns Yu]\ۗ`P9S0/8AbhIRVzőL7D8}%ґ:a)t =e+_|O5cH9ہ+w?Y醯|4_IV\'YplfkW+tA&HSAi_/\}Z*z ӥaeW^;#Qy t#OUumߴ&=9w:g9rG-8 (|u׿ ƶ}Ҿ{aYx.$92./I[sP;K5Nޞ ii.{J/f~IKN~%?M{L$\-KWǩ 5_|Zu/+FNi㞡pJ/ȉO>Iy6(,>7*hZI{0PvPLLz&Ґʢ8Gë:8HԵ&)m/3cԠm([,5ۼrjJ_DNd'V+MŊ3}[r吅유 Y!Y(7=B WHFҏd-bfkڰ^MS9-L>hA'ÛWHjsF^55^Zfa[7&?>mYt 4]lg[0eRrM? wlϤz2&7i |M΅lK8=ISip"N7*+l#哲;+Ӛ/^Xu\Iw5֎ݗnm:%t1.\צU& oҵ_ϗ>h.۾=/t[OHS+i[ҵ_x~x__rz0.W}YYڬ£۳^_e[p PA?I[KnpEFd6>kO9ah_0Vã dDx@fȻc2 :V5@:@&;x_Eigj*>Un2HWJ]?tr 'cL(/_8X ٖ R5(㨋 Џ'-I $upCGv֒>(k]-l0,ϒ7ce*Wj㬵)7kEC *˼9(OxYԠw5Oiryʉ?+]~<0M#OzId+ +}^DwL?F6&g%C''>t'>ҸtyrRĚH:ܞɓoϘW3}/صo1үT&[t7NISKo k%۴Xh)/N[=h}囑I\~pi?It\(ϼ;pErqz^~^xfG,W|7IWx7hyٳjݗn;o t_P<雘ϐ)'[Ĵ#WK~s'M?8=g |]iJ+?=T׃Q\!o˭;Vi@QH\7&' {t:M97]*Ix }M_o*r{cwN.r2_~ Ť(~MšD}{D}֟_u_@4.:k Jlg#OX=o~标 Pb;]q&^r-]sqa: vK՝}By߫?z /[sz韗xΚ lzLֱ&QZ;&קk"'%ɀO~A؆nW%] Gn"tRnV(ְb_3 $?)okjn=,Į#h ia+J`AK CB̘ )k1#Q'4I _ܦ{dj*vd~YJˮM~ePsNj6X5~y|/%>7>z0W%:&ѸjMJeU0BnBP"p -~8cdJq[mvmΣZhS59PfMA`W8SShluDk6W IP1:ƶp銐 =+QX^ >+~Z YDŽ\F JX[ Wej%gNNȋ4vݥTi}?9 ȽU{4N|p )W5*|s| *W?,t='`QM~ Y#6[j4}@!ōx&V\Yv29=]{-vٻ@M,t~E?LjQ#L0C({>+S+n:͒f.ui=;e9Ph~4qy7?rgC'G=)/ R\S% \ӽOvT!06bĐtE4rI#z.Ts F4S;c@fs+Vl;tg/T3 3u=qu}4uQ Qߕ7_ E ކNT' A,O0ȝx/ITBP"c4o)/M I āa@`|52^?gفS“7nXg09hV`bbr9;|5uR_D4ܠSZ7^EyO1 ~3_ _~7OI}NS=|Vn;_Nw8p6fc;0*γpf'D 8XY}*}vyo_E31 =P 5mFY:wyOuo?c/12aluc/Q $tMBN]*J^DQ$, eRqА0~:]0WypΊ+J0/ڤ~"Mjر72Îc"O|Z 7C\ׁr56̼Tji`guV v pWócRťMW<Qxbl=dՅS oI Ѝ#k1텻=dK]\ /r}<xcZ\2.oŋp#_!ؕeW]Vcƣh-GݞtW?Ҽ #-V޸ yJ>riקy.׬H7I=i ˑ' ҝ|_\Mį u}GQ99\yBңn=-'% ~j R塉iϋ@gX]E_;hmnx\@vn0k&Vslne+7*:զ8%-ӮᏧ״Nӛw2QHwALƔЦj<qҀ0U):1B'`Zree*뫌1^ FrL(&(x(OtCx'a7<:/O뼟ukO~l'vOp䫹>"P|\"SJw&-  ? ?Ez_$t }!;p`Gke[.ȣȲEUr5e/en^씱%4H=7ˬ8b'gl,ֱlOÆ07q"FصK"PsU[[7$NS]C5vWg t!#I(_R_! $hW6'0k9 :LFg,8GmH9 (-lD,bm̯[}-i5V2,:Qv4FǙq3 si2B]tޒGNժ-vٳJoJδԾtMeg]xhTo^]ӻ 嫖ǞtM]&@:Uh|J~}{xIȣ[} ݢ-2[캗/rɧߝFkCy[Bt|=O\޴37 5y\X||ޔVܸ$~ΏY~O˗N&/ZڏlۑTQ}_PÁn$eܗ2[bxAKBHӽ GM@D@.eE6P6(IsDTte* ƕYDBü W_P'<C&:9޷3$ Ҁ 6tAmi$V6V!o%^m>V@u˷hzň,n #x`iƱZXO&g9οrQrFy=XO<rP]~id?ZZyL(ld^Nt{A6-}M wGi>A:#٣̊=4//}Σp- P9H,=郃CcɆd*w;Ą{_%)McK6mwq 2d3C 0Y(WI-F|RJ*2VLn=LX P+ҲI;ИxT xKyqTIX< _fYx-xПѺN!DP`w1P_rz%TH &(Lj4$>bq.Z=4& )?m?ygy긧/4j߰ e t}.:z׋9tO7z2ď2gS};o7f.aGd`Gcͬvh5,Yu[9AN7n^n_7x yFnW-D-HY&o]r"-_l:74ﹱm[D[sPRyƫw|To}15 }Zl LH0=a}|nciB',s=)xG)9؉`IЮC FYfK`u#zKo>FǤ]WM+F~5XTNLW7' z ջ.\q4 t9F:A0U@B҆:BZDgo :(d{6;ak{Ki+E=&*<- G(:^uW>+~=QHkc/d +Pp_#_c},3~"(l e0e.{F1-eglZh7<䏤_V{UR&+~`o-JGY/BG 9|cz5ilx%cbPoX} fݑȘ@@=luZ\zh-΋m,IW)OZä|0)R=)$kpAM@IDATЖ*Rj~ >'=yX\i iO|P y>J&xLH~o;&9\MIQDZ9Ei=_!cW Оغ5™ēgG,-(<[/PD^!? &B>PE? X%#_T4 D躝d0{qy/ؒ+se{o Z_L+oZU#OpW@ q(_˯K?oW7wGN^z/bnw sg3'|Ҭͷ*]|/~|'>Y?}نkek'<[ަPxW>Zx(P AO-%"0QY#'1W`p-t3f"5 5nF T|ՙ#ShEg z#?'wGu|z%CcR2F&"2 jL dW\Ng|4vϪ4?wI}2$dOXtb#Lt$>:= cɋWK0„BԘn̛TL; "jxsԽPn$/6;kiqe8% $,!A^Z@:5ƶuM}s}ȫ/+ d/ |}?~V "}ĴeQm:@/o?Q/w_P^ug݈#V9n9r?x=tI @??5,qeo~_j_KTx}kU|^; ,s׾(RFzζ;ٲ k/ɟ/#Rs'fA>JձI 'O0X:!/ u84WLH$oGL>c}zƦ42aGV֌aI`&TC+rV:wxL[Ei;sBZBp?(CjMW/-_kv| :G8wл?%"zMZ(NtS5ǥw]t;}&Gzo#3n7 ]0t?4ϝk8eb&-kHפsk_Ukc֨_7󎗶!J/Z(c4qU|bY ~0A]}>S8t q|/s=Oc׍UZ52|/'-LJJMTt";8݌]]ux9VcZvG*_~"\ezҚ;3J-WS_D?ƒdjZd Z H\p#c" YĂN *\Pm֙qϐ5#DQ\]m|?kFѐ#3FeqMP]< dkk#YY:ĿMRZ7?;~L[xJ[N]uQZz9 L9;yzuƍ$po??! H*-sQik94`y5}u5`.~h\diytxœC*9I?*m&W[.+Y"$e/ϻʁy"w_4obE'" L*}Ӣ##O0?Hs7;5]?'b]wi6V|J:ufϗU +tȍ(9$r吗9+3]C7?^{s3rY'>Q㿾$?#t~ʊHgԲ^|[W·»T.xJ ο'UV!}.0YqΨkkEۥ=_[LN^0ZEY &=TG&tJ_nn&/a9w<_YZt5HǼ3~ ?4ǟuE[wj\:Loʋpɽ-G}?*^E!dF'I޵2s^=]I ҁ'Y63~!s X#J믖0 6UDh)dRN2W#AJi"`\&v8ϸdU nbIՊ@NƤout WDdwΒ?mG=9ЬŦ 4 0mA܋vmA2F5IF?uR;؈Z\}s.tF#ҩ6ae2:R9zswymփ^{E_BK|D V.$!W%Ag*VRӚ(V$TD\ Rk> V\Hgkپ}<c=c9z}k꫏1oٯ>{V} ]?g.\mV،ߕzW@~.+g \w?!3Qswz<˯ۛY!&+~[/x]OeqO~ 7L??7,W˯-ks[?E.r _]Q*h/ۗ_#D\/\>Aͻ߻SҿyٯҕOm[q!ƃLD5b/u{JSΝW-~9bM|(`9e0m{# U Mʣu.dʥ ٺ8/ԊC!l|^6e#w夫$ل榙\7^']jOcEFF.: 6HvepU7Al=$wqqP)<^*vUzpvbw@N\zuo|B4Čf?f_IO\V c(BvZwqAgJo6*O$yg/*GyAodCx t 4oC2oߵ{>q6{{?s( h-Ѳۿ|>>Hb$3Ux~9e)\4tBGó(rЅav^A4RY۹ sgfYwmb ^YN"4-oJ_zCyΟ+gB+6C 96ŌHTo ?B,vҥs O'E]iZS =`]Ue)І S-p6 Πt"-ViSNDpCujHkvH`{4vg}$^_"_Ӈ?=͇BOí?9l0XgbcB3c] 8!BٞH8Y>[oؕL?:K81T v Fm&x&-#+WXN$Kkm-ƖE:[n,FL2DhY0˞8Vm:B[Vr!9no_tAϞ+E?x{<{\/OI#ڵ[ZaO'8bq4#EK'__QdػP:2_f`քånw_[>얏`7%u*+a3^3rD}~#v'04|-D&]Hc&j;;e J5bb@=ߡc8rLiFJʕqd՝Dԭk0-3m-&Rm?nAYb}Vr= *(C2郾H=m9yj_w֚tmMl} fPlpg>xxn9۷|fdx#ߥ0xOm [ߚხ| F(!?94ȥ8[십"([Kkg|9/^vFPTn"؀y4ƥrFY+:TA͠Tl(83cIcDW v{md;*sv>Wx3HYM-SHK@Dz }9tOj%nUh'F,q 㚥J 4 ԙ{3)oJ*j"/YXfϐv~oƿw23$%5Tܬ) . NjH 0Wcâ`AB4[y% ZoioӠɉljz=PD ~sWt.Gqboelp߶yk .Na6mG48V2D;/ɋ*NuqR,oK1s^6PTfl[MWqmרv˜tDJT_V L PF\_w9f`ƸOT!T>f3NKÈ}>s3uVoVw>G;Rd|{{>Dw}flmz<?}o_`!g"?n _2ٸ+)3"eDb%Zn-7$0_>o2rrhg1*r6"&)]qgR\Gy5P( fژL:Y"[Pc[{!X<5hU ~}C7k8v8oy. Lq;3)ݢk{dd\v;G̯!X\xJ<+WP|bMWΩr %;h8 n*ԐJJT5UDɼtA}4#@#|[Ba-+S)+ 8G)38}nj#4mu״gl8!"iΗCWئi_kס:YTҷ 'H<>ڭ{ޓޙ*/ ``pm_!4f96&%3+P/ `+ | DO.̭݅)lm;b.u1SEiV$=1p;,U])P3Af>͠HdbJV5J[.,2(*8XfW1e6R.{V~_Hcu-1xĹfes]`3ً_)vmi  ' _Go('9muH斓-khЩhX:A4 PVX56< ELk<@_ mo`"?" vȳ@mA:t8dUz:^)i$RVmk׎on%'7s e%=EV0e#sk#!-G7,ExEKJ6M3-a0&K+V;KiDJ4FvKdlz'B񉳴CdvuU/=jے)7:oiu#lաQV#8O6#<<=?lK %[̉}?g0,00vB,rdU["q&HЬ,LBѪCVe;Q5XK)=@ANQ0g6llߡ+k1֬ʢF(*ۍ0QPr}-gԿ0=jS9%Djjޏjxƣr}"t[98g.qFӹ.>xO{{.?yo%pXxݷ{ Tt31DV%6(@jwx\J!Y }~&b,*@ :דBvF@9C30/hUWԈfa65<9;f^qlB7Impb).g+ ޒ}i{ݿs]>d:=IZެTx13VG n|,l%Ҽw&gNXěۨ'ZI!Rar W}Қe7@KlFhϚVʹ9RX6dloK@hsR--fύya{-JFJS9*[K; 1_2ʣ\&V RŌi1tKM HӬrx,+5?ɢT=rN25vRɲ T$@4d4¾˜,R)|PDڲЯUGQXnexw{:!+HІU2^Erz?I#fQ;>6cbϜ_ڃox;? e5y rFZ2.dp"\v1) +Jȴn;!JNf Z `bVݺn,tVaX$n$)8g%HYM)Ux{tn7(\3tgR>R߾!f}6I\J;ruhea\°W%>HH.|2KA Iow$GyH|C%1,HQuLj98E N_vEezݠmJ~%kau>5r89{vK_޼[6|kB,CzEsm*qhz^ASa`=ϵz_[zk-`m&=jɺ|q ـ2vH9B{`c3kjފ\N%O^r{^w)i$RVS~KйOI NO `8@`'E;ϴhdiE1TH6KwIYC]2̧3Cu|M3;>ǰ}}D%hP[D(=%<1@©^eCT[ɖ ^:nn'B{ EzOˊuGaZߖOB @?ٓ'ۯ-wtnA}^__j;\  eX1Η+y@U<N;æT\UKgW}(U,k~-f⮇n;kƯ'W^`8< J>}_Ć/5u"}~9b8 TZD% xmhq<_ctOiNŻv5,7lՁ RU|ag׋ynSHqBZ;9aCCqV"KՖۣ={}ʖ#}b%:QMvޕ)|p<>7|ϥ` &MS^;4p[ Q9h,Q/`4|`!3 $JhOϥs9b'uĬK̸gUִq-tԣ6>"*<(TŖbdCɈ{bHJvSYHj{i>O`YQOVl0.ĻZW*<. h_1%G%YfO=dr'cz\rPIJ[2M[ #됣J[$W+_Iʲں|u*vA.֖L zpСȝG L6M`!Z30>XG gy^4p0?ħHYM~)!o|h-xy>K'g#H,Td#i WrSS;ry|k%?-يU9+? N`~o|JRǺ C)T` :-PB+Od2,ٱxð%VvGuMmE~bU+n&Ț6A6W 0MMpojHe f(ސ!fC4"~h[0]8ذ_ (遶!N {`K3#*fȾ@*)9dM`=zڠO ?壵1 ~ ]| k Щ(7^0Beӑ rYGl+JWM .ʒ{z8۲eޖ)+bUt/P*16Xpul*mča/BmQvc*6T5q94Djj k#x3nLHئ$te0 8!×#|Mx)諌C87܇oh/;Syo?4K (TuJg'tt3|1WjHf_U)n.07B=nݑSZ_k}\Lo^6#%:anKf4)!+ ;8񊴮59+x[R\Klˢ{f@dm[[˽>Áӯg8˳x+~w^o8t1弄?˚"nIe ï!Z화{SݽL x/ d"iQL xAK`U/.+jO $91'U%D]xj)gڼj lf#̽fP'z >ᩭ-tU u,{x'nmA;8|TNJJv9~hb,7,LkWQ60 ƒQ47vZ\rEt9s*OThky=q66_vq |mo) l>qjךgv>1.2npvurS+&j5?՘0V}Sd{E+ZOR!Z3AlkBՑ[~#=+xR=hIgKC΀7ݠCsh59aO#,56'VwIv3&}FS\x$W*q5p?\l~\lC Qt[6{tZRo}{>e"Cj$Sg@sHRޙ~ © [@!b{R9S||Fq 7v!j=qX:' oUvl}a:Ny'ꂷ';Sn$Wx@X BˆQn 0!;éDoqtBْFJoϠƀ+K .٦㿐EE`jMg0|m&4̦j~]uTl__.lCjJ )`1H<*윭rI1!'\ś6fӪ8mYj\_v.ztmmLNwpxOB݂]_YC y>˂j`aBz-5v}{Gk')d ,@f1@A'`hro+zkukM+TJ?гQ,q/fD날SL ΗY`jWԷꑹs_kGώE=ƶ6jVx[?>%J#%)S|EXcCXcU뙼8l巶Ƌ|_Vaozf%p{)V٥\Ǎ0Mx=Ы =</t e[ iys[ θϩݐ s[gn3mk0ͱL֭}Yd~<\eUh5 DȉT*he!֯*Vz**aq5r.C? :G+MՆR!&"ʍq rx? B[mp򀗳7q6 J]Zf[*+jI&(Y?A([ho0Ac ڟH}F/$ʤjK:UY].[yM[ [?D{󙐾oIaZve+Guܛ*>)%sًs*/1x tV`Վ.qC ~ìdW.,DԔ:O<<OjIgW葇LKK;i]gfgeϤ\l8{I LY" Fѳ-3JyfVsBzܹ,uƭi<@QP5~W;I^ut\[fsk= U5v[-[sIcʶk5;Kʶ56^r"^־^S2yÇy'];v [zdz{[+x/h˒ d69(G˦f"T4lgWB,t2@=J=AD=i;) P9Ƃ Ȭ$r emFf<l=m{B{)n)}\e/nΩu|!kjoH`#5 %ɕ7S4"ǡдM*9)+0t[Lǃ͞`߆ѹ%h %",WdOZc`b_D|cw_]fR*>Z저>Օ^Y]vaG$7*-UbxBrTV`LFęx:zB!@JqCz LXSVj7ÚKHR=Xoײ[M_MmĘ?^rcWq>GEXC*@N٭&"ʆOMQ*Ņ$v̞;y1$_[Ίcgc1qm lhjH왔O6b `_UJ#WcQy+֦Գ=\;NF*+-&%iDRevSDL\ȫ0Mp|VaA؋@ 1 Ξ5Y'7[qU*K,'k^}N;8Y_Eq:J:"/Э O tY8b[TSa ÷b]ԿndiE8Xt$r'8sOqpbGij4[￾Wc{.#hB] .`:=ȧYF > ljʠ* KҌxx-5Hxed-l(DA7p0'cp }qt#M87L]>ee h?t5spbNAG[Wm۲  Oe:(fO[ӱ5e󩷐uh1*Km,G7wVQ1tbYրNkY zďD¶YB%*7RrGx }xW}n?2Ta+C3vT!)0EV0ٌ4& *Vc6#~!+{Pc^H0%aB!.qfZj)e5vb9\vGlz#7'YdfKTJBn/1,Djj7xe/nΩv^$q58>:ld[<8locdwffV#ت/d"/e̘GJHwd'eq/ í3k)ĎOeOgsBx$-ne}AwٻwfQ=rlLE_!]WmځcֲnqDlK^*kZOn@G|-m.QңƎ‹t%WZY #S@,@ .o[ S:ͰA?{m;6i/՚HcnӲ@-p26|[;.~[mUw-qAb OvM ]BFI|/XWn'zEFd.i %krJg 7C:*VLf2V(?lJo3? =:5eت7UxPUsfm樍Jmѫ}HHYMCg$;8qyWB{^C.LQv*[xdWCR.`/kقI5_3:9$|Q .40/ e1j0Eܺn۷pwmOwbCwġC:VT ɣDX5(*`2㲕u.xP3ep[;EChG37n9 l$G}=olY[3&/PeI4A`?@AlpL_g&rmI)$JyY W< s.\kcqZNq\KP 6Tv(5RB.琟ê|IC }#Upx,FKȼ!`ӕM8j7l%X?iMˇXѾnðb<pP)'9m9U!i b.~L|pD|>67 Ph gɑV3!;"mVı-7I?刔^ElDϴ\rWs"%\>b M*[+ 5o sA6>a(u1lha)ۦ:lҧ]:qCY>Ct0[TTD*+[4Z`QLG-W"P9#Z`FE(EE1~RݝEJ>uӞK8:fM傾gdCPB%"T7FJbb6E{}}(]o Kr?smF[f;,fRMavXNw}1;R9^'B[ߎu7#r@8`RphDKuW8Y3LY0%-ův),!lU W2OYph '9մ2/>+y }2DCGL~5,+6 D%t`0.q[?bSY։ (LY 73itp#|hDQ .~G{/[ʣ=[qYGY bJ?H&ɲ)ꑶ,c/OlG&7+_#oہCJUqP}13M6qLdd̒Ą5^͎S(s{_wDp҆eո6yMNmbKv;_t5ldt%;tIG:@xXCe \Y.7ZlE{k|qWXJRrSS3bBj{P[,[L ')LbV`֡h/vmycl /xΤ)<+t;ߡI)DfPFq&ցj z1[[gaL 4$uBPG;*IaΨSȸÆSd"B^ %IR{LwhDjj/]}v}o}}j~ַwͤ\KX1MjDb]Y`/!jCAk0PxX{/lERc(X^`Ba-]Ax f̌,n)~"_y/eb[+{A#nsprn1[-̓c?Dp*9#msn&K|3`ީAtMC ޞ\1(ѭPq2ݺ6)Q9OÌK{$lf9Al[^umaȈ6b3)3)-.v;SD]de=b~ J,8$֑C(Jڊzf5tj[<ʰuFt4H1<(AW(C,) 3 mΌKYYąG{29pS xT09#_Նjbwݼ[7SHRu@KT&vVAp@mm| J"0lP G#v <00-ݶcA3%hbȾqNym}gR0X._A.o5U{pjpLKEn)SG-RB|;VHp` 2< DP3R2U zZҲ: L>U PR\D3c @n_{bڪchf=9M24'' 7eW!]Ww`^IcxD ARY n3GtYLጏ51ZDgB x0H'y!?"/USj/OG6z84@zoHm7a&^n̄"F3%O3(l;Q6N:TD ÏwJ5 hMx0l:ǵ“Z(8OF54P$S75z1sMTM.Vam)遟`kЎ]s93m'V %!ԅߪrj݂al+m'Nt{Z 0`@ 09'Gar~&P 8 5eޱ[UL k.~l +%g+6Ԋ4mB2Hx k cly|7vg"7tǸGJᮖq8ә`iZ PHHH#A .a3mN.h&]dUiP؋9kW]h+lOl!Դ ))֮'u[}rqs|lV wRꍌ3"1 H0`#>WE5 mA$6ǹf ]ߵd/4:Cg܃J43n<]:[9/g(q6;b^ӵ_ֵBrЇ18 m_֏4QUS6A`# 3WX}oHOnfw$hϿδ#vo2|zrX2߮ni t@3BǷm[AÝ60 Φ@[50%|ɐ oNSL(lW|!ħn |sV%"鯲RBc{p{{v TlQC^,.Out5re<&V1 6+@=wfP`g Adܙ&%+(\mm #Vm9;T3P)yߦz[K(Q#nm-k16p޸s^mV5w6w06``#f@61Pzʧzr 97=2ja, b˼b^:/6&0Rb}J 8o/7|*M~)}?%%?2U1:BVV;%:4 JKi`#*3 !l$+.2pQH*Q 휻Dg!"vUeUDMwaWi4mxb}"[_vQL,Imי`+v5FV޶b|e߻"",j74 jȜ@c@S; Ć͜!;9 3uO.' (4cCO?7BDZƷRg\+,S'`m I⹺Y>1vNmczi:TtE=! Cb{Tjd5X\1)a| ӱ0VZyq#8SRHtb҆ yU&Ŏv;' Q>3 ~ܩ_?II*sXlS$Am<ρY;6-1l0,CN M \|BW) B`.9Vx( MORxy7hQԟ36훢AXh5x47-3)W˞N`8,*rR@ H !ǞPaiX؀W9v> k/KR\5`kB Km*].7l,,F)6/P@::MBqd.ಏ2W8F~[J%F.9X~rNds1`&Z=]o,yNe Ej\U+VjypQY\̧`-Ow6s ȭAH-_ΒV]VIUxi>JLUBvPS3Q#lz&l_Rfۍ^")ll[moXMU ۦ'y:Rb1ʟ"s(?,i`98zy8Y)vVyK8oLvkp*a:yۻu~kgq I[T-o{[+0N#qIR *tnG(GuV%pޑ8EԬھU&T7fq2څ@ZI2D൩= z/MF1]5SB)m$*~jUl"C|wd1}TO8/ڶisCK2d;| 0|@a|5b f@\pW`InC(V&|˻TK.fiLWFvY#x9޶LKh^TzojgRfRrɢ=XV`b] ȓPAITB&m/ l^ؚБʁ"4cvC@.-f9#1tLԄ\ڞp2i ~an*ƃ~PWKQ O` ohgRL N\tu԰r?[D[,]GU0k (E@hhz ٫ $odY*,tl?sRO!W|'sWA30Bnm^N 3VReyVdR,d>- JdVrAßPXbrKvd"{7#9W|C<̻u&=1-5' 铫plo=)\载ï ͤ(ڵC?Ieݒ+Jiuw(ߩt&U@ɯ{Qĉ5-k p<J(|с7.SSᛞ&"dS3T^?k&'24=Dg"̿O> L 2 mMP01V̈*ݖC 2)22u+dO^ޣx\r"[i&鼰/Kuhc&+B}mS3)i&?0kO5}fzlOf`PwtV51""(V.2WIީ@d:oAb {,&\uRO¶&ho+9.WKmI(<XvhUĤmK^C9耛m 9 #&P%>5l4{,>pZb-#IOp,>jWg{N'j5U Rږ΍`ݶ}L vvϦb\Q PĞ=,tpN[44)DV+s4 mHH8'r>G]^0ˆ+8(Z3xajK E?_Gq[ W6zlO0ޓЛh\ƌrR7%]78C:g; (cQD 8}x:1l@@fD>!#: \&- DhՄþ5MST.7=;+qxkecU6Ғ9:&Qy89DHBWnqz+g4̤[z&e։~fd36# t|LʤZR9Z*I*uiqh#67ŧU )~TjF"|(2!l.fX*u(/n۬ eb7Nb@ݑ'K#aR2v@<| 4N*-%ntq&dECZ-5MN^Z8ϠyMoUr!bCg*o8clvJ8lpXVYC|tBJ,S];^4_jb`"8AaFaX^#!ߨ$q Z<~_4p ߸0blHo%%L/3[CE9pNH497xT%*| ;E `a;@4Q8P;GY8YyrPmlDKYm9ESv$x adc*uVD2nGQ2+5N_)bP ,C7 ~#oصa!Օɯ?6vL7yfNOxO'ۘy'pqA1Tv ̠L' Ov" ,$YԲ0TXZ*-GYl_;8Aa+9e&p))'hj/0яT98(:Ǐ`x#5*PP5*$U\66- A2d:bL8Rs|pޛ[[wtQ1fZo}Ǽl]u7"ʄ;&m'cq00g?/{oŷ{c@'I܄h6@;5c' ۈ['cJo0#KOY}р!JVX'5$Gy" E~ڦ4'BwXY|j&ǝL/d&;0_CO#Ӷuw8Y a?TT+<+@ MJ9roU|Qobqцɀ6L8ZElWN9D,s [h&RJR,[){ gJ45 UƷy$cx`%l~5/M7zY8u 4BK3씴 m`y`JƗ گ ŭcVɞx 23{p^O Gھqju7-v`YN,)eRZ wԠQn >Z,$_9D)@ /p f J69Uk=e1I"걲%C`K+Ea7nBO1'OK>UDV(I`MC9\mڇ;)PW$7r4i22>% (KfɅ0Ke n@.娚$f!+G\|js  y6ψۃy >i$/Um"RuD+K eF<]VІ.ypJh"@)I LLɎv?d.nsg5EO m\ڤ ]apt&ާ޻,|B q% G(_5NcB9h D:{(4Eo.xí\!d!6myo%AJ _fJ+h\e'I]\jw/dm"yjZ@&ꏬ*u->B +X,3 &`l;؜ T{y~fLyڍM"7U{Ȋ]A8ZG}F˛{{ٻoWЫq6xZ @kg j @3?uz@&3'$R>~gϠ[|,a5?#=v[p:gs ߚci;QQtG3IENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/hdf_compass/compass_viewer/icons/readme.txt0000666000000000000000000000041414544243123021536 0ustar00This folder contains icons from the KDE Oxygen project, used in the HDFCompass GUI. Refer to "license.txt" for the icon licensing terms. The following images are NOT under the LGPL, and are (c) Heliosphere Research LLC (all rights reserved): logo.png logo.xcf././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/hdf_compass/compass_viewer/icons/save_24.png0000666000000000000000000000214114544243123021510 0ustar00PNG  IHDRw=bKGD pHYs a aJ%tIME #/dIDATHǵKUfz  ~!AMd!_A]{nč 1$$+$p-Џzs[]5 ;}79?7.vY3N9Ky|x>'!T1K˙R՗`揝8~ggfν⡗X;ʶfݓ8ށs7G709{Lɂ?L07sCMQU 35 +ڍ8gѢxx F(`DQ T 2QP"tb7'dF@1osE8z`G"DQ*0[j EHlI$`Em94]@c(ԢH#S2HW"UϿ㽧т,³`^&e${a*LĪRq@dӿpunN5HxIeL)_yo4M `f ^ZWֳszloL-7EfbE~O>m%j*>viXrs/^ﯢlg玝&Xm񈍍?yUeu*ᰩ !cvDbL ر9pӽ{|w Ng8bfuM#UYaJ#]hT;TPICKDR1<bEAHD`{TU  f ňR%$2F꺦k|JYͨ) @=F1L%ISp>;~, Aʲ9ѨcDU),0R[|֤w_><|e˃ݽ{g{ 8DիM3h4*Eyo;-jc`f5pkc?yTo}={f 缪>oH|Y5XL|y2_0 0z|zIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/hdf_compass/compass_viewer/icons/view_icon_24.png0000666000000000000000000000175514544243123022546 0ustar00PNG  IHDRw=IDATxڵh[UyiM 4g1̱ڡ( vE2h@D0\n 1wQغvĴM]׼Z;ҭ?{{~/bv "Jnwn~.}WD5_'^;v49Ϻ$IPUuB^óܴO)f yye+PBܜ:ؽ%[AggWiL&Q T,I^tt|T`&x |]>M|;0JRekih}. )R)+[ \o$^]T`kF\ T< N1?/;pM9:}I_KOSA|nLLأjYn.o:*+`x"ӑ(A/]&pOab|X*V00ƫڼWp.PTŌruMk:ż&@$A~ Mz.`X0[ncr#0 UZ$b03K3XiBg{띹ڶT?DM&Ec~=;K3) P? TLwTjދH‘+x?>< ,O_O%Z4nka"p$WJsZs܏B ƃqZ]pF0fGF[itNPMk @' 3.2Nm]4xJ,˲f3T*1}s6ڂ`0R)R]TJN$j>}M^Ƀd2)Rj$4~st݁fd4B&%4&nnnrdǏ$k-;Ps䏋ݫjsg(!E)Zh{{h\.GNyL>Y,kpba2)F(G@9Coz_~/IENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/hdf_compass/compass_viewer/icons/view_list_32.png0000666000000000000000000000077414544243123022570 0ustar00PNG  IHDR szzIDATxMjPo1jR+X#^ZW݊p$x^@vebԪI_L Uхycf~L`xCpd#%@Ob1i*&;!Iғq 0L=p$(`ˇ@zȲX;f.*B+{dY!:6wi!67AƐ$8rٽ6wiΫ ҿmƲ~DjH4Xb@" CS w տfAN'Zu,KvRVa6r!NuZb@&?*Lu0}!xm\.f>7r܉u P*(AA>_bt ۍBZ8&@<jޯvC$TRSiolG HIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/hdf_compass/compass_viewer/icons/view_tree_32.png0000666000000000000000000000134714544243123022551 0ustar00PNG  IHDR szzIDATxŗ?LQǿ?+m( n-5.Eea`a`;@ ,ԁ0ԚŪqМ'RO65|%i]{<U.bԑ0́UU- ˲pԘ <_)dl6g`Y `ssKud(Y*wZ\,(`LUY ^ ey,A28.W1@y hlkk$T_q.45 :jRd$SxCKK"no PWw t"D"`0Hf zX ]]]GwwOxvo-[, Bכq30;;#_}/pn .tfffk`=aaE7߰ʣ×LOO/7iPoKt -W| w Z`>$R翻aTVV_ONNT.WZ,3W:;;|>?ۛSSSwQX.}t;|Vݣ>>> [ZZ>< @ ɁCCCZ􅅅90U?88E(jFF)FYBCC)eeemB?|QBa077Gt=-;;&,]R$g cbKBcquu5$&&ٸ R$u]][]qq` Sدo-,,<Txee巑D^orٙH"ǍӁBt'|zzzvww;1`T??? ^dZ[[K?9CDU-C&h455`~~^ Vm+^Έfuuurr蒊4|- + toqqi'899ck>9 C-}8L e(p~O - j'6nnn߱XH:>>V`n~HvKKK///lmm]kw׀*&I0CS >SVCb 7.F"װwc2@(LkD-1rQqxA (IENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/hdf_compass/compass_viewer/icons/viz_hist_24.png0000666000000000000000000000150314544243330022412 0ustar00PNG  IHDRw=sBIT|d pHYs``zxEtEXtSoftwarewww.inkscape.org<IDATHOkEoy<77mI4MR(fW \H⦸r} }RA\ƍҺljrs~.IIf13s3bzo⃮4ף?= eB~tk~o<hNp`0nNLٻ}dw\o_2}n'/v _y8J]hcmUqnY7O~~$@w$=HmB1(VX_tB/v7,.t@ \#9Q`/ow\;{%)PPcV]` Nڕ:YilrY_8[/hI {B[X[ó#g fF/NB*IO9IQ]l>m}i4%g9_Xt4RyRLײ!)BR4UQ. jFQP`Y&3@NE:hbQ,TR"&d[d= Sdoq~0/sOhs|ti, ~>B6ƫxFl@#yIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/hdf_compass/compass_viewer/icons/viz_plot_24.png0000666000000000000000000000156514544243123022431 0ustar00PNG  IHDRw= )^PF,7 oZ8`  O/+?l5w4͛Qr " ȑƏu *s' b>} UWwZ$nzX&<< WQP mHK+z( ,ISS,)HSt4 ڹجV6C+\w7fR(7]~ 6ǎҮW)&"''@(eImPFY[*͸hHJ~XGf} {RLȉ>8@uW䖷7$!EJnحYctZa)fU*:X2[Z5~~t$a9“`,wѭBN=[SCN>믩 9a,ql+57ړKM6$2WaIIOXX?<\0̿ssIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/hdf_compass/compass_viewer/icons/viz_plot_32.png0000666000000000000000000000316414544243123022425 0ustar00PNG  IHDR szz;IDATx͗KLIǿ` ]b"G7xuP5&ca WeDU8l@FV+":wI1U]}z ~@(,lxtҏ$#>r~#|}{|X,9z(B@ Yl9y"4 nԟ!޽{sXZZbBCx%MI$~= YI!O›7oc!˲jںw*  JsH@wfh(: `:H( '5 pπ;,zuCgC͛ $68晌BZ*xдU>KDp怆 ذa2tP @6a~ދ9 тu\rzŤ$,kh14BF`^)x-BOa;vpΝ;r@(3(f`l,/_zq7.b09c`LA=8y.BSS &@R QYƆ8UE^$dܛ٧WmrB^Kl時u)D\{΄\AmmGtj}xҊ9=7Duzc:|-hbN^1'*^ЂIg^v.C} "j:4U3 .Y3ڂ>1 Lզo ֦%%萈۳#x:xaw0[l5*l" L^{z \zOz0qќt-Y@S[ 륣_'Î,x5fI8#N~knJ,MPć (%:h}|ulnIE1feyʋ!` >Jm+T1Y&HeTۧ"ŝ5D *J$dϱh&tt@@ Q@!wϊ7Û1*9Jʹ9:UIE0h I2T]fv*o:DJKypϬO!4'ǁ9hh|{=PX> n Em^-AhɌ+zĶQj,=3hѷ$D P߳ vy;-^Fv_+"?2 ̬dߴ/MQ"JIENDB`././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1714825026.9729922 hdf_compass-0.7b15/hdf_compass/compass_viewer/image/0000777000000000000000000000000014615423503017511 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/hdf_compass/compass_viewer/image/__init__.py0000666000000000000000000000202314544243330021616 0ustar00############################################################################## # Copyright by The HDF Group. # # All rights reserved. # # # # This file is part of the HDF Compass Viewer. The full HDF Compass # # copyright notice, including terms governing use, modification, and # # terms governing use, modification, and redistribution, is contained in # # the file COPYING, which can be found at the root of the source code # # distribution tree. If you do not have access to this file, you may # # request a copy from help@hdfgroup.org. # ############################################################################## from hdf_compass.compass_viewer.image.frame import ImageFrame import logging logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/hdf_compass/compass_viewer/image/frame.py0000666000000000000000000000440614544243330021160 0ustar00############################################################################## # Copyright by The HDF Group. # # All rights reserved. # # # # This file is part of the HDF Compass Viewer. The full HDF Compass # # copyright notice, including terms governing use, modification, and # # terms governing use, modification, and redistribution, is contained in # # the file COPYING, which can be found at the root of the source code # # distribution tree. If you do not have access to this file, you may # # request a copy from help@hdfgroup.org. # ############################################################################## """ Implements a simple true-color image viewer. """ import wx import wx.grid import logging logger = logging.getLogger(__name__) from hdf_compass.compass_viewer.frame import NodeFrame class ImageFrame(NodeFrame): """ Top-level frame displaying objects of type compass_model.Image. """ def __init__(self, node, **kwds): """ Create a new array viewer, to display *node*. """ NodeFrame.__init__(self, node, title=node.display_name, size=(800, 400), **kwds) self.node = node p = ImagePanel(self, node) self.view = p class ImagePanel(wx.Panel): """ Panel inside the image viewer pane which displays the image. """ def __init__(self, parent, node): """ Display a true color, pixel-interlaced (not pixel-planar) image """ wx.Panel.__init__(self, parent) b = wx.BitmapFromBuffer(node.width, node.height, node.data) b.CopyFromBuffer(node.data) sizer = wx.BoxSizer(wx.HORIZONTAL) sb = wx.StaticBitmap(self, wx.ID_ANY, b) sizer.AddStretchSpacer() sizer.Add(sb, 1, wx.EXPAND | wx.ALIGN_CENTER | wx.ALIGN_CENTER_VERTICAL) sizer.AddStretchSpacer() sizer2 = wx.BoxSizer(wx.VERTICAL) sizer2.AddStretchSpacer() sizer2.Add(sizer, 1) sizer2.AddStretchSpacer() self.SetSizer(sizer2) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/hdf_compass/compass_viewer/info.py0000666000000000000000000001135214544243330017735 0ustar00############################################################################## # Copyright by The HDF Group. # # All rights reserved. # # # # This file is part of the HDF Compass Viewer. The full HDF Compass # # copyright notice, including terms governing use, modification, and # # terms governing use, modification, and redistribution, is contained in # # the file COPYING, which can be found at the root of the source code # # distribution tree. If you do not have access to this file, you may # # request a copy from help@hdfgroup.org. # ############################################################################## """ Defines the left-hand side information panel used to display context info. """ import wx import logging logger = logging.getLogger(__name__) from hdf_compass import compass_model from hdf_compass.utils import is_win # Info panel width PANEL_WIDTH = 200 # Size of the main title font FONTSIZE = 16 if is_win else 18 class InfoPanel(wx.Panel): """ Panel displaying general information about the selected object. Designed to be displayed vertically; sets its own width (PANEL_WIDTH). """ def __init__(self, parent): """ Constructor. *parent* should be the containing frame. """ wx.Panel.__init__(self, parent, size=(PANEL_WIDTH, 10), style=wx.BORDER_NONE) font = wx.Font(FONTSIZE, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL) # Sidebar title text self.name_text = wx.StaticText(self, style=wx.ALIGN_LEFT | wx.ST_ELLIPSIZE_MIDDLE, size=(PANEL_WIDTH - 40, 30)) self.name_text.SetFont(font) # Sidebar icon (see display method) self.static_bitmap = None # Descriptive text below the icon self.prop_text = wx.StaticText(self, style=wx.ALIGN_LEFT) self.sizer = sizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(self.name_text, 0, wx.LEFT | wx.TOP | wx.RIGHT, border=20) sizer.Add(self.prop_text, 0, wx.LEFT | wx.RIGHT, border=20) sizer.AddStretchSpacer(1) self.SetSizer(sizer) self.SetBackgroundColour(wx.Colour(255, 255, 255)) def display(self, node): """ Update displayed information on the node. See the get* methods for specifics on what's displayed. """ self.name_text.SetLabel(node.display_name) self.prop_text.SetLabel(describe(node)) if self.static_bitmap is not None: self.sizer.Detach(self.static_bitmap) self.static_bitmap.Destroy() # We load the PNG icon directly from the appropriate Node class png = wx.Bitmap(type(node).icons[64], wx.BITMAP_TYPE_ANY) self.static_bitmap = wx.StaticBitmap(self, wx.ID_ANY, png) self.sizer.Insert(1, self.static_bitmap, 0, wx.ALL, border=20) self.sizer.Layout() self.Layout() def describe(node): """ Return a (possibly multi-line) text description of a node. """ desc = "%s\n\n" % type(node).class_kind if isinstance(node, compass_model.Array): desc += "Shape\n%s\n\nType\n%s\n" % \ (node.shape, dtype_text(node.dtype)) elif isinstance(node, compass_model.Container): desc += "%d items\n" % len(node) if not isinstance(node, compass_model.KeyValue): # see if there is a key-value handler for this node handlers = node.store.gethandlers(node.key) for h in handlers: kv_node = h(node.store, node.key) if isinstance(kv_node, compass_model.KeyValue): num_keys = len(kv_node.keys) if num_keys > 0: desc += "\n%d %s\n" % (len(kv_node.keys), type(kv_node).class_kind) return desc def dtype_text(dt): """ String description appropriate for a NumPy dtype """ logger.debug("dtype kind: %s, size: %d" % (dt.kind, dt.itemsize)) if dt.names is not None: logger.debug("dtype names: %s" % ",".join(n for n in dt.names)) return "Compound (%d fields)" % len(dt.names) if dt.kind == 'f': return "%d-byte floating point" % dt.itemsize if dt.kind == 'u': return "%d-byte unsigned integer" % dt.itemsize if dt.kind == 'i': return "%d-byte signed integer" % dt.itemsize if dt.kind == 'S': return "ASCII String (%d characters)" % dt.itemsize if dt.kind == 'U': return "Unicode String (%d characters)" % dt.itemsize return "Unknown" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1714825026.9769914 hdf_compass-0.7b15/hdf_compass/compass_viewer/keyvalue/0000777000000000000000000000000014615423503020254 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/hdf_compass/compass_viewer/keyvalue/__init__.py0000666000000000000000000000202714544243330022365 0ustar00############################################################################## # Copyright by The HDF Group. # # All rights reserved. # # # # This file is part of the HDF Compass Viewer. The full HDF Compass # # copyright notice, including terms governing use, modification, and # # terms governing use, modification, and redistribution, is contained in # # the file COPYING, which can be found at the root of the source code # # distribution tree. If you do not have access to this file, you may # # request a copy from help@hdfgroup.org. # ############################################################################## from hdf_compass.compass_viewer.keyvalue.frame import KeyValueFrame import logging logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/hdf_compass/compass_viewer/keyvalue/frame.py0000666000000000000000000000650514544243330021725 0ustar00############################################################################## # Copyright by The HDF Group. # # All rights reserved. # # # # This file is part of the HDF Compass Viewer. The full HDF Compass # # copyright notice, including terms governing use, modification, and # # terms governing use, modification, and redistribution, is contained in # # the file COPYING, which can be found at the root of the source code # # distribution tree. If you do not have access to this file, you may # # request a copy from help@hdfgroup.org. # ############################################################################## """ Implements a viewer for key-value stores (instances of compass_model.KeyValue). Keys are strings, values are any data type HDFCompass can understand. Presently this means all NumPy types, plus Python str/unicode. """ import wx import logging logger = logging.getLogger(__name__) from hdf_compass.compass_viewer.frame import NodeFrame class KeyValueFrame(NodeFrame): """ A frame to display a list of key/value pairs, their types and shapes. """ def __init__(self, node, pos=None): """ Create a new frame. node: Container instance to display. """ title = 'Attributes of "%s"' % node.display_name NodeFrame.__init__(self, node, size=(800, 400), title=title, pos=pos) self.list = KeyValueList(self, node) self.view = self.list class KeyValueList(wx.ListCtrl): """ A simple list view of key/value attributes """ def __init__(self, parent, node): """ Create a new attribute list view. parent: wxPython parent object node: compass_model.KeyValue instance """ wx.ListCtrl.__init__(self, parent, style=wx.LC_REPORT | wx.LC_SINGLE_SEL | wx.BORDER_NONE | wx.LC_HRULES) self.node = node self.InsertColumn(0, "Name") self.InsertColumn(1, "Value") self.InsertColumn(2, "Type") self.InsertColumn(3, "Shape") names = list(node.keys) values = [self.node[n] for n in names] def itemtext(item, col_id): name = names[item] data = values[item] text = "" if col_id == 0: text = name elif col_id == 1: text = str(data) elif col_id == 2: if hasattr(data, 'dtype'): text = str(data.dtype) else: text = str(type(data)) elif col_id == 3: if hasattr(data, 'shape'): text = str(data.shape) else: text = "()" return text for n in names: row = self.InsertItem(9999, n) for col in range(1, 4): self.SetItem(row, col, itemtext(row, col)) self.SetColumnWidth(0, 200) self.SetColumnWidth(1, wx.LIST_AUTOSIZE) self.SetColumnWidth(2, wx.LIST_AUTOSIZE) self.SetColumnWidth(3, wx.LIST_AUTOSIZE) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1714825026.9829936 hdf_compass-0.7b15/hdf_compass/compass_viewer/text/0000777000000000000000000000000014615423503017413 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/hdf_compass/compass_viewer/text/__init__.py0000666000000000000000000000227314544243330021527 0ustar00############################################################################## # Copyright by The HDF Group. # # All rights reserved. # # # # This file is part of the HDF Compass Viewer. The full HDF Compass # # copyright notice, including terms governing use, modification, and # # terms governing use, modification, and redistribution, is contained in # # the file COPYING, which can be found at the root of the source code # # distribution tree. If you do not have access to this file, you may # # request a copy from help@hdfgroup.org. # # # # author: gmasetti@ccom.unh.edu # ############################################################################## from hdf_compass.compass_viewer.text.frame import TextFrame, XmlFrame import logging logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1713637114.0 hdf_compass-0.7b15/hdf_compass/compass_viewer/text/frame.py0000666000000000000000000003371314611003372021060 0ustar00############################################################################## # Copyright by The HDF Group. # # All rights reserved. # # # # This file is part of the HDF Compass Viewer. The full HDF Compass # # copyright notice, including terms governing use, modification, and # # terms governing use, modification, and redistribution, is contained in # # the file COPYING, which can be found at the root of the source code # # distribution tree. If you do not have access to this file, you may # # request a copy from help@hdfgroup.org. # # # # author: gmasetti@ccom.unh.edu # ############################################################################## """ Implements a viewer frame for compass_model.Array. """ import os import logging import wx logger = logging.getLogger(__name__) from hdf_compass.compass_viewer.text.text_ctrl import TextViewerFrame, XmlStc from hdf_compass.compass_viewer.frame import NodeFrame # Menu and button IDs ID_FIND_TEXT_MENU = wx.NewId() ID_SAVE_TEXT_MENU = wx.NewId() ID_VALIDATE_XML_MENU = wx.NewId() ID_FIND_XML_MENU = wx.NewId() ID_SAVE_XML_MENU = wx.NewId() class TextFrame(NodeFrame): icon_folder = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, 'icons')) """ Top-level frame displaying objects of type compass_model.Text. From top to bottom, has: 1. Toolbar (see TextFrame.init_toolbar) 2. A TextCtrl, which displays the text. """ def __init__(self, node, pos=None): """ Create a new array viewer, to display *node*. """ super(TextFrame, self).__init__(node, size=(800, 400), title=node.display_name, pos=pos) logger.debug("init") self.node = node self.txt = wx.TextCtrl(self, 1, style=wx.TE_MULTILINE | wx.TE_READONLY) self.txt.SetValue(node.text) self.find_data = None self.start = 0 self.last_search = None find_menu = wx.Menu() find_menu.Append(ID_FIND_TEXT_MENU, "Find Text\tCtrl-F") self.add_menu(find_menu, "Find") save_menu = wx.Menu() save_menu.Append(ID_SAVE_TEXT_MENU, "Save Text\tCtrl-T") self.add_menu(save_menu, "Save") self.toolbar = None self.init_toolbar() gridsizer = wx.BoxSizer(wx.VERTICAL) gridsizer.Add(self.txt, 1, wx.EXPAND) self.view = gridsizer self.Bind(wx.EVT_MENU, self.on_find_dialog, id=ID_FIND_TEXT_MENU) self.Bind(wx.EVT_MENU, self.on_save, id=ID_SAVE_TEXT_MENU) self.Bind(wx.EVT_FIND, self.on_find) self.Bind(wx.EVT_FIND_NEXT, self.on_find) self.Bind(wx.EVT_FIND_CLOSE, self.on_close_find) def init_toolbar(self): """ Set up the toolbar at the top of the window. """ t_size = (24, 24) find_bmp = wx.Bitmap(os.path.join(self.icon_folder, "find_24.png"), wx.BITMAP_TYPE_ANY) save_bmp = wx.Bitmap(os.path.join(self.icon_folder, "save_24.png"), wx.BITMAP_TYPE_ANY) self.toolbar = self.CreateToolBar(wx.TB_HORIZONTAL | wx.NO_BORDER | wx.TB_FLAT | wx.TB_TEXT) self.toolbar.SetToolBitmapSize(t_size) self.toolbar.AddStretchableSpace() self.toolbar.AddTool(ID_FIND_TEXT_MENU, " Find ", find_bmp) self.toolbar.AddTool(ID_SAVE_TEXT_MENU, " Save ", save_bmp) self.toolbar.Realize() def on_save(self, evt): """ User has chosen to save the current Text """ logger.debug("saving: %s" % self.node.key) save_file_dialog = wx.FileDialog(self, "Save XML file", "", "text.txt", "Text files (*.txt)|*.txt", wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT) if save_file_dialog.ShowModal() == wx.ID_CANCEL: return # the user changed idea... # save the current contents in the file # this can be done with e.g. wxPython output streams: with open(save_file_dialog.GetPath(), 'w') as fod: fod.write(self.node.text) def on_find_dialog(self, evt): # self.txt = self.txt.GetValue() self.find_data = wx.FindReplaceData() # initializes and holds search parameters self.find_data.SetFlags(wx.FR_DOWN) dlg = wx.FindReplaceDialog(self.txt, self.find_data, 'Find') dlg.Show() def on_find(self, evt): flags = evt.GetFlags() down = flags & wx.FR_DOWN > 0 whole = flags & wx.FR_WHOLEWORD > 0 case = flags & wx.FR_MATCHCASE > 0 logger.debug("Down search: %s, whole words: %s, case sensitive: %s > %s" % (down, whole, case, flags)) end_of_words = [" ", "\n", ",", ";", ".", "-"] find_string = evt.GetFindString() if find_string != self.last_search: self.last_search = find_string self.start = 0 len_str = len(find_string) txt = self.txt.GetValue() if not case: find_string = find_string.lower() txt = txt.lower() if down: if whole: while True: pos = txt.find(find_string, self.start) if pos == -1: break logger.debug("%s: %s, %s" % (txt[pos - 1: pos + len_str], txt[pos - 1], txt[pos + len_str + 1])) if txt[pos - 1] in end_of_words and txt[pos + len_str] in end_of_words: logger.debug("%s from %s down > pos: %s" % (find_string, self.start, pos)) break self.start = pos + 1 else: pos = txt.find(find_string, self.start) logger.debug("%s from %s down > pos: %s" % (find_string, self.start, pos)) else: if whole: while True: pos = txt.rfind(find_string, 0, self.start) if pos == -1: break if txt[pos - 1] in end_of_words and txt[pos + len_str + 1] in end_of_words: logger.debug("%s from %s down > pos: %s" % (find_string, self.start, pos)) break self.start = pos - 1 else: pos = txt.rfind(find_string, 0, self.start) logger.debug("%s from %s up > pos: %s" % (find_string, self.start, pos)) if pos == -1: dlg = wx.MessageDialog(self, 'No match for %s' % find_string, 'Text Search', wx.OK | wx.ICON_INFORMATION) dlg.ShowModal() dlg.Destroy() self.start = 0 return lines = len(txt[:pos].splitlines()) - 1 logger.debug("lines: %d" % lines) end = pos + len_str self.txt.SetSelection(pos + lines, end + lines) self.txt.SetFocus() self.start = pos + 1 def on_close_find(self, evt): evt.GetDialog().Destroy() self.start = 0 class XmlFrame(NodeFrame): icon_folder = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, 'icons')) """ Top-level frame displaying objects of type compass_model.Text. From top to bottom, has: 1. Toolbar (see ArrayFrame.init_toolbar) 2. A TextCtrl, which displays the text. """ def __init__(self, node, pos=None): """ Create a new array viewer, to display *node*. """ super(XmlFrame, self).__init__(node, size=(800, 400), title=node.display_name, pos=pos) logger.debug("init") self.node = node self.xml = XmlStc(self, xml_string=self.node.text) self.find_data = None self.start = 0 self.last_search = None find_menu = wx.Menu() find_menu.Append(ID_FIND_XML_MENU, "Find Text\tCtrl-F") self.add_menu(find_menu, "Find") self.text_viewer = None save_menu = wx.Menu() save_menu.Append(ID_SAVE_XML_MENU, "Save xml\tCtrl-X") self.add_menu(save_menu, "Save") if self.node.has_validation(): val_menu = wx.Menu() val_menu.Append(ID_VALIDATE_XML_MENU, "Validate xml\tCtrl-V") self.add_menu(val_menu, "Validate") self.toolbar = None self.init_toolbar() gridsizer = wx.BoxSizer(wx.VERTICAL) gridsizer.Add(self.xml, 1, wx.EXPAND) self.view = gridsizer self.Bind(wx.EVT_MENU, self.on_find_dialog, id=ID_FIND_XML_MENU) self.Bind(wx.EVT_MENU, self.on_save, id=ID_SAVE_XML_MENU) if self.node.has_validation(): self.Bind(wx.EVT_MENU, self.on_validate, id=ID_VALIDATE_XML_MENU) self.Bind(wx.EVT_FIND, self.on_find) self.Bind(wx.EVT_FIND_NEXT, self.on_find) self.Bind(wx.EVT_FIND_CLOSE, self.on_close_find) def init_toolbar(self): """ Set up the toolbar at the top of the window. """ t_size = (24, 24) find_bmp = wx.Bitmap(os.path.join(self.icon_folder, "find_24.png"), wx.BITMAP_TYPE_ANY) save_bmp = wx.Bitmap(os.path.join(self.icon_folder, "save_24.png"), wx.BITMAP_TYPE_ANY) validate_bmp = None if self.node.has_validation(): validate_bmp = wx.Bitmap(os.path.join(self.icon_folder, "xml_validate_24.png"), wx.BITMAP_TYPE_ANY) self.toolbar = self.CreateToolBar(wx.TB_HORIZONTAL | wx.NO_BORDER | wx.TB_FLAT | wx.TB_TEXT) self.toolbar.SetToolBitmapSize(t_size) self.toolbar.AddStretchableSpace() self.toolbar.AddTool(ID_FIND_XML_MENU, "Find", find_bmp) self.toolbar.AddTool(ID_SAVE_XML_MENU, "Save", save_bmp) if self.node.has_validation(): self.toolbar.AddTool(ID_VALIDATE_XML_MENU, "Validate", validate_bmp) self.toolbar.Realize() def on_find_dialog(self, evt): # self.txt = self.txt.GetValue() self.find_data = wx.FindReplaceData() # initializes and holds search parameters self.find_data.SetFlags(wx.FR_DOWN) dlg = wx.FindReplaceDialog(self.xml, self.find_data, 'Find') dlg.Show() def on_find(self, evt): flags = evt.GetFlags() down = flags & wx.FR_DOWN > 0 whole = flags & wx.FR_WHOLEWORD > 0 case = flags & wx.FR_MATCHCASE > 0 logger.debug("Down search: %s, whole words: %s, case sensitive: %s > %s" % (down, whole, case, flags)) end_of_words = [" ", "\n", ",", ";", ".", "-"] find_string = evt.GetFindString() if find_string != self.last_search: self.last_search = find_string self.start = 0 len_str = len(find_string) txt = self.xml.GetValue() if not case: find_string = find_string.lower() txt = txt.lower() if down: if whole: while True: pos = txt.find(find_string, self.start) if pos == -1: break logger.debug("%s: %s, %s" % (txt[pos - 1: pos + len_str], txt[pos - 1], txt[pos + len_str + 1])) if txt[pos - 1] in end_of_words and txt[pos + len_str] in end_of_words: logger.debug("%s from %s down > pos: %s" % (find_string, self.start, pos)) break self.start = pos + 1 else: pos = txt.find(find_string, self.start) logger.debug("%s from %s down > pos: %s" % (find_string, self.start, pos)) else: if whole: while True: pos = txt.rfind(find_string, 0, self.start) if pos == -1: break if txt[pos - 1] in end_of_words and txt[pos + len_str + 1] in end_of_words: logger.debug("%s from %s down > pos: %s" % (find_string, self.start, pos)) break self.start = pos - 1 else: pos = txt.rfind(find_string, 0, self.start) logger.debug("%s from %s up > pos: %s" % (find_string, self.start, pos)) if pos == -1: dlg = wx.MessageDialog(self, 'No match for %s' % find_string, 'Text Search', wx.OK | wx.ICON_INFORMATION) dlg.ShowModal() dlg.Destroy() self.start = 0 return end = pos + len_str self.xml.SetFocus() self.xml.GotoPos(pos) self.xml.SetSelection(pos, end) self.start = pos + 1 def on_close_find(self, evt): evt.GetDialog().Destroy() self.start = 0 def on_validate(self, evt): """ User has chosen to validate the current XML """ if self.node.has_validation(): logger.debug("validating: %s" % self.node.key) self.text_viewer = TextViewerFrame(self.node.validation) self.text_viewer.Show() else: logger.warning("this node type has not validation: %s" % self.node) def on_save(self, evt): """ User has chosen to save the current XML """ logger.debug("saving: %s" % self.node.key) save_file_dialog = wx.FileDialog(self, "Save XML file", "", "text.xml", "Xml files (*.xml)|*.xml", wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT) if save_file_dialog.ShowModal() == wx.ID_CANCEL: return # the user changed idea... # save the current contents in the file # this can be done with e.g. wxPython output streams: with open(save_file_dialog.GetPath(), 'w') as fod: fod.write(self.node.text.decode()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/hdf_compass/compass_viewer/text/text_ctrl.py0000666000000000000000000002521714544243330022003 0ustar00############################################################################## # Copyright by The HDF Group. # # All rights reserved. # # # # This file is part of the HDF Compass Viewer. The full HDF Compass # # copyright notice, including terms governing use, modification, and # # terms governing use, modification, and redistribution, is contained in # # the file COPYING, which can be found at the root of the source code # # distribution tree. If you do not have access to this file, you may # # request a copy from help@hdfgroup.org. # # # # author: gmasetti@ccom.unh.edu # ############################################################################## """ Implements a viewer frame for compass_model.Array. """ import logging import wx import wx.stc as stc logger = logging.getLogger(__name__) from hdf_compass.compass_viewer.frame import BaseFrame if wx.Platform == '__WXMSW__': faces = {'times': 'Times New Roman', 'mono': 'Courier New', 'helv': 'Arial', 'other': 'Comic Sans MS', 'size': 10, 'size2': 8, } else: faces = {'times': 'Times', 'mono': 'Courier', 'helv': 'Helvetica', 'other': 'new century schoolbook', 'size': 13, 'size2': 11, } class TextViewerFrame(BaseFrame): """ Base class for Matplotlib plot windows. Override draw_figure() to plot your figure on the provided axes. """ def __init__(self, data, title="Validation"): """ Create a new Matplotlib plotting window for a 1D line plot """ BaseFrame.__init__(self, id=wx.ID_ANY, title=title, size=(800, 400)) self.data = data self.txt = wx.TextCtrl(self, 1, style=wx.TE_MULTILINE | wx.TE_READONLY) self.txt.SetValue(self.data) gridsizer = wx.BoxSizer(wx.VERTICAL) gridsizer.Add(self.txt, 1, wx.LEFT | wx.TOP | wx.GROW) self.view = gridsizer class XmlStc(stc.StyledTextCtrl): def __init__(self, parent, xml_string, keywords=None): stc.StyledTextCtrl.__init__(self, parent, -1) self.SetLexer(stc.STC_LEX_XML) if keywords is not None: self.SetKeyWords(0, keywords) # Global default styles for all languages self.StyleSetSpec(stc.STC_STYLE_DEFAULT, "face:%(mono)s,size:%(size)d" % faces) self.StyleClearAll() # Reset all to be like the default # Global default styles for all languages self.StyleSetSpec(stc.STC_STYLE_DEFAULT, "face:%(mono)s,size:%(size)d" % faces) self.StyleSetSpec(stc.STC_STYLE_LINENUMBER, "back:#C0C0C0,face:%(helv)s,size:%(size2)d" % faces) self.StyleSetSpec(stc.STC_STYLE_CONTROLCHAR, "face:%(other)s" % faces) self.StyleSetSpec(stc.STC_STYLE_BRACELIGHT, "fore:#FFFFFF,back:#0000FF,bold") self.StyleSetSpec(stc.STC_STYLE_BRACEBAD, "fore:#000000,back:#FF0000,bold") self.StyleSetSpec(stc.STC_H_VALUE, "fore:#0000cc,face:%(helv)s,size:%(size)d" % faces) self.StyleSetSpec(stc.STC_H_DEFAULT, "fore:#0000cc,face:%(helv)s,size:%(size)d" % faces) self.StyleSetSpec(stc.STC_H_ENTITY, "fore:#0000cc,face:%(helv)s,size:%(size)d" % faces) # initial XML tag self.StyleSetSpec(stc.STC_H_XMLSTART, "fore:#ffcccc,bold,face:%(helv)s,size:%(size)d" % faces) self.StyleSetSpec(stc.STC_H_XMLEND, "fore:#ffcccc,bold,face:%(helv)s,size:%(size)d" % faces) # XML tags self.StyleSetSpec(stc.STC_H_TAG, "fore:#555555,face:%(helv)s,size:%(size)d" % faces) self.StyleSetSpec(stc.STC_H_TAGEND, "fore:#555555,face:%(helv)s,size:%(size)d" % faces) self.StyleSetSpec(stc.STC_H_TAGUNKNOWN, "fore:#555555,face:%(helv)s,size:%(size)d" % faces) # XML attributes self.StyleSetSpec(stc.STC_H_ATTRIBUTE, "fore:#BDB76B,face:%(helv)s,size:%(size)d" % faces) self.StyleSetSpec(stc.STC_H_ATTRIBUTEUNKNOWN, "fore:#BDB76B,face:%(helv)s,size:%(size)d" % faces) # XML comments and quotes self.StyleSetSpec(stc.STC_H_COMMENT, "fore:#888888,face:%(helv)s,size:%(size)d" % faces) self.StyleSetSpec(stc.STC_H_SINGLESTRING, "fore:#aa2222,face:%(helv)s,size:%(size)d" % faces) self.StyleSetSpec(stc.STC_H_DOUBLESTRING, "fore:#aa2222,face:%(helv)s,size:%(size)d" % faces) self.StyleSetSpec(stc.STC_H_SGML_SIMPLESTRING, "fore:#aa2222,face:%(helv)s,size:%(size)d" % faces) self.StyleSetSpec(stc.STC_H_SGML_DOUBLESTRING, "fore:#aa2222,face:%(helv)s,size:%(size)d" % faces) # Keyword self.StyleSetSpec(stc.STC_P_WORD, "fore:#990000,size:%(size)d" % faces) # Caret color self.SetCaretForeground("BLUE") # Selection background self.SetSelBackground(1, '#66CCFF') try: self.SetSelBackground(True, wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHT)) self.SetSelForeground(True, wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHTTEXT)) except: self.SetSelBackground(True, wx.SystemSettings_GetColour(wx.SYS_COLOUR_HIGHLIGHT)) self.SetSelForeground(True, wx.SystemSettings_GetColour(wx.SYS_COLOUR_HIGHLIGHTTEXT)) self.SetProperty("fold", "1") # Enable folding self.SetProperty("fold.html", "1") # Enable folding self.SetProperty("tab.timmy.whinge.level", "1") # Highlight tab/space mixing (shouldn't be any) self.SetMargins(3, 3) # Set left and right margins self.SetMarginType(1, stc.STC_MARGIN_NUMBER) # Set up the numbers in the margin for margin #1 self.SetMarginWidth(1, 40) # Reasonable value for, say, 4-5 digits using a mono font (40 pix) # self.SetViewWhiteSpace(False) self.SetWrapMode(stc.STC_WRAP_WORD) self.SetUseVerticalScrollBar(True) self.SetUseHorizontalScrollBar(True) # Setup a margin to hold fold markers self.SetMarginType(2, stc.STC_MARGIN_SYMBOL) self.SetMarginMask(2, stc.STC_MASK_FOLDERS) self.SetMarginSensitive(2, True) self.SetMarginWidth(2, 15) # marker style self.MarkerDefine(stc.STC_MARKNUM_FOLDEREND, stc.STC_MARK_BOXPLUSCONNECTED, "white", "black") self.MarkerDefine(stc.STC_MARKNUM_FOLDEROPENMID, stc.STC_MARK_BOXMINUSCONNECTED, "white", "black") self.MarkerDefine(stc.STC_MARKNUM_FOLDERMIDTAIL, stc.STC_MARK_TCORNER, "white", "black") self.MarkerDefine(stc.STC_MARKNUM_FOLDERTAIL, stc.STC_MARK_LCORNER, "white", "black") self.MarkerDefine(stc.STC_MARKNUM_FOLDERSUB, stc.STC_MARK_VLINE, "white", "black") self.MarkerDefine(stc.STC_MARKNUM_FOLDER, stc.STC_MARK_BOXPLUS, "white", "#cccccc") self.MarkerDefine(stc.STC_MARKNUM_FOLDEROPEN, stc.STC_MARK_BOXMINUS, "white", "black") self.Bind(stc.EVT_STC_MARGINCLICK, self.on_margin_click) self.SetText(xml_string) self.SetEditable(False) def on_margin_click(self, evt): """ Folds and unfolds as needed """ if evt.GetMargin() == 2: if evt.GetShift() and evt.GetControl(): self.fold_all() else: line_clicked = self.LineFromPosition(evt.GetPosition()) if self.GetFoldLevel(line_clicked) & stc.STC_FOLDLEVELHEADERFLAG: if evt.GetShift(): self.SetFoldExpanded(line_clicked, True) self.expand_item(line_clicked, True, True, 1) elif evt.GetControl(): if self.GetFoldExpanded(line_clicked): self.SetFoldExpanded(line_clicked, False) self.expand_item(line_clicked, False, True, 0) else: self.SetFoldExpanded(line_clicked, True) self.expand_item(line_clicked, True, True, 100) else: self.ToggleFold(line_clicked) def fold_all(self): line_count = self.GetLineCount() expanding = True # find out if we are folding or unfolding for line_num in range(line_count): if self.GetFoldLevel(line_num) & stc.STC_FOLDLEVELHEADERFLAG: expanding = not self.GetFoldExpanded(line_num) break line_num = 0 while line_num < line_count: level = self.GetFoldLevel(line_num) if level & stc.STC_FOLDLEVELHEADERFLAG and (level & stc.STC_FOLDLEVELNUMBERMASK) == stc.STC_FOLDLEVELBASE: if expanding: self.SetFoldExpanded(line_num, True) line_num = self.expand_item(line_num, True) line_num -= 1 else: last_child = self.GetLastChild(line_num, -1) self.SetFoldExpanded(line_num, False) if last_child > line_num: self.HideLines(line_num + 1, last_child) line_num += 1 def expand_item(self, line, do_expand, force=False, vis_levels=0, level=-1): last_child = self.GetLastChild(line, level) line += 1 while line <= last_child: if force: if vis_levels > 0: self.ShowLines(line, line) else: self.HideLines(line, line) else: if do_expand: self.ShowLines(line, line) if level == -1: level = self.GetFoldLevel(line) if level & stc.STC_FOLDLEVELHEADERFLAG: if force: if vis_levels > 1: self.SetFoldExpanded(line, True) else: self.SetFoldExpanded(line, False) line = self.expand_item(line, do_expand, force, vis_levels - 1) else: if do_expand and self.GetFoldExpanded(line): line = self.expand_item(line=line, do_expand=True, force=force, vis_levels=(vis_levels - 1)) else: line = self.expand_item(line=line, do_expand=False, force=force, vis_levels=(vis_levels - 1)) else: line += 1 return line ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/hdf_compass/compass_viewer/viewer.py0000666000000000000000000002172314544243330020306 0ustar00############################################################################## # Copyright by The HDF Group. # # All rights reserved. # # # # This file is part of the HDF Compass Viewer. The full HDF Compass # # copyright notice, including terms governing use, modification, and # # terms governing use, modification, and redistribution, is contained in # # the file COPYING, which can be found at the root of the source code # # distribution tree. If you do not have access to this file, you may # # request a copy from help@hdfgroup.org. # ############################################################################## """ Main module for HDFCompass. Defines the App class, along with supporting infrastructure. """ # Must be at the top, to ensure we're the first to call matplotlib.use. import matplotlib matplotlib.use('WXAgg') import traceback import sys import wx import logging logger = logging.getLogger(__name__) from hdf_compass import compass_model from hdf_compass import utils from hdf_compass.compass_viewer.events import ID_COMPASS_OPEN from hdf_compass.compass_viewer import container, array, geo_surface, geo_array, keyvalue, image, frame, text __version__ = utils.__version__ class CompassImageList(wx.ImageList): """ A specialized type of image list, to support icons from Node subclasses. Instances of this class hold only square icons, of the size specified when created. The appropriate icon index for a particular Node subclass is retrieved using get_index(nodeclass). Image addition and indexing is completely bootstrapped; there's no need to manually add or register Node classes with this class. Just call get_index and the object will figure it out. """ def __init__(self, size): """ Create a new list holding square icons of the given size. """ wx.ImageList.__init__(self, size, size) self._indices = {} self._size = size def get_index(self, node_class): """ Retrieve an index appropriate for the given Node subclass. """ if node_class not in self._indices: png = wx.Bitmap(node_class.icons[self._size], wx.BITMAP_TYPE_ANY) idx = self.Add(png) self._indices[node_class] = idx return self._indices[node_class] class CompassApp(wx.App): """ The main application object for HDFCompass. This mainly handles ID_COMPASS_OPEN events, which are requests to launch a new window viewing a particular node. Also contains a dict of CompassImageLists, indexed by image width. """ def __init__(self, redirect): """ Constructor. If *redirect*, show a windows with console output. """ wx.App.__init__(self, redirect) self.imagelists = {} for size in (16, 24, 32, 48, 64): self.imagelists[size] = CompassImageList(size) self.Bind(wx.EVT_MENU, self.on_compass_open, id=ID_COMPASS_OPEN) self.SetAppName("HDFCompass") def on_compass_open(self, evt): """ A request has been made to open a node from somewhere in the GUI """ open_node(evt.node, evt.kwds.get('pos')) def MacOpenFile(self, filename): """ A file has been dropped onto the app icon """ url = 'file://' + filename open_store(url) def open_node(node, pos=None): """ Open a viewer frame appropriate for the given Node instance. node: Node instance to open pos: 2-tuple with current window position (used to avoid overlap). """ if pos is not None: # The thing we get from GetPosition isn't really a tuple, so # you have to manually cast entries to int or it silently fails. new_pos =(int(pos[0])+40, int(pos[1])+40) else: new_pos = wx.DefaultPosition logger.debug("Top-level open called for %s" % node) if isinstance(node, compass_model.Container): f = container.ContainerFrame(node, pos=new_pos) f.Show() elif isinstance(node, compass_model.GeoSurface): f = geo_surface.GeoSurfaceFrame(node, pos=new_pos) f.Show() elif isinstance(node, compass_model.GeoArray): f = geo_array.GeoArrayFrame(node, pos=new_pos) f.Show() elif isinstance(node, compass_model.Array): f = array.ArrayFrame(node, pos=new_pos) f.Show() elif isinstance(node, compass_model.Xml): f = text.XmlFrame(node, pos=new_pos) f.Show() elif isinstance(node, compass_model.Text): f = text.TextFrame(node, pos=new_pos) f.Show() elif isinstance(node, compass_model.KeyValue): f = keyvalue.KeyValueFrame(node, pos=new_pos) f.Show() elif isinstance(node, compass_model.Image): f = image.ImageFrame(node, pos=pos) f.Show() else: pass def open_store(url): """ Open the url using the first matching registered Store class. Returns True if the url was successfully opened, False otherwise. """ stores = [x for x in compass_model.get_stores() if x.can_handle(url)] if len(stores) > 0: instance = stores[0](url) open_node(instance.root) return True return False def can_open_store(url): """ checks url for first matching registered Store class. Returns True if the url can be successfully opened, False otherwise. """ stores = [x for x in compass_model.get_stores() if x.can_handle(url)] if len(stores) > 0: instance = stores[0](url) return True return False def load_plugins(): """ Helper function that attempts to load all the plugins """ # provide some info about the env in use import platform logger.debug("Python %s %s on %s %s (%s)" % (platform.python_version(), platform.architecture()[0], platform.uname()[0], platform.uname()[2], platform.uname()[4])) import numpy logger.debug("numpy %s" % numpy.__version__) logger.debug("matplotlib %s" % matplotlib.__version__) logger.debug("wxPython %s" % wx.__version__) try: import cartopy logger.debug("cartopy %s" % cartopy.__version__) except ImportError: logger.debug("cartopy N/A") from hdf_compass import compass_model try: from hdf_compass import filesystem_model except ImportError: logger.warning("Filesystem plugin: NOT loaded") try: from hdf_compass import array_model except ImportError: logger.warning("Array plugin: NOT loaded") try: from hdf_compass import hdf5_model import h5py logger.debug("h5py %s" % h5py.__version__) except ImportError as e: logger.info(e) logger.warning("HDF5 plugin: NOT loaded") try: from hdf_compass import bag_model from hyo2 import bag from lxml import etree logger.debug("hyo2.bag %s" % bag.__version__) logger.debug("lxml %s (libxml %s, libxslt %s)" % (etree.__version__, ".".join(str(i) for i in etree.LIBXML_VERSION), ".".join(str(i) for i in etree.LIBXSLT_VERSION))) except (ImportError, OSError): traceback.print_exc() logger.warning("BAG plugin: NOT loaded (%s)") try: from hdf_compass import asc_model except ImportError: logger.warning("Ascii grid plugin: NOT loaded") try: from hdf_compass import opendap_model from pydap import lib logger.debug("pydap %s (protocol %s)" % (".".join(str(i) for i in lib.__version__), ".".join(str(i) for i in lib.__dap__))) except ImportError: logger.warning("Opendap plugin: NOT loaded") try: from hdf_compass import hdf5rest_model except ImportError: logger.warning("HDF5 REST plugin: NOT loaded") try: from hdf_compass import adios_model import adios logger.debug("ADIOS %s" % adios.__version__) except ImportError: logger.warning("ADIOS plugin: NOT loaded") def run(): """ Run HDFCompass. Handles all command-line arguments, etc. """ import os.path as op app = CompassApp(False) load_plugins() urls = sys.argv[1:] for url in urls: if "://" not in url: # assumed to be file path url = utils.path2url(op.abspath(url)) if not open_store(url): logger.warning('Failed to open "%s"; no handlers' % url) f = frame.InitFrame() if utils.is_darwin: wx.MenuBar.MacSetCommonMenuBar(f.GetMenuBar()) else: f.Show() app.MainLoop() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1714825026.9889996 hdf_compass-0.7b15/hdf_compass/filesystem_model/0000777000000000000000000000000014615423503016745 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/hdf_compass/filesystem_model/__init__.py0000666000000000000000000000204014544243330021051 0ustar00############################################################################## # Copyright by The HDF Group. # # All rights reserved. # # # # This file is part of the HDF Compass Viewer. The full HDF Compass # # copyright notice, including terms governing use, modification, and # # terms governing use, modification, and redistribution, is contained in # # the file COPYING, which can be found at the root of the source code # # distribution tree. If you do not have access to this file, you may # # request a copy from help@hdfgroup.org. # ############################################################################## from hdf_compass.filesystem_model.model import Filesystem, Directory, File import logging logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/hdf_compass/filesystem_model/model.py0000666000000000000000000001137014544243330020420 0ustar00############################################################################## # Copyright by The HDF Group. # # All rights reserved. # # # # This file is part of the HDF Compass Viewer. The full HDF Compass # # copyright notice, including terms governing use, modification, and # # terms governing use, modification, and redistribution, is contained in # # the file COPYING, which can be found at the root of the source code # # distribution tree. If you do not have access to this file, you may # # request a copy from help@hdfgroup.org. # ############################################################################## """ Example data model which represents the file system. Subclasses just two node types: Container and Array, representing directories and files respectively. """ import os import os.path as op import numpy as np import logging logger = logging.getLogger(__name__) from hdf_compass import compass_model class Filesystem(compass_model.Store): """ A "data store" represented by the file system. Keys are absolute paths on the local file system. """ @staticmethod def plugin_name(): return "Filesystem" @staticmethod def plugin_description(): return "A plugin used to browse local files and folders." def __contains__(self, key): return op.exists(key) @property def url(self): return self._url @property def display_name(self): return "Local file system" @property def root(self): return self['/'] @property def valid(self): return self._valid @staticmethod def can_handle(url): if url == "file://localhost": logger.debug("able to handle %s? yes" % url) return True logger.debug("able to handle %s? no" % url) return False def __init__(self, url): if not self.can_handle(url): raise ValueError(url) self._url = url self._valid = True def close(self): self._valid = False def get_parent(self, key): if key == "/": return None return self[op.dirname(key)] class Directory(compass_model.Container): """ Represents a directory in the filesystem. """ class_kind = "Directory" @staticmethod def can_handle(store, key): return op.isdir(key) def __init__(self, store, key): self._store = store self._key = key try: self._names = os.listdir(key) except OSError: # Permissions, etc. self._names = [] @property def key(self): return self._key @property def store(self): return self._store @property def display_name(self): bn = op.basename(self.key) if len(bn) == 0: return "/" return bn @property def description(self): return 'Folder "%s" (%d members)' % (self.display_name, len(self)) def __len__(self): return len(self._names) def __iter__(self): for name in self._names: key = op.join(self.key, name) yield self._store[key] def __getitem__(self, idx): key = op.join(self.key, self._names[idx]) return self._store[key] class File(compass_model.Array): """ Represents a file (all loaded as an array of bytes) """ class_kind = "File" @staticmethod def can_handle(store, key): return op.isfile(key) def __init__(self, store, key): self._store = store self._key = key @property def key(self): return self._key @property def store(self): return self._store @property def display_name(self): return op.basename(self.key) @property def description(self): return 'File "%s", size %d bytes' % (self.display_name, op.getsize(self.key)) @property def shape(self): return (op.getsize(self.key),) @property def dtype(self): return np.dtype('u1') def __getitem__(self, args): try: with open(self.key, 'rb') as f: data = np.fromstring(f.read(), dtype='u1') except (OSError, IOError): data = np.zeros((len(self),), dtype='u1') return data[args] Filesystem.push(File) Filesystem.push(Directory) compass_model.push(Filesystem) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/hdf_compass/filesystem_model/test.py0000666000000000000000000000213114544243330020272 0ustar00############################################################################## # Copyright by The HDF Group. # # All rights reserved. # # # # This file is part of the HDF Compass Viewer. The full HDF Compass # # copyright notice, including terms governing use, modification, and # # terms governing use, modification, and redistribution, is contained in # # the file COPYING, which can be found at the root of the source code # # distribution tree. If you do not have access to this file, you may # # request a copy from help@hdfgroup.org. # ############################################################################## from hdf_compass.compass_model.test import container, store from hdf_compass.filesystem_model import Filesystem, Directory url = "file://localhost" s = store(Filesystem, url) c = container(Filesystem, url, Directory, "/") ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1714825026.993991 hdf_compass-0.7b15/hdf_compass/hdf5_model/0000777000000000000000000000000014615423503015407 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/hdf_compass/hdf5_model/__init__.py0000666000000000000000000000205014544243330017514 0ustar00############################################################################## # Copyright by The HDF Group. # # All rights reserved. # # # # This file is part of the HDF Compass Viewer. The full HDF Compass # # copyright notice, including terms governing use, modification, and # # terms governing use, modification, and redistribution, is contained in # # the file COPYING, which can be found at the root of the source code # # distribution tree. If you do not have access to this file, you may # # request a copy from help@hdfgroup.org. # ############################################################################## from hdf_compass.hdf5_model.model import HDF5Store, HDF5Group, HDF5Dataset, HDF5KV import logging logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/hdf_compass/hdf5_model/model.py0000666000000000000000000002375414544243330017073 0ustar00############################################################################## # Copyright by The HDF Group. # # All rights reserved. # # # # This file is part of the HDF Compass Viewer. The full HDF Compass # # copyright notice, including terms governing use, modification, and # # terms governing use, modification, and redistribution, is contained in # # the file COPYING, which can be found at the root of the source code # # distribution tree. If you do not have access to this file, you may # # request a copy from help@hdfgroup.org. # ############################################################################## """ Implementation of compass_model classes for HDF5 files. """ from itertools import groupby import sys import os.path as op import posixpath as pp import h5py import logging logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) # Py2App can't successfully import otherwise from hdf_compass import compass_model from hdf_compass.utils import url2path def sort_key(name): """ Sorting key for names in an HDF5 group. We provide "natural" sort order; e.g. "7" comes before "12". """ return [(int(''.join(g)) if k else ''.join(g)) for k, g in groupby(name, key=str.isdigit)] class HDF5Store(compass_model.Store): """ Data store implementation using an HDF5 file. Keys are the full names of objects in the file. """ @staticmethod def plugin_name(): return "HDF5" @staticmethod def plugin_description(): return "A plugin used to browse HDF5 files." file_extensions = {'HDF5 File': ['*.hdf5', '*.h5', '*.nc']} def __contains__(self, key): return key in self.f @property def url(self): return self._url @property def display_name(self): return op.basename(self.f.filename) @property def root(self): return self['/'] @property def valid(self): return bool(self.f) @staticmethod def can_handle(url): if not url.startswith('file://'): logger.debug("able to handle %s? no, not starting with file://" % url) return False path = url2path(url) if not h5py.is_hdf5(path): logger.debug("able to handle %s? no, not hdf5 file" % url) return False logger.debug("able to handle %s? yes" % url) return True def __init__(self, url): if not self.can_handle(url): raise ValueError(url) self._url = url path = url2path(url) self.f = h5py.File(path, 'r') def close(self): self.f.close() def get_parent(self, key): # HDFCompass requires the parent of the root container be None if key == "" or key == "/": return None pkey = pp.dirname(key) if pkey == "": pkey = "/" return self[pkey] class HDF5Group(compass_model.Container): """ Represents an HDF5 group, to be displayed in the browser view. """ class_kind = "HDF5 Group" @staticmethod def can_handle(store, key): return key in store and isinstance(store.f[key], h5py.Group) @property def _names(self): # Lazily build the list of names; this helps when browsing big files if self._xnames is None: self._xnames = list(self._group) # Natural sort is expensive if len(self._xnames) < 1000: self._xnames.sort(key=sort_key) return self._xnames def __init__(self, store, key): self._store = store self._key = key self._group = store.f[key] self._xnames = None @property def key(self): return self._key @property def store(self): return self._store @property def display_name(self): name = pp.basename(self.key) if name == "": name = '/' return name @property def display_title(self): return "%s %s" % (self.store.display_name, self.key) @property def description(self): return 'Group "%s" (%d members)' % (self.display_name, len(self)) def __len__(self): return len(self._group) def __iter__(self): for name in self._names: yield self.store[pp.join(self.key, name)] def __getitem__(self, idx): name = self._names[idx] return self.store[pp.join(self.key, name)] class HDF5Dataset(compass_model.Array): """ Represents an HDF5 dataset. """ class_kind = "HDF5 Dataset" @staticmethod def can_handle(store, key): return key in store and isinstance(store.f[key], h5py.Dataset) def __init__(self, store, key): self._store = store self._key = key self._dset = store.f[key] @property def key(self): return self._key @property def store(self): return self._store @property def display_name(self): return pp.basename(self.key) @property def description(self): return 'Dataset "%s"' % (self.display_name,) @property def shape(self): return self._dset.shape @property def dtype(self): return self._dset.dtype def __getitem__(self, args): return self._dset[args] def is_plottable(self): if self.dtype.kind == 'S': logger.debug("Not plottable since ASCII String (characters: %d)" % self.dtype.itemsize) return False if self.dtype.kind == 'U': logger.debug("Not plottable since Unicode String (characters: %d)" % self.dtype.itemsize) return False return True class HDF5Text(compass_model.Text): """ Represents a text array (both ASCII and UNICODE). """ class_kind = "HDF5 Dataset[text]" @staticmethod def can_handle(store, key): if key in store and isinstance(store.f[key], h5py.Dataset): if store.f[key].dtype.kind == 'S': # log.debug("ASCII String (characters: %d)" % DATA[key].dtype.itemsize) return True if store.f[key].dtype.kind == 'U': # log.debug("Unicode String (characters: %d)" % DATA[key].dtype.itemsize) return True return False def __init__(self, store, key): self._store = store self._key = key self.data = store.f[key] @property def key(self): return self._key @property def store(self): return self._store @property def display_name(self): return pp.basename(self.key) @property def description(self): return 'Text "%s"' % (self.display_name,) @property def shape(self): return self.data.shape @property def text(self): txt = str() if len(self.shape) == 0: # print(type(self.data)) txt += self.data[()].decode() elif len(self.shape) == 1: for el in self.data: txt += el.decode() elif len(self.shape) == 2: for i in range(self.shape[0]): for j in range(self.shape[1]): txt += self.data[i, j].decode() txt += "\n" else: txt = ">> display of more than 2D string array not implemented <<" return txt class HDF5KV(compass_model.KeyValue): """ A KeyValue node used for HDF5 attributes. """ class_kind = "HDF5 Attributes" @staticmethod def can_handle(store, key): return key in store.f def __init__(self, store, key): self._store = store self._key = key self._obj = store.f[key] self._names = self._obj.attrs.keys() @property def key(self): return self._key @property def store(self): return self._store @property def display_name(self): n = pp.basename(self.key) return n if n != '' else '/' @property def description(self): return self.display_name @property def keys(self): return self._names def __getitem__(self, name): return self._obj.attrs[name] class HDF5Image(compass_model.Image): """ True-color images. """ class_kind = "HDF5 Truecolor Image" @staticmethod def can_handle(store, key): if key not in store: return False obj = store.f[key] if obj.attrs.get('CLASS') != 'IMAGE': return False if obj.attrs.get('IMAGE_SUBCLASS') != 'IMAGE_TRUECOLOR': return False if obj.attrs.get('INTERLACE_MODE') != 'INTERLACE_PIXEL': return False return True def __init__(self, store, key): self._store = store self._key = key self._obj = store.f[key] @property def key(self): return self._key @property def store(self): return self._store @property def display_name(self): n = pp.basename(self.key) return n if n != '' else '/' @property def description(self): return self.display_name @property def width(self): return self._obj.shape[1] @property def height(self): return self._obj.shape[0] @property def palette(self): return None @property def data(self): return self._obj[:] # Register handlers HDF5Store.push(HDF5KV) HDF5Store.push(HDF5Dataset) HDF5Store.push(HDF5Text) HDF5Store.push(HDF5Group) HDF5Store.push(HDF5Image) compass_model.push(HDF5Store) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/hdf_compass/hdf5_model/test.py0000666000000000000000000000224014544243330016735 0ustar00############################################################################## # Copyright by The HDF Group. # # All rights reserved. # # # # This file is part of the HDF Compass Viewer. The full HDF Compass # # copyright notice, including terms governing use, modification, and # # terms governing use, modification, and redistribution, is contained in # # the file COPYING, which can be found at the root of the source code # # distribution tree. If you do not have access to this file, you may # # request a copy from help@hdfgroup.org. # ############################################################################## from hdf_compass.compass_model.test import container, store from hdf_compass.hdf5_model import HDF5Group, HDF5Store from hdf_compass.utils import data_url import os url = os.path.join(data_url(), "hdf5", "tall.h5") s = store(HDF5Store, url) c = container(HDF5Store, url, HDF5Group, "/") ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1714825027.0019908 hdf_compass-0.7b15/hdf_compass/hdf5rest_model/0000777000000000000000000000000014615423503016305 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/hdf_compass/hdf5rest_model/__init__.py0000666000000000000000000000207414544243330020420 0ustar00############################################################################## # Copyright by The HDF Group. # # All rights reserved. # # # # This file is part of the HDF Compass Viewer. The full HDF Compass # # copyright notice, including terms governing use, modification, and # # terms governing use, modification, and redistribution, is contained in # # the file COPYING, which can be found at the root of the source code # # distribution tree. If you do not have access to this file, you may # # request a copy from help@hdfgroup.org. # ############################################################################## from hdf_compass.hdf5rest_model.model import HDF5RestStore, HDF5RestGroup, HDF5RestDataset, HDF5RestKV import logging logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/hdf_compass/hdf5rest_model/hdf5dtype.py0000666000000000000000000004251514544243330020561 0ustar00############################################################################## # Copyright by The HDF Group. # # All rights reserved. # # # # This file is part of H5Serv (HDF5 REST Server) Service, Libraries and # # Utilities. The full HDF5 REST Server copyright notice, including # # terms governing use, modification, and redistribution, is contained in # # the file COPYING, which can be found at the root of the source code # # distribution tree. If you do not have access to this file, you may # # request a copy from help@hdfgroup.org. # ############################################################################## """ This class is used to map between HDF5 type representations and numpy types """ import numpy as np from h5py.h5t import special_dtype from h5py.h5t import check_dtype from h5py.h5r import Reference from h5py.h5r import RegionReference """ Convert the given type item to a predefined type string for predefined integer and floating point types ("H5T_STD_I64LE", et. al). For compound types, recursively iterate through the typeItem and do same conversion for fields of the compound type. """ def getTypeResponse(typeItem): response = None if 'uuid' in typeItem: # committed type, just return uuid response = 'datatypes/' + typeItem['uuid'] elif typeItem['class'] == 'H5T_INTEGER' or typeItem['class'] == 'H5T_FLOAT': # just return the class and base for pre-defined types response = {} response['class'] = typeItem['class'] response['base'] = typeItem['base'] elif typeItem['class'] == 'H5T_OPAQUE': response = {} response['class'] = 'H5T_OPAQUE' response['size'] = typeItem['size'] elif typeItem['class'] == 'H5T_REFERENCE': response = {} response['class'] = 'H5T_REFERENCE' response['base'] = typeItem['base'] elif typeItem['class'] == 'H5T_COMPOUND': response = {} response['class'] = 'H5T_COMPOUND' fieldList = [] for field in typeItem['fields']: fieldItem = { } fieldItem['name'] = field['name'] fieldItem['type'] = getTypeResponse(field['type']) # recursive call fieldList.append(fieldItem) response['fields'] = fieldList else: response = {} # otherwise, return full type for k in typeItem.keys(): if k == 'base': if type(typeItem[k]) == dict: response[k] = getTypeResponse(typeItem[k]) # recursive call else: response[k] = typeItem[k] # predefined type elif k not in ('size', 'base_size'): response[k] = typeItem[k] return response """ Return type info. For primitive types, return string with typename For compound types return array of dictionary items """ def getTypeItem(dt): type_info = {} if len(dt) <= 1: type_info = getTypeElement(dt) else: names = dt.names type_info['class'] = 'H5T_COMPOUND' fields = [] for name in names: field = { 'name': name } field['type'] = getTypeElement(dt[name]) fields.append(field) type_info['fields'] = fields return type_info """ Get element type info - either a complete type or element of a compound type Returns dictionary Note: only getTypeItem should call this! """ def getTypeElement(dt): if len(dt) > 1: raise Exception("unexpected numpy type passed to getTypeElement") type_info = {} if dt.kind == 'O': # numpy object type - assume this is a h5py variable length extension h5t_check = check_dtype(vlen=dt) if h5t_check is not None: if h5t_check == str: type_info['class'] = 'H5T_STRING' type_info['length'] = 'H5T_VARIABLE' type_info['charSet'] = 'H5T_CSET_ASCII' type_info['strPad'] = 'H5T_STR_NULLTERM' elif h5t_check == unicode: type_info['class'] = 'H5T_STRING' type_info['length'] = 'H5T_VARIABLE' type_info['charSet'] = 'H5T_CSET_UTF8' type_info['strPad'] = 'H5T_STR_NULLTERM' elif type(h5t_check) == np.dtype: # vlen data type_info['class'] = 'H5T_VLEN' type_info['size'] = 'H5T_VARIABLE' type_info['base'] = getBaseType(h5t_check) else: #unknown vlen type raise TypeError("Unknown h5py vlen type: " + h5t_check) else: # check for reference type h5t_check = check_dtype(ref=dt) if h5t_check is not None: type_info['class'] = 'H5T_REFERENCE' if h5t_check is Reference: type_info['base'] = 'H5T_STD_REF_OBJ' # objref elif h5t_check is RegionReference: type_info['base'] = 'H5T_STD_REF_DSETREG' # region ref else: raise TypeError("unexpected reference type") else: raise TypeError("unknown object type") elif dt.kind == 'V': baseType = getBaseType(dt) if dt.shape: # array type type_info['dims'] = dt.shape type_info['class'] = 'H5T_ARRAY' type_info['base'] = baseType elif baseType['class'] == 'H5T_OPAQUE': # expecting this to be an opaque type type_info = baseType # just promote the base type else: raise TypeError("unexpected Void type") elif dt.kind == 'S': # String type baseType = getBaseType(dt) type_info = baseType # just use base type elif dt.kind == 'U': # Unicode String type baseType = getBaseType(dt) type_info = baseType # just use base type elif dt.kind == 'i' or dt.kind == 'u': # integer type baseType = getBaseType(dt) # numpy integer type - but check to see if this is the hypy # enum extension mapping = check_dtype(enum=dt) if mapping: # yes, this is an enum! type_info['class'] = 'H5T_ENUM' type_info['mapping'] = mapping type_info['base'] = baseType else: type_info = baseType # just use base type elif dt.kind == 'f': # floating point type baseType = getBaseType(dt) type_info = baseType # just use base type else: # unexpected kind raise TypeError("unexpected dtype kind: " + dt.kind) return type_info """ Get Base type info for given type element. """ def getBaseType(dt): if len(dt) > 1: raise TypeError("unexpected numpy type passed to getTypeElement") predefined_int_types = { 'int8': 'H5T_STD_I8', 'uint8': 'H5T_STD_U8', 'int16': 'H5T_STD_I16', 'uint16': 'H5T_STD_U16', 'int32': 'H5T_STD_I32', 'uint32': 'H5T_STD_U32', 'int64': 'H5T_STD_I64', 'uint64': 'H5T_STD_U64' } predefined_float_types = { 'float32': 'H5T_IEEE_F32', 'float64': 'H5T_IEEE_F64' } type_info = {} #type_info['base_size'] = dt.base.itemsize # primitive type if dt.base.kind == 'S': # Fixed length string type type_info['class'] = 'H5T_STRING' type_info['charSet'] = 'H5T_CSET_ASCII' type_info['length'] = dt.base.itemsize type_info['strPad'] = 'H5T_STR_NULLPAD' elif dt.base.kind == 'V': type_info['class'] = 'H5T_OPAQUE' type_info['size'] = dt.itemsize type_info['tag'] = '' # todo - determine tag elif dt.base.kind == 'i' or dt.base.kind == 'u': type_info['class'] = 'H5T_INTEGER' byteorder = 'LE' if dt.base.byteorder == '>': byteorder = 'BE' if dt.base.name in predefined_int_types: #maps to one of the HDF5 predefined types type_info['base'] = predefined_int_types[dt.base.name] + byteorder elif dt.base.kind == 'f': type_info['class'] = 'H5T_FLOAT' byteorder = 'LE' if dt.base.byteorder == '>': byteorder = 'BE' if dt.base.name in predefined_float_types: #maps to one of the HDF5 predefined types type_info['base'] = predefined_float_types[dt.base.name] + byteorder elif dt.base.kind == 'O': # check for reference type h5t_check = check_dtype(ref=dt) if h5t_check is not None: type_info['class'] = 'H5T_REFERENCE' if h5t_check is Reference: type_info['base'] = 'H5T_STD_REF_OBJ' # objref elif h5t_check is RegionReference: type_info['base'] = 'H5T_STD_REF_DSETREG' # region ref else: raise TypeError("unexpected reference type") else: raise TypeError("unknown object type") else: # unexpected kind raise TypeError("unexpected dtype base kind: " + dt.base.kind) return type_info def getNumpyTypename(hdf5TypeName, typeClass=None): predefined_int_types = { 'H5T_STD_I8': 'i1', 'H5T_STD_U8': 'u1', 'H5T_STD_I16': 'i2', 'H5T_STD_U16': 'u2', 'H5T_STD_I32': 'i4', 'H5T_STD_U32': 'u4', 'H5T_STD_I64': 'i8', 'H5T_STD_U64': 'u8' } predefined_float_types = { 'H5T_IEEE_F32': 'f4', 'H5T_IEEE_F64': 'f8' } if len(hdf5TypeName) < 3: raise Exception("Type Error: invalid typename: ") endian = '<' # default endian key = hdf5TypeName if hdf5TypeName.endswith('LE'): key = hdf5TypeName[:-2] elif hdf5TypeName.endswith('BE'): key = hdf5TypeName[:-2] endian = '>' if key in predefined_int_types and (typeClass == None or typeClass == 'H5T_INTEGER'): return endian + predefined_int_types[key] if key in predefined_float_types and (typeClass == None or typeClass == 'H5T_FLOAT'): return endian + predefined_float_types[key] raise TypeError("Type Error: invalid type") def createBaseDataType(typeItem): dtRet = None if type(typeItem) == str or type(typeItem) == unicode: # should be one of the predefined types dtName = getNumpyTypename(typeItem) dtRet = np.dtype(dtName) return dtRet # return predefined type if type(typeItem) != dict: raise TypeError("Type Error: invalid type") if 'class' not in typeItem: raise KeyError("'class' not provided") typeClass = typeItem['class'] dims = '' if 'dims' in typeItem: dims = None if type(typeItem['dims']) == int: dims = (typeItem['dims']) # make into a tuple elif type(typeItem['dims']) not in (list, tuple): raise TypeError("expected list or integer for dims") else: dims = typeItem['dims'] dims = str(tuple(dims)) if typeClass == 'H5T_INTEGER': if 'base' not in typeItem: raise KeyError("'base' not provided") baseType = getNumpyTypename(typeItem['base'], typeClass='H5T_INTEGER') dtRet = np.dtype(dims + baseType) elif typeClass == 'H5T_FLOAT': if 'base' not in typeItem: raise KeyError("'base' not provided") baseType = getNumpyTypename(typeItem['base'], typeClass='H5T_FLOAT') dtRet = np.dtype(dims + baseType) elif typeClass == 'H5T_STRING': if 'length' not in typeItem: raise KeyError("'length' not provided") if 'charSet' not in typeItem: raise KeyError("'charSet' not provided") if typeItem['length'] == 'H5T_VARIABLE': if dims: raise TypeError("ArrayType is not supported for variable len types") if typeItem['charSet'] == 'H5T_CSET_ASCII': dtRet = special_dtype(vlen=str) elif typeItem['charSet'] == 'H5T_CSET_UTF8': dtRet = special_dtype(vlen=unicode) else: raise TypeError("unexpected 'charSet' value") else: nStrSize = typeItem['length'] if type(nStrSize) != int: raise TypeError("expecting integer value for 'length'") type_code = None if typeItem['charSet'] == 'H5T_CSET_ASCII': type_code = 'S' elif typeItem['charSet'] == 'H5T_CSET_UTF8': raise TypeError("fixed-width unicode strings are not supported") else: raise TypeError("unexpected 'charSet' value") dtRet = np.dtype(dims + type_code + str(nStrSize)) # fixed size string elif typeClass == 'H5T_VLEN': if dims: raise TypeError("ArrayType is not supported for variable len types") if 'base' not in typeItem: raise KeyError("'base' not provided") baseType = createBaseDataType(typeItem['base']) dtRet = special_dtype(vlen=np.dtype(baseType)) elif typeClass == 'H5T_OPAQUE': if dims: raise TypeError("Opaque Type is not supported for variable len types") if 'size' not in typeItem: raise KeyError("'size' not provided") nSize = int(typeItem['size']) if nSize <= 0: raise TypeError("'size' must be non-negative") dtRet = np.dtype('V' + str(nSize)) elif typeClass == 'H5T_ARRAY': if not dims: raise KeyError("'dims' must be provided for array types") if 'base' not in typeItem: raise KeyError("'base' not provided") arrayBaseType = typeItem['base'] if type(arrayBaseType) is dict: if "class" not in arrayBaseType: raise KeyError("'class' not provided for array base type") if arrayBaseType["class"] not in ('H5T_INTEGER', 'H5T_FLOAT', 'H5T_STRING'): raise TypeError("Array Type base type must be integer, float, or string") baseType = createDataType(arrayBaseType) dtRet = np.dtype(dims+baseType.str) return dtRet # return predefined type elif typeClass == 'H5T_REFERENCE': if 'base' not in typeItem: raise KeyError("'base' not provided") if typeItem['base'] == 'H5T_STD_REF_OBJ': dtRet = special_dtype(ref=Reference) elif typeItem['base'] == 'H5T_STD_REF_DSETREG': dtRet = special_dtype(ref=RegionReference) else: raise TypeError("Invalid base type for reference type") else: raise TypeError("Invalid type class") return dtRet def createDataType(typeItem): dtRet = None if type(typeItem) == str or type(typeItem) == unicode: # should be one of the predefined types dtName = getNumpyTypename(typeItem) dtRet = np.dtype(dtName) return dtRet # return predefined type if type(typeItem) != dict: raise TypeError("invalid type") if 'class' not in typeItem: raise KeyError("'class' not provided") typeClass = typeItem['class'] if typeClass == 'H5T_COMPOUND': if 'fields' not in typeItem: raise KeyError("'fields' not provided for compound type") fields = typeItem['fields'] if type(fields) is not list: raise TypeError("Type Error: expected list type for 'fields'") if not fields: raise KeyError("no 'field' elements provided") subtypes = [] for field in fields: if type(field) != dict: raise TypeError("Expected dictionary type for field") if 'name' not in field: raise KeyError("'name' missing from field") if 'type' not in field: raise KeyError("'type' missing from field") field_name = field['name'] if type(field_name) == unicode: # convert to ascii ascii_name = field_name.encode('ascii') if ascii_name != field_name: raise TypeError("non-ascii field name not allowed") field['name'] = ascii_name dt = createDataType(field['type']) # recursive call if dt is None: raise Exception("unexpected error") subtypes.append((field['name'], dt)) # append tuple dtRet = np.dtype(subtypes) else: dtRet = createBaseDataType(typeItem) # create non-compound dt return dtRet ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/hdf_compass/hdf5rest_model/model.py0000666000000000000000000003320014544243330017754 0ustar00############################################################################## # Copyright by The HDF Group. # # All rights reserved. # # # # This file is part of the HDF Compass Viewer. The full HDF Compass # # copyright notice, including terms governing use, modification, and # # terms governing use, modification, and redistribution, is contained in # # the file COPYING, which can be found at the root of the source code # # distribution tree. If you do not have access to this file, you may # # request a copy from help@hdfgroup.org. # ############################################################################## """ Implementation of compass_model classes for HDF5 REST API. """ from itertools import groupby import sys import os.path as op import posixpath as pp import json import requests import numpy as np import logging logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) from hdf_compass import compass_model from hdf_compass.utils import url2path from hdf_compass.hdf5rest_model import hdf5dtype def get_json(endpoint, domain=None, uri=None): # try to do a GET from the domain req = endpoint if uri is not None: req += uri headers = {} if domain is not None: headers['host'] = domain logger.debug("GET: " + req) rsp = requests.get(req, headers=headers, verify=False) logger.debug("RSP: " + str(rsp.status_code) + ':' + rsp.text) if rsp.status_code != 200: raise IOError(rsp.reason) rsp_json = json.loads(rsp.text) return rsp_json def sort_key(name): """ Sorting key for names in an HDF5 group. We provide "natural" sort order; e.g. "7" comes before "12". """ return [(int(''.join(g)) if k else ''.join(g)) for k, g in groupby(name, key=unicode.isdigit)] class HDF5RestStore(compass_model.Store): """ Data store implementation for an HDF Service endpoint Keys are valid h5paths to the object: e.g.: /g1 /g1/dset1.1 /g1/dtype Values are URI's to the resource /groups/ /datasets/ /datatypes/ """ @staticmethod def plugin_name(): return "HDF5 Rest" @staticmethod def plugin_description(): return "A plugin used to access HDF Services." def __contains__(self, key): if key in self.f: return True try: pkey = self.get_parent(key) except KeyError: return False if pkey not in self.f: # assumes that we've loaded any parents, before querying for children return False pkey_uri = self.f[pkey] if not pkey_uri.startswith("/groups/"): # if the parent is not a group, this is not a valid key return False # do a get on the link name linkname = pp.basename(key) contains = False try: link_json = self.get(pkey_uri + "/links/" + linkname) if link_json["class"] == "H5L_TYPE_HARD": logger.debug("add key to store:" + key) self.f[key] = '/' + link_json["collection"] + '/' + link_json["id"] contains = True else: pass # todo support soft/external links except IOError: # invalid link # todo - verify it is a 404 logger.debug("invalid key:"+key) return contains @property def url(self): return self._url @property def display_name(self): if self.domain: return self.domain else: return self.endpoint @property def root(self): return self['/'] @property def valid(self): return '/' in self.f @staticmethod def can_handle(url): logger.debug("hdf5rest can_handle: " + url) try: flag = True rsp_json = get_json(url) for key in ("root", "created", "hrefs", "lastModified"): if key not in rsp_json: flag = False break logger.debug("able to handle %s? %r" % (url, flag)) return flag except Exception: logger.debug("able to handle %s? no" % url) return False return True def __init__(self, url): if not self.can_handle(url): raise ValueError(url) self._url = url # extract domain if there's a "host" query param queryParam = "host=" nindex = url.find('?' + queryParam) if nindex < 0: nindex = url.find('&' + queryParam) if nindex > 0: domain = url[(nindex + len(queryParam) + 1):] # trim any additional query params nindex = domain.find('&') if nindex > 0: domain = domain[:nindex] self._domain = domain else: self._domain = None nindex = url.find('?') if nindex < 0: self._endpoint = url else: self._endpoint = url[:(nindex)] if self._endpoint.endswith('/'): # trim any trailing '/' self._endpoint = self._endpoint[:-1] self.cache = {} # http cache rsp = self.get('/') self.f = {} self.f['/'] = "/groups/" + rsp['root'] @property def endpoint(self): return self._endpoint @property def domain(self): return self._domain def get(self, uri): if uri in self.cache: rsp = self.cache[uri] else: rsp = get_json(self.endpoint, domain=self.domain, uri=uri) self.cache[uri] = rsp return rsp def close(self): self.f = {} # clear the key store def get_parent(self, key): # HDFCompass requires the parent of the root container be None if key == "" or key == "/": return None pkey = pp.dirname(key) if pkey == "": pkey = "/" if pkey not in self.f: # is it possible to get to a key without traversing the parents? # if so, we should query the server for the given path raise KeyError("parent not found: " + pkey) return self[pkey] class HDF5RestGroup(compass_model.Container): """ Represents an HDF5 group, to be displayed in the browser view. """ class_kind = "HDF5 Group" @staticmethod def can_handle(store, key): return key in store and store.f[key].startswith("/groups/") def get_names(self): # Lazily build the list of names; this helps when browsing big files if self._xnames is None: rsp = self.store.get(self._uri + "/links") self._xnames = [] links = rsp["links"] logger.debug("got %d links for key: %s" % (len(links), self._key)) for link in links: name = link["title"] self._xnames.append(name) link_key = pp.join(self.key, name) if link_key not in self.store.f: if link["class"] == "H5L_TYPE_HARD": logger.debug("add key to store:" + link_key) self.store.f[link_key] = '/' + link["collection"] + '/' + link["id"] else: pass # todo support soft/external links # Natural sort is expensive if len(self._xnames) < 1000: self._xnames.sort(key=sort_key) return self._xnames def __init__(self, store, key): self._store = store self._key = key self._uri = store.f[key] self._xnames = None rsp = store.get(self._uri) self._count = rsp["linkCount"] logger.debug("new group node: " + self._key) self.get_names() @property def key(self): return self._key @property def store(self): return self._store @property def display_name(self): name = pp.basename(self.key) if name == "": name = '/' return name @property def display_title(self): return "%s %s" % (self.store.display_name, self.key) @property def description(self): return 'Group "%s" (%d members)' % (self.display_name, len(self)) def __len__(self): return self._count def __iter__(self): for name in self._xnames: yield self.store[pp.join(self.key, name)] def __getitem__(self, idx): name = self._xnames[idx] return self.store[pp.join(self.key, name)] class HDF5RestDataset(compass_model.Array): """ Represents an HDF5 dataset. """ class_kind = "HDF5 Dataset" @staticmethod def can_handle(store, key): return key in store and store.f[key].startswith("/datasets/") def __init__(self, store, key): self._store = store self._key = key self._uri = store.f[key] rsp = store.get(self._uri) shape_json = rsp["shape"] if shape_json["class"] == "H5S_SCALAR": self._shape = () elif shape_json["class"] == "H5S_SIMPLE": self._shape = shape_json["dims"] else: raise IOError("Unexpected shape class: " + shape_json["class"]) type_json = rsp["type"] self._dtype = hdf5dtype.createDataType(type_json) @property def key(self): return self._key @property def store(self): return self._store @property def display_name(self): return pp.basename(self.key) @property def description(self): return 'Dataset "%s"' % (self.display_name,) @property def shape(self): return self._shape @property def dtype(self): return self._dtype def __getitem__(self, args): logger.debug("getitem: " + str(args)) req = self._uri + "/value" rank = len(self._shape) if rank > 0: sel_query = '[' for dim in range(rank): if dim < len(args): s = args[dim] else: s = slice(0, self._shape[dim]) sel_query += str(s.start) sel_query += ':' if s.stop > self._shape[dim]: sel_query += str(self._shape[dim]) else: sel_query += str(s.stop) sel_query += ',' sel_query = sel_query[:-1] # trim trailing comma sel_query += ']' req += "?select=" + sel_query rsp = self.store.get(req) value = rsp["value"] arr = np.array(value, dtype=self._dtype) return arr def is_plottable(self): if self.dtype.kind == 'S': logger.debug("Not plottable since ASCII String (characters: %d)" % self.dtype.itemsize) return False if self.dtype.kind == 'U': logger.debug("Not plottable since Unicode String (characters: %d)" % self.dtype.itemsize) return False return True class HDF5RestKV(compass_model.KeyValue): """ A KeyValue node used for HDF5 attributes. """ class_kind = "HDF5 Attributes" @staticmethod def can_handle(store, key): canhandle = False if key in store: uri = store.f[key] if uri.startswith("/groups/"): canhandle = True elif uri.startswith("/datasets/"): canhandle = True elif uri.startswith("/datatypes/"): canhandle = True return canhandle def __init__(self, store, key): self._store = store self._key = key self._uri = store.f[key] rsp = store.get(self._uri + "/attributes") attributes = rsp["attributes"] names = [] for attr in attributes: names.append(attr["name"]) self._names = names @property def key(self): return self._key @property def store(self): return self._store @property def display_name(self): n = pp.basename(self.key) return n if n != '' else '/' @property def description(self): return self.display_name @property def keys(self): return self._names[:] def __getitem__(self, name): rsp = self._store.get(self._uri + "/attributes/" + name) type_json = rsp["type"] value_json = rsp["value"] arr_dtype = hdf5dtype.createDataType(type_json) arr = np.array(value_json, dtype=arr_dtype) return arr # Register handlers HDF5RestStore.push(HDF5RestKV) HDF5RestStore.push(HDF5RestDataset) #HDF5RestStore.push(HDF5Text) HDF5RestStore.push(HDF5RestGroup) #HDF5RestStore.push(HDF5Image) compass_model.push(HDF5RestStore) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/hdf_compass/hdf5rest_model/test.py0000666000000000000000000000260314544243330017636 0ustar00############################################################################## # Copyright by The HDF Group. # # All rights reserved. # # # # This file is part of the HDF Compass Viewer. The full HDF Compass # # copyright notice, including terms governing use, modification, and # # terms governing use, modification, and redistribution, is contained in # # the file COPYING, which can be found at the root of the source code # # distribution tree. If you do not have access to this file, you may # # request a copy from help@hdfgroup.org. # ############################################################################## # # Run test, from hdf-compass directory:: # python -m unittest hdf_compass.my_model.test # from hdf_compass.compass_model.test import container, store from hdf_compass.hdf5rest_model import HDF5RestGroup, HDF5RestStore from hdf_compass.utils import data_url import os # use this url if you are running h5serv locally url = "http://127.0.0.1:5000" #url = "https://data.hdfgroup.org:7258/?host=tall.test.data.hdfgroup.org" s = store(HDF5RestStore, url) c = container(HDF5RestStore, url, HDF5RestGroup, "/") ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1714825027.0079901 hdf_compass-0.7b15/hdf_compass/opendap_model/0000777000000000000000000000000014615423503016207 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/hdf_compass/opendap_model/__init__.py0000666000000000000000000000205414544243330020320 0ustar00############################################################################## # Copyright by The HDF Group. # # All rights reserved. # # # # This file is part of the HDF Compass Viewer. The full HDF Compass # # copyright notice, including terms governing use, modification, and # # terms governing use, modification, and redistribution, is contained in # # the file COPYING, which can be found at the root of the source code # # distribution tree. If you do not have access to this file, you may # # request a copy from help@hdfgroup.org. # ############################################################################## from hdf_compass.opendap_model.model import Server, Dataset, Structure, Attributes, Base import logging logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler())././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/hdf_compass/opendap_model/model.py0000666000000000000000000001756514544243330017676 0ustar00############################################################################## # Copyright by The HDF Group. # # All rights reserved. # # # # This file is part of the HDF Compass Viewer. The full HDF Compass # # copyright notice, including terms governing use, modification, and # # terms governing use, modification, and redistribution, is contained in # # the file COPYING, which can be found at the root of the source code # # distribution tree. If you do not have access to this file, you may # # request a copy from help@hdfgroup.org. # ############################################################################## import posixpath as pp import numpy as np import pydap as dap from pydap.client import open_url import logging logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) from hdf_compass import compass_model def check_key(key, dataset): if '/' not in key: return key, dataset new_dataset = dataset[key.split('/')[0]] return key.split('/')[1], new_dataset class Server(compass_model.Store): """ Represents the remote OpENDAP server to be accessed """ @staticmethod def plugin_name(): return "OpENDAP" @staticmethod def plugin_description(): return "A plugin used to access OpENDAP Servers." def __contains__(self, key): if '/' not in key: return key in self.dataset new_dset = self.dataset[key.split('/')[0]] new_key = key.rsplit('/')[1] return new_key in new_dset @staticmethod def can_handle(url): try: flag = isinstance(open_url(url), dap.model.DatasetType) logger.debug("able to handle %s? %r" % (url, flag)) return flag except Exception: logger.debug("able to handle %s? no" % url) return False def __init__(self, url): if not self.can_handle(url): raise ValueError(url) self._url = url self._valid = True self._dataset = open_url(self.url) self._datalength = len(self._dataset.data) self._dataset.setdefault('') def close(self): self._valid = False def get_parent(self, key): return None @property def url(self): return self._url @property def display_name(self): return self.dataset.name @property def root(self): return self[''] @property def valid(self): return self._valid @property def dataset(self): return self._dataset @property def datalength(self): return self._datalength class Dataset(compass_model.Container): """ Represents Dataset/DatasetType Object in OpENDAP/Pydap. """ class_kind = "Dataset" def __len__(self): return self._store.datalength def __getitem__(self, index): name = self._dset.keys()[index] return self.store[pp.join(self.key, name)] def __iter__(self): pass @staticmethod def can_handle(store, key): return key == '' def __init__(self, store, key): if not key == '': raise ValueError("A Dataset object may only represent the root group") self._store = store self._key = key self._url = store.url self._dset = store.dataset @property def key(self): return self._key @property def store(self): return self._store @property def display_name(self): return self._dset.name @property def description(self): return "A Pydap DatasetType Object." @property def dset(self): return self._dset class Structure(compass_model.Container): """ Represents Structure/StructureType Object in OpENDAP/Pydap. """ class_kind = "Structure/Grid/Sequence" def __len__(self): return len(self._dset.data) def __getitem__(self, index): name = self._dset.keys()[index] return self.store[pp.join(self.key, name)] def __iter__(self): pass @staticmethod def can_handle(store, key): new_key, new_dset = check_key(key, store.dataset) try: return new_key in new_dset and isinstance(new_dset[new_key], dap.model.StructureType) except isinstance(new_dset[new_key], dap.model.DatasetType): return False def __init__(self, store, key): new_key, new_dset = check_key(key, store.dataset) self._store = store self._key = new_key self._url = store.url self._dset = new_dset[new_key] @property def key(self): return self._key @property def store(self): return self._store @property def display_name(self): return self._dset.name @property def description(self): return "A Pydap StructureType Object." class Base(compass_model.Array): """ Represents Array/BaseType Object in OpENDAP/Pydap. """ class_kind = "Array" @property def shape(self): return self._shape @property def dtype(self): return np.dtype(self._dtype.typecode) def __getitem__(self, index): return self._data[index] @staticmethod def can_handle(store, key): new_key, new_dset = check_key(key, store.dataset) return new_key in new_dset and isinstance(new_dset[new_key], dap.model.BaseType) def __init__(self, store, key): new_key, new_dset = check_key(key, store.dataset) self._store = store self._key = new_key self._url = store.url self._id = new_dset[new_key].id self._shape = new_dset[new_key].shape self._dtype = new_dset[new_key].type self._name = new_dset[new_key].name self._data = new_dset[new_key].data @property def key(self): return self._key @property def store(self): return self._store @property def display_name(self): return self._name @property def description(self): return "A Pydap BaseType Object." def is_plottable(self): if self.dtype.kind == 'S': logger.debug("Not plottable since ASCII String (characters: %d)" % self.dtype.itemsize) return False if self.dtype.kind == 'U': logger.debug("Not plottable since Unicode String (characters: %d)" % self.dtype.itemsize) return False return True class Attributes(compass_model.KeyValue): """ Represents the Attributes member of Pydap Objects. """ class_kind = "Attributes" @property def keys(self): return self._keys.keys() def __getitem__(self, name): return self._keys[name] @staticmethod def can_handle(store, key): new_key, new_dset = check_key(key, store.dataset) return new_key != '' def __init__(self, store, key): new_key, new_dset = check_key(key, store.dataset) self._store = store self._key = new_key self._keys = new_dset[self._key].attributes @property def key(self): return self._key @property def store(self): return self._store @property def display_name(self): return "%s Attributes" % self._key @property def description(self): return "Attributes of %s" % self._key # Register Handlers Server.push(Attributes) Server.push(Dataset) Server.push(Structure) Server.push(Base) compass_model.push(Server) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/hdf_compass/opendap_model/test.py0000666000000000000000000000046014544243123017537 0ustar00from hdf_compass.compass_model.test import container, store from hdf_compass.opendap_model import Server, Dataset url = "http://test.opendap.org/opendap/hyrax/data/hdf5/grid_1_2d.h5" s_1 = store(Server, url) url = "http://test.opendap.org/opendap/hyrax/data/nc/bears.nc" s_2 = store(Server, url)././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1714825027.0119898 hdf_compass-0.7b15/hdf_compass/utils/0000777000000000000000000000000014615423503014541 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1714824901.0 hdf_compass-0.7b15/hdf_compass/utils/__init__.py0000666000000000000000000000212014615423305016645 0ustar00############################################################################## # Copyright by The HDF Group. # # All rights reserved. # # # # This file is part of the HDF Compass Viewer. The full HDF Compass # # copyright notice, including terms governing use, modification, and # # terms governing use, modification, and redistribution, is contained in # # the file COPYING, which can be found at the root of the source code # # distribution tree. If you do not have access to this file, you may # # request a copy from help@hdfgroup.org. # ############################################################################## import logging logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) from hdf_compass.utils.utils import is_darwin, is_win, is_linux, url2path, path2url, data_url __version__ = "0.7.b15" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019672.0 hdf_compass-0.7b15/hdf_compass/utils/utils.py0000666000000000000000000000372114544243330016255 0ustar00############################################################################## # Copyright by The HDF Group. # # All rights reserved. # # # # This file is part of the HDF Compass Viewer. The full HDF Compass # # copyright notice, including terms governing use, modification, and # # terms governing use, modification, and redistribution, is contained in # # the file COPYING, which can be found at the root of the source code # # distribution tree. If you do not have access to this file, you may # # request a copy from help@hdfgroup.org. # ############################################################################## """ Implementation of utils and helper functions """ import sys import os import logging logger = logging.getLogger(__name__) is_darwin = sys.platform == 'darwin' is_win = sys.platform == 'win32' is_linux = sys.platform == 'linux2' def url2path(url): """ Helper function that returns the file path from an url, dealing with Windows peculiarities """ if is_win: return url.replace('file:///', '') else: return url.replace('file://', '') def path2url(path): """ Helper function that returns the url from a file path, dealing with Windows peculiarities """ if is_win: return 'file:///' + path else: return 'file://' + path def data_url(): """ Helper function used to return the url to the project data folder """ prj_root_folder = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir)) data_folder = os.path.join(prj_root_folder, "data") if not os.path.exists(data_folder): raise RuntimeError("data path %s does not exist" % data_folder) return path2url(data_folder) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1714825027.0249906 hdf_compass-0.7b15/hdf_compass.egg-info/0000777000000000000000000000000014615423503015073 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1714825026.0 hdf_compass-0.7b15/hdf_compass.egg-info/PKG-INFO0000666000000000000000000001310014615423502016162 0ustar00Metadata-Version: 2.1 Name: hdf_compass Version: 0.7b15 Summary: An experimental viewer program for HDF5 and related formats. Home-page: https://github.com/HDFGroup/hdf-compass/ Author: HDFGroup Author-email: help@hdfgroup.org License: BSD-like license Keywords: data hdf bag ascii grid opendap Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Science/Research Classifier: Natural Language :: English Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Topic :: Scientific/Engineering :: Information Analysis Classifier: Topic :: Office/Business :: Office Suites Classifier: Topic :: Utilities License-File: COPYING Requires-Dist: numpy Requires-Dist: matplotlib Requires-Dist: h5py Requires-Dist: pypubsub Requires-Dist: wxPython Requires-Dist: requests Provides-Extra: geonodes Requires-Dist: cartopy[plotting]; extra == "geonodes" Provides-Extra: bag Requires-Dist: hyo2.bag>=1.2.4; extra == "bag" Provides-Extra: opendap Requires-Dist: pydap>=3.2; extra == "opendap" Provides-Extra: adios Requires-Dist: adios>=1.9.1b19; extra == "adios" HDF Compass =========== .. image:: https://badge.fury.io/py/hdf_compass.svg :target: https://badge.fury.io/py/hdf_compass :alt: PyPI Status .. image:: https://readthedocs.org/projects/hdf-compass/badge/?version=stable :target: http://hdf-compass.readthedocs.org/en/stable/?badge=stable :alt: Stable Documentation Status .. image:: https://readthedocs.org/projects/hdf-compass/badge/?version=latest :target: http://hdf-compass.readthedocs.org/en/latest/?badge=latest :alt: Latest Documentation Status .. image:: https://github.com/HDFGroup/hdf-compass/actions/workflows/hdf-compass_on_windows.yml/badge.svg?branch=py3 :target: https://github.com/HDFGroup/hdf-compassg/actions/workflows/hdf-compass_on_windows.yml :alt: Windows .. image:: https://github.com/HDFGroup/hdf-compass/actions/workflows/hdf-compass_on_linux.yml/badge.svg?branch=py3 :target: https://github.com/HDFGroup/hdf-compass/actions/workflows/hdf-compass_on_linux.yml :alt: Linux .. image:: https://coveralls.io/repos/github/HDFGroup/hdf-compass/badge.svg?branch=py3 :target: https://coveralls.io/github/HDFGroup/hdf-compass?branch=py3 :alt: coverall Welcome to the project! HDF Compass is an experimental viewer program for HDF5 and related formats, designed to complement other more complex applications like HDFView. Strong emphasis is placed on clean minimal design, and maximum extensibility through a plugin system for new formats. HDF Compass is written in Python, but ships as a native application on Windows, OS X, and Linux, by using PyInstaller to package the app. Binary executables are available for Windows (Windows 7 or later) and Mac OS X (Yosemite or later) at the Project Page listed below. Bug reports and pull requests are welcome! For non-trivial PRs please open an issue first, so the core developers can give feedback on your idea. Development Environment ----------------------- You will need: * `Python 3.6 `_ * `NumPy `_ * `Matplotlib `_ * `wxPython Phoenix 4.0.0+ `_ (`PyPI `_ and `extra wheels for Linux `_) * `Cartopy `_ * `h5py `_ *[HDF plugin]* * `hyo2.bag `_ *[BAG plugin]* * `Pydap `_ *[OPeNDAP plugin]* (>=3.3) * `Requests `_ *[HDF Rest API plugin]* * `adios `_ *[ADIOS Plugin]* (Linux/OSX only) For packaging the app: * `PyInstaller `_ *(>= 3.3 or `latest dev `_ )* Running the Program ------------------- ``$ python HDFCompass.py`` Note: If you are using the Anaconda distribution on the Mac, you will see the message: "This program needs access to the screen. Please run with a Framework build of python...". In this case use the pythonw command: ``$ pythonw HDFCompass.py`` Note: on Mac, HDF Compass doesn't create an initial window, use the system Application menu to open a file or remote resource. Note: If you are using conda and see debug message like "No module named 'h5py'" with the h5py package installed, install python.app: ``$ conda install python.app`` Packaging --------- Single-file: ``$ pyinstaller --clean -y HDFCompass.1file.spec`` Single-folder (useful for debugging the ``pyinstaller`` settings): ``$ pyinstaller --clean -y HDFCompass.1folder.spec`` Other info ---------- * Github: `http://github.com/HDFGroup/hdf-compass `_ * Project page: `https://www.hdfgroup.org/projects/compass/ `_ * License: BSD-like HDF Group license (See `COPYING `_) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1714825026.0 hdf_compass-0.7b15/hdf_compass.egg-info/SOURCES.txt0000666000000000000000000001262114615423502016760 0ustar00COPYING HDFCompass.desktop HDFCompass.icns HDFCompass.ico HDFCompass.py MANIFEST.in README.rst setup.cfg setup.py additional_legal/ADIOS_Copyrights_and_Licenses.txt additional_legal/HydroPro_Icons_Terms.txt additional_legal/KDE_Oxygen_Icon_Set_Copyright_and_License.txt additional_legal/NumPy_Copyright_and_License.txt additional_legal/PyDAP_Copyright_and_License.txt additional_legal/PyInstaller_Copyrights_and_Licenses.txt additional_legal/Python_Copyright_and_License.txt additional_legal/cartopy_Copyrights_and_Licenses.txt additional_legal/h5py_Copyrights_and_Licenses.txt additional_legal/hydroffice_bag_Copyrights_and_Licenses.txt additional_legal/matplotlib_Copyright_and_License.txt additional_legal/wxWidgets_Copyrights_and_Licenses.txt data/asc/sample.asc data/bag/bdb_00.bag data/hdf5/tall.h5 data/hdf5/Download/download_data.py docs/conf.py docs/data_model.rst docs/how_to_contribute.rst docs/how_to_freeze.rst docs/how_to_install.rst docs/how_to_release.rst docs/how_to_use.rst docs/index.rst docs/license.rst docs/make.bat docs/requirements.rst hdf_compass/__init__.py hdf_compass.egg-info/PKG-INFO hdf_compass.egg-info/SOURCES.txt hdf_compass.egg-info/dependency_links.txt hdf_compass.egg-info/entry_points.txt hdf_compass.egg-info/namespace_packages.txt hdf_compass.egg-info/requires.txt hdf_compass.egg-info/top_level.txt hdf_compass/adios_model/__init__.py hdf_compass/adios_model/model.py hdf_compass/adios_model/test.py hdf_compass/array_model/__init__.py hdf_compass/array_model/model.py hdf_compass/array_model/test.py hdf_compass/asc_model/__init__.py hdf_compass/asc_model/model.py hdf_compass/asc_model/test.py hdf_compass/bag_model/__init__.py hdf_compass/bag_model/model.py hdf_compass/bag_model/test.py hdf_compass/compass_model/__init__.py hdf_compass/compass_model/model.py hdf_compass/compass_model/test.py hdf_compass/compass_model/icons/array_16.png hdf_compass/compass_model/icons/array_64.png hdf_compass/compass_model/icons/folder_16.png hdf_compass/compass_model/icons/folder_64.png hdf_compass/compass_model/icons/image_16.png hdf_compass/compass_model/icons/image_64.png hdf_compass/compass_model/icons/kv_16.png hdf_compass/compass_model/icons/kv_64.png hdf_compass/compass_model/icons/license.txt hdf_compass/compass_model/icons/readme.txt hdf_compass/compass_model/icons/text_16.png hdf_compass/compass_model/icons/text_64.png hdf_compass/compass_model/icons/unknown_16.png hdf_compass/compass_model/icons/unknown_64.png hdf_compass/compass_model/icons/xml_16.png hdf_compass/compass_model/icons/xml_64.png hdf_compass/compass_viewer/__init__.py hdf_compass/compass_viewer/__main__.py hdf_compass/compass_viewer/events.py hdf_compass/compass_viewer/frame.py hdf_compass/compass_viewer/info.py hdf_compass/compass_viewer/viewer.py hdf_compass/compass_viewer/array/__init__.py hdf_compass/compass_viewer/array/frame.py hdf_compass/compass_viewer/array/plot.py hdf_compass/compass_viewer/container/__init__.py hdf_compass/compass_viewer/container/frame.py hdf_compass/compass_viewer/container/list.py hdf_compass/compass_viewer/geo_array/__init__.py hdf_compass/compass_viewer/geo_array/frame.py hdf_compass/compass_viewer/geo_array/plot.py hdf_compass/compass_viewer/geo_surface/__init__.py hdf_compass/compass_viewer/geo_surface/frame.py hdf_compass/compass_viewer/geo_surface/plot.py hdf_compass/compass_viewer/icons/favicon_32.png hdf_compass/compass_viewer/icons/favicon_48.png hdf_compass/compass_viewer/icons/find_24.png hdf_compass/compass_viewer/icons/go_back_24.png hdf_compass/compass_viewer/icons/go_back_32.png hdf_compass/compass_viewer/icons/go_next_24.png hdf_compass/compass_viewer/icons/go_next_32.png hdf_compass/compass_viewer/icons/go_top_24.png hdf_compass/compass_viewer/icons/go_top_32.png hdf_compass/compass_viewer/icons/go_up_24.png hdf_compass/compass_viewer/icons/go_up_32.png hdf_compass/compass_viewer/icons/license.txt hdf_compass/compass_viewer/icons/logo.png hdf_compass/compass_viewer/icons/readme.txt hdf_compass/compass_viewer/icons/save_24.png hdf_compass/compass_viewer/icons/view_icon_24.png hdf_compass/compass_viewer/icons/view_icon_32.png hdf_compass/compass_viewer/icons/view_list_24.png hdf_compass/compass_viewer/icons/view_list_32.png hdf_compass/compass_viewer/icons/view_tree_32.png hdf_compass/compass_viewer/icons/viz_copy_24.png hdf_compass/compass_viewer/icons/viz_hist_24.png hdf_compass/compass_viewer/icons/viz_plot_24.png hdf_compass/compass_viewer/icons/viz_plot_32.png hdf_compass/compass_viewer/icons/viz_plot_xy_24.png hdf_compass/compass_viewer/icons/xml_validate_24.png hdf_compass/compass_viewer/image/__init__.py hdf_compass/compass_viewer/image/frame.py hdf_compass/compass_viewer/keyvalue/__init__.py hdf_compass/compass_viewer/keyvalue/frame.py hdf_compass/compass_viewer/text/__init__.py hdf_compass/compass_viewer/text/frame.py hdf_compass/compass_viewer/text/text_ctrl.py hdf_compass/filesystem_model/__init__.py hdf_compass/filesystem_model/model.py hdf_compass/filesystem_model/test.py hdf_compass/hdf5_model/__init__.py hdf_compass/hdf5_model/model.py hdf_compass/hdf5_model/test.py hdf_compass/hdf5rest_model/__init__.py hdf_compass/hdf5rest_model/hdf5dtype.py hdf_compass/hdf5rest_model/model.py hdf_compass/hdf5rest_model/test.py hdf_compass/opendap_model/__init__.py hdf_compass/opendap_model/model.py hdf_compass/opendap_model/test.py hdf_compass/utils/__init__.py hdf_compass/utils/utils.py tests/HDFCompassTestLog.rst tests/HDFCompassTestPlan.rst tests/__init__.py tests/sample.asc tests/test_init.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1714825026.0 hdf_compass-0.7b15/hdf_compass.egg-info/dependency_links.txt0000666000000000000000000000000114615423502021140 0ustar00 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1714825026.0 hdf_compass-0.7b15/hdf_compass.egg-info/entry_points.txt0000666000000000000000000000010114615423502020360 0ustar00[gui_scripts] HDFCompass = hdf_compass.compass_viewer.viewer:run ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1714825026.0 hdf_compass-0.7b15/hdf_compass.egg-info/namespace_packages.txt0000666000000000000000000000001414615423502021420 0ustar00hdf_compass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1714825026.0 hdf_compass-0.7b15/hdf_compass.egg-info/requires.txt0000666000000000000000000000022514615423502017471 0ustar00numpy matplotlib h5py pypubsub wxPython requests [ADIOS] adios>=1.9.1b19 [BAG] hyo2.bag>=1.2.4 [GeoNodes] cartopy[plotting] [OpenDAP] pydap>=3.2 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1714825026.0 hdf_compass-0.7b15/hdf_compass.egg-info/top_level.txt0000666000000000000000000000001414615423502017617 0ustar00hdf_compass ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1714825027.0289924 hdf_compass-0.7b15/setup.cfg0000666000000000000000000000036514615423503012740 0ustar00[bumpversion] current_version = 0.7.b5 files = setup.py hdf_compass/utils/__init__.py docs/conf.py HDFCompass.1file.spec [bdist_wheel] universal = 0 [metadata] description-file = README.rst [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1714824901.0 hdf_compass-0.7b15/setup.py0000666000000000000000000001146514615423305012634 0ustar00############################################################################## # Copyright by The HDF Group. # # All rights reserved. # # # # This file is part of the HDF Compass Viewer. The full HDF Compass # # copyright notice, including terms governing use, modification, and # # terms governing use, modification, and redistribution, is contained in # # the file COPYING, which can be found at the root of the source code # # distribution tree. If you do not have access to this file, you may # # request a copy from help@hdfgroup.org. # ############################################################################## """ A setuptools based setup module. See: https://packaging.python.org/en/latest/distributing.html https://github.com/pypa/sampleproject Run `python setup.py --help-commands` for available options """ import os import sys # Always prefer setuptools over distutils from setuptools import setup, find_packages # --------------------------------------------------------------------------- # Some helper stuff # --------------------------------------------------------------------------- here = os.path.abspath(os.path.dirname(__file__)) def txt_read(*paths): """ Build a file path from *paths* and return the textual contents """ with open(os.path.join(here, *paths), encoding='utf-8') as f: return f.read() # --------------------------------------------------------------------------- # Populate dictionary with settings # --------------------------------------------------------------------------- # Create a dict with the basic information that is passed to setup after keys are added. setup_args = dict() setup_args['name'] = 'hdf_compass' # The adopted versioning scheme follow PEP40 setup_args['version'] = '0.7.b15' setup_args['url'] = 'https://github.com/HDFGroup/hdf-compass/' setup_args['license'] = 'BSD-like license' setup_args['author'] = 'HDFGroup' setup_args['author_email'] = 'help@hdfgroup.org' # # descriptive stuff # description = 'An experimental viewer program for HDF5 and related formats.' setup_args['description'] = description setup_args['long_description'] = txt_read('README.rst') setup_args['classifiers'] = \ [ # https://pypi.python.org/pypi?%3Aaction=list_classifiers 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Science/Research', 'Natural Language :: English', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Topic :: Scientific/Engineering :: Information Analysis', 'Topic :: Office/Business :: Office Suites', 'Topic :: Utilities' ] setup_args['keywords'] = "data hdf bag ascii grid opendap" # # code stuff # # requirements setup_args['setup_requires'] =\ [ "setuptools", "wheel", ] setup_args['install_requires'] =\ [ "numpy", "matplotlib", "h5py", "pypubsub", "wxPython", "requests" ] setup_args['extras_require'] =\ { "GeoNodes": ["cartopy[plotting]", ], # required for visualization of GeoArray and GeoSurface nodes "BAG": ["hyo2.bag>=1.2.4", ], # required by BAG plugin "OpenDAP": ["pydap>=3.2", ], # required by OpenDAP plugin, there is an issue # with pydap 3.2: https://github.com/pydap/pydap/issues/66 "ADIOS": ["adios>=1.9.1b19", ], # required by ADIOS plugin } # hdf_compass namespace, packages and other files setup_args['namespace_packages'] = ['hdf_compass'] setup_args['packages'] = find_packages(exclude=["*.tests", "*.tests.*", "tests.*", "tests", "*.test*", ]) setup_args['package_data'] =\ { '': ['icons/*.png', 'icons/*.ico', 'icons/*.icns', 'icons/*.txt'], } setup_args['data_files'] = [] setup_args['entry_points'] =\ { 'gui_scripts': ['HDFCompass = hdf_compass.compass_viewer.viewer:run'], } # --------------------------------------------------------------------------- # Do the actual setup now # --------------------------------------------------------------------------- setup(**setup_args) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1714825027.0229902 hdf_compass-0.7b15/tests/0000777000000000000000000000000014615423503012255 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/tests/HDFCompassTestLog.rst0000666000000000000000000003235714544243123016251 0ustar00As of Oct. 19\ :sup:`th`, 2015 1) Files under ftp://ftp.hdfgroup.uiuc.edu/pub/outgoing/HDFCompass/kent-files/ A) Directory /hdf5-handler-fake +------------------------------+----------------------------------------------------------------------------------------+---------------------------------------------+ | Files | Issues-view | Issues -plot | +==============================+========================================================================================+=============================================+ | Comp\_complex.h5 | 1. Datatype cmp1 treats an HDF5 Attributes | Two lines are plotted, which | | | | | | | 2. No information about compound datatype cmp1, cmp2 | doesn’t make sense | | | | | | | 3. Attribute of phony\_compound\_var | | | | | | | | DIMENSION\_LIST Value doesn’t make sense. | | | | | | | | 4. The value of phony\_compound\_var | | | | | | | | Is not right | | +------------------------------+----------------------------------------------------------------------------------------+---------------------------------------------+ | comp\_scalar.h5 | All right | No plot | +------------------------------+----------------------------------------------------------------------------------------+---------------------------------------------+ | comp\_complex\_scalar.h5 | Only display the first element of the member | No plot | +------------------------------+----------------------------------------------------------------------------------------+---------------------------------------------+ | Compound\_more\_types.h5 | No data is displayed | Crash the compass when clicking plot data | +------------------------------+----------------------------------------------------------------------------------------+---------------------------------------------+ | D\_compound.h5 | OK | No plot | +------------------------------+----------------------------------------------------------------------------------------+---------------------------------------------+ | D\_dset\_many.h5 | OK and fast | OK | +------------------------------+----------------------------------------------------------------------------------------+---------------------------------------------+ | D\_link\_hard.h5 | https://github.com/HDFGroup/hdf-compass/issues/90 | N/A | +------------------------------+----------------------------------------------------------------------------------------+---------------------------------------------+ | D\_link\_soft.h5 | https://github.com/HDFGroup/hdf-compass/issues/91 | N/A | +------------------------------+----------------------------------------------------------------------------------------+---------------------------------------------+ | D\_objref.h5 | https://github.com/HDFGroup/hdf-compass/issues/92 | N/A | +------------------------------+----------------------------------------------------------------------------------------+---------------------------------------------+ | D\_regref.h5 | https://github.com/HDFGroup/hdf-compass/issues/93 | N/A | +------------------------------+----------------------------------------------------------------------------------------+---------------------------------------------+ | Grid\_1\_2d.h5 | Ok but the display of HDF5 string dataset is all in one. | OK | | | | | | | https://github.com/HDFGroup/hdf-compass/issues/94 | | +------------------------------+----------------------------------------------------------------------------------------+---------------------------------------------+ | Grid\_1\_3d\_xyz\_aug.h5 | Object reference issues discovered by others | OK | +------------------------------+----------------------------------------------------------------------------------------+---------------------------------------------+ | Nc4\_group\_atomic.h5 | Object reference issues discovered by others | N/A | +------------------------------+----------------------------------------------------------------------------------------+---------------------------------------------+ | Nc4\_group\_comp.h5 | Wait | N/A | +------------------------------+----------------------------------------------------------------------------------------+---------------------------------------------+ | Nest\_comp\_scalar.h5 | Wait | N/A | +------------------------------+----------------------------------------------------------------------------------------+---------------------------------------------+ | Ntypes.h5 | See issues 95-99 | N/A | +------------------------------+----------------------------------------------------------------------------------------+---------------------------------------------+ | t\_flatten\_name\_clash.h5 | OK | N/A | +------------------------------+----------------------------------------------------------------------------------------+---------------------------------------------+ | t\_link\_comment.h5 | Comment doesn’t show up, no need to report this since comment is deprecated in HDF5. | N/A | +------------------------------+----------------------------------------------------------------------------------------+---------------------------------------------+ | T\_vl\_string\_cstr.h5 | Covered | N/A | +------------------------------+----------------------------------------------------------------------------------------+---------------------------------------------+ | T\_space\_null.h5 | OK | N/A | +------------------------------+----------------------------------------------------------------------------------------+---------------------------------------------+ | T\_space\_zero.h5 | OK | N/A | +------------------------------+----------------------------------------------------------------------------------------+---------------------------------------------+ B) Directory /hdf5-testing-files +---------------------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------+----------------+ | Files | Issues-view | Issues -plot | +===============================================================================================================+====================================================================================+================+ | Charsets.h5 | ok | | +---------------------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------+----------------+ | tldoublebad.h5 | Cannot open by h5dump, the file may be corrupted tldouble.h5 is under /testfiles | | +---------------------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------+----------------+ | tldouble.h5 | Cannot open the file | | +---------------------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------+----------------+ | tbigdims.h5 | Crash the testing machine | | +---------------------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------+----------------+ | tfilters.h5 | OK | | +---------------------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------+----------------+ | This may wrap up the testing for all the fake files. | | | | | | | | STOP testing and wait for the lead to review the testing plan and provide the guidelines for the next step. | | | +---------------------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------+----------------+ C) Directory /hdf5-real-files ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/tests/HDFCompassTestPlan.rst0000666000000000000000000003260114544243123016412 0ustar00HDF Compass Test Plan ===================== The test plan includes Testing environment, Installation, Basic functionality, Basic viewer tests, Plug-in tests, and a table to carry out a test with a sample file. kyang2014 write comments starting with kyang2014: kyang2014: From the ticket and JR's comments(https://github.com/HDFGroup/hdf-compass/issues/71) , it seems this is just for the release validation. The tester doesn't need to build HDFCompass from the source. The developer will provide the binaries to the tester. I. Testing environment 1) Platforms +----------------+----------------------------------+ | **Platform** | **Versions** | +================+==================================+ | Mac OS | MacOS 10.8, 10.9, 10.10, 10.11 | +----------------+----------------------------------+ | Windows | 7,8 and 10 | +----------------+----------------------------------+ | Linux | 64-bit CentOS 6 and 7? | +----------------+----------------------------------+ kyang2014: Do we need the Python,h5py etc. versions? Seems that we don't need these information just for this purpose? 2) Where to obtain the testing files kyang2014: May use Amazon S3 or google cloud or azure. I put some testing files for a quick test under ftp://ftp.hdfgroup.uiuc.edu/pub/outgoing/HDFCompass/kent-files/ II. Installation +----------------+-------------------------------------------+ | **Platform** | **Experience** | +================+===========================================+ | Mac OS | ? | +----------------+-------------------------------------------+ | Windows | Download and follow the instructions | +----------------+-------------------------------------------+ | Linux | may need to install python dependencies | +----------------+-------------------------------------------+   III. Basic functionality 1) Launch HDFCompass Click HDFCompass icon or type HDFCompass.exe under the command-line to see if the program can get started. 2) Menu Tests +-----------------+------------------------+----------------------------------+ | **File Menu** | **File Menu Hotkey** | **About HDFCompass** | +=================+========================+==================================+ | Open | Ctrl- O | HDFCompass Version number(0.5) | | | | | | | | @2014-2015 The HDF Group | +-----------------+------------------------+----------------------------------+ | Open Resource | Ctrl-R | | +-----------------+------------------------+----------------------------------+ | Close Window | Ctrl-W | | +-----------------+------------------------+----------------------------------+ | Close File | Shift-Ctrl-W | | +-----------------+------------------------+----------------------------------+ | Exit |   | | +-----------------+------------------------+----------------------------------+ 3) Toolbar Icon displays (On mac). Can drag files to Toolbar?   IV. Basic viewer Tests 1) Basic Tests A) +--------------------+--------------------------------------------------------------------+-----------------------------------------+----------------------------+--------------------------------+--------------------------------------+ | **Platform** | **HDF5 Group** | **HDF5 Group Attributes** | **HDF5 Dataset** | **HDF5 Dataset Plots** | **HDF5 Dataset Attributes** | +====================+====================================================================+=========================================+============================+================================+======================================+ | (items to check) | 1. Tree view or List view | 1. Click  "Reopen as HDF5 Attributes" | name, shape,type, values | Plot by clicking "Plot Data" | Click  "Reopen as HDF5 Attributes" | | | | | | | | | | 2. Name and Kind | 2. name, values,type and shapes | | Icons on the Plot windows | name, value, type, values | | | | | | | | | | 3. Click "Reopen as HDF5 Group"  to check if it is still working | | | | | +--------------------+--------------------------------------------------------------------+-----------------------------------------+----------------------------+--------------------------------+--------------------------------------+ | Windows 7 |   |   |   |   |   | +--------------------+--------------------------------------------------------------------+-----------------------------------------+----------------------------+--------------------------------+--------------------------------------+ | Linux CentOS 6 |   |   |   |   |   | +--------------------+--------------------------------------------------------------------+-----------------------------------------+----------------------------+--------------------------------+--------------------------------------+ | MacOS 10.10 |   |   |   |   |   | +--------------------+--------------------------------------------------------------------+-----------------------------------------+----------------------------+--------------------------------+--------------------------------------+ | ....... |   |   |   |   |   | +--------------------+--------------------------------------------------------------------+-----------------------------------------+----------------------------+--------------------------------+--------------------------------------+ B) On each window, check the "Go" icon 2) Advanced Tests Same as the above but may need to provide the information that needs specific attentions for some tests. kyang2014: Do we want to test if one can open an HDF5 file >1 times at the same time? 3) Files for the basic Tests A) Really basic one: A file that just includes one group, one HDF5 dataset of 2-D floating-point array, a string attribute under the group and an integer attribute for the dataset. B) Basic tests: a) Dataset: One group, one scalar, one 1-D array and one 3-D floating-point array. b) Group: multi-groups(no cycle) c) Attributes: Attributes of different types, from int8(signed and unsigned) to int64, floating-points, HDF5 string under a group d) Compound datatype datasets: a simple one and a nested one datasets as well as a simple and a nested attributes   C) Advanced tests: a) HDF5 object references: One dataset and one attribute b) HDF5 region references: One dataset and one attribute c) HDF5 softlink and hardlink: Links for groups and datasets d) HDF5 external links: Link to datasets and groups from another HDF5 file(Will this work?) e) HDF5 Image, table, packed table kyang2014: f) to i) should be more advanced. f) HDF5 datatypes(advanced): f1) HDF5 string: variable length string with different pad options, fixed-size string with different pad options f2) Big endian and Little endian mixed f3) Bitfield, enum, opaque and even array type g) HDF5 datasets(advanced) g1) HDF5 datasets applied to different filters and storage: gzip(different levels), shuffle, nbit, scaleoffset, szip, chunking, compact  g2) One 4-D and one 5-D array(Mainly for the plot feature) g3) Unlimited dimension HDF5 datasets : 1-D and 2-D h) HDF5 spaces(advanced) h1) Null space i) Stress tests: i1)Giant dataset > 4GB i2) Many attributes i3) Many objects kyang2014: Need to get approved to implement the above testing plan. In the mean time, I've just tried out the HDFCompass with some files I collected by my own. Issues I found are submitted to github issues 87-99. My own testing files(included a few from JR) can be found under ftp://ftp.hdfgroup.uiuc.edu/pub/outgoing/HDFCompass/kent-files/ V. Plug-in Tests 1) OPeNDAP kyang2014: *Needs the clarification about the purpose of OPeNDAP plug-in. DAP is a protocol. Various formats such as netCDF, HDF4, HDF5, grib, excel etc. can be accessed via DAP. Also there are different DAP implementations. Different DAP implementations may provide different output. So we need to clarify what implementation we should test.* *Unidata's THREDDS(java) and OPeNDAP's Hyrax(C++) are two major ones. A python implementation(pydap) is also available. * *Also the OPeNDAP plug-in uses Pydap underneath to access the DAP data. The Pydap client may also have its own limitation(See the last section of kyang2014's comments for issue 60. Since the HDF group implemented HDF modules for Hyrax, so first we should target the HDF5 files served via Hyrax implementation of OPeNDAP. A) Testing the access of HDF5 via Hyrax Hyrax provides an option to dynamically enable different outputs. To serve our NASA customers, we implement an option(CF option) in addition to the default option for Hyrax output. In my view, default option is more fit for HDFCompass although the CF option may be more practical for users.Since the output is different for different options, we need to provide the expected output for different options.  The testing files can be found under ftp://ftp.hdfgroup.uiuc.edu/pub/outgoing/HDFCompass/kent-files/hdf5-handler-fake/ A1) Default option a) DAP2 testing This mapping keeps the original HDF5 structure via DAP2. d_compound.h5 d_group.h5 d_objref.h5 d_regref.h5 d_link_soft.h5 d_link_hard.h5 d_int.h5 t_string.h5 t_vl_string.h5 b) DAP4 testing Prerequiste: Pydap supports DAP4 d_compound.h5 d_group.h5 d_objref.h5 d_regref.h5 d_link_soft.h5 d_link_hard.h5 d_int.h5 d_int64.h5 t_string_cstr.h5 t_vl_string_cstr.h5 nc4_group_atomic.h5 nc4_group_comp.h5   A2) CF option The output is more like netCDF output, which is like a subset of HDF5. The variable/attribute names are following CF conventions. Groups are flattened out. Bunch of datatypes not supported by CF are ignored. Some customized features specifically requested from NASA are also added. The DAP4 output is strictly mapped from the DAP2 output. So the testing files are the same. t_float.h5 t_group_scalar_attrs.h5 t_int.h5 t_2d_2dll.nc4.h5 t_cf_1dll.h5 t_size8.h5 t_string.h5 t_unsupported.h5 t_vl_string.h5 t_name_clash.h5 t_non_cf_char.h5 t_fillvalue_2d_2x2y.nc4.h5 grid_1_2d.h5 B) (Maybe) Testing the access of HDF4 via Hyrax C) (Maybe) Testing the access of HDF5 via THREDDS D) (Maybe) Testing the access of netCDF via THREDDS ....... 2) ASCII Grid sample.asc can be found under the test directory. kyang2014: Just have a check with the sample.asc of this plug-in. Actually I don't think the plot is right although the data values in the viewer looks like corresponding to the sample.asc. The problem is the starting point of the plot index. HDFcompass starting from the upper-left corner with index (0,0). The (0,0) in the ASCII Grid should start from lower-left. So the plot is upside-down.This example shows the compass contour plot feature is not sufficient. The contour plot is not appropriate. The plot should be like the one at  https://en.wikipedia.org/wiki/Esri_grid. This Basic testing for this plug-in is relatively easy. It just needs three tests. 1) Sample.asc 2) Sample.asc with xllcorner as 300.00 and yllcorner as 200.00 3) Sample.asc without having NODATA\_Value VI. An example to check the functionality of the basic viewer kyang2014: Fill in the information later if necessary         ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019017.0 hdf_compass-0.7b15/tests/__init__.py0000666000000000000000000000000014544242111014347 0ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704019539.0 hdf_compass-0.7b15/tests/sample.asc0000666000000000000000000000030414544243123014222 0ustar00ncols 4 nrows 6 xllcorner 0.0 yllcorner 0.0 cellsize 50.0 NODATA_value -9999 -9999 -9999 5 2 -9999 20 100 36 3 8 35 10 32 42 50 6 88 75 27 9 13 5 1 -9999 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1714821727.0 hdf_compass-0.7b15/tests/test_init.py0000666000000000000000000000062414615415137014637 0ustar00import unittest from hdf_compass.utils import __version__ class TestHDFCompass(unittest.TestCase): def test_version(self): self.assertEqual(len(__version__.split(".")), 3) self.assertGreaterEqual(int(__version__.split(".")[0]), 0) def suite(): s = unittest.TestSuite() s.addTests(unittest.TestLoader().loadTestsFromTestCase(TestHDFCompass)) return s