diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..47c694d98 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +open_collective: openrocket diff --git a/core/.classpath b/core/.classpath index f561ddc6f..b21c5e70c 100644 --- a/core/.classpath +++ b/core/.classpath @@ -17,7 +17,7 @@ - + @@ -36,5 +36,8 @@ + + + diff --git a/core/resources-src/pix/componenticons/stage-large.xcf.gz b/core/resources-src/pix/componenticons/stage-large.xcf.gz index 21e9bea86..b200ce7f3 100644 Binary files a/core/resources-src/pix/componenticons/stage-large.xcf.gz and b/core/resources-src/pix/componenticons/stage-large.xcf.gz differ diff --git a/core/resources-src/pix/icon/icon-016-4bit.png b/core/resources-src/pix/icon/icon-016-4bit.png deleted file mode 100644 index 52db21e2f..000000000 Binary files a/core/resources-src/pix/icon/icon-016-4bit.png and /dev/null differ diff --git a/core/resources-src/pix/icon/icon-016-8bit.png b/core/resources-src/pix/icon/icon-016-8bit.png deleted file mode 100644 index eb1d02083..000000000 Binary files a/core/resources-src/pix/icon/icon-016-8bit.png and /dev/null differ diff --git a/core/resources-src/pix/icon/icon-016.png b/core/resources-src/pix/icon/icon-016.png index c2074c7e3..c81cfd9ad 100644 Binary files a/core/resources-src/pix/icon/icon-016.png and b/core/resources-src/pix/icon/icon-016.png differ diff --git a/core/resources-src/pix/icon/icon-016.xcf.gz b/core/resources-src/pix/icon/icon-016.xcf.gz deleted file mode 100644 index 2534b4006..000000000 Binary files a/core/resources-src/pix/icon/icon-016.xcf.gz and /dev/null differ diff --git a/core/resources-src/pix/icon/icon-032-4bit.png b/core/resources-src/pix/icon/icon-032-4bit.png deleted file mode 100644 index 3df2842ac..000000000 Binary files a/core/resources-src/pix/icon/icon-032-4bit.png and /dev/null differ diff --git a/core/resources-src/pix/icon/icon-032-8bit.png b/core/resources-src/pix/icon/icon-032-8bit.png deleted file mode 100644 index ba46c28eb..000000000 Binary files a/core/resources-src/pix/icon/icon-032-8bit.png and /dev/null differ diff --git a/core/resources-src/pix/icon/icon-032.png b/core/resources-src/pix/icon/icon-032.png index a2be11332..893830b05 100644 Binary files a/core/resources-src/pix/icon/icon-032.png and b/core/resources-src/pix/icon/icon-032.png differ diff --git a/core/resources-src/pix/icon/icon-032.xcf.gz b/core/resources-src/pix/icon/icon-032.xcf.gz deleted file mode 100644 index 017c2c478..000000000 Binary files a/core/resources-src/pix/icon/icon-032.xcf.gz and /dev/null differ diff --git a/core/resources-src/pix/icon/icon-048-4bit.png b/core/resources-src/pix/icon/icon-048-4bit.png deleted file mode 100644 index 1dec546a4..000000000 Binary files a/core/resources-src/pix/icon/icon-048-4bit.png and /dev/null differ diff --git a/core/resources-src/pix/icon/icon-048-8bit.png b/core/resources-src/pix/icon/icon-048-8bit.png deleted file mode 100644 index 2a1111cab..000000000 Binary files a/core/resources-src/pix/icon/icon-048-8bit.png and /dev/null differ diff --git a/core/resources-src/pix/icon/icon-048.png b/core/resources-src/pix/icon/icon-048.png index 85a2e8392..b59b75f27 100644 Binary files a/core/resources-src/pix/icon/icon-048.png and b/core/resources-src/pix/icon/icon-048.png differ diff --git a/core/resources-src/pix/icon/icon-048.xcf.gz b/core/resources-src/pix/icon/icon-048.xcf.gz deleted file mode 100644 index 439d927b3..000000000 Binary files a/core/resources-src/pix/icon/icon-048.xcf.gz and /dev/null differ diff --git a/core/resources-src/pix/icon/icon-064-4bit.png b/core/resources-src/pix/icon/icon-064-4bit.png deleted file mode 100644 index a4679ac16..000000000 Binary files a/core/resources-src/pix/icon/icon-064-4bit.png and /dev/null differ diff --git a/core/resources-src/pix/icon/icon-064-8bit.png b/core/resources-src/pix/icon/icon-064-8bit.png deleted file mode 100644 index bc54a6b30..000000000 Binary files a/core/resources-src/pix/icon/icon-064-8bit.png and /dev/null differ diff --git a/core/resources-src/pix/icon/icon-064.png b/core/resources-src/pix/icon/icon-064.png index cc00eb2bf..b32b6752c 100644 Binary files a/core/resources-src/pix/icon/icon-064.png and b/core/resources-src/pix/icon/icon-064.png differ diff --git a/core/resources-src/pix/icon/icon-064.xcf.gz b/core/resources-src/pix/icon/icon-064.xcf.gz deleted file mode 100644 index 34ac64144..000000000 Binary files a/core/resources-src/pix/icon/icon-064.xcf.gz and /dev/null differ diff --git a/core/resources-src/pix/icon/icon-128.png b/core/resources-src/pix/icon/icon-128.png new file mode 100644 index 000000000..9410a9945 Binary files /dev/null and b/core/resources-src/pix/icon/icon-128.png differ diff --git a/core/resources-src/pix/icon/icon-256-1.png b/core/resources-src/pix/icon/icon-256-1.png deleted file mode 100644 index 7b1817e71..000000000 Binary files a/core/resources-src/pix/icon/icon-256-1.png and /dev/null differ diff --git a/core/resources-src/pix/icon/icon-256.png b/core/resources-src/pix/icon/icon-256.png index c2170d358..4a9b49960 100644 Binary files a/core/resources-src/pix/icon/icon-256.png and b/core/resources-src/pix/icon/icon-256.png differ diff --git a/core/resources-src/pix/icon/icon-256.xcf b/core/resources-src/pix/icon/icon-256.xcf new file mode 100644 index 000000000..7c4af7a59 Binary files /dev/null and b/core/resources-src/pix/icon/icon-256.xcf differ diff --git a/core/resources-src/pix/icon/icon-design.ai b/core/resources-src/pix/icon/icon-design.ai new file mode 100644 index 000000000..7a810e79c --- /dev/null +++ b/core/resources-src/pix/icon/icon-design.ai @@ -0,0 +1,363 @@ +%PDF-1.6 % +1 0 obj <>/OCGs[19 0 R 20 0 R 21 0 R 22 0 R]>>/Pages 3 0 R/Type/Catalog>> endobj 2 0 obj <>stream + + + + + xmp.did:0c5d40f3-4c1f-4a89-b3b0-d75ef1e133b9 + uuid:8860d152-566f-db4d-a842-532582442190 + xmp.did:6cddd160-d5ac-4bea-9174-43a369de6f55 + proof:pdf + + uuid:09e6f6b8-6d8a-5644-8f38-6177f7bba706 + xmp.did:267567e7-72d2-4df0-9e4a-35a79a06ad9f + xmp.did:6cddd160-d5ac-4bea-9174-43a369de6f55 + proof:pdf + + + + + saved + xmp.iid:6cddd160-d5ac-4bea-9174-43a369de6f55 + 2022-10-04T22:07:02+02:00 + Adobe Illustrator 25.2 (Macintosh) + / + + + saved + xmp.iid:0c5d40f3-4c1f-4a89-b3b0-d75ef1e133b9 + 2022-12-24T00:39:59+01:00 + Adobe Illustrator 25.2 (Macintosh) + / + + + + application/pdf + + + icon-design + + + Adobe Illustrator 25.2 (Macintosh) + 2023-01-05T20:57:22+01:00 + 2023-01-05T20:57:22+01:00 + 2023-01-05T20:57:22+01:00 + + + + 256 + 240 + JPEG + /9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgA8AEAAwER AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE 1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp 0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo +DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A8qYq7FXYq7FXYq7FXYq7 FXYq7FUw0rQNW1WO9lsbdpYdOt3ur2XokcSCpLMe56AdTjaRElL8UOxV2KuxV+gX/OMH/ki/LH+r ef8AUdPirf8Azk7/AOSM8z/6tp/1HQYq/PzFXYq7FXYq7FXYq7FXYq7FXYq7FXYqmfl3yzr3mTVI 9L0Oylv76XcRRCtF6FnY0VFFd2YgYpAtM/PvkmbyZq0Oi3t5FdassCTahFb1Mdu8u6Q8zTm/CjN8 NNxSuAFMo0xnCxdirsVdirsVdirsVdirsVdiqb+VfK+seaNdtdE0iH1by6agJqERBu8khFaIg3J/ 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 + + + + AIRobin + Document + Adobe PDF library 15.00 + 1 + False + False + + 612.000000 + 558.701341 + Points + + + + Cyan + Magenta + Yellow + Black + + + + + + Default Swatch Group + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + endstream endobj 3 0 obj <> endobj 5 0 obj <>/Resources<>/Properties<>/Shading<>>>/TrimBox[0.0 0.0 612.0 558.701]/Type/Page>> endobj 18 0 obj <>/Resources<>/Properties<>/Shading<>>>/TrimBox[0.0 0.0 612.0 558.701]/Type/Page>> endobj 24 0 obj <>/Resources<>/Properties<>/Shading<>>>/TrimBox[0.0 0.0 612.0 558.701]/Type/Page>> endobj 33 0 obj <>stream +Hn7 )%R۸AWAf6Yh ;I^^ o>χmxmT?-{Կt 0CV1tD&yX0ǔYeH&324bU^kŒ=nN{4FxU*ˢJF&~ij8-c."!>b3}llzbT%%) Hp`#7 ppS>6Үu5O6I2 +/(&cS Ϗ[2e12 YY)ݰoٜA݃=@AA5lUUtpac8Kn`[`wفO-3墐ktltY%v݌'>;.ҘBܞzrα^F՘IHMHayel6Jdhe\Y?=& 5҉Xq'%#b$3;jDmҺ{^Β륐)PJ7 +0}bJ~w*D DS){rG*aOR1PTv.*ښY4H/dmvw|Aqw+>=Îݎaϡ-~w t!@֙m xKRU*Et=F8Q̵@UjϰԮY VKb5f5e;o2>6sY\$Nra`]U.w,w~6kD|bi +w^KcTL-,&R2cMOJPRm9”yla"P1LxȚfmݟw`,W#8hIy`J FQ@KL1:؀Ijippf7SK'U{ԓ{ :tª)ms)tlct[}`Ikx`}j*ks}Zivf am,.A#B|rb&[^ǏZwoZWͷ؀H&Ǡຬ'^~o* l4nb);֡af(Fh s+Lv~>"u0V|> endobj 32 0 obj <> endobj 28 0 obj <> endobj 36 0 obj <> endobj 37 0 obj <> endobj 38 0 obj <> endobj 35 0 obj <> endobj 39 0 obj <> endobj 34 0 obj <> endobj 40 0 obj <> endobj 41 0 obj <> endobj 19 0 obj <> endobj 20 0 obj <> endobj 21 0 obj <> endobj 22 0 obj <> endobj 48 0 obj [/View/Design] endobj 49 0 obj <>>> endobj 46 0 obj [/View/Design] endobj 47 0 obj <>>> endobj 44 0 obj [/View/Design] endobj 45 0 obj <>>> endobj 42 0 obj [/View/Design] endobj 43 0 obj <>>> endobj 26 0 obj <> endobj 7 0 obj <> endobj 14 0 obj <> endobj 15 0 obj <>stream +%!PS-Adobe-3.0 %%Creator: Adobe Illustrator(R) 24.0 %%AI8_CreatorVersion: 25.2.3 %%For: (Sibo Van Gool) () %%Title: (icon-design.ai) %%CreationDate: 05/01/2023 20:57 %%Canvassize: 16383 %%BoundingBox: -1 -403 1292 793 %%HiResBoundingBox: -0.5 -402.934324450407 1291.33352486711 792.500000000001 %%DocumentProcessColors: Cyan Magenta Yellow Black %AI5_FileFormat 14.0 %AI12_BuildNumber: 259 %AI3_ColorUsage: Color %AI7_ImageSettings: 0 %%RGBProcessColor: 0 0 0 ([Registration]) %AI3_Cropmarks: 0 -402.434324450407 612 156.267016394291 %AI3_TemplateBox: 305.5 396.5 305.5 396.5 %AI3_TileBox: -97 -402.583654028058 686 156.416345971942 %AI3_DocumentPreview: None %AI5_ArtSize: 14400 14400 %AI5_RulerUnits: 2 %AI24_LargeCanvasScale: 1 %AI9_ColorModel: 1 %AI5_ArtFlags: 0 0 0 1 0 0 1 0 0 %AI5_TargetResolution: 800 %AI5_NumLayers: 4 %AI9_OpenToView: -1710.40002246701 1607.1786819888 0.323444444411965 1716 1053 18 0 0 6 58 0 0 0 1 1 0 1 1 0 1 %AI5_OpenViewLayers: 7777 %%PageOrigin:0 0 %AI7_GridSettings: 72 8 72 8 1 0 0.800000011920929 0.800000011920929 0.800000011920929 0.899999976158142 0.899999976158142 0.899999976158142 %AI9_Flatten: 1 %AI12_CMSettings: 00.MS %%EndComments endstream endobj 16 0 obj <>stream +%AI24_ZStandard_Data(/X +#Ý,{K3^}׊pYMSff£"  F +=g8y 8A H87Ǽ=a#셦Xv=c[hXd)~g3q{^n]ļxusl<{p/o=Yß}g!3Ǿhc#PIYo޽8e}ýA b &{s C̏X(ba"ͰB/&~~^}cؗb1yDz)1Kld&CK|93B+=l z?C#X#3,qir$C#;1S$Eы\b<"V >]~$˱˰˯J̗.uK\$9" Nrr$&ͱq=1x4RHQC?E.j1X,C2C1 /p:ġ `z`ͯO0mz7zza/y]<%!Z!e7r8Xw5p9x>y=aqbƍfX(j R1$Br{5A,y061aw"91Wv>/?q]{;oys8Aǁ;w?xs8zߛs8^{9bI@X{k=r=?߳7@o}{{9sYs=15=v93Ͻ=ȿ;3qc_ 0:!{C/ľ P ǐ .z?CQGKp 1r$1IMjrɑ$KL,2{wGz !H$I$\r^{Xba#޼cH&Facw7'E0c8Xoӓ~=͎}$Aq{7s3olオI1ԟ0<^glMK\X{u]sͷZce.~7~7b(8HXhLfy'cY=}ocov#._׽{}u=q7Ky3+= =<*R?{/g{0gp}?yYƞZc9%5A&n֫tuLgjmXG0<=7%j1砷 vI 5v}1~%)9s) >ck9&.=[>EUD|4 U~WL˄MtC 5 :zuP)K:gœg]͟fgB cTXUd9`&nWi%5 ˸s=q\u_T( .  ";e@`fJ'’Z:+t^ +Y-bE,%o*+c + ȳ.`߄@9[lgpgU6S6r΄*Ag{~ e~ЀR})j'POabXW ْkG6Q˜:!cJOjFsV{rگS4a^>_ꊺ)z=ۯ~Vakmg{A5ӪQP `%Wup4]==Mk{ +U*S4$B_2(u'UEۓIN `UY+sԚ}Pj&sluz MY:LLl}3jZkA3=U*}Q5Sϔ^ﺡ3IzfMeꙩT4f_YWo3_%8DV{rZh ,{ "Wx&mX2IOΙd3a{Lңs&VɖT*`]) +RAhjfL0C_5LOY4A Y: OJ~t=*ڶ,hKfLh꓎u^.5ղ){T~k:U_\=T>q.N9\sr.˱NnU+gMO+}s=}bW_tr9Vfԗ:(: {4x@ s3W8r-Jn{5_uڞ~jX-*>cT/ֈNs i諞N){r<=Xq*Cb4šӫ~]9TOgjzZ[\8ab֋\δFp-ˮ+eSQk Ų_˲(.}]r~}BesezWuzO()5cZ Ug,LM,r?ot=ԳLkD9^ܼkPծ鲁y}dEj5@9Me[_=u[TES=AiHZ*ڲ+m&lk=e,5M ,st +(繧=}u@u=봢=?_>O#hjMLcNQ?{p6tdc?9 wEvd_uTYu53!kP08|A'ZGuQR|TNM gus*\woZۆJ~/(M_f&d9P*7*O4=3UϬ矢NJ!7M\lku'0 +]1sL&LfjV &dQ(l/˛kU+|W*ӳ'zt&/J(d{~}Y&tNo{ޕu9Eyhzz}Ӫ~j{fꊾgjn~d30 +mUـۓuճ}30OsMUY5]Ogݻ _4EM+'d?@z3ax6dvȧP)Z4(տ|0ʑβױ?4e)Kg|tv=e:GP= L {|z) 6KSMLqϑA/ÝZOGP6t4,Ay 8SEI}Qw8}ք^]=3jno?'@]Wߴ]M:|0 =rs~W*}B@ͨ~P{~uUQuZ=zTrUIm$ߗ mTJi}?触m|O;}L [Q*UM[JEeQkgu_P({JF @"H$.z4,}zd2v*mhOuE:ɺIԲ5ׯnDv`F&ߟZdOv6aQ}FPv`8YY"2d002es`:p~]sAǹ̠9ur{g00v2S:,鲇Û'$SXUM?6Yv`&ԒZ~T{_u''iwރzcr8Z{ߵgAc,,xsCSyo]S5F.S$NFcqLRX$wGq)j\ ›"0KbI +KbI8F +*ZE'Q3Y m L*;0hGAMU}Y d A@@ 44٠edl W$P6Ky1Qa 4"/(6iS.ˮ|(L!\;~c\X.hTGw9Ώ˄iv,&3l2 W dl45bRL۠c4AQq:!6F#U>D0яb4d9L K_P +X.nin󄐘ʇr +RC%$4%Pj +xHM 1-!CHNpZúȫeh3TX|hoapq)8ᚇ@cb42TUo{8SZX\6@rx|< ī yHEX@`0lc,V!_ ɗ*$/HN3L((IED!hH,^V&8I$X0&*SA"q`셀cA H/ /Z`Mj/<XxɃhxpևarq+4ŝsKE0݂ClΑ hר t +u4Ȣl I DID)|H"h ٜF\r.(shu`q-Wyܐk[ e?r-$,eD2n*M5<" 8A"}5ł1JCw  /@4@@ 慉!8/aK=`!p^da#W޶!PD% Q0'8W"cI\b!oۗ5p9SHpkP.g+ \o,8{H([;ζP;!2hڱ"VKl6fkCTVNhX@1RB3t?4ㄱ7Tc#iQ(t>ɅS)IN)Rs;N +BZ'[[ –QrD_$qygְ]B =i!m)rT䦄޶rTZ='@PP:#a"VǝEF'IIBGԑȝy$<0N,y +'t t: )̹MtXR@2aaaaEVmiY :jagaRā$$0#y٥F(`aUMy,I +ht B  cش.Wi[ #5B ik.*8/N.U^VW) %îM&˅l  [O8Amqd/ +_N,ddnʦ?,KvxKǑZ K?*iZ4}?(Ŝ2d؅.KF=aLK' rba0FUӰ Тsbz `#MJ_&m;ˁ|SLm9&, 5LauHZ#A*A8FIT(5ıh"\Ш-;"VN'j(mc@B9iXW2;\BJ! l+jYn>J@!@ JW V O> !T ,M'`R$híaZ\ +hq e~ +:x>Hr:ELjAjl!>YZ5 X 栆2\Y!(/Ph2#b !4,Q% mqR$ @u.0:e"-E.t#eK'IDD#1:pXR$atKhű%:N2tK>"bUe#6"Ku ،ƘְlJ0TAtXQRYqB ྡྷ1u7JhAԨ1bDe4q9lGpA`p`) &{Gh!x9ãd:$, XIms5Rm0NY@`#l(;mU#PyDeKhgNp,t'(D(bILWM ι,iᒿq8 58QD(- 0OLO)Mm$CleჇ:!)5PB`utN%EBh 胆xSnTZͷJ:e[zV`lXS蔭H-Q8yR@pNt<NٰP(T} ], hyhQY$(ѐBT\*~,ְN$\Ќ ʶC'NپL;NrEN2aNٸMeBm*)dW009}CqUa&GtqH 9mHְ|jDr&q/-Bn)ѥٙ;2D)0683v׻)x;ܒ:%`.P6Mʭ +5Ml,7<֖.QlDŽ #QzmQ(=hb@vj0@FS4=ˉHWa7Ѹ,k|ӹJUƠNHznHln`mp.,Bl 綞uqمN^mF`u[->R"yE.Ԉ&7)DB@-#alc۠d^QyL&"Q\ݒJ蠁oRҔ45F] +"Z$d RkHr !$jB-% JmKsylBtT 1١1*aK\tِ3ӠRSNj8nbJ0Ɨ蓬# c\yC|5Je dvj`- +(pXR%MlY`fc()rX|R[(6;XƦ fGD9r3@"і!sΚz~fszF7t9Hq7QԬKї} P^ Z^?f2,WW jiM +Qv>JH:EiKK+!HN +ݳ7N̖:oN3h*O?Ql6F0h@wv~XV$Ďbv?ݳj]y]Wyc hխ7Ӓ%F(:#z\B(WzY{ ͯe(,*=%[)zcKl`Q~(is,Š>_qƟOUds_\84$5׬+g^_6J)c=1==1Dxŀ;Uf|~z$wٞgT?qh5:ZͤN|}j)UF)Y)#n=JEєᢐYV^>ToyݾJBasE5biӚ:脳 Z~}m;T->t084P2͉?0yk^o bH~:T[ycf+=!(yQܫ(ۧm#ꈱEg͋b+~/{?K7]K= +3H49 3Oo~gi5I 6BiE%.MR'k@<4^%mURVJSqh- +uM}&ӯ.ԥ(MZ^\U1c@EúyuRAuc}gw0}p30i6WczڈQlؠyX^=+ H+N.hiSLif!䔰{\W*Z gbvA +J66zΘ?y 6Kw52ͤ6O)Y@#~WlyȝAa4cg{A~8]aU5Rz'8lb6HB˴}BBcֹ 1zi A*ط+`ފf $30 vMS AYV6#Oz ms?~&)L``"ƬE"$"5"^ZR_&mN~t:g_8ƴV>1Jr2\l؂e ٧IJXpҏ$ժus|cG]WdT]˚kcŜHY'j#ס[2JX TQE05WxX_]O5IfhY&JV[|rbɀCe^áP<;35>97|=㜛dj$p|bv좀m1TH:_u,7U^*e٦V9|gpsV%8/EkW73éw:GBXc'g\g~L+FfZt^oPБ45z}RiS7W}y%S}jNfT?Frm(!b"qE E`6Z23z ݔ{Je4Ye)ܤy?ێ2Lΐ9P2o1o%OVEpWYT(;k,._0r$tbFtF1|a$ÀQ=ڹHZ0>Uw+\X&>ikF,͙4fAƺL&:GN~^;\ML.uɾ,B[()" j>$Z!ҟ(Exx.mMXR'%6F + +4Nbd@k ('TɘQC +VTQԖ]mv +%ƙOW^r,~#Ċ9-r¤v~' ~Ǹsu `,YIc@|Cq<81^ؑۓXOW`KŗQkVq}%ί)$[m0 h`a Uj#8I4VN ñZ6033|ak`ߖ<7/o~ QaWR-O+%t;f%VŮ>UUP_\'N.dtks5ω +1y5YL+Bs'npXQc;NV  E"#t 'W @۴Eb([:7=^kjEZ- ԭWYpc/L]o6ԋY4j!YĔ s'zUOJ'bf(h/nQGo-sEgѭF-(:)/%mES8IUE.uJ!:&lŽ&Y-EX+B-c(GW%;j¬ǦeLZ7zڝPh1\;fuKN2"I'X:FNI\\Wj9!?8\QECT\NIR;-wF֋3MR,Wj: NIO,f g}iQnݮ4ИB]b}A0KexGz& KQW-Tz.1|վ-b ;Pԛ7p} +Ln_C'A(g)sD~)Y|l}=}O-\| GpMD &šupV hE)03X7Kk%H*@WFޖ(0E@iiI gJ([Rh , `ʫYXL1ij== %_ېn}eQccdv!e +fhKrU !}n"TyV*$qXbʐ>%?WNYVu3@umyLYVTId\7b?~WSq-[+S`ʵ.H`e1 S:gn( F AK5 u/Jx-Aޓ*WJ ]w4 +ueuXDV>yg9+ɝ iR ٥, x6YC[o,Qͮr$ \&.efCe g5[q]@"R+ꥦ>`f˲KńT$f!߃o75w!ߨi*Vp!tgIjpLߤџ;UF{E (Qf4uN s@M~|'q'#AenR=gdrdrŹ3dY|=X*d842%mM^qOy?6>LT~}M_3)78 9S`Y:~V#{7-vz$҅_NP_ 6hR`y'~M4|E.2ԨANdC_-G`^BdM`S*0u^v.349 q~໫}қsBVxp*Řd'e}vdYa_s㤶m|- +1r Owk`> +(9ds :ӕsbVh+Ǘha}29-v1LJyaL38JyA1 Mw*TctE(N#{Soſ|:wVB0_`u]:)CbDۅe^nS pw nڷܘdXfx ^4ZM1FW2u].[65/*vju0]ظ*Ȃ @氃sdX]bKĜeAh.pMt 53 %4io&wI +l'cmN:vsBj8XZV +œ̅%rkf{r9N5vM+Y]_p eʉP "q!K_vKۧķ}[~x1ϔR,]G7~ȓh>-bRzMx)sĩIú-t ET_g}%k"IL21 ZCّ +HKiI-謑A[BJ2 s PV66: lg2p2ZWQtTZYTnS7qŇ@).8h\˥m_?}1T;W_ A$'ţ{ `ѱ7|= w~5[OV)C]A²{aL%lVrɷQKr~ME^ni1Q=Fs{&z3h2ZZLG J92wWxUX`{'<*)U*0뺽…>W \ o0nL4X]ܴ `0N2`̄U)Y3SxXOL uZ]7@\x:‹L@Hf +A]m唬=jh2`+ͫ>?ET;rtboXV,4̲b+6Amy]H*û/ *k_~u1c'sR(+52w(+9cRoܼ X+v-3Jo/BHp sK.HDVd_KȇpnzvT┑9q]E$__0vc^U-e=p 6_(2K)FF*$Z~ 'ay47Ν 71c,+ZyΩ$2C׽&IEΆQ#j-g!Gvj&;'^;GGvZLQӤNCg9~ڔJF3ڃfΰ7L›n֙hd1&hA2 J9I{ۜNV[P`$peVjǹfֽvZYIRfHwJ2K׈ ۀaɬ_A ri1r3Sǡ<%F;9{|ꉮx6!X ep{]pJ/Ǫ{>u@/.AGDnX=\tzcgs,PELWާcP%D(%f2. +EF?lLnV:}GM[t?퓣dr~#S)^iw|ؾA +͚f<;RG8;ktBb bkcOkMQ1=I9m~kO])Y+"(<r 9Ӹ%p/*E Dj_D9M(IȯḸja]u6ǽ.0`N+.蠠ń` BUm#]>Bv a}]k:~,p X2DmP7JZsn?q%+矯)J)pzݠJ[7hwi$~S0GBGoRkeRWb&*x iIքT y *#~6_I<#9fŹ. !}$^bFr0c"&JMᱸ,y'.&DX%XdsgB Z۫ئ1ѳZ7 ߴqι얤qY yMҖR>܃U4\W 3o?- +3 P>3İY6犑Y.T>"b1m9{3 iiK0CS??s$eB~$;dtB-_RX;l;xYKSt eoA)n$=9aMR7xLrE ~x߼Fĺ$+i"dQ~`;Xߧ5&SxB'6ikzԥP҅"n'aFWk|N-߶' H.pu@pD"Hs6:Rxx]uꖶ${EKt$JH>9v(/ݦ%N'*<ѶN> xެҚav +(YyiSl ˘),z#%)2_,n+D#7ZNW j%bQ^)(H!zHE,)k<P ]i>i Od%6l{Ck.mE .D_ kq±j˝r wjUC s#5ƻ/d!9!dոcv(~d\Y˙cbiCG`q4O6}5yM,y<b9Rt/="q ?|͟*VyD9*j0D_3E,=ɤ,ay$bimOA'qqm +lUQt[ñ8%lIdn Dg2Ym˱eA @~p0KuS(Y +qx.Ump-gԁeX8(QtjAOLFʌq)&: >a +(<\$ʲ&;C}:ͦs!zm\H?MiD1r]By뤠pzB +mIr$3&,# *'!&$:d`b+A[ #N9XjtzTr|WpH4߬Uv#dXHǝ J [52w)ü[L\& þOmjJ7E{Yhp?eqYxM^vYR8E+ +I@ԭAeAm3(ZP* ^L ?G%[tJ%0 Ckl-~<.8O@_f aЭ_UƽdhɌ.M/[AG d1UoO{\*ByC +@ww>XS}8*_9`nM +бZܖ7Il$EWoTo;)ͷvjpfF@K63UgEO0VL +CI:M+Έ\J~@ՅXĵ/kdY>^SDm*%La z+"9U˕%Os\> u+AU47b:؆;*R gKP4tHLR&_Bl\S!_Us_Wu0.Ư*'Sh"~DM_qaA'J?QN8?a+PLw^+.G1Al|aeQl XaЌEZԻP$v}TY{Y.e^sZzyͩ?Q^d/BOu,&sOqǰ5ǯIxMJʵ8; LB%:k·y|6R&G.ho\0\J.M:.Ņp \P~V-aS(#<ׂ,G9-yf3KC~@~qn.o6t&nQ-8Uc =A ֕UՓ:Yy]1|˺_O`SqMFQ*:J +@r `"0!\jnO-M툜M񤦲N ꃿtx[EqGR{4tj_V׹k7'^@5DZ qx9GNg-sK9RyF@оQB7* +ĝ6>IJAʥup@Iy.P&qoI{: C758R \ +&xd~k= cbqAo%Yo0aċ)<94M nlV%:|z"_nRl9)=:}&q21NnT0"+{"x[ *:Y\qiFJ:aEudkr;mW0wQrw뚷ǡz)Gz[?kѲpu:ի7VY]_M೛khiE QF{۱H8͉_׈AۘmekSn֤A䉳~1з%kӐA$P<eWRxw'M~X=qֵ\TzzQΗ;ƸpeDjww BPECR`AJU ӊ?WH6lUr +}^Ly 3pU퐰8`Ej MrZ +N,7[r&vc/C$/\QtЅ< a-K;&ɧq)e54 +r&| ;k|[VEO`H8,LDVJ+5V,VD]eFR+ ֬rål +4@f; *e;*F'_EyON/346Xf1`0 4'39tVy/Y-:%E\V;<%*nz0 K!q0YQ3 kL6 7P߸0zѓCo6$C; O»[?SxY^0N6#V4YlY &-tsprEp_AK{G2&a\tz8u @s5}ٌ*r5\Pw8QhwY6ǁ'b~8͇UdWw{=k99l*I\)N\⥂ǡ3H hK翯9ptׁ"}gV\#őߨ5)H-:^-np+_(ONX{ $8-VS$+dQa./wqDůW};' +ȡ$C'Hȉ0TBY:Xv-/ Q="Y#X2%j+%`Uof}(.[b}GJbLs^R3OFMS콘UlhE|v3r{ǬS4yYpoi(1y9,Ft-+Q9´YGB8k+)ʏ3Q~]ݜkQ/w֗ڹ'%/ Qw2)ʫO$擢{iԫ+XbYul:)SHEU[6L _J,iVE:|fI2qhR-Ss^Vy~,+>/K;33?UlɗtsSQY'ʢn>klZVZIz!_Iեj׭9hNWnI肋YOׯʭEz:z)x&#֘.V㪂d|Ui2KTŅR(_rhJR6/oMޗs&ךwWD+%"wnXNnm6U[hUq(6R!I1*ԟrȑLsKV2qg+QR1vﱠ/)W^J=i>*6_dߊQҦu>ș4T\ŚӤ.}{6vIOKw۪] G-TmkhLԗ{e"=9bkMs|EYSʙΌSi )߶<*kXEt(fқs˭ Wu4s3ARj%ͷLvuP%剷{v)'M_>i֒GqQ?EEZ,Mڧ2)T5W Q\g!roylV$ReU7)O[9)eߨwH>Un渔]'jN +l0+'Nsb{p .JMW%)riY'|C.>"E&P +L  I(A(HE>PLʔw (:,QQ"]%)B/P,Rt*T0H1A 16D„LtT`Epbb +@"UG`D><6=J$t_]U*|TTz!UiqXDŅbAV^6f)(P(2#,.R`,HhBq"!GȇGBWH.A#Z0QAE Pa# th "gPZ A@p< +n#cCh'@耀^`(jp#ŀ` lHA H pA,8I .EAF "L$!+ # +T ' NhF$(v">IaBF($T`!4 @t%$ AP \|A,X`0 !tb.D\h + +(ldldЀ34PH62h@a#CBP` ЂB`!q h,pVL`@1!+("@Tй!<0" ,pP,< X84>l30Lpa + 8l@LE` pa  VÄ F[DF Ҁ%0|pq0h .XA.\hBI"ă D y#Xa  0胅2 g ^1a&Lp E<$P4id s@ ᑁ>Zhā 0`2 ,'ր` J4耂pIcPBDPB%hlC0"Ȱ48 "Ȱqh p&h +'Zn#cF0A .L0 +@LD 47TB40D4h3H,txd  `0Fit ,$i ">qa0`@$4,a*ȐX( Έ!1"xd VpdXQ,0Prh@@ o0 1u4#2)B +b)#QpYE` :A1d:8 NheUJgSIIu~q*)xU>CRm$_*:=EC_G{hr]t]Bw9꽤ܪCu aGI3+BDi*)fYJʝʿ,%%FsgZZym{=̮H֮IM# ,izGJn=tsr̫ے걜3҄fã_ڟCWpN4Hv}!|(WWkXo~.]1B"L,;6oVYjӪRoinQ":n!.9HuGP,wӨ +]}5T9Z1)!jh^/W=qso,Mc>j֫Xk_\*h?GˬB|ZxDreRzJ2cN;{ocrg2MŤSo阯joӆ*Gq;E2+gWMVҥꍥ /t.!GCYY㽋9.WDtebRl-mPRJU{Λ#ҡtez9 \h5Qy>)ZǛ*OR"1#RlvFxWf-JYMíߖ&eDEyֱV9C2$,+{J*23?Oe=3\jU1h\ftYڱpwjWoUsdbdhWw+MDzG-K_¬}&o@ټVUU$T$YͺJifT:I QV/qCTpw*XSM5kͥ=YfJ&"I[SUkfFoqE%b[얫Lbzj{Κ_iOSwRν"ގITݳyO*VUZvzU)ލO#! ttͬ"ڴFBxX-T^d֢E8domTS:b*]Ip"mj(P$Jm_hxݳU#nfKUިm;2Jo1'q#(qmOX)7wdRiFkW*ayp7﫟#+goV턖GY:1aLԳRMܿzF;5!'LJԲnTژU6[Hxi4[?Wq=j77OhX 'JHje|{ڳ7R~_kj(eޔ.ՑWAs’U'*$X|Kjk7sc5is8WIݟ5"ťi##]㩌(<̽,+Zcx\xLטe$uNMUE1rq \xKISlϴ3_ )ȇRF݅j*ArVgUz5QJf6xZ#+U_ۼիR.ngfFBZue QUuZ-,YlKFc[!Mv9)& 7DB8ڣ}jN^RVЩgV:$aYYD&9< &K3)[M=#JW͹d-' jBoZo*r7*jEN},ke.Mʟh +iU:{ux$٥ Q.oJ1ḅYlJG<֗sR7¤2 #h,˘Zwk4)=cyʻD/KW҆[y]ufCUMNՆXj"h#b*ƒ.Z`A1 &\(&@E&P +0lWV0)MaY:ĨeWbR\e%* +=ywGKWxZ%zV!E|ROd)U'HW+''[ 1LD)f4RiN5[%Q_,nUoВ"+RO0){>iW?]Kd˻LJ%"JgvWpq~I!&^Trlu*!׎l6_jG"%2"H:'tYtJDURju&b"վ:d />&n#>Lcą.`FhB_\INmw{ұvp\*ҭ]smWNΒyjILw{<oUz*ԻgRb~Povkը jjߜ&_44/Ч,;z/̦by+_^Y=kWRjiLK ыK:i^t&Q_6}e4dz{zOXӋiG8v/RUSyVv&Rm.y6ç0=Et+O?ܨgxHeܯ-eܵqj]Iu,We^1J\=着/FXUyx6ƤbyIkz-1ɜ>#*γ<1t]UgSȇGy{RF]u(:32+*RjftRҥJLo\6D]W=)ed:;/n4D2MOeRLtib)ڔ*^R_%K*;{_X,3;/lʴW,rdM]_I!{ sAQp)Q@4*M^ͯ¥'eL)MOdMa:MUU,%,5Z2)0)f{3T^J^jUk U)/,<:ҡxy4ZO&։^_EC r5)J9'ҺݤjlZTX"Y|弹#!UףbMiԮ=On=z}]RkXVڻ碄ūZ1hkI9чдQKٳ\^ՙю[VEK>eq\" O9$M=IU%JVv!*9qTa`mNbI,U}|"p\%_IGA,[rY6JWGVD&ٕ(/OZbZ^ZFLJ*fh>ng^k?a]Jٸ^&xHJ?];dj*-ю8.X{Ei&%T]4y ͈ivCsp#!FCLm̴ݨ:l 苮Yiҕe>\uY5y5'nKw5IqK_Ymg3)wd/)Mn R+G_FDRArvKY^izD]sŦ*:Qtc꒏ZzfViJ]FIjdd67q|cX]jiN +o>G,#4HrEK8kzjGj:^(lMTuMmLF;9j޽ڲN!6jX;yg;+VKGmb"tYD5{F\ͯsR~US,)I[jDs:SרrN5Uw uk)yooiyN[s,&P?N +HiM5NJzMzĵkTU;$Bmֿo6u,Ľ1uymH3whw|#&bbAkVT̻=vk^Um,J+:M +bix]մJN}4zIVxLOucWL'E{] VNDAzR()%ɯ]n~Jvijוllav~WyL!ʯF|RV_% #^YMѾmI뵏4{pK;+VEBk1zR.g.HΜs[%UVmڃҝT*,Rotf9K$ cc0E(˒ c j Dbl00GBQ( +a ` a L)XM%n#lTĽ?(kT^eamNd֕hVdQұ/R6ѱ;mp!VH +6݊ +RO-K sfM2iRlFzȪDKef'8du,6ԾRH տFd/I`jC4qyGz@5otMt`5"8@J;Pf:af~?>XB9 +'5 +.D#(t3纓D$ɋvS^ȮW#_.z)+ 4!q}>wF܃| ,iiÎXg>'gelyn졑>7n&GEG{O@2N$>,☌q~w!I`'8 Jx>Ѵ31B\Ӿ astc@+tcgHgmO\%M ~^C0aD7“eʠ'~)WO?yH6=#ajf߆mjEk|#G)֭(BiYuބAS9Ǖ[-(O (Կmg *cfخMEA'W%Y6H[ gwqiCs:>K5|)JGt# +)R6x"1FPфL m^=+E.h׃7h78^ eNYm/VWNW$A5-(;kbJkx]0%pu2:(cY.KI@$M"4wsy+t9e3aKťemBo +ƱaZW~iLD9qm !E]A}T7JޟMhഡIqn0o){95z).NrD@ثSۂʋlὈ2&B9AxTрCrM R$y$S\w';Zb\Eby%7%]S\NYd;:ݦg+u}ndE.-f/xU]CigEϙ3Y>6?d~xtrTl4ǙXK8e"SsrT>-QE&k{D /dƅY%U%8"K,jKhY+}:U0wa;ص)cEJeQ8kNW@DZ7eÁ%Pqc"ze!.=\åkk?DžO!hKMjBw@򜭛'6m%FML,ӣ;Eٱ F;>br62F]fpnD➇)*[|r,b7Yބ;j!2=e0!v2ek{~TʥU$lEl @:ʖ(x%F }ݐ*;հ*~ZkAD^~DFWt贊q>$Ω)>)nP憯TZN@ œ_𙝈V"MZ9PMmhv6H_}')PM|!^7pscSW<5|ٱ=RPF/Q*S׌!ne*֝S&VZ|c׃P70 +e®h_8o>g; +"GUe͔qPɞDMM1MdٱU T#Vܴy7`4~ 5tF9OL]DD"ܨ]nB͆ m4w HIݡsRXuÖ9!l'[J.24AੋVZRDt`o @j&n>)oPd%d?[BP45 Z.#̒AB5eE4|iVKWzs~ZLxu$meeŲw= TjD`tܾU"Ū+bEЪ? + ]-en 0$)9tOQ$*#?hKt 'KS1;JQ%^|@y1}WAꆢ.Pl"e 2I \t3ֺ6fah3$"KjCD%yP`,cda3Qiⶸ"NbFĊ_/5Jb_G3e+x8"pj]v7uqR&SBjÕ(3 @1@erFFHQP&]ta܂?J2tɂP>hKA/'!і3tpRL㕢1i 6U,&FCpg>?P0{ټ 8ЬR0t핳C 2]aQ>JV؞UOX.칻̠N\Ԇ(.pqAsēGF?VoR6?|| ō28_έQGÊc5C6 *-+3 Iu^frr +cV7m)V7 CHJ*_@@6Ej BW =Y:K̐RUH*ؗjMIpN P;t$װT$a5SpfT'q,xt i{8zCe:)z -ipӀoLTv=.js`0(>ş4.FǃYڠU4^{o<2;G A0\+-՟;NVeH40w]|Z߹>tB?ױ\g1cp~#`^YeAӀUTwD|NM#<[~< G5ee2#/e&5T_rdZ.{Jpz{qw[YٹQ}k92_+)M ֊Ȓ@%*Ïxda~wpI C)--8wcD!rIfC [8x1!)AT@3Xb[n>}0#CՊR8QO҃\W )f8.M x PgpO,e-%%.a/V'F6<گ2C`1VtS+ԾZk@GuL0`cՆ^dڽ,_W*dnsc;2r) c1T$p17Q66嘻 +(&c1j卐PE9~k̥`ᦸp^"{7é\FHe@o;y E+ +h +[%;Z)(NIʫ +2=yjH%xY~jcD:íP +,q0jCX[u$iG/mו'?\1}A8uT {BPv/QTxtV/A权K^D(2[bET7\%iO!n~O}F 8TXuLd^cgQIlAyUo.l['guo%CjGNAϬ0>}H̃<ư#|a^_ƣ餓\eq]ȵSE/09#0d{Y}L^5/v]7S:pE;w8H% DCjhڗh iƒE&mY]7 R%D?-^|Hd1DK{K9VHc9>:LqV+v[ѽJk-5>i`MC] r7J;9|J<2ZOQQ\*>Bg*K!6lj_QIؖq^eD!,T+ sc4}8MN8gH[ swqSDA$)$ot/Q1C_|O_#KկPڤ̣b2.I$618)0 +'3gNpF)MP鲮JN,=_;w.jx" + Sf{akrōQB__\s?@Rq`^=&*r9Z |==Ĥ1|H{>$ڹDPHY<+>5B.)<)*yU/ d*_M|5|Ýԇ| ~;CXo Pmlp&a{;wmc)BG@-GDr +CEnnLVFKG}L\m^ςԥ_|By۲Oí|FmG(=q`peGDX/RUkR44<5ZPWm{.{;tegH~Z`\Xp0ḱZr@"rPSC!-9|Nމmkj6sj;@fwP{~qC3>c,H͆@a.Rx:H0#iVFRe.'m"2@e@31n3wwc77Aov,$cpMQ"돭K9LuOV4C@/VѲo?bw_'Z#/Qfv(eXjK%.M7XQq6S FW[v-+-@N%DW 3ڻq_QZ<01>0 rh䜆"# )ْ{>$&F@D_fW3FtqB3s$ wQb@ZsـxLi]Wb~2ސޱ$_DZ!L0 pyEnvA6Lq)xy#p8sr\pa HŠYxj:W[,  +\X~@RFՋ2"hd>#[9A\01_i$m/ IP/2jIɻ];!Ƀ2FLJ .%;\'k (STA g= [!6>,k4Uk>} @A@L5Qj8pKjDX1fnIp,e&R &¡p[Q C4ZĞ>[Ds"*+Q7!M8jKw}ud/w.>Uѩk +j3cWi83s_rC5cÅf8y% 0Z"od +_?+]MgGצ@_B;4-hG2Ds-\9oS3UZ<[#R ތguTں5?A 5+0WDŽc28r}J"rE(r0?&' ?lR3epj +^~pJ&8I!+sOlH+HU~Hmtt+f5daKVbT=$FRg&,wn;AZ ͦW1KDƟeX_!qʂ"Nez/w]O endstream endobj 31 0 obj <>stream +Hn7 z+~qp4 QgE+Qrex»_.Ö_o)~ÜHcRzlX*N__aS8gz/#=gs +䜂IϦ+bk7=Z-AKUed9YxV4/*H:$lAԔy&%lRb\dpC4֎bbz9> >sR2jXh:ڷXva qoC4wEۻO.owp5a#҃RQQN`^2F"l1W .xMs/#eF̸yGsr߀1X vo4Vo +b3kp$U]ŶZQ+k ʞR)H"Q]т %?vZZ1 Ke_?8wk,s[ƥsX ɮdcWCB) %-K,/2IuQtjx18`jN::~^EFWFAܷ?('̀g^jSK%")(C ՄRq%[-4d$+|Xn$7gcI)P/59w +"A\8 +f;LA3R Um'Q2-mR-P1k("Q։p? pzs.TH&oE!bA1UT[moEc_$S8A +Ur 7WxFB-4iDɁ"_UU8!FSn]!(buB%wǻ5Z\Gmw +E44q *uaJ :tϯ鬆|#E-yY-$ .XY:z ^( )n)ɘt@g!ќ/fK 2G40sZz~Vg;ZYӽ"mxrWx>ira#4*Eq[GnT]2ϖת{x@3sڥy78?Tb!/0[t3ĔZ[R7nTyf=A>stream +Hn7|C~Z12Nj^GLnJgE"@,Ocp~xt[}>\:|?`e_v(1"FKl?|/G)QԜ8o筄Km.Kt-Gjn?Fuv|1Ғ/)K,J5aSkρ]J==o~q4Nsqs" +' cX艂HM)eW}. +WRfkǫŗZcY#Wł.]ug-Tc 'Nth3:dtFm\w!Jr~Rx9ԲK egB!MjLj,y32G_o~" B&mRG*_DXL$qV5)gu+92RHxrfYr#cKŗ3=eV$ *DrZdDsBR!=oR+REfY}'!ayWzl)p()V_bo[rbɣ5ޔ=ڸ(esEu3'BNa XfkaOj8PEsG5}L:E&'ptrXt|C XkʹhqwsrAD@M_ú ނ10 oTB] A{쏃K2^ɅZC N{2U2jhGnytrRş"s]cFO=bNBͲaaR0+@+Q_ٻʙZԮh>B(_%E ƙs5N7N-g5dpFKęvj*ADcYNjSey}STJ྅=; ,_ų0IjPal:e·33h}Gg=<9Y5l } 7D>hȎGmÍum:08pXɜc"%vև=7=LUT+H禼ŸF! [&0@غREp:1`rj0G-g7}AI,R!%;e M-)GFdV4TsΪЎyzHH GAv8cP]&͹{u\znR ȪL%% jh Q: j)3EtK*S )M>RV&8ZgFpW,hyZٯ,J֤{Z$ЖǦ٤(LT}V!f$N3O-#BJw\SRחL)NvE[W%TL}p/\Ηi-p7-ٖqf5D[24 {~$*'*@7 j#lpXTKUG@ si}C>MeMܭ)N -Cf#íiP㢧{ٌVuheO6UjqL xiDqתgP|i5(2eTH/7oX£8B*T>>MAJ` Ӽ\FٯGγ1 +h8 +#ckؽwܿ:\uL^X +X6LZO#]KQ׺gy@{٨'9bטVAm?0ٽw4i>JKhнν9{ ECkDht9 ,tzCH_uKa$x6 +\酑gY6V)*I5!W8B]cY9'q.ŸwL83㺽v&-%@NU䨯?0u7f P] &3O"I@LMU ]τAhYMt쭚'" QU53go-+4i +>4 +0 ix8+r.\Tri]@:jq}bCLz+Hcn{=|xLPGV>>n +0W endstream endobj 27 0 obj <> endobj 30 0 obj <> endobj 51 0 obj <> endobj 52 0 obj <> endobj 53 0 obj <> endobj 50 0 obj <> endobj 54 0 obj <> endobj 55 0 obj <> endobj 23 0 obj [22 0 R 21 0 R 20 0 R 19 0 R] endobj 56 0 obj <> endobj xref +0 57 +0000000004 65535 f +0000000016 00000 n +0000000189 00000 n +0000022051 00000 n +0000000000 00000 f +0000022116 00000 n +0000000000 00000 f +0000027329 00000 n +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000027402 00000 n +0000027543 00000 n +0000028772 00000 n +0000000000 00000 f +0000022555 00000 n +0000026464 00000 n +0000026530 00000 n +0000026604 00000 n +0000026678 00000 n +0000075282 00000 n +0000022984 00000 n +0000072087 00000 n +0000027216 00000 n +0000074270 00000 n +0000025397 00000 n +0000025105 00000 n +0000074416 00000 n +0000070731 00000 n +0000025251 00000 n +0000023413 00000 n +0000026115 00000 n +0000025903 00000 n +0000025543 00000 n +0000025661 00000 n +0000025780 00000 n +0000025998 00000 n +0000026233 00000 n +0000026343 00000 n +0000027100 00000 n +0000027131 00000 n +0000026984 00000 n +0000027015 00000 n +0000026868 00000 n +0000026899 00000 n +0000026752 00000 n +0000026783 00000 n +0000074911 00000 n +0000074570 00000 n +0000074688 00000 n +0000074794 00000 n +0000075053 00000 n +0000075162 00000 n +0000075328 00000 n +trailer <]>> startxref 75519 %%EOF \ No newline at end of file diff --git a/core/resources-src/pix/icon/logo_rocket.ork b/core/resources-src/pix/icon/logo_rocket.ork new file mode 100644 index 000000000..87bc4de03 Binary files /dev/null and b/core/resources-src/pix/icon/logo_rocket.ork differ diff --git a/core/resources-src/pix/splashscreen-2.1.png b/core/resources-src/pix/splashscreen-2.1.png new file mode 100644 index 000000000..e6924c692 Binary files /dev/null and b/core/resources-src/pix/splashscreen-2.1.png differ diff --git a/core/resources-src/pix/splashscreen-2.1.xcf b/core/resources-src/pix/splashscreen-2.1.xcf new file mode 100644 index 000000000..e3403d52e Binary files /dev/null and b/core/resources-src/pix/splashscreen-2.1.xcf differ diff --git a/core/resources/l10n/messages.properties b/core/resources/l10n/messages.properties index 4721c4e23..bc6eabbeb 100644 --- a/core/resources/l10n/messages.properties +++ b/core/resources/l10n/messages.properties @@ -50,6 +50,7 @@ RocketActions.MoveDownAct.ttip.Movedown = Move this component downwards. ! RocketPanel RocketPanel.FigTypeAct.SideView = Side view +RocketPanel.FigTypeAct.TopView = Top view RocketPanel.FigTypeAct.BackView = Back view RocketPanel.FigTypeAct.Figure3D = 3D Figure RocketPanel.FigTypeAct.Finished = 3D Finished @@ -562,6 +563,7 @@ SimuRunDlg.msg.Unabletosim = Unable to simulate: SimuRunDlg.msg.errorOccurred = An error occurred during the simulation: BasicEventSimulationEngine.error.noMotorsDefined = No motors defined in the simulation. +BasicEventSimulationEngine.error.cantCalculateStability = Can't calculate rocket stability. BasicEventSimulationEngine.error.earlyMotorBurnout = Motor burnout without liftoff. BasicEventSimulationEngine.error.noConfiguredIgnition = No motors configured to ignite at liftoff BasicEventSimulationEngine.error.noIgnition = No motors ignited. @@ -718,6 +720,11 @@ simplotpanel.RIGHT_NAME = Right simplotpanel.CUSTOM = Custom SimulationPlotPanel.error.noPlotSelected = Please add one or more variables to plot on the Y-axis. SimulationPlotPanel.error.noPlotSelected.title = Nothing to plot +simplotpanel.MarkerStyle.lbl.MarkerStyle = Marker style: +simplotpanel.MarkerStyle.lbl.MarkerStyle.ttip = Style of the flight event marker (how it's drawn in the simulation plot) +simplotpanel.MarkerStyle.btn.VerticalMarker = Vertical line +simplotpanel.MarkerStyle.btn.Icon = Icon +simplotpanel.MarkerStyle.OnlyInTime = Only available for time domain, other domains only support icon markers ! Component add buttons compaddbuttons.AxialStage = Stage @@ -819,7 +826,6 @@ ringcompcfg.Radialdirection = Radial direction: ringcompcfg.radialdirectionfrom = The radial direction from the rocket centerline ringcompcfg.but.Reset = Reset ringcompcfg.but.Resetcomponant = Resets the component to the rocket centerline. -ringcompcfg.EngineBlock.desc = An engine block stops the motor from moving forwards in the motor mount tube.

In order to add a motor, create a body tube or inner tube and mark it as a motor mount in the Motor tab. ringcompcfg.note.desc = Note: An inner tube will not affect the aerodynamics of the rocket even if it is located outside of the body tube. @@ -972,6 +978,37 @@ RocketCompCfg.tab.Outside = Outside RocketCompCfg.tab.Inside = Inside RocketCompCfg.tab.RightSide = Right Side RocketCompCfg.tab.LeftSide = Left Side +RocketCompCfg.btn.OK.ttip = Keep changes and close the dialog +RocketCompCfg.btn.Cancel.ttip = Discard changes and close the dialog +RocketCompCfg.CancelOperation.msg.discardChanges = Are you sure you want to discard your changes to this component? +RocketCompCfg.CancelOperation.msg.undoAdd = Are you sure you want to undo adding this component? +RocketCompCfg.CancelOperation.title = Cancel operation +RocketCompCfg.CancelOperation.checkbox.dontAskAgain = Don't ask me again +RocketCompCfg.btn.ComponentInfo.ttip = Show/hide informative text about this component. + +! ComponentInfo +ComponentInfo.Rocket = This is your rocket. Nothing more to say about it. Have fun designing and building it! :) +ComponentInfo.AxialStage = A stage is a section of the model rocket that contains motors which ignite successively and separate after motor burnout.
Each stage must safely descend after separation. The main stage, often called the 'Sustainer', usually descends with a recovery device. +ComponentInfo.ParallelStage = A booster is a fictional component that acts as an attachment point to a body tube and can separate from that body tube at a specified time during flight.
The most common use of a booster is the attachment of a strap-on booster. +ComponentInfo.PodSet = A pod is a fictional component that acts as an attachment point to a body tube.
Once defined, other components can be attached to the pod to create complex rocket geometries. +ComponentInfo.NoseCone = A nose cone is the front end of the rocket airframe. Nose cones vary widely in shape.
The back end can be cut down to form an internal shoulder that slides inside of a body tube to hold the nose cone in place. +ComponentInfo.BodyTube = A body tube is primarily used for the structure of the exterior body of the model rocket. +ComponentInfo.Transition = A transition is the part of the airframe that connects two body tubes having different outside diameters.
Both ends of a transition are cut down to form an internal shoulder that slides inside of each body tube to hold the transition in place at both ends. +ComponentInfo.TrapezoidFinSet = Fins help stabilize the rocket. The most common fin shape is trapezoidal, four sides with parallel root and tip chords (the edge that glues to the body tube and the outer edge). +ComponentInfo.EllipticalFinSet = Fins help stabilize the rocket. The most efficient fin shape is elliptical. This shape induces the least amount of drag of any fin shape. It is commonly used for competition flying events. +ComponentInfo.FreeformFinSet = Fins help stabilize the rocket. The most versatile fin component is the freeform fin. This fin component allows the creation of virtually any solid fin shape, with the ability to manually enter data points or import a shape from an image file. +ComponentInfo.TubeFinSet = Tube fins are used to keep the model rocket going straight after launch. Tube fins vary in length and diameter, and may have either straight or curved ends.
Currently, OpenRocket only supports straight-perpendicular cut ends. +ComponentInfo.RailButton = Rail buttons keep the model rocket from changing orientation when it launches. Rail buttons are most commonly used on larger model rockets, in pairs, instead of launch lugs.
They can be mounted to the outside of the body tube with a screw and internal nut.
A rail button looks like two washers with a spacer in between; one washer is against the body tube and the other slides inside the launch rail. +ComponentInfo.LaunchLug = A launch lug is the most common guide used to keep smaller model rockets from changing orientation until the rocket leaves the end of the launch rod. +ComponentInfo.InnerTube = Inner tubes are most commonly used for motor tubes, but may also be part of other internal systems, such as an ejection baffles and adjustable weight designs.
Inner tubes may be positioned radially inside of a body tube and, when used as a motor tube, may be clustered. +ComponentInfo.TubeCoupler = A coupler is a short tube that connects two tubes with the same diameters.
In model rockets that use electronics, a coupler may be used for the avionics bay.
The tubes are often glued to the coupler to stop them from sliding. +ComponentInfo.CenteringRing = Centering rings are used to position one or more inner tubes inside of a body tube.
Fin sets have an automatic fin tab feature for determining an ideal fin tab configuration based on an inner tube positioned below the root chord of the fins. This also requires two centering below the root chord. +ComponentInfo.Bulkhead = A bulkhead is a disk that may or may not have a small hole in the center. Bulkheads are used to seal one area of a model rocket from another or to mount recovery system eyes for shock cord attachment, such as nose cone caps and avionics bay lids. +ComponentInfo.EngineBlock = An engine block stops the motor from moving forward in the motor mount tube.
In order to add a motor, create a body tube or inner tube and mark it as a motor mount in the Motor tab. +ComponentInfo.Parachute = The parachute is the most common recovery device. Smal model rocket parachutes are made of thin, light-weight materials, such as polyethylene or Mylar, while larger parachutes are most commonly made of rip-stop nylon. Parachutes may be decorated in bright and contrasting colors/patterns to improve visibility.
Shroud lines attach the parachute canopy to the shock cord. The shroud lines are sometimes brought together and attached using a swivel. +ComponentInfo.Streamer = A streamer is a recovery device that, when ejected, unrolls and slows the model rocket during descent.
A streamer is a long, narrow, rectangular strip of crepe paper or thin plastic film. The width and length of the streamer are adjusted to match the weight of the rocket, but a 10 to 1 length to width ratio is considered the best for a streamer. +ComponentInfo.ShockCord = A shock cord attaches the nose cone and recovery device to the body tube, keeping the model rocket together during descent.
Smaller rockets commonly have short elastic shock cords, while larger rockets have long shock cords made from materials such as Nylon or heat resistant Kevlar webbing. +ComponentInfo.MassComponent = A mass component may be used to simulate a component that is not otherwise listed.
In OpenRocket, this component type may be designated for instance as an altimeter, a flight computer, a battery\u2026 Each designation has its own unique icon to ease identification. ! BulkheadConfig BulkheadCfg.tab.Diameter = Diameter: @@ -1148,6 +1185,8 @@ NoseConeCfg.tab.General = General NoseConeCfg.tab.ttip.General = General properties NoseConeCfg.tab.Shoulder = Shoulder NoseConeCfg.tab.ttip.Shoulder = Shoulder properties +NoseConeCfg.checkbox.Flip = Flip to tail cone +NoseConeCfg.checkbox.Flip.ttip = Flips the nose cone direction to a tail cone. ! ParachuteConfig Parachute.Parachute = Parachute @@ -1847,6 +1886,7 @@ PlotConfiguration.Groundtrack = Ground track Warning.LargeAOA.str1 = Large angle of attack encountered. Warning.LargeAOA.str2 = Large angle of attack encountered ( Warning.DISCONTINUITY = Discontinuity in rocket body diameter +Warning.OPEN_AIRFRAME_FORWARD = Forward end of airframe is open (radius is > 0) Warning.THICK_FIN = Thick fins may not simulate accurately. Warning.JAGGED_EDGED_FIN = Jagged-edged fin predictions may be inaccurate. Warning.LISTENERS_AFFECTED = Listeners modified the flight simulation @@ -2150,7 +2190,9 @@ ExportDecalDialog.source.exception = Could not find decal source file ''{0}''..كتلة المحرك تمنعه من التحرك للأمام في أنبوب الحامل للمحرك

.من أجل إضافة محرك ، قم بإنشاء أنبوب جسم أو أنبوب داخلي وقم بتمييزه على أنه حامل محرك في علامة تبويب المحرك ringcompcfg.note.desc = .ملاحظة: الأنبوب الداخلي لن يؤثر على الديناميكا الهوائية للصاروخ حتى لو كان موجودًا خارج أنبوب الجسم @@ -971,6 +970,9 @@ RocketCompCfg.tab.Inside = من الداخل RocketCompCfg.tab.RightSide = الجانب اليميني RocketCompCfg.tab.LeftSide = الجانب اليساري +! ComponentInfo +ComponentInfo.EngineBlock = .كتلة المحرك تمنعه من التحرك للأمام في أنبوب الحامل للمحرك

.من أجل إضافة محرك ، قم بإنشاء أنبوب جسم أو أنبوب داخلي وقم بتمييزه على أنه حامل محرك في علامة تبويب المحرك + ! BulkheadConfig BulkheadCfg.tab.Diameter = :القطر BulkheadCfg.tab.Thickness = :السماكة diff --git a/core/resources/l10n/messages_cs.properties b/core/resources/l10n/messages_cs.properties index 48cb441fd..1c6ddaf10 100644 --- a/core/resources/l10n/messages_cs.properties +++ b/core/resources/l10n/messages_cs.properties @@ -568,7 +568,6 @@ ringcompcfg.Radialdirection = Radi ringcompcfg.radialdirectionfrom = Radiln vzdlenost od smeru osy ringcompcfg.but.Reset = Reset ringcompcfg.but.Resetcomponant = Resetuj komponentu od osy rakety -ringcompcfg.EngineBlock.desc = An blok motoru zastav motor v pohybu vzad v motorov trubici.

Pokud se prid motor vytvor telo nebo skrytou trubku tube a oznac ji jako mont\u017E motoru vMotoru tab. ringcompcfg.note.desc = Poznmka: Vnitrn trubka nem effekt na aerodynamiku motoru i kdy\u017E je umstena mimo telo rakety. @@ -662,6 +661,9 @@ RocketCompCfg.title.Aftshoulder = Dr\u017E RocketCompCfg.border.Foreshoulder = Dr\u017Ek prdi !RocketCompCfg.lbl.Length = Dlka: +! ComponentInfo +ComponentInfo.EngineBlock = An blok motoru zastav motor v pohybu vzad v motorov trubici.

Pokud se prid motor vytvor telo nebo skrytou trubku tube a oznac ji jako mont\u017E motoru vMotoru tab. + ! BulkheadConfig BulkheadCfg.tab.Diameter = Prumer: BulkheadCfg.tab.Thickness = Tlou\u0161tka: diff --git a/core/resources/l10n/messages_de.properties b/core/resources/l10n/messages_de.properties index 11418eb82..4f96295fe 100644 --- a/core/resources/l10n/messages_de.properties +++ b/core/resources/l10n/messages_de.properties @@ -625,7 +625,6 @@ ringcompcfg.Radialdirection = Radiale Richtung ringcompcfg.radialdirectionfrom = Die radiale Richtung von der Raketenmittellinie ringcompcfg.but.Reset = Zurcksetzen ringcompcfg.but.Resetcomponant = Komponente auf die Raketenmittellinie zurcksetzen -ringcompcfg.EngineBlock.desc = Eine Motorhalterung verhindert, dass der Motor sich im Rohr nach vorne bewegt.

Um einen Motor hinzuzufgen, ein Krperrohr oder ein Innenrohr hinzufgen und im Reiter Motor als Motorhalterung markieren. ringcompcfg.note.desc = Hinweis: Innenrohre beeinflussen die Aerodynamik der Rakete nicht, auch wenn sie auerhalb des Krperohres liegen. @@ -719,6 +718,9 @@ RocketCompCfg.title.Aftshoulder = Schulter hinten RocketCompCfg.border.Foreshoulder = Schulter vorn !RocketCompCfg.lbl.Length = Length: +! ComponentInfo +ComponentInfo.EngineBlock = Eine Motorhalterung verhindert, dass der Motor sich im Rohr nach vorne bewegt.

Um einen Motor hinzuzufgen, ein Krperrohr oder ein Innenrohr hinzufgen und im Reiter Motor als Motorhalterung markieren. + ! BulkheadConfig BulkheadCfg.tab.Radius = Radius: BulkheadCfg.tab.Thickness = Wandstrke: diff --git a/core/resources/l10n/messages_es.properties b/core/resources/l10n/messages_es.properties index 92cf2afc9..ab3aa4b89 100644 --- a/core/resources/l10n/messages_es.properties +++ b/core/resources/l10n/messages_es.properties @@ -869,6 +869,9 @@ RocketCompCfg.title.Noseconeshoulder = Acople de la ojiva RocketCompCfg.ttip.Endcapped = Si el extremo del soporte est\u00e1 truncado. RocketCompCfg.lbl.Componentname.ttip = El nombre del componente. +! ComponentInfo +ComponentInfo.EngineBlock = Un ret\u00e9n de motor impide que el motor se desplace hacia delante, por dentro del tubo porta motor.

Para a\u00f1adir un motor, cree un Cuerpo tubular o Tubo interior y des\u00edgnelo como porta motor en la pesta\u00f1a Motor. + RocketComponent.Position.ABSOLUTE = Extremo de la ojiva RocketComponent.Position.AFTER = Despu\u00e9s del componente RocketComponent.Position.BOTTOM = Extremo inferior del componente @@ -1705,7 +1708,6 @@ update.dlg.latestVersion = Usted est\u00e1 utilizando la \u00faltima versi\u00f3 ringcompcfg.Automatic = Autom\u00e1tico ringcompcfg.Distancefrom = Distancia desde la l\u00ednea central del cohete: -ringcompcfg.EngineBlock.desc = Un ret\u00e9n de motor impide que el motor se desplace hacia delante, por dentro del tubo porta motor.

Para a\u00f1adir un motor, cree un Cuerpo tubular o Tubo interior y des\u00edgnelo como porta motor en la pesta\u00f1a Motor. ringcompcfg.InnerRadius = Radio interior: ringcompcfg.Length = Longitud: ! Ring Component Config diff --git a/core/resources/l10n/messages_fr.properties b/core/resources/l10n/messages_fr.properties index 015842dc5..979878fba 100644 --- a/core/resources/l10n/messages_fr.properties +++ b/core/resources/l10n/messages_fr.properties @@ -861,6 +861,9 @@ RocketCompCfg.title.Noseconeshoulder = Epaulement du c\u00F4ne RocketCompCfg.ttip.Endcapped = Pr\u00E9cise si l'arri\u00E8re du c\u00F4ne est clos. RocketCompCfg.lbl.Componentname.ttip = Le nom de la pi\u00E8ce. +! ComponentInfo +ComponentInfo.EngineBlock = Un bloc moteur emp\u00EAche le moteur de se d\u00E9placer vers l'avant dans le tube porte moteur.

Pour ajouter un moteur, cr\u00E9er un tube ou un tube interne et marquer le comme porte moteur dans l'onglet Moteur. + RocketComponent.Position.ABSOLUTE = Pointe de l'ogive RocketComponent.Position.AFTER = Apr\u00E8s la pi\u00E8ce parente RocketComponent.Position.BOTTOM = Bas de la pi\u00E8ce parente @@ -1696,7 +1699,6 @@ update.dlg.latestVersion = Vous utilisez la derni\u00E8re version d'OpenRocket, ringcompcfg.Automatic = Automatique ringcompcfg.Distancefrom = Distance de l'axe central de la fus\u00E9e -ringcompcfg.EngineBlock.desc = Un bloc moteur emp\u00EAche le moteur de se d\u00E9placer vers l'avant dans le tube porte moteur.

Pour ajouter un moteur, cr\u00E9er un tube ou un tube interne et marquer le comme porte moteur dans l'onglet Moteur. ringcompcfg.InnerRadius = Diam\u00E8tre int\u00E9rieur ringcompcfg.Length = Longueur ! Ring Component Config diff --git a/core/resources/l10n/messages_it.properties b/core/resources/l10n/messages_it.properties index 96f4a35c3..f3e926105 100644 --- a/core/resources/l10n/messages_it.properties +++ b/core/resources/l10n/messages_it.properties @@ -626,7 +626,6 @@ ringcompcfg.Radialdirection = direzione Radiale: ringcompcfg.radialdirectionfrom = La direzione radiale dall linea centrale del razzo ringcompcfg.but.Reset = Azzera ringcompcfg.but.Resetcomponant = Azzera il componente alla linea centrale del razzo -ringcompcfg.EngineBlock.desc = Un blocca motore ferma il motore dal movimento in avanti nel tubo di montaggio del motore.

Per aggiungere un motore, crea un corpo o un tubo interno e segnalo come tubo di montaggio motore nella scheda Motore . ringcompcfg.note.desc = Nota: Un tubo interno non modifica l'aerodinamica del razzo anche se e' posto all'esterno del corpo. @@ -720,6 +719,9 @@ RocketCompCfg.title.Aftshoulder = Spalla posteriore RocketCompCfg.border.Foreshoulder = Spalla anteriore !RocketCompCfg.lbl.Length = Lunghezza: +! ComponentInfo +ComponentInfo.EngineBlock = Un blocca motore ferma il motore dal movimento in avanti nel tubo di montaggio del motore.

Per aggiungere un motore, crea un corpo o un tubo interno e segnalo come tubo di montaggio motore nella scheda Motore . + ! BulkheadConfig BulkheadCfg.tab.Diameter = Diametro: BulkheadCfg.tab.Thickness = Spessore: diff --git a/core/resources/l10n/messages_ja.properties b/core/resources/l10n/messages_ja.properties index 862f12ab1..235d42cd4 100644 --- a/core/resources/l10n/messages_ja.properties +++ b/core/resources/l10n/messages_ja.properties @@ -656,7 +656,6 @@ ringcompcfg.Radialdirection = \u534A\u5F84\u65B9\u5411\u5411\u304D ringcompcfg.radialdirectionfrom = \u30ED\u30B1\u30C3\u30C8\u30BB\u30F3\u30BF\u30FC\u30E9\u30A4\u30F3\u304B\u3089\u306E\u534A\u5F84\u65B9\u5411\u306E\u5411\u304D ringcompcfg.but.Reset = \u30EA\u30BB\u30C3\u30C8 ringcompcfg.but.Resetcomponant = \u90E8\u54C1\u3092\u30ED\u30B1\u30C3\u30C8\u30BB\u30F3\u30BF\u30FC\u30E9\u30A4\u30F3\u306B\u30EA\u30BB\u30C3\u30C8\u3059\u308B -ringcompcfg.EngineBlock.desc = \u30A8\u30F3\u30B8\u30F3\u30D6\u30ED\u30C3\u30AF\u306F\u30E2\u30FC\u30BF\u30FC\u30DE\u30A6\u30F3\u30C8\u30C1\u30E5\u30FC\u30D6\u306E\u4E2D\u3067\u30E2\u30FC\u30BF\u30FC\u304C\u524D\u306B\u52D5\u304F\u306E\u3092\u6B62\u3081\u308B\u5F79\u5272\u3002

\u30E2\u30FC\u30BF\u30FC\u3092\u8FFD\u52A0\u3059\u308B\u306B\u306F \u30DC\u30C7\u30A3\u30C1\u30E5\u30FC\u30D6\u3082\u3057\u304F\u306F\u30A4\u30F3\u30CA\u30FC\u30C1\u30E5\u30FC\u30D6\u3092\u4F5C\u3063\u3066 and mark it as a motor mount in the \u30E2\u30FC\u30BF\u30FC\u30BF\u30D6\u3067\u30E2\u30FC\u30BF\u30FC\u30DE\u30A6\u30F3\u30C8\u3068\u3057\u3066\u30C1\u30A7\u30C3\u30AF\u3059\u308B\u3002 ringcompcfg.note.desc = \u30E1\u30E2\uFF1A\u30A4\u30F3\u30CA\u30FC\u30C1\u30E5\u30FC\u30D6\u306F\u30DC\u30C7\u30A3\u30C1\u30E5\u30FC\u30D6\u306E\u5916\u5074\u306B\u51FA\u306A\u3044\u9650\u308A\u306F\u7A7A\u529B\u3078\u306E\u5F71\u97FF\u306F\u7121\u3044 @@ -750,6 +749,9 @@ RocketCompCfg.title.Aftshoulder = \u5F8C\u65B9\u30B7\u30E7\u30EB\u30C0\u30FC RocketCompCfg.border.Foreshoulder = \u524D\u65B9\u30B7\u30E7\u30EB\u30C0\u30FC !RocketCompCfg.lbl.Length +! ComponentInfo +ComponentInfo.EngineBlock = \u30A8\u30F3\u30B8\u30F3\u30D6\u30ED\u30C3\u30AF\u306F\u30E2\u30FC\u30BF\u30FC\u30DE\u30A6\u30F3\u30C8\u30C1\u30E5\u30FC\u30D6\u306E\u4E2D\u3067\u30E2\u30FC\u30BF\u30FC\u304C\u524D\u306B\u52D5\u304F\u306E\u3092\u6B62\u3081\u308B\u5F79\u5272\u3002

\u30E2\u30FC\u30BF\u30FC\u3092\u8FFD\u52A0\u3059\u308B\u306B\u306F \u30DC\u30C7\u30A3\u30C1\u30E5\u30FC\u30D6\u3082\u3057\u304F\u306F\u30A4\u30F3\u30CA\u30FC\u30C1\u30E5\u30FC\u30D6\u3092\u4F5C\u3063\u3066 and mark it as a motor mount in the \u30E2\u30FC\u30BF\u30FC\u30BF\u30D6\u3067\u30E2\u30FC\u30BF\u30FC\u30DE\u30A6\u30F3\u30C8\u3068\u3057\u3066\u30C1\u30A7\u30C3\u30AF\u3059\u308B\u3002 + ! BulkheadConfig BulkheadCfg.tab.Diameter = \u534A\u5F84\uFF1A BulkheadCfg.tab.Thickness = \u539A\u3055\uFF1A @@ -1046,7 +1048,7 @@ TCMotorSelPan.lbl.Datapoints = \u30C7\u30FC\u30BF\u70B9\uFF1A TCMotorSelPan.lbl.Digest = \u30C0\u30A4\u30B8\u30A7\u30B9\u30C8\uFF1A TCMotorSelPan.title.Thrustcurve = \u63A8\u529B\u5C65\u6B74\uFF1A TCMotorSelPan.title.Thrust = \u63A8\u529B -TCMotorSelPan.delayBox.None = None +TCMotorSelPan.delayBox.None = Plugged (none) TCMotorSelPan.noDescription = No description available. diff --git a/core/resources/l10n/messages_nl.properties b/core/resources/l10n/messages_nl.properties index 2cbf92ceb..cdae10dc1 100644 --- a/core/resources/l10n/messages_nl.properties +++ b/core/resources/l10n/messages_nl.properties @@ -751,7 +751,6 @@ ringcompcfg.Radialdirection = Radiale richting: ringcompcfg.radialdirectionfrom = De radiale richting vanaf de middellijn van de raket ringcompcfg.but.Reset = Herstel ringcompcfg.but.Resetcomponant = Zet het onderdeel terug naar de raketmiddellijn -ringcompcfg.EngineBlock.desc = Een motorblok voorkomt dat de motor voorwaarts beweegt in de buis van de motorsteun.

Om een motor toe te voegen, maak een rompbuis of binnenbuis en markeer het als een motorbevestiging in het Motortabblad. ringcompcfg.note.desc = Opmerking: Een binnenbuis heeft geen invloed op de aërodynamica van de raket, zelfs niet als hij buiten de rompbuis is geplaatst. @@ -871,6 +870,9 @@ RocketCompCfg.border.Foreshoulder = Voorschouder RocketCompCfg.lbl.InstanceCount = Aantal instanties RocketCompCfg.lbl.InstanceSeparation = Instantie afscheiding +! ComponentInfo +ComponentInfo.EngineBlock = Een motorblok voorkomt dat de motor voorwaarts beweegt in de buis van de motorsteun.

Om een motor toe te voegen, maak een rompbuis of binnenbuis en markeer het als een motorbevestiging in het Motortabblad. + ! BulkheadConfig BulkheadCfg.tab.Diameter = Diameter: BulkheadCfg.tab.Thickness = Dikte: diff --git a/core/resources/l10n/messages_pl.properties b/core/resources/l10n/messages_pl.properties index f65d3dda8..0ed164831 100644 --- a/core/resources/l10n/messages_pl.properties +++ b/core/resources/l10n/messages_pl.properties @@ -570,7 +570,6 @@ update.dlg.latestVersion = Korzystasz z najnowszej wersji OpenRocket: %s. ringcompcfg.radialdirectionfrom = Kierunek prostopad\u0142y do osi centralnej rakiety ringcompcfg.but.Reset = Reset ringcompcfg.but.Resetcomponant = Resetuj po\u0142o\u017Cenie cz\u0119\u015Bci wzgl\u0119dem osi rakiety - ringcompcfg.EngineBlock.desc = Blokada silnika unieruchamia silnik wewn\u0105trz elementu pe\u0142ni\u0105cego funkcj\u0119 gniazda silnikowego.

Aby doda\u0107 silnik, stwrz najpierw Korpus rakiety lub rur\u0119 wewn\u0119trzn\u0105 i oznacz j\u0105 jako gniazdo silnika w zak\u0142adce Silnik. ringcompcfg.note.desc = Uwaga: Rura wewn\u0119trzna nie wp\u0142ywa na aerodynamik\u0119 rakiety, nawet je\u017Celi znajduje si\u0119 poza korpusem. @@ -662,7 +661,11 @@ update.dlg.latestVersion = Korzystasz z najnowszej wersji OpenRocket: %s. RocketCompCfg.title.Noseconeshoulder = Wpust g\u0142owicy RocketCompCfg.title.Aftshoulder = Wpust tylny RocketCompCfg.border.Foreshoulder = Wpust przedni - !RocketCompCfg.lbl.Length + !RocketCompCfg.lbl.Length + +! ComponentInfo +ComponentInfo.EngineBlock = Blokada silnika unieruchamia silnik wewn\u0105trz elementu pe\u0142ni\u0105cego funkcj\u0119 gniazda silnikowego.

Aby doda\u0107 silnik, stwrz najpierw Korpus rakiety lub rur\u0119 wewn\u0119trzn\u0105 i oznacz j\u0105 jako gniazdo silnika w zak\u0142adce Silnik. + ! BulkheadConfig BulkheadCfg.tab.Diameter = \u015Arednica: diff --git a/core/resources/l10n/messages_pt.properties b/core/resources/l10n/messages_pt.properties index a4a3d1931..e4db99f01 100644 --- a/core/resources/l10n/messages_pt.properties +++ b/core/resources/l10n/messages_pt.properties @@ -844,6 +844,9 @@ RocketCompCfg.title.Noseconeshoulder = Ressalto da ogiva RocketCompCfg.ttip.Endcapped = Quando a extremidade do ressalto \u00e9 limitada. RocketCompCfg.lbl.Componentname.ttip = Nome do componente. +! ComponentInfo +ComponentInfo.EngineBlock = Um bloco do motor p\u00e1ra o motor de se mover para a frente no tubo de montagem do motor.

Para adicionar um motor, criar um tubo de corpo ou tubo interno e marc\u00e1-lo como uma montagem do motor na aba Motor. + RocketComponent.Position.ABSOLUTE = Dica da ogiva RocketComponent.Position.AFTER = Depois do componente pai RocketComponent.Position.BOTTOM = Parte do componente pai @@ -1650,7 +1653,6 @@ update.dlg.latestVersion = Voc\u00ea est\u00e1 executando a vers\u00e3o mais rec ringcompcfg.Automatic = Autom\u00e1tico ringcompcfg.Distancefrom = Dist\u00e2ncia a partir da linha de centro do foguete -ringcompcfg.EngineBlock.desc = Um bloco do motor p\u00e1ra o motor de se mover para a frente no tubo de montagem do motor.

Para adicionar um motor, criar um tubo de corpo ou tubo interno e marc\u00e1-lo como uma montagem do motor na aba Motor. ringcompcfg.InnerRadius = Raio interno ringcompcfg.Length = Comprimento # Ring Component Config diff --git a/core/resources/l10n/messages_ru.properties b/core/resources/l10n/messages_ru.properties index de6876838..0be529d74 100644 --- a/core/resources/l10n/messages_ru.properties +++ b/core/resources/l10n/messages_ru.properties @@ -804,7 +804,6 @@ ringcompcfg.Radialdirection = \u0420\u0430\u0434\u0438\u0430\u043B\u044C\u043D\u ringcompcfg.radialdirectionfrom = \u0420\u0430\u0434\u0438\u0430\u043B\u044C\u043D\u043E\u0435 \u043D\u0430\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u0435 \u043E\u0442 \u043E\u0441\u0438 \u0440\u0430\u043A\u0435\u0442\u044B ringcompcfg.but.Reset = \u0421\u0431\u0440\u043E\u0441 ringcompcfg.but.Resetcomponant = \u0412\u043E\u0441\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u044C \u043A\u043E\u043C\u043F\u043E\u043D\u0435\u043D\u0442 \u043D\u0430 \u043E\u0441\u0438 \u0440\u0430\u043A\u0435\u0442\u044B -ringcompcfg.EngineBlock.desc = \u0423\u043F\u043E\u0440 \u0434\u0432\u0438\u0433\u0430\u0442\u0435\u043B\u044F \u043F\u0440\u0435\u043F\u044F\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0434\u0432\u0438\u0436\u0435\u043D\u0438\u044E \u0434\u0432\u0438\u0433\u0430\u0442\u0435\u043B\u044F \u0432\u043F\u0435\u0440\u0435\u0434 \u0432 \u0442\u0440\u0443\u0431\u0435.

\u0414\u043B\u044F \u0442\u043E\u0433\u043E \u0447\u0442\u043E\u0431\u044B \u0434\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u0434\u0432\u0438\u0433\u0430\u0442\u0435\u043B\u044C, \u0441\u043E\u0437\u0434\u0430\u0439\u0442\u0435 \u041A\u043E\u0440\u043F\u0443\u0441\u043D\u0443\u044E \u0442\u0440\u0443\u0431\u0443 \u0438\u043B\u0438 \u0412\u043D\u0443\u0442\u0440\u0435\u043D\u043D\u044E\u044E \u0442\u0440\u0443\u0431\u0443 \u0438 \u043E\u0442\u043C\u0435\u0442\u044C\u0442\u0435 \u044D\u0442\u043E\u0442 \u044D\u043B\u0435\u043C\u0435\u043D\u0442 \u043A\u0430\u043A \u043A\u0440\u0435\u043F\u0435\u0436 \u0434\u0432\u0438\u0433\u0430\u0442\u0435\u043B\u044F \u043D\u0430 \u0432\u043A\u043B\u0430\u0434\u043A\u0435 \u0414\u0432\u0438\u0433\u0430\u0442\u0435\u043B\u044C . ringcompcfg.note.desc = \u041F\u0440\u0438\u043C\u0435\u0447\u0430\u043D\u0438\u0435: \u0412\u043D\u0443\u0442\u0440\u0435\u043D\u043D\u044F\u044F \u0442\u0440\u0443\u0431\u0430 \u043D\u0435 \u0432\u043B\u0438\u044F\u0435\u0442 \u043D\u0430 \u0430\u044D\u0440\u043E\u0434\u0438\u043D\u0430\u043C\u0438\u043A\u0443, \u0434\u0430\u0436\u0435 \u0431\u0443\u0434\u0443\u0447\u0438 \u0440\u0430\u0437\u043C\u0435\u0449\u0435\u043D\u043D\u043E\u0439 \u0437\u0430 \u043F\u0440\u0435\u0434\u0435\u043B\u0430\u043C\u0438 \u043A\u043E\u0440\u043F\u0443\u0441\u0430. @@ -949,6 +948,9 @@ RocketCompCfg.tab.Inside = \u0412\u043D\u0443\u0442\u0440\u0438 RocketCompCfg.tab.RightSide = \u0421\u043F\u0440\u0430\u0432\u0430 RocketCompCfg.tab.LeftSide = \u0421\u043B\u0435\u0432\u0430 +! ComponentInfo +ComponentInfo.EngineBlock = \u0423\u043F\u043E\u0440 \u0434\u0432\u0438\u0433\u0430\u0442\u0435\u043B\u044F \u043F\u0440\u0435\u043F\u044F\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0434\u0432\u0438\u0436\u0435\u043D\u0438\u044E \u0434\u0432\u0438\u0433\u0430\u0442\u0435\u043B\u044F \u0432\u043F\u0435\u0440\u0435\u0434 \u0432 \u0442\u0440\u0443\u0431\u0435.

\u0414\u043B\u044F \u0442\u043E\u0433\u043E \u0447\u0442\u043E\u0431\u044B \u0434\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u0434\u0432\u0438\u0433\u0430\u0442\u0435\u043B\u044C, \u0441\u043E\u0437\u0434\u0430\u0439\u0442\u0435 \u041A\u043E\u0440\u043F\u0443\u0441\u043D\u0443\u044E \u0442\u0440\u0443\u0431\u0443 \u0438\u043B\u0438 \u0412\u043D\u0443\u0442\u0440\u0435\u043D\u043D\u044E\u044E \u0442\u0440\u0443\u0431\u0443 \u0438 \u043E\u0442\u043C\u0435\u0442\u044C\u0442\u0435 \u044D\u0442\u043E\u0442 \u044D\u043B\u0435\u043C\u0435\u043D\u0442 \u043A\u0430\u043A \u043A\u0440\u0435\u043F\u0435\u0436 \u0434\u0432\u0438\u0433\u0430\u0442\u0435\u043B\u044F \u043D\u0430 \u0432\u043A\u043B\u0430\u0434\u043A\u0435 \u0414\u0432\u0438\u0433\u0430\u0442\u0435\u043B\u044C . + ! BulkheadConfig BulkheadCfg.tab.Diameter = \u0414\u0438\u0430\u043C\u0435\u0442\u0440: BulkheadCfg.tab.Thickness = \u0422\u043E\u043B\u0449\u0438\u043D\u0430: diff --git a/core/resources/l10n/messages_uk_UA.properties b/core/resources/l10n/messages_uk_UA.properties index 842b601b4..c82045449 100644 --- a/core/resources/l10n/messages_uk_UA.properties +++ b/core/resources/l10n/messages_uk_UA.properties @@ -703,7 +703,6 @@ ringcompcfg.Radialdirection = Radial direction: ringcompcfg.radialdirectionfrom = The radial direction from the rocket centerline ringcompcfg.but.Reset = Reset ringcompcfg.but.Resetcomponant = Reset the component to the rocket centerline -ringcompcfg.EngineBlock.desc = An engine block stops the motor from moving forwards in the motor mount tube.

In order to add a motor, create a body tube or inner tube and mark it as a motor mount in the Motor tab. ringcompcfg.note.desc = Note: An inner tube will not affect the aerodynamics of the rocket even if it is located outside of the body tube. @@ -822,6 +821,9 @@ RocketCompCfg.title.Aftshoulder = Aft shoulder RocketCompCfg.border.Foreshoulder = Fore shoulder !RocketCompCfg.lbl.Length = Length: +! ComponentInfo +ComponentInfo.EngineBlock = An engine block stops the motor from moving forwards in the motor mount tube.

In order to add a motor, create a body tube or inner tube and mark it as a motor mount in the Motor tab. + ! BulkheadConfig BulkheadCfg.tab.Diameter = Diameter: BulkheadCfg.tab.Thickness = Thickness: @@ -1119,7 +1121,7 @@ TCMotorSelPan.lbl.Datapoints = Data points: TCMotorSelPan.lbl.Digest = Digest: TCMotorSelPan.title.Thrustcurve = Thrust curve: TCMotorSelPan.title.Thrust = Thrust -TCMotorSelPan.delayBox.None = None +TCMotorSelPan.delayBox.None = Plugged (none) TCMotorSelPan.noDescription = No description available. TCMotorSelPan.btn.checkAll = Select All TCMotorSelPan.btn.checkNone = Clear All diff --git a/core/resources/l10n/messages_zh_CN.properties b/core/resources/l10n/messages_zh_CN.properties index 2d3eed4ab..f9f331f8e 100644 --- a/core/resources/l10n/messages_zh_CN.properties +++ b/core/resources/l10n/messages_zh_CN.properties @@ -933,6 +933,9 @@ RocketCompCfg.title.Noseconeshoulder = \u5934\u9525\u8FDE\u63A5\u59 RocketCompCfg.ttip.Endcapped = \u8FDE\u63A5\u5904\u7EC8\u7AEF\u662F\u5426\u6709\u76D6. RocketCompCfg.lbl.Componentname.ttip = \u7EC4\u4EF6\u540D\u79F0. +! ComponentInfo +ComponentInfo.EngineBlock = \u53D1\u52A8\u673A\u5EA7\u7528\u4E8E\u9632\u6B62\u53D1\u52A8\u673A\u5411\u524D\u7A9C\u51FA\u7BAD\u4F53.

\u6DFB\u52A0\u53D1\u52A8\u673A\u524D\u8BF7\u5148\u6DFB\u52A0\u7BAD\u4F53\u6216\u5185\u7BA1\u5E76\u5728\u53D1\u52A8\u673A\u9875\u9762\u4E0A\u6807\u8BB0\u4E3A\u53D1\u52A8\u673A\u5EA7. + RocketComponent.Position.ABSOLUTE = \u5934\u9525\u5C16\u7AEF RocketComponent.Position.AFTER = \u7236\u7EC4\u4EF6\u4E4B\u540E RocketComponent.Position.BOTTOM = \u7236\u7EC4\u4EF6\u5E95\u90E8 @@ -1787,7 +1790,6 @@ update.dlg.latestVersion = \u60A8\u4F7F\u7528\u7684\u5DF2\u7ECF\u662FOpenRocket\ ringcompcfg.Automatic = \u81EA\u52A8 ringcompcfg.Distancefrom = \u5230\u706B\u7BAD\u4E2D\u5FC3\u7EBF\u7684\u8DDD\u79BB -ringcompcfg.EngineBlock.desc = \u53D1\u52A8\u673A\u5EA7\u7528\u4E8E\u9632\u6B62\u53D1\u52A8\u673A\u5411\u524D\u7A9C\u51FA\u7BAD\u4F53.

\u6DFB\u52A0\u53D1\u52A8\u673A\u524D\u8BF7\u5148\u6DFB\u52A0\u7BAD\u4F53\u6216\u5185\u7BA1\u5E76\u5728\u53D1\u52A8\u673A\u9875\u9762\u4E0A\u6807\u8BB0\u4E3A\u53D1\u52A8\u673A\u5EA7. ringcompcfg.InnerRadius = \u5185\u76F4\u5F84 ringcompcfg.Length = \u957F\u5EA6 ! Ring Component Config diff --git a/core/resources/pix/componenticons/stage-large.png b/core/resources/pix/componenticons/stage-large.png index e8df43fe0..6d0c8fc32 100644 Binary files a/core/resources/pix/componenticons/stage-large.png and b/core/resources/pix/componenticons/stage-large.png differ diff --git a/core/resources/pix/icon/icon-016.png b/core/resources/pix/icon/icon-016.png index c2074c7e3..c81cfd9ad 100644 Binary files a/core/resources/pix/icon/icon-016.png and b/core/resources/pix/icon/icon-016.png differ diff --git a/core/resources/pix/icon/icon-032.png b/core/resources/pix/icon/icon-032.png index a2be11332..893830b05 100644 Binary files a/core/resources/pix/icon/icon-032.png and b/core/resources/pix/icon/icon-032.png differ diff --git a/core/resources/pix/icon/icon-048.png b/core/resources/pix/icon/icon-048.png index 85a2e8392..b59b75f27 100644 Binary files a/core/resources/pix/icon/icon-048.png and b/core/resources/pix/icon/icon-048.png differ diff --git a/core/resources/pix/icon/icon-064.png b/core/resources/pix/icon/icon-064.png index cc00eb2bf..b32b6752c 100644 Binary files a/core/resources/pix/icon/icon-064.png and b/core/resources/pix/icon/icon-064.png differ diff --git a/core/resources/pix/icon/icon-128.png b/core/resources/pix/icon/icon-128.png new file mode 100644 index 000000000..9410a9945 Binary files /dev/null and b/core/resources/pix/icon/icon-128.png differ diff --git a/core/resources/pix/icon/icon-256.png b/core/resources/pix/icon/icon-256.png index c2170d358..4a9b49960 100644 Binary files a/core/resources/pix/icon/icon-256.png and b/core/resources/pix/icon/icon-256.png differ diff --git a/core/resources/pix/icon/icon-about.png b/core/resources/pix/icon/icon-about.png deleted file mode 100644 index 0ce17f774..000000000 Binary files a/core/resources/pix/icon/icon-about.png and /dev/null differ diff --git a/core/resources/pix/splashscreen.png b/core/resources/pix/splashscreen.png index d6b4793e3..e6924c692 100644 Binary files a/core/resources/pix/splashscreen.png and b/core/resources/pix/splashscreen.png differ diff --git a/core/src/net/sf/openrocket/aerodynamics/AerodynamicCalculator.java b/core/src/net/sf/openrocket/aerodynamics/AerodynamicCalculator.java index 9889a4987..76429c276 100644 --- a/core/src/net/sf/openrocket/aerodynamics/AerodynamicCalculator.java +++ b/core/src/net/sf/openrocket/aerodynamics/AerodynamicCalculator.java @@ -69,7 +69,7 @@ public interface AerodynamicCalculator extends Monitorable { public AerodynamicCalculator newInstance(); /** - * Test component assembly for continuity (esp. diameter), and post any needed warnings + * Check component assembly for geometric problems and post any needed warnings */ - public void testIsContinuous(FlightConfiguration configuration, final RocketComponent component, WarningSet warnings); + public void checkGeometry(FlightConfiguration configuration, final RocketComponent component, WarningSet warnings); } diff --git a/core/src/net/sf/openrocket/aerodynamics/BarrowmanCalculator.java b/core/src/net/sf/openrocket/aerodynamics/BarrowmanCalculator.java index ac75e95a4..264c1298b 100644 --- a/core/src/net/sf/openrocket/aerodynamics/BarrowmanCalculator.java +++ b/core/src/net/sf/openrocket/aerodynamics/BarrowmanCalculator.java @@ -21,6 +21,7 @@ import net.sf.openrocket.rocketcomponent.InstanceMap; import net.sf.openrocket.rocketcomponent.Rocket; import net.sf.openrocket.rocketcomponent.RocketComponent; import net.sf.openrocket.rocketcomponent.SymmetricComponent; +import net.sf.openrocket.unit.UnitGroup; import net.sf.openrocket.util.Coordinate; import net.sf.openrocket.util.MathUtil; import net.sf.openrocket.util.PolyInterpolator; @@ -44,7 +45,6 @@ public class BarrowmanCalculator extends AbstractAerodynamicCalculator { private double cacheDiameter = -1; private double cacheLength = -1; - public BarrowmanCalculator() { } @@ -252,7 +252,7 @@ public class BarrowmanCalculator extends AbstractAerodynamicCalculator { if (calcMap == null) buildCalcMap(configuration); - testIsContinuous(configuration, configuration.getRocket(), warnings); + checkGeometry(configuration, configuration.getRocket(), warnings); final InstanceMap imap = configuration.getActiveInstances(); @@ -276,7 +276,7 @@ public class BarrowmanCalculator extends AbstractAerodynamicCalculator { } @Override - public void testIsContinuous(FlightConfiguration configuration, final RocketComponent treeRoot, WarningSet warnings ){ + public void checkGeometry(FlightConfiguration configuration, final RocketComponent treeRoot, WarningSet warnings ){ Queue queue = new LinkedList<>(); for (RocketComponent child : treeRoot.getChildren()) { // Ignore inactive stages @@ -299,14 +299,20 @@ public class BarrowmanCalculator extends AbstractAerodynamicCalculator { } SymmetricComponent sym = (SymmetricComponent) comp; + prevComp = sym.getPreviousSymmetricComponent(); if( null == prevComp){ - prevComp = sym; - continue; - } - - // Check for radius discontinuity - if ( !MathUtil.equals(sym.getForeRadius(), prevComp.getAftRadius())) { - warnings.add( Warning.DIAMETER_DISCONTINUITY, sym + ", " + prevComp); + if (sym.getForeRadius() - sym.getThickness() > MathUtil.EPSILON) { + warnings.add(Warning.OPEN_AIRFRAME_FORWARD, sym.toString()); + } + } else { + // Check for radius discontinuity + // We're going to say it's discontinuous if it is presented to the user as having two different + // string representations. Hopefully there are enough digits in the string that it will + // present as different if the discontinuity is big enough to matter. + if (!UnitGroup.UNITS_LENGTH.getDefaultUnit().toStringUnit(2.0*sym.getForeRadius()).equals(UnitGroup.UNITS_LENGTH.getDefaultUnit().toStringUnit(2.0*prevComp.getAftRadius()))) { + // if ( !MathUtil.equals(sym.getForeRadius(), prevComp.getAftRadius())) { + warnings.add( Warning.DIAMETER_DISCONTINUITY, sym + ", " + prevComp); + } } // double x = component.toAbsolute(Coordinate.NUL)[0].x; @@ -318,9 +324,8 @@ public class BarrowmanCalculator extends AbstractAerodynamicCalculator { //} //componentX = component.toAbsolute(new Coordinate(component.getLengthAerodynamic()))[0].x; - prevComp = sym; }else if( comp instanceof ComponentAssembly ){ - testIsContinuous(configuration, comp, warnings); + checkGeometry(configuration, comp, warnings); } } diff --git a/core/src/net/sf/openrocket/aerodynamics/Warning.java b/core/src/net/sf/openrocket/aerodynamics/Warning.java index cba62d3e5..0558cc762 100644 --- a/core/src/net/sf/openrocket/aerodynamics/Warning.java +++ b/core/src/net/sf/openrocket/aerodynamics/Warning.java @@ -358,6 +358,9 @@ public abstract class Warning { /** A Warning that the body diameter is discontinuous. */ ////Discontinuity in rocket body diameter. public static final Warning DIAMETER_DISCONTINUITY = new Other(trans.get("Warning.DISCONTINUITY")); + + /** A Warning that a ComponentAssembly has an open forward end */ + public static final Warning OPEN_AIRFRAME_FORWARD = new Other(trans.get("Warning.OPEN_AIRFRAME_FORWARD")); /** A Warning that the fins are thick compared to the rocket body. */ ////Thick fins may not be modeled accurately. diff --git a/core/src/net/sf/openrocket/aerodynamics/barrowman/RailButtonCalc.java b/core/src/net/sf/openrocket/aerodynamics/barrowman/RailButtonCalc.java new file mode 100644 index 000000000..a94bd76e5 --- /dev/null +++ b/core/src/net/sf/openrocket/aerodynamics/barrowman/RailButtonCalc.java @@ -0,0 +1,102 @@ +package net.sf.openrocket.aerodynamics.barrowman; + +import java.util.List; +import java.lang.Math; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.sf.openrocket.aerodynamics.AerodynamicForces; +import net.sf.openrocket.aerodynamics.FlightConditions; +import net.sf.openrocket.aerodynamics.WarningSet; +import net.sf.openrocket.rocketcomponent.RailButton; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.MathUtil; +import net.sf.openrocket.util.Transformation; + +public class RailButtonCalc extends RocketComponentCalc { + private final static Logger log = LoggerFactory.getLogger(RailButtonCalc.class); + + // values transcribed from Gowen and Perkins, "Drag of Circular Cylinders for a Wide Range + // of Reynolds Numbers and Mach Numbers", NACA Technical Note 2960, Figure 7 + private static final List cdDomain = List.of(0.0, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 1.0, 1.6, 2.0, 2.8, 100.0); + private static final List cdRange = List.of(1.2, 1.22, 1.25, 1.3, 1.4, 1.5, 1.6, 2.1, 1.5, 1.45, 1.33, 1.33); + + private final RailButton button; + + public RailButtonCalc(RocketComponent component) { + super(component); + + // need to stash the button + button = (RailButton) component; + } + + @Override + public double calculateFrictionCD(FlightConditions conditions, double componentCf, WarningSet warnings) { + // very small relative surface area, and slick + return 0.0; + } + + @Override + public void calculateNonaxialForces(FlightConditions conditions, Transformation transform, + AerodynamicForces forces, WarningSet warnings) { + // Nothing to be done + } + + @Override + public double calculatePressureCD(FlightConditions conditions, + double stagnationCD, double baseCD, WarningSet warnings) { + + // grab relevant button params + final int instanceCount = button.getInstanceCount(); + final Coordinate[] instanceOffsets = button.getInstanceOffsets(); + + // compute button reference area + final double buttonHt = button.getTotalHeight(); + final double outerArea = buttonHt * button.getOuterDiameter(); + final double notchArea = (button.getOuterDiameter() - button.getInnerDiameter()) * button.getInnerHeight(); + final double refArea = outerArea - notchArea; + + // accumulate Cd contribution from each rail button + double CDmul = 0.0; + for (int i = 0; i < button.getInstanceCount(); i++) { + + // compute boundary layer height at button location. I can't find a good reference for the + // formula, e.g. https://aerospaceengineeringblog.com/boundary-layers/ simply says it's the + // "scientific consensus". + double x = (button.toAbsolute(instanceOffsets[i]))[0].x; // location of button + double rex = calculateReynoldsNumber(x, conditions); // Reynolds number of button location + double del = 0.37 * x / Math.pow(rex, 0.2); // Boundary layer thickness + + // compute mean airspeed over button + // this assumes airspeed changes linearly through boundary layer + // and that all parts of the railbutton contribute equally to Cd, + // neither of which is true but both are plenty close enough for our purposes + + double mach; + if (buttonHt > del) { + // Case 1: button extends beyond boundary layer + // Mean velocity is 1/2 rocket velocity up to limit of boundary layer, + // full velocity after that + mach = (buttonHt - 0.5*del) * conditions.getMach()/buttonHt; + } else { + // Case 2: button is entirely within boundary layer + mach = MathUtil.map(buttonHt/2.0, 0, del, 0, conditions.getMach()); + } + + // look up Cd as function of speed. It's pretty constant as a function of Reynolds + // number when slow, so we can just use a function of Mach number + double cd = MathUtil.interpolate(cdDomain, cdRange, mach); + + // Since later drag force calculations don't consider boundary layer, compute "effective Cd" + // based on rocket velocity + cd = cd * MathUtil.pow2(mach)/MathUtil.pow2(conditions.getMach()); + + // add to CDmul + CDmul += cd; + } + + return CDmul * stagnationCD * refArea / conditions.getRefArea(); + } +} diff --git a/core/src/net/sf/openrocket/database/motor/ThrustCurveMotorSet.java b/core/src/net/sf/openrocket/database/motor/ThrustCurveMotorSet.java index b48ed3344..edadd5f8f 100644 --- a/core/src/net/sf/openrocket/database/motor/ThrustCurveMotorSet.java +++ b/core/src/net/sf/openrocket/database/motor/ThrustCurveMotorSet.java @@ -220,6 +220,9 @@ public class ThrustCurveMotorSet implements Comparable { (type != m.getMotorType())) { return false; } + + if (!designation.equalsIgnoreCase(m.getDesignation())) + return false; if (!commonName.equalsIgnoreCase(m.getCommonName())) return false; diff --git a/core/src/net/sf/openrocket/document/Simulation.java b/core/src/net/sf/openrocket/document/Simulation.java index 9a75dbbf8..95a93af50 100644 --- a/core/src/net/sf/openrocket/document/Simulation.java +++ b/core/src/net/sf/openrocket/document/Simulation.java @@ -384,15 +384,19 @@ public class Simulation implements ChangeSource, Cloneable { simulatedData = simulator.simulate(simulationConditions); t2 = System.currentTimeMillis(); log.debug("Simulation: returning from simulator, simulation took " + (t2 - t1) + "ms"); - - // Set simulated info after simulation, will not be set in case of exception + + } catch (SimulationException e) { + simulatedData = e.getFlightData(); + throw e; + } finally { + // Set simulated info after simulation simulatedConditions = options.clone(); simulatedConfigurationDescription = descriptor.format( this.rocket, getId()); simulatedRocketID = rocket.getFunctionalModID(); status = Status.UPTODATE; fireChangeEvent(); - } finally { + mutex.unlock("simulate"); } } diff --git a/core/src/net/sf/openrocket/file/iterator/DirectoryIterator.java b/core/src/net/sf/openrocket/file/iterator/DirectoryIterator.java index e80a1e2fd..4fd2be2cc 100644 --- a/core/src/net/sf/openrocket/file/iterator/DirectoryIterator.java +++ b/core/src/net/sf/openrocket/file/iterator/DirectoryIterator.java @@ -56,7 +56,7 @@ public class DirectoryIterator extends FileIterator { @Override - protected Pair findNext() { + protected Pair findNext() { // Check if we're recursing if (subIterator != null) { @@ -86,7 +86,7 @@ public class DirectoryIterator extends FileIterator { } InputStream is = new BufferedInputStream(new FileInputStream(file)); - return new Pair(file.getName(), is); + return new Pair<>(file, is); } catch (IOException e) { logger.warn("Error opening file/directory " + file, e); } diff --git a/core/src/net/sf/openrocket/file/iterator/FileIterator.java b/core/src/net/sf/openrocket/file/iterator/FileIterator.java index d0a943959..14d28d347 100644 --- a/core/src/net/sf/openrocket/file/iterator/FileIterator.java +++ b/core/src/net/sf/openrocket/file/iterator/FileIterator.java @@ -1,5 +1,6 @@ package net.sf.openrocket.file.iterator; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.Iterator; @@ -20,10 +21,10 @@ import net.sf.openrocket.util.Pair; * * @author Sampo Niskanen */ -public abstract class FileIterator implements Iterator> { +public abstract class FileIterator implements Iterator> { private static final Logger logger = LoggerFactory.getLogger(FileIterator.class); - private Pair next = null; + private Pair next = null; private int fileCount = 0; @Override @@ -37,7 +38,7 @@ public abstract class FileIterator implements Iterator @Override - public Pair next() { + public Pair next() { if (next == null) { next = findNext(); } @@ -45,7 +46,7 @@ public abstract class FileIterator implements Iterator throw new NoSuchElementException("No more files"); } - Pair n = next; + Pair n = next; next = null; fileCount++; return n; @@ -86,10 +87,10 @@ public abstract class FileIterator implements Iterator } /** - * Return the next pair of file name and InputStream. + * Return the next pair of file and InputStream. * - * @return a pair with the file name and input stream reading the file. + * @return a pair with the file and input stream reading the file. */ - protected abstract Pair findNext(); + protected abstract Pair findNext(); } diff --git a/core/src/net/sf/openrocket/file/iterator/ZipDirectoryIterator.java b/core/src/net/sf/openrocket/file/iterator/ZipDirectoryIterator.java index ae50fa8a8..b82240c0e 100644 --- a/core/src/net/sf/openrocket/file/iterator/ZipDirectoryIterator.java +++ b/core/src/net/sf/openrocket/file/iterator/ZipDirectoryIterator.java @@ -78,7 +78,7 @@ public class ZipDirectoryIterator extends FileIterator { @Override - protected Pair findNext() { + protected Pair findNext() { if (entries == null) { return null; } @@ -90,7 +90,7 @@ public class ZipDirectoryIterator extends FileIterator { if (name.startsWith(directory) && filter.accept(file)) { try { InputStream is = zipFile.getInputStream(entry); - return new Pair(name, is); + return new Pair<>(file, is); } catch (IOException e) { logger.error("IOException when reading ZIP file " + zipFileName, e); } diff --git a/core/src/net/sf/openrocket/file/motor/ZipFileMotorLoader.java b/core/src/net/sf/openrocket/file/motor/ZipFileMotorLoader.java index 176da2dc6..5bed72943 100644 --- a/core/src/net/sf/openrocket/file/motor/ZipFileMotorLoader.java +++ b/core/src/net/sf/openrocket/file/motor/ZipFileMotorLoader.java @@ -44,7 +44,7 @@ public class ZipFileMotorLoader implements MotorLoader { @Override - public List load(InputStream stream, String filename) throws IOException { + public List load(InputStream stream, String filename) throws IOException, IllegalArgumentException { List motors = new ArrayList<>(); ZipInputStream is = new ZipInputStream(stream); diff --git a/core/src/net/sf/openrocket/file/openrocket/importt/BooleanSetter.java b/core/src/net/sf/openrocket/file/openrocket/importt/BooleanSetter.java index b0aef58c0..8de389fa8 100644 --- a/core/src/net/sf/openrocket/file/openrocket/importt/BooleanSetter.java +++ b/core/src/net/sf/openrocket/file/openrocket/importt/BooleanSetter.java @@ -10,9 +10,11 @@ import net.sf.openrocket.util.Reflection; //// BooleanSetter - set a boolean value class BooleanSetter implements Setter { private final Reflection.Method setMethod; + private Object[] extraParameters = null; - public BooleanSetter(Reflection.Method set) { + public BooleanSetter(Reflection.Method set, Object... parameters) { setMethod = set; + this.extraParameters = parameters; } @Override @@ -20,12 +22,23 @@ class BooleanSetter implements Setter { WarningSet warnings) { s = s.trim(); + final boolean setValue; if (s.equalsIgnoreCase("true")) { - setMethod.invoke(c, true); + setValue = true; } else if (s.equalsIgnoreCase("false")) { - setMethod.invoke(c, false); + setValue = false; } else { warnings.add(Warning.FILE_INVALID_PARAMETER); + return; + } + + if (extraParameters != null) { + Object[] parameters = new Object[extraParameters.length + 1]; + parameters[0] = setValue; + System.arraycopy(extraParameters, 0, parameters, 1, extraParameters.length); + setMethod.invoke(c, parameters); + } else { + setMethod.invoke(c, setValue); } } } \ No newline at end of file diff --git a/core/src/net/sf/openrocket/file/openrocket/importt/DocumentConfig.java b/core/src/net/sf/openrocket/file/openrocket/importt/DocumentConfig.java index 2366345ef..00b6c1467 100644 --- a/core/src/net/sf/openrocket/file/openrocket/importt/DocumentConfig.java +++ b/core/src/net/sf/openrocket/file/openrocket/importt/DocumentConfig.java @@ -248,7 +248,9 @@ class DocumentConfig { Reflection.findMethod(Transition.class, "setAftShoulderThickness", double.class))); setters.put("Transition:aftshouldercapped", new BooleanSetter( Reflection.findMethod(Transition.class, "setAftShoulderCapped", boolean.class))); - + + setters.put("NoseCone:isflipped", new BooleanSetter( + Reflection.findMethod(NoseCone.class, "setFlipped", boolean.class, boolean.class), false)); // NoseCone - disable disallowed elements setters.put("NoseCone:foreradius", null); setters.put("NoseCone:foreshoulderradius", null); diff --git a/core/src/net/sf/openrocket/file/openrocket/savers/NoseConeSaver.java b/core/src/net/sf/openrocket/file/openrocket/savers/NoseConeSaver.java index 9733254f8..e5c24e571 100644 --- a/core/src/net/sf/openrocket/file/openrocket/savers/NoseConeSaver.java +++ b/core/src/net/sf/openrocket/file/openrocket/savers/NoseConeSaver.java @@ -1,7 +1,10 @@ package net.sf.openrocket.file.openrocket.savers; +import net.sf.openrocket.rocketcomponent.NoseCone; + import java.util.ArrayList; import java.util.List; +import java.util.Locale; public class NoseConeSaver extends TransitionSaver { @@ -20,8 +23,23 @@ public class NoseConeSaver extends TransitionSaver { @Override protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List elements) { + NoseCone noseCone = (NoseCone) c; super.addParams(c, elements); - - // Transition handles nose cone saving as well + + if (noseCone.isBaseRadiusAutomatic()) + elements.add("auto " + noseCone.getBaseRadiusNoAutomatic() + ""); + else + elements.add("" + noseCone.getBaseRadius() + ""); + + elements.add("" + noseCone.getShoulderRadius() + + ""); + elements.add("" + noseCone.getShoulderLength() + + ""); + elements.add("" + noseCone.getShoulderThickness() + + ""); + elements.add("" + noseCone.isShoulderCapped() + + ""); + + elements.add("" + noseCone.isFlipped() + ""); } } diff --git a/core/src/net/sf/openrocket/file/openrocket/savers/TransitionSaver.java b/core/src/net/sf/openrocket/file/openrocket/savers/TransitionSaver.java index 054506eae..ca02534d7 100644 --- a/core/src/net/sf/openrocket/file/openrocket/savers/TransitionSaver.java +++ b/core/src/net/sf/openrocket/file/openrocket/savers/TransitionSaver.java @@ -30,8 +30,6 @@ public class TransitionSaver extends SymmetricComponentSaver { protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List elements) { super.addParams(c, elements); net.sf.openrocket.rocketcomponent.Transition trans = (net.sf.openrocket.rocketcomponent.Transition) c; - boolean nosecone = (trans instanceof NoseCone); - Transition.Shape shape = trans.getType(); elements.add("" + shape.name().toLowerCase(Locale.ENGLISH) + ""); @@ -41,14 +39,16 @@ public class TransitionSaver extends SymmetricComponentSaver { if (shape.usesParameter()) { elements.add("" + trans.getShapeParameter() + ""); } - - - if (!nosecone) { - if (trans.isForeRadiusAutomatic()) - elements.add("auto " + trans.getForeRadiusNoAutomatic() + ""); - else - elements.add("" + trans.getForeRadius() + ""); + + // Nose cones need other parameter saving, due to the isFlipped() parameter + if (trans instanceof NoseCone) { + return; } + + if (trans.isForeRadiusAutomatic()) + elements.add("auto " + trans.getForeRadiusNoAutomatic() + ""); + else + elements.add("" + trans.getForeRadius() + ""); if (trans.isAftRadiusAutomatic()) elements.add("auto " + trans.getAftRadiusNoAutomatic() + ""); @@ -56,16 +56,14 @@ public class TransitionSaver extends SymmetricComponentSaver { elements.add("" + trans.getAftRadius() + ""); - if (!nosecone) { - elements.add("" + trans.getForeShoulderRadius() - + ""); - elements.add("" + trans.getForeShoulderLength() - + ""); - elements.add("" + trans.getForeShoulderThickness() - + ""); - elements.add("" + trans.isForeShoulderCapped() - + ""); - } + elements.add("" + trans.getForeShoulderRadius() + + ""); + elements.add("" + trans.getForeShoulderLength() + + ""); + elements.add("" + trans.getForeShoulderThickness() + + ""); + elements.add("" + trans.isForeShoulderCapped() + + ""); elements.add("" + trans.getAftShoulderRadius() + ""); diff --git a/core/src/net/sf/openrocket/file/rocksim/export/PodSetDTO.java b/core/src/net/sf/openrocket/file/rocksim/export/PodSetDTO.java index 7baa9b56f..d2be13493 100644 --- a/core/src/net/sf/openrocket/file/rocksim/export/PodSetDTO.java +++ b/core/src/net/sf/openrocket/file/rocksim/export/PodSetDTO.java @@ -65,7 +65,11 @@ public class PodSetDTO extends BasePartDTO implements AttachableParts { } else if (child instanceof BodyTube) { addAttachedPart(new BodyTubeDTO((BodyTube) child)); } else if (child instanceof NoseCone) { - addAttachedPart(new NoseConeDTO((NoseCone) child)); + if (((NoseCone) child).isFlipped()) { + addAttachedPart(new TransitionDTO((NoseCone) child)); + } else { + addAttachedPart(new NoseConeDTO((NoseCone) child)); + } } else if (child instanceof Transition) { addAttachedPart(new TransitionDTO((Transition) child)); } diff --git a/core/src/net/sf/openrocket/file/rocksim/export/StageDTO.java b/core/src/net/sf/openrocket/file/rocksim/export/StageDTO.java index 7cbe4a7cf..f0c167966 100644 --- a/core/src/net/sf/openrocket/file/rocksim/export/StageDTO.java +++ b/core/src/net/sf/openrocket/file/rocksim/export/StageDTO.java @@ -93,8 +93,12 @@ public class StageDTO { externalPart.add(theExternalPartDTO); } - private NoseConeDTO toNoseConeDTO(NoseCone nc) { - return new NoseConeDTO(nc); + private AbstractTransitionDTO toNoseConeDTO(NoseCone nc) { + if (nc.isFlipped()) { + return new TransitionDTO(nc); + } else { + return new NoseConeDTO(nc); + } } private BodyTubeDTO toBodyTubeDTO(BodyTube bt) { diff --git a/core/src/net/sf/openrocket/l10n/ClassBasedTranslator.java b/core/src/net/sf/openrocket/l10n/ClassBasedTranslator.java index 65589df06..a62e00f65 100644 --- a/core/src/net/sf/openrocket/l10n/ClassBasedTranslator.java +++ b/core/src/net/sf/openrocket/l10n/ClassBasedTranslator.java @@ -78,8 +78,16 @@ public class ClassBasedTranslator implements Translator { public String getBaseText(String base, String translation) { return translator.getBaseText(base, translation); } - - + + @Override + public boolean checkIfKeyExists(String key) { + try { + get(key); + return true; + } catch (MissingResourceException e) { + return false; + } + } private String findClassName() { Throwable trace = new Throwable(); diff --git a/core/src/net/sf/openrocket/l10n/DebugTranslator.java b/core/src/net/sf/openrocket/l10n/DebugTranslator.java index ccf72e58b..bbc9baea2 100644 --- a/core/src/net/sf/openrocket/l10n/DebugTranslator.java +++ b/core/src/net/sf/openrocket/l10n/DebugTranslator.java @@ -1,5 +1,7 @@ package net.sf.openrocket.l10n; +import java.util.MissingResourceException; + /** * A translator implementation that returns the logical key in brackets instead * of an actual translation. The class optionally verifies that the translation @@ -47,6 +49,15 @@ public class DebugTranslator implements Translator { } return translation; } - + + @Override + public boolean checkIfKeyExists(String key) { + try { + translator.get(key); + return true; + } catch (MissingResourceException e) { + return false; + } + } } diff --git a/core/src/net/sf/openrocket/l10n/ExceptionSuppressingTranslator.java b/core/src/net/sf/openrocket/l10n/ExceptionSuppressingTranslator.java index 1287822cc..0a80427a1 100644 --- a/core/src/net/sf/openrocket/l10n/ExceptionSuppressingTranslator.java +++ b/core/src/net/sf/openrocket/l10n/ExceptionSuppressingTranslator.java @@ -52,7 +52,16 @@ public class ExceptionSuppressingTranslator implements Translator { public String getBaseText(String base, String translation) { return translator.getBaseText(base, translation); } - + + @Override + public boolean checkIfKeyExists(String key) { + try { + translator.get(key); + return true; + } catch (MissingResourceException e) { + return false; + } + } private static synchronized void handleError(String key, MissingResourceException e) { if (!errorReported) { diff --git a/core/src/net/sf/openrocket/l10n/ResourceBundleTranslator.java b/core/src/net/sf/openrocket/l10n/ResourceBundleTranslator.java index d210e01e9..1ce14ae1f 100644 --- a/core/src/net/sf/openrocket/l10n/ResourceBundleTranslator.java +++ b/core/src/net/sf/openrocket/l10n/ResourceBundleTranslator.java @@ -66,4 +66,14 @@ public class ResourceBundleTranslator implements Translator { } return translation; } + + @Override + public boolean checkIfKeyExists(String key) { + try { + get(key); + return true; + } catch (MissingResourceException e) { + return false; + } + } } diff --git a/core/src/net/sf/openrocket/l10n/Translator.java b/core/src/net/sf/openrocket/l10n/Translator.java index 5211ed730..7fd2957c8 100644 --- a/core/src/net/sf/openrocket/l10n/Translator.java +++ b/core/src/net/sf/openrocket/l10n/Translator.java @@ -50,5 +50,12 @@ public interface Translator { * @return the base text, or the translation if not found. */ public String getBaseText(String base, String translation); + + /** + * Checks whether a certain translation key exists. + * @param key the key to check + * @return true if it exists, false otherwise + */ + public boolean checkIfKeyExists(String key); } diff --git a/core/src/net/sf/openrocket/optimization/services/DefaultSimulationModifierService.java b/core/src/net/sf/openrocket/optimization/services/DefaultSimulationModifierService.java index 47a122811..79f6e3ef8 100644 --- a/core/src/net/sf/openrocket/optimization/services/DefaultSimulationModifierService.java +++ b/core/src/net/sf/openrocket/optimization/services/DefaultSimulationModifierService.java @@ -56,7 +56,7 @@ public class DefaultSimulationModifierService implements SimulationModifierServi */ addModifier("optimization.modifier.nosecone.length", UnitGroup.UNITS_LENGTH, 1.0, NoseCone.class, "Length"); - addModifier("optimization.modifier.nosecone.diameter", UnitGroup.UNITS_LENGTH, 2.0, NoseCone.class, "AftRadius", "isAftRadiusAutomatic"); + addModifier("optimization.modifier.nosecone.diameter", UnitGroup.UNITS_LENGTH, 2.0, NoseCone.class, "BaseRadius", "isBaseRadiusAutomatic"); addModifier("optimization.modifier.nosecone.thickness", UnitGroup.UNITS_LENGTH, 1.0, NoseCone.class, "Thickness", "isFilled"); addModifier("optimization.modifier.transition.length", UnitGroup.UNITS_LENGTH, 1.0, Transition.class, "Length"); diff --git a/core/src/net/sf/openrocket/rocketcomponent/AxialStage.java b/core/src/net/sf/openrocket/rocketcomponent/AxialStage.java index df2285c37..504969e27 100644 --- a/core/src/net/sf/openrocket/rocketcomponent/AxialStage.java +++ b/core/src/net/sf/openrocket/rocketcomponent/AxialStage.java @@ -241,6 +241,7 @@ public class AxialStage extends ComponentAssembly implements FlightConfigurableC @Override public void clearConfigListeners() { super.clearConfigListeners(); + // StageSeparationConfiguration also has config listeners, so clear them as well StageSeparationConfiguration thisConfig = getSeparationConfiguration(); thisConfig.clearConfigListeners(); } diff --git a/core/src/net/sf/openrocket/rocketcomponent/BodyTube.java b/core/src/net/sf/openrocket/rocketcomponent/BodyTube.java index 212aadd48..40b292044 100644 --- a/core/src/net/sf/openrocket/rocketcomponent/BodyTube.java +++ b/core/src/net/sf/openrocket/rocketcomponent/BodyTube.java @@ -549,6 +549,7 @@ public class BodyTube extends SymmetricComponent implements BoxBounded, MotorMou @Override public void clearConfigListeners() { super.clearConfigListeners(); + // The motor config also has listeners, so clear them as well getDefaultMotorConfig().clearConfigListeners(); } } diff --git a/core/src/net/sf/openrocket/rocketcomponent/InnerTube.java b/core/src/net/sf/openrocket/rocketcomponent/InnerTube.java index 272de11ac..581d8583c 100644 --- a/core/src/net/sf/openrocket/rocketcomponent/InnerTube.java +++ b/core/src/net/sf/openrocket/rocketcomponent/InnerTube.java @@ -482,6 +482,7 @@ public class InnerTube extends ThicknessRingComponent implements AxialPositionab @Override public void clearConfigListeners() { super.clearConfigListeners(); + // The motor config also has listeners, so clear them as well getDefaultMotorConfig().clearConfigListeners(); } diff --git a/core/src/net/sf/openrocket/rocketcomponent/NoseCone.java b/core/src/net/sf/openrocket/rocketcomponent/NoseCone.java index ed436981d..d35f1928d 100644 --- a/core/src/net/sf/openrocket/rocketcomponent/NoseCone.java +++ b/core/src/net/sf/openrocket/rocketcomponent/NoseCone.java @@ -1,18 +1,16 @@ package net.sf.openrocket.rocketcomponent; -import net.sf.openrocket.appearance.Appearance; -import net.sf.openrocket.appearance.Decal; import net.sf.openrocket.l10n.Translator; import net.sf.openrocket.preset.ComponentPreset; import net.sf.openrocket.preset.ComponentPreset.Type; import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.StateChangeListener; - -import java.util.EventObject; /** * Rocket nose cones of various types. Implemented as a transition with the * fore radius == 0. + *

+ * The normal nose cone can be converted to a tail cone by setting the {@link #isFlipped} parameter. + * This will flip all the aft side dimensions with the fore side dimensions. * * @author Sampo Niskanen */ @@ -21,6 +19,7 @@ public class NoseCone extends Transition implements InsideColorComponent { private static final Translator trans = Application.getTranslator(); private InsideColorComponentHandler insideColorComponentHandler = new InsideColorComponentHandler(this); + private boolean isFlipped; // If true, the nose cone is converted to a tail cone /********* Constructors **********/ public NoseCone() { @@ -29,16 +28,12 @@ public class NoseCone extends Transition implements InsideColorComponent { public NoseCone(Transition.Shape type, double length, double radius) { super(); + this.isFlipped = false; super.setType(type); - super.setForeRadiusAutomatic(false); - super.setForeRadius(0); - super.setForeShoulderLength(0); - super.setForeShoulderRadius(0.9 * radius); - super.setForeShoulderThickness(0); - super.setForeShoulderCapped(filled); super.setThickness(0.002); super.setLength(length); super.setClipped(false); + resetForeRadius(); super.setAftRadiusAutomatic(false); super.setAftRadius(radius); @@ -46,86 +41,227 @@ public class NoseCone extends Transition implements InsideColorComponent { super.displayOrder_side = 1; // Order for displaying the component in the 2D side view super.displayOrder_back = 0; // Order for displaying the component in the 2D back view } - - - /********** Get/set methods for component parameters **********/ - - @Override - public double getForeRadius() { - return 0; - } - - @Override - public void setForeRadius(double r) { - // No-op - } - - @Override - public boolean isForeRadiusAutomatic() { - return false; - } - - @Override - public void setForeRadiusAutomatic(boolean b) { - // No-op + + /********** Nose cone dimensions **********/ + + /** + * Returns the base radius of the nose cone (independent of whether the nose cone is flipped or not). + * This method should be used over {@link #getAftRadius()} because it works for both normal and flipped nose cones. + */ + public double getBaseRadius() { + return isFlipped ? getForeRadius() : getAftRadius(); } - @Override - public boolean usesPreviousCompAutomatic() { - return false; + /** + * Returns the raw base radius of the nose cone (independent of whether the nose cone is flipped or not). + * This method should be used over {@link #getAftRadiusNoAutomatic()} because it works for both normal and flipped nose cones. + */ + public double getBaseRadiusNoAutomatic() { + return isFlipped ? getForeRadiusNoAutomatic() : getAftRadiusNoAutomatic(); } - @Override - public double getForeShoulderLength() { - return 0; + /** + * Sets the base radius of the nose cone (independent of whether the nose cone is flipped or not). + * This method should be used over {@link #setAftRadius(double)} because it works for both normal and flipped nose cones. + */ + public void setBaseRadius(double radius) { + if (isFlipped) { + setForeRadius(radius); + } else { + setAftRadius(radius); + } } - - @Override - public double getForeShoulderRadius() { - return 0; + + /** + * Returns whether the base radius of the nose cone takes it settings from the previous/next component + * (independent of whether the nose cone is flipped or not). + * This method should be used over {@link #isAftRadiusAutomatic()} because it works for both normal and flipped nose cones. + */ + public boolean isBaseRadiusAutomatic() { + return isFlipped ? isForeRadiusAutomatic() : isAftRadiusAutomatic(); } - - @Override - public double getForeShoulderThickness() { - return 0; + + /** + * Sets whether the base radius of the nose cone takes it settings from the previous/next component + * (independent of whether the nose cone is flipped or not). + * This method should be used over {@link #setAftRadiusAutomatic(boolean)} because it works for both normal and flipped nose cones. + */ + public void setBaseRadiusAutomatic(boolean auto) { + if (isFlipped) { + setForeRadiusAutomatic(auto); + } else { + setAftRadiusAutomatic(auto); + } + } - - @Override - public boolean isForeShoulderCapped() { - return false; + + /** + * Returns the shoulder length, regardless of how the nose cone is flipped (independent of whether the nose cone is flipped or not). + * This method should be used over {@link #getAftShoulderLength()} because it works for both normal and flipped nose cones. + */ + public double getShoulderLength() { + return isFlipped ? getForeShoulderLength() : getAftShoulderLength(); } - - @Override - public void setForeShoulderCapped(boolean capped) { - // No-op + + /** + * Sets the shoulder length (independent of whether the nose cone is flipped or not). + * This method should be used over {@link #setAftShoulderLength(double)} because it works for both normal and flipped nose cones. + */ + public void setShoulderLength(double length) { + if (isFlipped) { + setForeShoulderLength(length); + } else { + setAftShoulderLength(length); + } } - - @Override - public void setForeShoulderLength(double foreShoulderLength) { - // No-op + + /** + * Returns the shoulder radius (independent of whether the nose cone is flipped or not). + * This method should be used over {@link #getAftShoulderRadius()} because it works for both normal and flipped nose cones. + */ + public double getShoulderRadius() { + return isFlipped ? getForeShoulderRadius() : getAftShoulderRadius(); } - - @Override - public void setForeShoulderRadius(double foreShoulderRadius) { - // No-op + + /** + * Sets the shoulder radius (independent of whether the nose cone is flipped or not). + * This method should be used over {@link #setAftShoulderRadius(double)} because it works for both normal and flipped nose cones. + */ + public void setShoulderRadius(double radius) { + if (isFlipped) { + setForeShoulderRadius(radius); + } else { + setAftShoulderRadius(radius); + } } - - @Override - public void setForeShoulderThickness(double foreShoulderThickness) { - // No-op + + /** + * Returns the shoulder thickness (independent of whether the nose cone is flipped or not). + * This method should be used over {@link #getAftShoulderThickness()} because it works for both normal and flipped nose cones. + */ + public double getShoulderThickness() { + return isFlipped ? getForeShoulderThickness() : getAftShoulderThickness(); } - + + /** + * Sets the shoulder thickness (independent of whether the nose cone is flipped or not). + * This method should be used over {@link #setAftShoulderRadius(double)} because it works for both normal and flipped nose cones. + */ + public void setShoulderThickness(double thickness) { + if (isFlipped) { + setForeShoulderThickness(thickness); + } else { + setAftShoulderThickness(thickness); + } + } + + /** + * Returns the shoulder cap (independent of whether the nose cone is flipped or not). + * This method should be used over {@link #isAftShoulderCapped()} because it works for both normal and flipped nose cones. + */ + public boolean isShoulderCapped() { + return isFlipped ? isForeShoulderCapped() : isAftShoulderCapped(); + } + + /** + * Sets the shoulder cap (independent of whether the nose cone is flipped or not). + * This method should be used over {@link #setAftShoulderCapped(boolean)} because it works for both normal and flipped nose cones. + */ + public void setShoulderCapped(boolean capped) { + if (isFlipped) { + setForeShoulderCapped(capped); + } else { + setAftShoulderCapped(capped); + } + } + + /********** Other **********/ + + /** + * Return true if the nose cone is flipped, i.e. converted to a tail cone, false if it is a regular nose cone. + */ + public boolean isFlipped() { + return isFlipped; + } + + /** + * Set the nose cone to be flipped, i.e. converted to a tail cone, or set it to be a regular nose cone. + * @param flipped if true, the nose cone is converted to a tail cone, if false it is a regular nose cone. + * @param sanityCheck whether to check if the auto radius parameter can be used for the new nose cone orientation + */ + public void setFlipped(boolean flipped, boolean sanityCheck) { + for (RocketComponent listener : configListeners) { + if (listener instanceof NoseCone) { + ((NoseCone) listener).setFlipped(flipped, sanityCheck); + } + } + + if (isFlipped == flipped) { + return; + } + + boolean previousByPass = isBypassComponentChangeEvent(); + setBypassChangeEvent(true); + if (flipped) { + setForeRadius(getAftRadiusNoAutomatic()); + setForeRadiusAutomatic(isAftRadiusAutomatic(), sanityCheck); + setForeShoulderLength(getAftShoulderLength()); + setForeShoulderRadius(getAftShoulderRadius()); + setForeShoulderThickness(getAftShoulderThickness()); + setForeShoulderCapped(isAftShoulderCapped()); + + resetAftRadius(); + } else { + setAftRadius(getForeRadiusNoAutomatic()); + setAftRadiusAutomatic(isForeRadiusAutomatic(), sanityCheck); + setAftShoulderLength(getForeShoulderLength()); + setAftShoulderRadius(getForeShoulderRadius()); + setAftShoulderThickness(getForeShoulderThickness()); + setAftShoulderCapped(isForeShoulderCapped()); + + resetForeRadius(); + } + setBypassChangeEvent(previousByPass); + + isFlipped = flipped; + fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); + } + + /** + * Set the nose cone to be flipped, i.e. converted to a tail cone, or set it to be a regular nose cone. + * @param flipped if true, the nose cone is converted to a tail cone, if false it is a regular nose cone. + */ + public void setFlipped(boolean flipped) { + setFlipped(flipped, true); + } + + private void resetForeRadius() { + setForeRadius(0); + setForeRadiusAutomatic(false); + setForeShoulderLength(0); + setForeShoulderRadius(0); + setForeShoulderThickness(0); + setForeShoulderCapped(false); + } + + private void resetAftRadius() { + setAftRadius(0); + setAftRadiusAutomatic(false); + setAftShoulderLength(0); + setAftShoulderRadius(0); + setAftShoulderThickness(0); + setAftShoulderCapped(false); + } + @Override public boolean isClipped() { return false; } - + @Override public void setClipped(boolean b) { // No-op } - - /********** RocketComponent methods **********/ @@ -136,9 +272,12 @@ public class NoseCone extends Transition implements InsideColorComponent { @Override protected void loadFromPreset(ComponentPreset preset) { - + // We first need to unflip, because the preset loading always applies settings for a normal nose cone (e.g. aft diameter) + boolean flipped = isFlipped; + setFlipped(false); //Many parameters are handled by the super class Transition.loadFromPreset super.loadFromPreset(preset); + setFlipped(flipped); } /** diff --git a/core/src/net/sf/openrocket/rocketcomponent/RailButton.java b/core/src/net/sf/openrocket/rocketcomponent/RailButton.java index 504de01e3..a25e616a0 100644 --- a/core/src/net/sf/openrocket/rocketcomponent/RailButton.java +++ b/core/src/net/sf/openrocket/rocketcomponent/RailButton.java @@ -207,12 +207,6 @@ public class RailButton extends ExternalComponent implements AnglePositionable, clearPreset(); fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); } - - @Override - public boolean isAerodynamic(){ - // TODO: implement aerodynamics - return false; - } @Override public double getAngleOffset(){ diff --git a/core/src/net/sf/openrocket/rocketcomponent/RecoveryDevice.java b/core/src/net/sf/openrocket/rocketcomponent/RecoveryDevice.java index 3b0c0bbde..9fc15a8e4 100644 --- a/core/src/net/sf/openrocket/rocketcomponent/RecoveryDevice.java +++ b/core/src/net/sf/openrocket/rocketcomponent/RecoveryDevice.java @@ -177,6 +177,7 @@ public abstract class RecoveryDevice extends MassObject implements FlightConfigu @Override public void clearConfigListeners() { super.clearConfigListeners(); + // The DeploymentConfiguration also has listeners, so clear them as well DeploymentConfiguration thisConfig = getDeploymentConfigurations().getDefault(); thisConfig.clearConfigListeners(); } diff --git a/core/src/net/sf/openrocket/rocketcomponent/Rocket.java b/core/src/net/sf/openrocket/rocketcomponent/Rocket.java index ae6dfe554..f1363f25f 100644 --- a/core/src/net/sf/openrocket/rocketcomponent/Rocket.java +++ b/core/src/net/sf/openrocket/rocketcomponent/Rocket.java @@ -406,37 +406,37 @@ public class Rocket extends ComponentAssembly { * and therefore fires an UNDO_EVENT, masked with all applicable mass/aerodynamic/tree * changes. */ - public void loadFrom(Rocket r) { + public void loadFrom(Rocket source) { // Store list of components to invalidate after event has been fired - List toInvalidate = this.copyFrom(r); + List toInvalidate = this.copyFrom(source); int type = ComponentChangeEvent.UNDO_CHANGE | ComponentChangeEvent.NONFUNCTIONAL_CHANGE; - if (this.massModID != r.massModID) + if (this.massModID != source.massModID) type |= ComponentChangeEvent.MASS_CHANGE; - if (this.aeroModID != r.aeroModID) + if (this.aeroModID != source.aeroModID) type |= ComponentChangeEvent.AERODYNAMIC_CHANGE; // Loading a rocket is always a tree change since the component objects change type |= ComponentChangeEvent.TREE_CHANGE; - this.modID = r.modID; - this.massModID = r.massModID; - this.aeroModID = r.aeroModID; - this.treeModID = r.treeModID; - this.functionalModID = r.functionalModID; - this.refType = r.refType; - this.customReferenceLength = r.customReferenceLength; - this.stageMap = r.stageMap; + this.modID = source.modID; + this.massModID = source.massModID; + this.aeroModID = source.aeroModID; + this.treeModID = source.treeModID; + this.functionalModID = source.functionalModID; + this.refType = source.refType; + this.customReferenceLength = source.customReferenceLength; + this.stageMap = source.stageMap; // these flight configurations need to reference the _this_ Rocket: this.configSet.reset(); this.configSet.setDefault(new FlightConfiguration(this)); - for (FlightConfigurationId key : r.configSet.map.keySet()) { + for (FlightConfigurationId key : source.configSet.map.keySet()) { this.configSet.set(key, new FlightConfiguration(this, key)); } - this.selectedConfiguration = this.configSet.get(r.getSelectedConfiguration().getId()); + this.selectedConfiguration = this.configSet.get(source.getSelectedConfiguration().getId()); - this.perfectFinish = r.perfectFinish; + this.perfectFinish = source.perfectFinish; this.checkComponentStructure(); @@ -453,25 +453,6 @@ public class Rocket extends ComponentAssembly { /////// Implement the ComponentChangeListener lists - /** - * Creates a new EventListenerList for this component. This is necessary when cloning - * the structure. - */ - public void resetListeners() { - // System.out.println("RESETTING LISTENER LIST of Rocket "+this); - listenerList = new HashSet(); - } - - - public void printListeners() { - System.out.println("" + this + " has " + listenerList.size() + " listeners:"); - int i = 0; - for (EventListener l : listenerList) { - System.out.println(" " + (i) + ": " + l); - i++; - } - } - @Override public void addComponentChangeListener(ComponentChangeListener l) { checkState(); @@ -497,10 +478,6 @@ public class Rocket extends ComponentAssembly { try { checkState(); - { // vvvv DEVEL vvvv - //System.err.println("fireEvent@rocket."); - } // ^^^^ DEVEL ^^^^ - // Update modification ID's only for normal (not undo/redo) events if (!cce.isUndoChange()) { modID = UniqueID.next(); diff --git a/core/src/net/sf/openrocket/rocketcomponent/RocketComponent.java b/core/src/net/sf/openrocket/rocketcomponent/RocketComponent.java index df7e618c6..e3f1cdcd4 100644 --- a/core/src/net/sf/openrocket/rocketcomponent/RocketComponent.java +++ b/core/src/net/sf/openrocket/rocketcomponent/RocketComponent.java @@ -448,6 +448,7 @@ public abstract class RocketComponent implements ChangeSource, Cloneable, Iterab } // Make sure the config listeners aren't cloned clone.configListeners = new LinkedList<>(); + clone.bypassComponentChangeEvent = false; return clone; } @@ -603,7 +604,7 @@ public abstract class RocketComponent implements ChangeSource, Cloneable, Iterab for (RocketComponent listener : configListeners) { listener.setBypassChangeEvent(false); listener.setMassOverridden(o); - listener.setBypassChangeEvent(false); + listener.setBypassChangeEvent(true); } if (massOverridden == o) { @@ -2277,7 +2278,11 @@ public abstract class RocketComponent implements ChangeSource, Cloneable, Iterab this.bypassComponentChangeEvent = newValue; } - public boolean getBypassComponentChangeEvent() { + /** + * Returns whether the current component if ignoring ComponentChangeEvents. + * @return true if the component is ignoring ComponentChangeEvents. + */ + public boolean isBypassComponentChangeEvent() { return this.bypassComponentChangeEvent; } @@ -2287,7 +2292,18 @@ public abstract class RocketComponent implements ChangeSource, Cloneable, Iterab * @return true if listener was successfully added, false if not */ public boolean addConfigListener(RocketComponent listener) { - if (listener == null || configListeners.contains(listener) || listener == this) { + if (isBypassComponentChangeEvent()) { + // This is a precaution. If you are multi-comp editing and the current component is bypassing events, + // the editing will be REALLY weird, see GitHub issue #1956. + throw new IllegalStateException("Cannot add config listener while bypassing events"); + } + if (listener == null) { + return false; + } + if (listener.getConfigListeners().size() > 0) { + throw new IllegalArgumentException("Listener already has config listeners"); + } + if (configListeners.contains(listener) || listener == this) { return false; } configListeners.add(listener); @@ -2555,6 +2571,7 @@ public abstract class RocketComponent implements ChangeSource, Cloneable, Iterab this.displayOrder_side = src.displayOrder_side; this.displayOrder_back = src.displayOrder_back; this.configListeners = new LinkedList<>(); + this.bypassComponentChangeEvent = false; if (this instanceof InsideColorComponent && src instanceof InsideColorComponent) { InsideColorComponentHandler icch = new InsideColorComponentHandler(this); icch.copyFrom(((InsideColorComponent) src).getInsideColorComponentHandler()); diff --git a/core/src/net/sf/openrocket/rocketcomponent/SymmetricComponent.java b/core/src/net/sf/openrocket/rocketcomponent/SymmetricComponent.java index bb76e77ce..f3152ef96 100644 --- a/core/src/net/sf/openrocket/rocketcomponent/SymmetricComponent.java +++ b/core/src/net/sf/openrocket/rocketcomponent/SymmetricComponent.java @@ -10,7 +10,7 @@ import net.sf.openrocket.preset.ComponentPreset; import net.sf.openrocket.util.BoundingBox; import net.sf.openrocket.util.Coordinate; import net.sf.openrocket.util.MathUtil; - +import net.sf.openrocket.rocketcomponent.position.RadiusMethod; /** * Class for an axially symmetric rocket component generated by rotating @@ -621,7 +621,11 @@ public abstract class SymmetricComponent extends BodyComponent implements BoxBou while (0 <= searchSiblingIndex) { final RocketComponent searchSibling = searchParent.getChild(searchSiblingIndex); if (searchSibling instanceof SymmetricComponent) { - return (SymmetricComponent) searchSibling; + SymmetricComponent candidate = (SymmetricComponent) searchSibling; + if (inline(candidate)) { + return candidate; + } + return null; } --searchSiblingIndex; } @@ -658,9 +662,12 @@ public abstract class SymmetricComponent extends BodyComponent implements BoxBou if(searchParent instanceof ComponentAssembly){ while (searchSiblingIndex < searchParent.getChildCount()) { final RocketComponent searchSibling = searchParent.getChild(searchSiblingIndex); - if (searchSibling instanceof SymmetricComponent) { - return (SymmetricComponent) searchSibling; + SymmetricComponent candidate = (SymmetricComponent) searchSibling; + if (inline(candidate)) { + return candidate; + } + return null; } ++searchSiblingIndex; } @@ -671,6 +678,29 @@ public abstract class SymmetricComponent extends BodyComponent implements BoxBou return null; } + /*** + * Determine whether a candidate symmetric component is in line with us + * + */ + private boolean inline(final SymmetricComponent candidate) { + // if we share a parent, we are in line + if (this.parent == candidate.parent) + return true; + + // if both of our parents are either not ring instanceable, or + // have a radial offset of 0 from their centerline, we are in line. + + if ((this.parent instanceof RingInstanceable) && + (!MathUtil.equals(this.parent.getRadiusMethod().getRadius(this.parent.parent, this, this.parent.getRadiusOffset()), 0))) + return false; + + if ((candidate.parent instanceof RingInstanceable) && + (!MathUtil.equals(candidate.parent.getRadiusMethod().getRadius(candidate.parent.parent, candidate, candidate.parent.getRadiusOffset()), 0))) + return false; + + return true; + } + /** * Checks whether the component uses the previous symmetric component for its auto diameter. */ diff --git a/core/src/net/sf/openrocket/rocketcomponent/Transition.java b/core/src/net/sf/openrocket/rocketcomponent/Transition.java index 9fd78a378..76c56b477 100644 --- a/core/src/net/sf/openrocket/rocketcomponent/Transition.java +++ b/core/src/net/sf/openrocket/rocketcomponent/Transition.java @@ -5,17 +5,13 @@ import static net.sf.openrocket.util.MathUtil.pow2; import static net.sf.openrocket.util.MathUtil.pow3; import java.util.Collection; -import java.util.EventObject; -import net.sf.openrocket.appearance.Appearance; -import net.sf.openrocket.appearance.Decal; import net.sf.openrocket.l10n.Translator; import net.sf.openrocket.preset.ComponentPreset; import net.sf.openrocket.preset.ComponentPreset.Type; import net.sf.openrocket.startup.Application; import net.sf.openrocket.util.Coordinate; import net.sf.openrocket.util.MathUtil; -import net.sf.openrocket.util.StateChangeListener; public class Transition extends SymmetricComponent implements InsideColorComponent { @@ -27,8 +23,8 @@ public class Transition extends SymmetricComponent implements InsideColorCompone private double shapeParameter; private boolean clipped; // Not to be read - use isClipped(), which may be overridden - private double foreRadius, aftRadius; - private boolean autoForeRadius, autoAftRadius2; // Whether the start radius is automatic + protected double foreRadius, aftRadius; // Warning: avoid using these directly, use getForeRadius() and getAftRadius() instead (because the definition of the two can change for flipped nose cones) + protected boolean autoForeRadius, autoAftRadius; // Whether the start radius is automatic private double foreShoulderRadius; @@ -53,7 +49,7 @@ public class Transition extends SymmetricComponent implements InsideColorCompone this.aftRadius = DEFAULT_RADIUS; this.length = DEFAULT_RADIUS * 3; this.autoForeRadius = true; - this.autoAftRadius2 = true; + this.autoAftRadius = true; this.type = Shape.CONICAL; this.shapeParameter = 0; @@ -81,19 +77,24 @@ public class Transition extends SymmetricComponent implements InsideColorCompone @Override public double getForeRadius() { if (isForeRadiusAutomatic()) { - // Get the automatic radius from the front - double r = -1; - SymmetricComponent c = this.getPreviousSymmetricComponent(); - if (c != null) { - r = c.getFrontAutoRadius(); - } - if (r < 0) - r = DEFAULT_RADIUS; - return r; + return getAutoForeRadius(); } return foreRadius; } + /** + * Returns the automatic radius from the front, taken from the previous component. Returns the default radius if there + * is no previous component. + */ + protected double getAutoForeRadius() { + SymmetricComponent c = this.getPreviousSymmetricComponent(); + if (c != null) { + return c.getFrontAutoRadius(); + } else { + return DEFAULT_RADIUS; + } + } + /** * Return the fore radius that was manually entered, so not the value that the component received from automatic * fore radius. @@ -136,13 +137,24 @@ public class Transition extends SymmetricComponent implements InsideColorCompone return autoForeRadius; } - public void setForeRadiusAutomatic(boolean auto) { + /** + * Set the fore radius to automatic mode (takes its value from the previous symmetric component's radius). + * + * @param auto whether to set the fore radius to automatic mode + * @param sanityCheck whether to sanity check auto mode for whether there is a previous component of which you can take the radius + */ + public void setForeRadiusAutomatic(boolean auto, boolean sanityCheck) { for (RocketComponent listener : configListeners) { if (listener instanceof Transition) { ((Transition) listener).setForeRadiusAutomatic(auto); } } + // You can only set the auto fore radius if it is possible + if (sanityCheck) { + auto = auto && canUsePreviousCompAutomatic(); + } + if (autoForeRadius == auto) return; @@ -152,25 +164,34 @@ public class Transition extends SymmetricComponent implements InsideColorCompone fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); } + public void setForeRadiusAutomatic(boolean auto) { + setForeRadiusAutomatic(auto, false); + } + //////// Aft radius ///////// @Override public double getAftRadius() { if (isAftRadiusAutomatic()) { - // Return the auto radius from the rear - double r = -1; - SymmetricComponent c = this.getNextSymmetricComponent(); - if (c != null) { - r = c.getRearAutoRadius(); - } - if (r < 0) - r = DEFAULT_RADIUS; - return r; + return getAutoAftRadius(); } return aftRadius; } + /** + * Returns the automatic radius from the rear, taken from the next component. Returns the default radius if there + * is no next component. + */ + protected double getAutoAftRadius() { + SymmetricComponent c = this.getNextSymmetricComponent(); + if (c != null) { + return c.getRearAutoRadius(); + } else { + return DEFAULT_RADIUS; + } + } + /** * Return the aft radius that was manually entered, so not the value that the component received from automatic * zft radius. @@ -191,10 +212,10 @@ public class Transition extends SymmetricComponent implements InsideColorCompone } } - if ((this.aftRadius == radius) && (autoAftRadius2 == false)) + if ((this.aftRadius == radius) && (autoAftRadius == false)) return; - this.autoAftRadius2 = false; + this.autoAftRadius = false; this.aftRadius = Math.max(radius, 0); if (doClamping && this.thickness > this.foreRadius && this.thickness > this.aftRadius) @@ -210,25 +231,40 @@ public class Transition extends SymmetricComponent implements InsideColorCompone @Override public boolean isAftRadiusAutomatic() { - return autoAftRadius2; + return autoAftRadius; } - public void setAftRadiusAutomatic(boolean auto) { + /** + * Set the aft radius to automatic mode (takes its value from the next symmetric component's radius). + * + * @param auto whether to set the aft radius to automatic mode + * @param sanityCheck whether to sanity check auto mode for whether there is a next component of which you can take the radius + */ + public void setAftRadiusAutomatic(boolean auto, boolean sanityCheck) { for (RocketComponent listener : configListeners) { if (listener instanceof Transition) { ((Transition) listener).setAftRadiusAutomatic(auto); } } - if (autoAftRadius2 == auto) + // You can only set the auto aft radius if it is possible + if (sanityCheck) { + auto = auto && canUseNextCompAutomatic(); + } + + if (autoAftRadius == auto) return; - autoAftRadius2 = auto; + autoAftRadius = auto; clearPreset(); fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); } + public void setAftRadiusAutomatic(boolean auto) { + setAftRadiusAutomatic(auto, false); + } + //// Radius automatics @@ -257,6 +293,32 @@ public class Transition extends SymmetricComponent implements InsideColorCompone return isAftRadiusAutomatic(); } + /** + * Checks whether this component can use the automatic radius of the previous symmetric component. + * @return false if there is no previous symmetric component, or if the previous component already has this component + * as its auto dimension reference + */ + public boolean canUsePreviousCompAutomatic() { + SymmetricComponent referenceComp = getPreviousSymmetricComponent(); + if (referenceComp == null) { + return false; + } + return !referenceComp.usesNextCompAutomatic(); + } + + /** + * Checks whether this component can use the automatic radius of the next symmetric component. + * @return false if there is no next symmetric component, or if the next component already has this component + * as its auto dimension reference + */ + public boolean canUseNextCompAutomatic() { + SymmetricComponent referenceComp = getNextSymmetricComponent(); + if (referenceComp == null) { + return false; + } + return !referenceComp.usesPreviousCompAutomatic(); + } + //////// Type & shape ///////// diff --git a/core/src/net/sf/openrocket/simulation/BasicEventSimulationEngine.java b/core/src/net/sf/openrocket/simulation/BasicEventSimulationEngine.java index 682b21b94..33b39174a 100644 --- a/core/src/net/sf/openrocket/simulation/BasicEventSimulationEngine.java +++ b/core/src/net/sf/openrocket/simulation/BasicEventSimulationEngine.java @@ -7,7 +7,9 @@ import java.util.Deque; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import net.sf.openrocket.aerodynamics.FlightConditions; import net.sf.openrocket.aerodynamics.Warning; +import net.sf.openrocket.aerodynamics.WarningSet; import net.sf.openrocket.l10n.Translator; import net.sf.openrocket.motor.IgnitionEvent; import net.sf.openrocket.motor.MotorConfiguration; @@ -58,12 +60,14 @@ public class BasicEventSimulationEngine implements SimulationEngine { // this is just a list of simulation branches to Deque toSimulate = new ArrayDeque(); + + FlightData flightData; @Override public FlightData simulate(SimulationConditions simulationConditions) throws SimulationException { // Set up flight data - FlightData flightData = new FlightData(); + flightData = new FlightData(); // Set up rocket configuration this.fcid = simulationConditions.getFlightConfigurationID(); @@ -82,6 +86,13 @@ public class BasicEventSimulationEngine implements SimulationEngine { throw new MotorIgnitionException(trans.get("BasicEventSimulationEngine.error.noMotorsDefined")); } + // Can't calculate stability + if (currentStatus.getSimulationConditions().getAerodynamicCalculator() + .getCP(currentStatus.getConfiguration(), + new FlightConditions(currentStatus.getConfiguration()), + new WarningSet()).weight < MathUtil.EPSILON) + throw new SimulationException(trans.get("BasicEventSimulationEngine.error.cantCalculateStability")); + // Problems that let us simulate, but result is likely bad // No recovery device @@ -268,6 +279,11 @@ public class BasicEventSimulationEngine implements SimulationEngine { // Add FlightEvent for Abort. currentStatus.getFlightData().addEvent(new FlightEvent(FlightEvent.Type.EXCEPTION, currentStatus.getSimulationTime(), currentStatus.getConfiguration().getRocket(), e.getLocalizedMessage())); + + flightData.addBranch(currentStatus.getFlightData()); + flightData.getWarningSet().addAll(currentStatus.getWarnings()); + + e.setFlightData(flightData); throw e; } @@ -380,7 +396,13 @@ public class BasicEventSimulationEngine implements SimulationEngine { case IGNITION: { MotorClusterState motorState = (MotorClusterState) event.getData(); - + + // If there are multiple ignition events (as is the case if the preceding stage has several burnout events, for instance) + // We get multiple ignition events for the upper stage motor. Ignore are all after the first. + if (motorState.getIgnitionTime() < currentStatus.getSimulationTime()) { + log.info("Ignoring motor " +motorState.toDescription()+" ignition event @"+currentStatus.getSimulationTime()); + continue; + } log.info(" Igniting motor: "+motorState.toDescription()+" @"+currentStatus.getSimulationTime()); motorState.ignite( event.getTime()); diff --git a/core/src/net/sf/openrocket/simulation/BasicLandingStepper.java b/core/src/net/sf/openrocket/simulation/BasicLandingStepper.java index 51fa56c4f..de7db0bab 100644 --- a/core/src/net/sf/openrocket/simulation/BasicLandingStepper.java +++ b/core/src/net/sf/openrocket/simulation/BasicLandingStepper.java @@ -1,6 +1,7 @@ package net.sf.openrocket.simulation; import net.sf.openrocket.models.atmosphere.AtmosphericConditions; +import net.sf.openrocket.rocketcomponent.InstanceMap; import net.sf.openrocket.rocketcomponent.RecoveryDevice; import net.sf.openrocket.simulation.exception.SimulationException; import net.sf.openrocket.util.Coordinate; @@ -35,8 +36,10 @@ public class BasicLandingStepper extends AbstractSimulationStepper { // Get total CD double mach = airSpeed.length() / atmosphere.getMachSpeed(); + + final InstanceMap imap = status.getConfiguration().getActiveInstances(); for (RecoveryDevice c : status.getDeployedRecoveryDevices()) { - totalCD += c.getCD(mach) * c.getArea() / refArea; + totalCD += imap.count(c) * c.getCD(mach) * c.getArea() / refArea; } // Compute drag force diff --git a/core/src/net/sf/openrocket/simulation/BasicTumbleStepper.java b/core/src/net/sf/openrocket/simulation/BasicTumbleStepper.java index 65c37d4d7..cd8750e23 100644 --- a/core/src/net/sf/openrocket/simulation/BasicTumbleStepper.java +++ b/core/src/net/sf/openrocket/simulation/BasicTumbleStepper.java @@ -69,6 +69,27 @@ public class BasicTumbleStepper extends AbstractSimulationStepper { double timeStep = MathUtil.min(0.5 / linearAcceleration.length(), RECOVERY_TIME_STEP); // Perform Euler integration + Coordinate newPosition = status.getRocketPosition().add(status.getRocketVelocity().multiply(timeStep)). + add(linearAcceleration.multiply(MathUtil.pow2(timeStep) / 2)); + + // If I've hit the ground, recalculate time step and position + if (newPosition.z < 0) { + + final double a = linearAcceleration.z; + final double v = status.getRocketVelocity().z; + final double z0 = status.getRocketPosition().z; + + // The new timestep is the solution of + // 1/2 at^2 + vt + z0 = 0 + timeStep = (-v - Math.sqrt(v*v - 2*a*z0))/a; + + newPosition = status.getRocketPosition().add(status.getRocketVelocity().multiply(timeStep)). + add(linearAcceleration.multiply(MathUtil.pow2(timeStep) / 2)); + + // avoid rounding error in new altitude + newPosition = newPosition.setZ(0); + } + status.setRocketPosition(status.getRocketPosition().add(status.getRocketVelocity().multiply(timeStep)). add(linearAcceleration.multiply(MathUtil.pow2(timeStep) / 2))); status.setRocketVelocity(status.getRocketVelocity().add(linearAcceleration.multiply(timeStep))); diff --git a/core/src/net/sf/openrocket/simulation/exception/SimulationException.java b/core/src/net/sf/openrocket/simulation/exception/SimulationException.java index f180f892c..e0e8ee8ec 100644 --- a/core/src/net/sf/openrocket/simulation/exception/SimulationException.java +++ b/core/src/net/sf/openrocket/simulation/exception/SimulationException.java @@ -1,7 +1,11 @@ package net.sf.openrocket.simulation.exception; +import net.sf.openrocket.simulation.FlightData; + public class SimulationException extends Exception { + private FlightData flightData = null; + public SimulationException() { } @@ -18,4 +22,11 @@ public class SimulationException extends Exception { super(message, cause); } + public void setFlightData(FlightData f) { + flightData = f; + } + + public FlightData getFlightData() { + return flightData; + } } diff --git a/core/src/net/sf/openrocket/startup/Preferences.java b/core/src/net/sf/openrocket/startup/Preferences.java index 26b9b1095..f9c31bd82 100644 --- a/core/src/net/sf/openrocket/startup/Preferences.java +++ b/core/src/net/sf/openrocket/startup/Preferences.java @@ -76,6 +76,7 @@ public abstract class Preferences implements ChangeSource { public static final String PREFERRED_THRUST_CURVE_MOTOR_NODE = "preferredThrustCurveMotors"; private static final String AUTO_OPEN_LAST_DESIGN = "AUTO_OPEN_LAST_DESIGN"; private static final String OPEN_LEFTMOST_DESIGN_TAB = "OPEN_LEFTMOST_DESIGN_TAB"; + public static final String MARKER_STYLE_ICON = "MARKER_STYLE_ICON"; private static final String SHOW_MARKERS = "SHOW_MARKERS"; private static final String SHOW_ROCKSIM_FORMAT_WARNING = "SHOW_ROCKSIM_FORMAT_WARNING"; diff --git a/core/src/net/sf/openrocket/thrustcurve/SerializeThrustcurveMotors.java b/core/src/net/sf/openrocket/thrustcurve/SerializeThrustcurveMotors.java index e1154d4c2..79b71f65a 100644 --- a/core/src/net/sf/openrocket/thrustcurve/SerializeThrustcurveMotors.java +++ b/core/src/net/sf/openrocket/thrustcurve/SerializeThrustcurveMotors.java @@ -185,8 +185,8 @@ public class SerializeThrustcurveMotors { System.exit(1); } else { while (iterator.hasNext()) { - Pair f = iterator.next(); - String fileName = f.getU(); + Pair f = iterator.next(); + String fileName = f.getU().getName(); InputStream is = f.getV(); List motors = loader.load(is, fileName); diff --git a/core/src/net/sf/openrocket/unit/Unit.java b/core/src/net/sf/openrocket/unit/Unit.java index 4c4195b15..fd0f97ac6 100644 --- a/core/src/net/sf/openrocket/unit/Unit.java +++ b/core/src/net/sf/openrocket/unit/Unit.java @@ -90,8 +90,11 @@ public abstract class Unit { * @return A string representation of the number in these units. */ public String toString(double value) { - double val = toUnit(value); + if (Double.isNaN(value)) + return "N/A"; + double val = toUnit(value); + if (Math.abs(val) > 1E6) { return expFormat.format(val); } diff --git a/core/src/net/sf/openrocket/util/LinearInterpolator.java b/core/src/net/sf/openrocket/util/LinearInterpolator.java index e16726bb5..8a9e786ac 100644 --- a/core/src/net/sf/openrocket/util/LinearInterpolator.java +++ b/core/src/net/sf/openrocket/util/LinearInterpolator.java @@ -82,7 +82,7 @@ public class LinearInterpolator implements Cloneable { if ( y1 != null ) { // Wow, x was a key in the map. Such luck. - return y1.doubleValue(); + return y1; } // we now know that x is not in the map, so we need to find the lower and higher keys. @@ -96,16 +96,16 @@ public class LinearInterpolator implements Cloneable { Double firstKey = sortMap.firstKey(); // x is smaller than the first entry in the map. - if ( x < firstKey.doubleValue() ) { + if ( x < firstKey) { y1 = sortMap.get(firstKey); - return y1.doubleValue(); + return y1; } // floor key is the largest key smaller than x - since we have at least one key, // and x>=firstKey, we know that floorKey != null. Double floorKey = sortMap.subMap(firstKey, x).lastKey(); - x1 = floorKey.doubleValue(); + x1 = floorKey; y1 = sortMap.get(floorKey); // Now we need to find the key that is greater or equal to x @@ -113,16 +113,16 @@ public class LinearInterpolator implements Cloneable { // Check if x is bigger than all the entries. if ( tailMap.isEmpty() ) { - return y1.doubleValue(); + return y1; } Double ceilKey = tailMap.firstKey(); // Check if x is bigger than all the entries. if ( ceilKey == null ) { - return y1.doubleValue(); + return y1; } - x2 = ceilKey.doubleValue(); + x2 = ceilKey; y2 = sortMap.get(ceilKey); return (x - x1)/(x2-x1) * (y2-y1) + y1; diff --git a/core/test/net/sf/openrocket/aerodynamics/BarrowmanCalculatorTest.java b/core/test/net/sf/openrocket/aerodynamics/BarrowmanCalculatorTest.java index d113cf003..2acd0f49c 100644 --- a/core/test/net/sf/openrocket/aerodynamics/BarrowmanCalculatorTest.java +++ b/core/test/net/sf/openrocket/aerodynamics/BarrowmanCalculatorTest.java @@ -290,7 +290,7 @@ public class BarrowmanCalculatorTest { FlightConfiguration configuration = rocket.getSelectedConfiguration(); WarningSet warnings = new WarningSet(); - calc.testIsContinuous(configuration, rocket, warnings); + calc.checkGeometry(configuration, rocket, warnings); assertTrue("Estes Alpha III should be continuous: ", warnings.isEmpty()); } @@ -301,7 +301,7 @@ public class BarrowmanCalculatorTest { FlightConfiguration configuration = rocket.getSelectedConfiguration(); WarningSet warnings = new WarningSet(); - calc.testIsContinuous(configuration, rocket, warnings); + calc.checkGeometry(configuration, rocket, warnings); assertTrue("F9H should be continuous: ", warnings.isEmpty()); } @@ -319,7 +319,7 @@ public class BarrowmanCalculatorTest { body.setOuterRadius( 0.012 ); body.setName( body.getName()+" << discontinuous"); - calc.testIsContinuous(configuration, rocket, warnings); + calc.checkGeometry(configuration, rocket, warnings); assertFalse(" Estes Alpha III has an undetected discontinuity:", warnings.isEmpty()); } @@ -340,7 +340,7 @@ public class BarrowmanCalculatorTest { body.setOuterRadius( 0.012 ); body.setName( body.getName()+" << discontinuous"); - calc.testIsContinuous(configuration, rocket, warnings); + calc.checkGeometry(configuration, rocket, warnings); assertFalse(" Missed discontinuity in Falcon 9 Heavy:" , warnings.isEmpty()); } diff --git a/core/test/net/sf/openrocket/aerodynamics/RailButtonCalcTest.java b/core/test/net/sf/openrocket/aerodynamics/RailButtonCalcTest.java new file mode 100644 index 000000000..513b0c922 --- /dev/null +++ b/core/test/net/sf/openrocket/aerodynamics/RailButtonCalcTest.java @@ -0,0 +1,101 @@ +package net.sf.openrocket.aerodynamics; + +import static org.junit.Assert.assertEquals; +import org.junit.BeforeClass; +import org.junit.Test; + +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Module; + +import net.sf.openrocket.ServicesForTesting; +import net.sf.openrocket.aerodynamics.BarrowmanCalculator; +import net.sf.openrocket.aerodynamics.barrowman.RailButtonCalc; +import net.sf.openrocket.plugin.PluginModule; +import net.sf.openrocket.rocketcomponent.FlightConfiguration; +import net.sf.openrocket.rocketcomponent.BodyTube; +import net.sf.openrocket.rocketcomponent.RailButton; +import net.sf.openrocket.rocketcomponent.LaunchLug; +import net.sf.openrocket.rocketcomponent.Rocket; +import net.sf.openrocket.rocketcomponent.position.AxialMethod; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.MathUtil; +import net.sf.openrocket.util.TestRockets; +import net.sf.openrocket.util.Transformation; + +public class RailButtonCalcTest { + protected final double EPSILON = 0.0001; + + private static Injector injector; + @BeforeClass + public static void setup() { + Module applicationModule = new ServicesForTesting(); + Module pluginModule = new PluginModule(); + + injector = Guice.createInjector( applicationModule, pluginModule); + Application.setInjector(injector); + } + + @Test + public void testRailButtons() { + + Rocket rocket = TestRockets.makeEstesAlphaIII(); + FlightConfiguration config = rocket.getSelectedConfiguration(); + + // Get the body tube... + BodyTube tube = (BodyTube)rocket.getChild(0).getChild(1); + + // Replace the launch lug with a (single) railbutton + LaunchLug lug = (LaunchLug)tube.getChild(1); + rocket.removeChild(lug); + + RailButton button = new RailButton(); + tube.addChild(button); + + // Button parameters from Binder Design standard 1010 + button.setOuterDiameter(0.011); + button.setInnerDiameter(0.006); + + button.setBaseHeight(0.002); + button.setFlangeHeight(0.002); + button.setTotalHeight(0.008); + + button.setAxialMethod(AxialMethod.ABSOLUTE); + button.setAxialOffset(1.0); + + // Set up flight conditions + FlightConditions conditions = new FlightConditions(config); + conditions.setMach(1.0); + + BarrowmanCalculator barrowmanObj = new BarrowmanCalculator(); + RailButtonCalc calcObj = new RailButtonCalc(button); + + // Calculate effective CD for rail button + // Boundary layer height + double rex = calcObj.calculateReynoldsNumber(1.0, conditions); // Reynolds number of button location + double del = 0.37 * 1.0 / Math.pow(rex, 0.2); // Boundary layer height + + // Interpolate velocity at midpoint of railbutton + double mach = MathUtil.map(0.008/2.0, 0, del, 0, 1.0); + + // Interpolate to get CD + double cd = MathUtil.map(mach, 0.2, 0.3, 1.22, 1.25); + + // Reference area of rail button + final double outerArea = button.getTotalHeight() * button.getOuterDiameter(); + final double notchArea = (button.getOuterDiameter() - button.getInnerDiameter()) * button.getInnerHeight(); + final double refArea = outerArea - notchArea; + + // Get "effective" CD + double calccd = cd * MathUtil.pow2(mach) * barrowmanObj.calculateStagnationCD(conditions.getMach()) * refArea / conditions.getRefArea() ; + + // Now compare with value from RailButtonCalc + WarningSet warnings = new WarningSet(); + AerodynamicForces assemblyForces = new AerodynamicForces().zero(); + AerodynamicForces componentForces = new AerodynamicForces(); + + double testcd = calcObj.calculatePressureCD(conditions, barrowmanObj.calculateStagnationCD(conditions.getMach()), 0, warnings); + + assertEquals("Calculated rail button CD incorrect", calccd, testcd, EPSILON); + } +} diff --git a/core/test/net/sf/openrocket/database/ThrustCurveMotorSetTest.java b/core/test/net/sf/openrocket/database/ThrustCurveMotorSetTest.java index 572b9a823..1c2d3306c 100644 --- a/core/test/net/sf/openrocket/database/ThrustCurveMotorSetTest.java +++ b/core/test/net/sf/openrocket/database/ThrustCurveMotorSetTest.java @@ -23,7 +23,7 @@ public class ThrustCurveMotorSetTest { private static final ThrustCurveMotor motor1 = new ThrustCurveMotor.Builder() .setManufacturer(Manufacturer.getManufacturer("A")) .setCommonName("F12") - .setDesignation("F12X") + .setDesignation("F12") .setDescription("Desc") .setMotorType(Motor.Type.UNKNOWN) .setStandardDelays(new double[] {}) @@ -38,7 +38,7 @@ public class ThrustCurveMotorSetTest { private static final ThrustCurveMotor motor2 = new ThrustCurveMotor.Builder() .setManufacturer(Manufacturer.getManufacturer("A")) .setCommonName("F12") - .setDesignation("F12H") + .setDesignation("F12") .setDescription("Desc") .setMotorType(Motor.Type.SINGLE) .setStandardDelays(new double[] { 5 }) @@ -51,20 +51,6 @@ public class ThrustCurveMotorSetTest { .build(); private static final ThrustCurveMotor motor3 = new ThrustCurveMotor.Builder() - .setManufacturer(Manufacturer.getManufacturer("A")) - .setCode("F12") - .setDescription("Desc") - .setMotorType(Motor.Type.UNKNOWN) - .setStandardDelays(new double[] { 0, Motor.PLUGGED_DELAY }) - .setDiameter(0.024) - .setLength(0.07) - .setTimePoints(new double[] { 0, 1, 2 }) - .setThrustPoints(new double[] { 0, 2, 0 }) - .setCGPoints(new Coordinate[] { Coordinate.NUL, Coordinate.NUL, Coordinate.NUL }) - .setDigest("digestC") - .build(); - - private static final ThrustCurveMotor motor4 = new ThrustCurveMotor.Builder() .setManufacturer(Manufacturer.getManufacturer("A")) .setDesignation("F12") .setDescription("Desc") @@ -113,39 +99,20 @@ public class ThrustCurveMotorSetTest { // Add motor2 assertTrue(set.matches(motor2)); set.addMotor(motor2); - assertEquals(motor1.getManufacturer(), set.getManufacturer()); - assertEquals(motor3.getCommonName(), set.getCommonName()); + assertEquals(motor2.getManufacturer(), set.getManufacturer()); + assertEquals(motor2.getCommonName(), set.getCommonName()); assertEquals(Motor.Type.SINGLE, set.getType()); - assertEquals(motor1.getDiameter(), set.getDiameter(), 0.00001); - assertEquals(motor1.getLength(), set.getLength(), 0.00001); + assertEquals(motor2.getDiameter(), set.getDiameter(), 0.00001); + assertEquals(motor2.getLength(), set.getLength(), 0.00001); assertEquals(2, set.getMotors().size()); - assertEquals(motor2, set.getMotors().get(0)); - assertEquals(motor1, set.getMotors().get(1)); + assertEquals(motor1, set.getMotors().get(0)); + assertEquals(motor2, set.getMotors().get(1)); assertEquals(Arrays.asList(5.0), set.getDelays()); - // Add motor3 - assertTrue(set.matches(motor3)); - set.addMotor(motor3); - assertEquals(motor1.getManufacturer(), set.getManufacturer()); - assertEquals(motor3.getCommonName(), set.getCommonName()); - assertEquals(Motor.Type.SINGLE, set.getType()); - assertEquals(motor1.getDiameter(), set.getDiameter(), 0.00001); - assertEquals(motor1.getLength(), set.getLength(), 0.00001); - assertEquals(3, set.getMotors().size()); - System.out.println("motor set"); - System.out.println(set.getMotors()); - System.out.println(motor3); - System.out.println(motor2); - System.out.println(motor1); - assertEquals(motor3, set.getMotors().get(0)); - assertEquals(motor2, set.getMotors().get(1)); - assertEquals(motor1, set.getMotors().get(2)); - assertEquals(Arrays.asList(0.0, 5.0, Motor.PLUGGED_DELAY), set.getDelays()); - - // Test that adding motor4 fails - assertFalse(set.matches(motor4)); + // Test that adding motor3 fails + assertFalse(set.matches(motor3)); try { - set.addMotor(motor4); + set.addMotor(motor3); fail("Did not throw exception"); } catch (IllegalArgumentException e) { } diff --git a/core/test/net/sf/openrocket/file/iterator/TestFileIterator.java b/core/test/net/sf/openrocket/file/iterator/TestFileIterator.java index c3e424ee0..9c59585bd 100644 --- a/core/test/net/sf/openrocket/file/iterator/TestFileIterator.java +++ b/core/test/net/sf/openrocket/file/iterator/TestFileIterator.java @@ -3,6 +3,7 @@ package net.sf.openrocket.file.iterator; import static org.junit.Assert.*; import java.io.ByteArrayInputStream; +import java.io.File; import java.io.InputStream; import net.sf.openrocket.util.Pair; @@ -13,14 +14,14 @@ public class TestFileIterator { @Test public void testFileIterator() { - final Pair one = new Pair("one", new ByteArrayInputStream(new byte[] { 1 })); - final Pair two = new Pair("two", new ByteArrayInputStream(new byte[] { 2 })); + final Pair one = new Pair<>(new File("one"), new ByteArrayInputStream(new byte[] { 1 })); + final Pair two = new Pair<>(new File("two"), new ByteArrayInputStream(new byte[] { 2 })); FileIterator iterator = new FileIterator() { private int count = 0; @Override - protected Pair findNext() { + protected Pair findNext() { count++; switch (count) { case 1: diff --git a/core/test/net/sf/openrocket/rocketcomponent/NoseConeTest.java b/core/test/net/sf/openrocket/rocketcomponent/NoseConeTest.java new file mode 100644 index 000000000..e02e8ce3a --- /dev/null +++ b/core/test/net/sf/openrocket/rocketcomponent/NoseConeTest.java @@ -0,0 +1,398 @@ +package net.sf.openrocket.rocketcomponent; + +import net.sf.openrocket.document.OpenRocketDocumentFactory; +import net.sf.openrocket.util.BaseTestCase.BaseTestCase; +import net.sf.openrocket.util.MathUtil; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +public class NoseConeTest extends BaseTestCase { + private final double EPSILON = MathUtil.EPSILON * 1000; + + @Test + public void testNormalNoseCone() { + NoseCone noseCone = new NoseCone(); + + // First set the parameters using the normal transition setters (i.e. using AftRadius and AftShoulder instead of Base and Shoulder) + noseCone.setType(Transition.Shape.OGIVE); + noseCone.setLength(0.06); + noseCone.setAftRadius(0.1); + noseCone.setAftShoulderLength(0.01); + noseCone.setAftShoulderRadius(0.05); + noseCone.setAftShoulderCapped(false); + noseCone.setAftShoulderThickness(0.001); + + assertEquals(Transition.Shape.OGIVE, noseCone.getType()); + assertEquals(0.06, noseCone.getLength(), EPSILON); + assertEquals(0.1, noseCone.getAftRadius(), EPSILON); + assertEquals(0.1, noseCone.getBaseRadius(), EPSILON); + assertEquals(0.1, noseCone.getAftRadiusNoAutomatic(), EPSILON); + assertEquals(0.1, noseCone.getBaseRadiusNoAutomatic(), EPSILON); + assertEquals(0.01, noseCone.getAftShoulderLength(), EPSILON); + assertEquals(0.01, noseCone.getShoulderLength(), EPSILON); + assertEquals(0.05, noseCone.getAftShoulderRadius(), EPSILON); + assertEquals(0.05, noseCone.getShoulderRadius(), EPSILON); + assertFalse(noseCone.isAftShoulderCapped()); + assertFalse(noseCone.isShoulderCapped()); + assertEquals(0.001, noseCone.getAftShoulderThickness(), EPSILON); + assertEquals(0.001, noseCone.getShoulderThickness(), EPSILON); + assertFalse(noseCone.isAftRadiusAutomatic()); + + assertEquals(0, noseCone.getForeRadius(), EPSILON); + assertEquals(0, noseCone.getForeRadiusNoAutomatic(), EPSILON); + assertEquals(0, noseCone.getForeShoulderLength(), EPSILON); + assertEquals(0, noseCone.getForeShoulderRadius(), EPSILON); + assertEquals(0, noseCone.getForeShoulderThickness(), EPSILON); + assertFalse(noseCone.isForeShoulderCapped()); + assertFalse(noseCone.isForeRadiusAutomatic()); + + // Test setting the specific nose cone setters + noseCone.setBaseRadius(0.2); + noseCone.setShoulderLength(0.03); + noseCone.setShoulderRadius(0.04); + noseCone.setShoulderCapped(true); + noseCone.setShoulderThickness(0.005); + + assertEquals(0.2, noseCone.getAftRadius(), EPSILON); + assertEquals(0.2, noseCone.getBaseRadius(), EPSILON); + assertEquals(0.2, noseCone.getAftRadiusNoAutomatic(), EPSILON); + assertEquals(0.2, noseCone.getBaseRadiusNoAutomatic(), EPSILON); + assertEquals(0.03, noseCone.getAftShoulderLength(), EPSILON); + assertEquals(0.03, noseCone.getShoulderLength(), EPSILON); + assertEquals(0.04, noseCone.getAftShoulderRadius(), EPSILON); + assertEquals(0.04, noseCone.getShoulderRadius(), EPSILON); + assertTrue(noseCone.isAftShoulderCapped()); + assertTrue(noseCone.isShoulderCapped()); + assertEquals(0.005, noseCone.getAftShoulderThickness(), EPSILON); + assertEquals(0.005, noseCone.getShoulderThickness(), EPSILON); + assertFalse(noseCone.isAftRadiusAutomatic()); + + assertEquals(0, noseCone.getForeRadius(), EPSILON); + assertEquals(0, noseCone.getForeRadiusNoAutomatic(), EPSILON); + assertEquals(0, noseCone.getForeShoulderLength(), EPSILON); + assertEquals(0, noseCone.getForeShoulderRadius(), EPSILON); + assertEquals(0, noseCone.getForeShoulderThickness(), EPSILON); + assertFalse(noseCone.isForeShoulderCapped()); + assertFalse(noseCone.isForeRadiusAutomatic()); + } + + @Test + public void testFlippedNoseCone() { + NoseCone noseCone = new NoseCone(); + + // First set the parameters using the normal transition setters (i.e. using AftRadius and AftShoulder instead of Base and Shoulder) + noseCone.setType(Transition.Shape.OGIVE); + noseCone.setLength(0.06); + noseCone.setAftRadius(0.1); + noseCone.setAftShoulderLength(0.01); + noseCone.setAftShoulderRadius(0.05); + noseCone.setAftShoulderCapped(false); + noseCone.setAftShoulderThickness(0.001); + noseCone.setFlipped(true); + + assertEquals(Transition.Shape.OGIVE, noseCone.getType()); + assertEquals(0.06, noseCone.getLength(), EPSILON); + assertEquals(0.1, noseCone.getForeRadius(), EPSILON); + assertEquals(0.1, noseCone.getBaseRadius(), EPSILON); + assertEquals(0.1, noseCone.getForeRadiusNoAutomatic(), EPSILON); + assertEquals(0.1, noseCone.getBaseRadiusNoAutomatic(), EPSILON); + assertEquals(0.01, noseCone.getForeShoulderLength(), EPSILON); + assertEquals(0.01, noseCone.getShoulderLength(), EPSILON); + assertEquals(0.05, noseCone.getForeShoulderRadius(), EPSILON); + assertEquals(0.05, noseCone.getShoulderRadius(), EPSILON); + assertFalse(noseCone.isForeShoulderCapped()); + assertFalse(noseCone.isShoulderCapped()); + assertEquals(0.001, noseCone.getForeShoulderThickness(), EPSILON); + assertEquals(0.001, noseCone.getShoulderThickness(), EPSILON); + assertFalse(noseCone.isForeRadiusAutomatic()); + + assertEquals(0, noseCone.getAftRadius(), EPSILON); + assertEquals(0, noseCone.getAftRadiusNoAutomatic(), EPSILON); + assertEquals(0, noseCone.getAftShoulderLength(), EPSILON); + assertEquals(0, noseCone.getAftShoulderRadius(), EPSILON); + assertEquals(0, noseCone.getAftShoulderThickness(), EPSILON); + assertFalse(noseCone.isAftShoulderCapped()); + assertFalse(noseCone.isAftRadiusAutomatic()); + + // Test setting the specific nose cone setters + noseCone.setBaseRadius(0.2); + noseCone.setShoulderLength(0.03); + noseCone.setShoulderRadius(0.04); + noseCone.setShoulderCapped(true); + noseCone.setShoulderThickness(0.005); + + assertEquals(0.2, noseCone.getForeRadius(), EPSILON); + assertEquals(0.2, noseCone.getBaseRadius(), EPSILON); + assertEquals(0.2, noseCone.getForeRadiusNoAutomatic(), EPSILON); + assertEquals(0.2, noseCone.getBaseRadiusNoAutomatic(), EPSILON); + assertEquals(0.03, noseCone.getForeShoulderLength(), EPSILON); + assertEquals(0.03, noseCone.getShoulderLength(), EPSILON); + assertEquals(0.04, noseCone.getForeShoulderRadius(), EPSILON); + assertEquals(0.04, noseCone.getShoulderRadius(), EPSILON); + assertTrue(noseCone.isForeShoulderCapped()); + assertTrue(noseCone.isShoulderCapped()); + assertEquals(0.005, noseCone.getForeShoulderThickness(), EPSILON); + assertEquals(0.005, noseCone.getShoulderThickness(), EPSILON); + assertFalse(noseCone.isForeRadiusAutomatic()); + + assertEquals(0, noseCone.getAftRadius(), EPSILON); + assertEquals(0, noseCone.getAftRadiusNoAutomatic(), EPSILON); + assertEquals(0, noseCone.getAftShoulderLength(), EPSILON); + assertEquals(0, noseCone.getAftShoulderRadius(), EPSILON); + assertEquals(0, noseCone.getAftShoulderThickness(), EPSILON); + assertFalse(noseCone.isAftShoulderCapped()); + assertFalse(noseCone.isAftRadiusAutomatic()); + + // Flip back to normal + noseCone.setFlipped(false); + + assertEquals(0.2, noseCone.getAftRadius(), EPSILON); + assertEquals(0.2, noseCone.getBaseRadius(), EPSILON); + assertEquals(0.2, noseCone.getAftRadiusNoAutomatic(), EPSILON); + assertEquals(0.2, noseCone.getBaseRadiusNoAutomatic(), EPSILON); + assertEquals(0.03, noseCone.getAftShoulderLength(), EPSILON); + assertEquals(0.03, noseCone.getShoulderLength(), EPSILON); + assertEquals(0.04, noseCone.getAftShoulderRadius(), EPSILON); + assertEquals(0.04, noseCone.getShoulderRadius(), EPSILON); + assertTrue(noseCone.isAftShoulderCapped()); + assertTrue(noseCone.isShoulderCapped()); + assertEquals(0.005, noseCone.getAftShoulderThickness(), EPSILON); + assertEquals(0.005, noseCone.getShoulderThickness(), EPSILON); + assertFalse(noseCone.isAftRadiusAutomatic()); + + assertEquals(0, noseCone.getForeRadius(), EPSILON); + assertEquals(0, noseCone.getForeRadiusNoAutomatic(), EPSILON); + assertEquals(0, noseCone.getForeShoulderLength(), EPSILON); + assertEquals(0, noseCone.getForeShoulderRadius(), EPSILON); + assertEquals(0, noseCone.getForeShoulderThickness(), EPSILON); + assertFalse(noseCone.isForeShoulderCapped()); + assertFalse(noseCone.isForeRadiusAutomatic()); + } + + @Test + public void testNormalNoseConeRadiusAutomatic() { + Rocket rocket = OpenRocketDocumentFactory.createNewRocket().getRocket(); + AxialStage stage = rocket.getStage(0); + + NoseCone noseCone = new NoseCone(Transition.Shape.CONICAL, 0.06, 0.01); + BodyTube tube1 = new BodyTube(0.06, 0.02); + tube1.setOuterRadiusAutomatic(false); + BodyTube tube2 = new BodyTube(0.06, 0.03); + tube2.setOuterRadiusAutomatic(false); + + // Test no previous or next component + stage.addChild(noseCone); + + assertFalse(noseCone.usesPreviousCompAutomatic()); + assertFalse(noseCone.usesNextCompAutomatic()); + assertSame(stage, noseCone.getPreviousComponent()); + assertNull(noseCone.getPreviousSymmetricComponent()); + assertNull(noseCone.getNextComponent()); + assertNull(noseCone.getNextSymmetricComponent()); + assertFalse(noseCone.isAftRadiusAutomatic()); + assertFalse(noseCone.isBaseRadiusAutomatic()); + assertFalse(noseCone.isForeRadiusAutomatic()); + assertEquals(noseCone.getAftRadius(), noseCone.getAftRadiusNoAutomatic(), EPSILON); + assertEquals(noseCone.getBaseRadius(), noseCone.getBaseRadiusNoAutomatic(), EPSILON); + assertEquals(noseCone.getForeRadius(), noseCone.getForeRadiusNoAutomatic(), EPSILON); + assertEquals(0.01, noseCone.getAftRadiusNoAutomatic(), EPSILON); + assertEquals(0, noseCone.getForeRadiusNoAutomatic(), EPSILON); + + noseCone.setAftRadiusAutomatic(true, true); + assertFalse(noseCone.isAftRadiusAutomatic()); + assertFalse(noseCone.isBaseRadiusAutomatic()); + noseCone.setForeRadiusAutomatic(true, true); + assertFalse(noseCone.isForeRadiusAutomatic()); + assertFalse(noseCone.isBaseRadiusAutomatic()); + + // Test with next component + stage.addChild(tube1); + + assertFalse(noseCone.usesPreviousCompAutomatic()); + assertFalse(noseCone.usesNextCompAutomatic()); + assertSame(stage, noseCone.getPreviousComponent()); + assertNull(noseCone.getPreviousSymmetricComponent()); + assertSame(tube1, noseCone.getNextComponent()); + assertSame(tube1, noseCone.getNextSymmetricComponent()); + assertFalse(noseCone.isAftRadiusAutomatic()); + assertFalse(noseCone.isBaseRadiusAutomatic()); + assertFalse(noseCone.isForeRadiusAutomatic()); + assertEquals(noseCone.getAftRadius(), noseCone.getAftRadiusNoAutomatic(), EPSILON); + assertEquals(noseCone.getBaseRadius(), noseCone.getBaseRadiusNoAutomatic(), EPSILON); + assertEquals(noseCone.getForeRadius(), noseCone.getForeRadiusNoAutomatic(), EPSILON); + assertEquals(0, noseCone.getForeRadiusNoAutomatic(), EPSILON); + + noseCone.setAftRadiusAutomatic(true, true); + assertFalse(noseCone.usesPreviousCompAutomatic()); + assertTrue(noseCone.usesNextCompAutomatic()); + assertSame(stage, noseCone.getPreviousComponent()); + assertNull(noseCone.getPreviousSymmetricComponent()); + assertSame(tube1, noseCone.getNextComponent()); + assertSame(tube1, noseCone.getNextSymmetricComponent()); + assertTrue(noseCone.isAftRadiusAutomatic()); + assertTrue(noseCone.isBaseRadiusAutomatic()); + assertFalse(noseCone.isForeRadiusAutomatic()); + assertEquals(tube1.getForeRadius(), noseCone.getAftRadius(), EPSILON); + assertEquals(0.01, noseCone.getAftRadiusNoAutomatic(), EPSILON); + assertEquals(tube1.getForeRadius(), noseCone.getBaseRadius(), EPSILON); + assertEquals(0.01, noseCone.getBaseRadiusNoAutomatic(), EPSILON); + assertEquals(noseCone.getForeRadius(), noseCone.getForeRadiusNoAutomatic(), EPSILON); + assertEquals(0, noseCone.getForeRadiusNoAutomatic(), EPSILON); + + noseCone.setAftRadiusAutomatic(false, true); + assertEquals(noseCone.getAftRadius(), noseCone.getAftRadiusNoAutomatic(), EPSILON); + assertEquals(noseCone.getBaseRadius(), noseCone.getBaseRadiusNoAutomatic(), EPSILON); + assertEquals(noseCone.getForeRadius(), noseCone.getForeRadiusNoAutomatic(), EPSILON); + assertEquals(0, noseCone.getForeRadiusNoAutomatic(), EPSILON); + + noseCone.setBaseRadiusAutomatic(true); + assertEquals(0.01, noseCone.getAftRadiusNoAutomatic(), EPSILON); + assertEquals(tube1.getForeRadius(), noseCone.getBaseRadius(), EPSILON); + assertEquals(0.01, noseCone.getBaseRadiusNoAutomatic(), EPSILON); + assertEquals(noseCone.getForeRadius(), noseCone.getForeRadiusNoAutomatic(), EPSILON); + assertEquals(0, noseCone.getForeRadiusNoAutomatic(), EPSILON); + + noseCone.setForeRadiusAutomatic(true, true); + assertFalse(noseCone.isForeRadiusAutomatic()); + assertTrue(noseCone.isBaseRadiusAutomatic()); + + // Test with previous component + stage.addChild(tube2, 0); + + assertFalse(noseCone.usesPreviousCompAutomatic()); + assertTrue(noseCone.usesNextCompAutomatic()); + assertSame(tube2, noseCone.getPreviousComponent()); + assertSame(tube2, noseCone.getPreviousSymmetricComponent()); + assertSame(tube1, noseCone.getNextComponent()); + assertSame(tube1, noseCone.getNextSymmetricComponent()); + assertTrue(noseCone.isAftRadiusAutomatic()); + assertTrue(noseCone.isBaseRadiusAutomatic()); + assertFalse(noseCone.isForeRadiusAutomatic()); + assertEquals(tube1.getForeRadius(), noseCone.getAftRadius(), EPSILON); + assertEquals(0.01, noseCone.getAftRadiusNoAutomatic(), EPSILON); + assertEquals(tube1.getForeRadius(), noseCone.getBaseRadius(), EPSILON); + assertEquals(0.01, noseCone.getBaseRadiusNoAutomatic(), EPSILON); + assertEquals(noseCone.getForeRadius(), noseCone.getForeRadiusNoAutomatic(), EPSILON); + assertEquals(0, noseCone.getForeRadiusNoAutomatic(), EPSILON); + + // Do a flip + noseCone.setFlipped(true); + assertTrue(noseCone.isForeRadiusAutomatic()); + assertTrue(noseCone.isBaseRadiusAutomatic()); + assertFalse(noseCone.isAftRadiusAutomatic()); + } + + @Test + public void testFlippedNoseConeRadiusAutomatic() { + Rocket rocket = OpenRocketDocumentFactory.createNewRocket().getRocket(); + AxialStage stage = rocket.getStage(0); + + NoseCone noseCone = new NoseCone(Transition.Shape.CONICAL, 0.06, 0.01); + noseCone.setFlipped(true); + BodyTube tube1 = new BodyTube(0.06, 0.02); + tube1.setOuterRadiusAutomatic(false); + BodyTube tube2 = new BodyTube(0.06, 0.03); + tube2.setOuterRadiusAutomatic(false); + + // Test no previous or next component + stage.addChild(noseCone); + + assertFalse(noseCone.usesPreviousCompAutomatic()); + assertFalse(noseCone.usesNextCompAutomatic()); + assertSame(stage, noseCone.getPreviousComponent()); + assertNull(noseCone.getPreviousSymmetricComponent()); + assertNull(noseCone.getNextComponent()); + assertNull(noseCone.getNextSymmetricComponent()); + assertFalse(noseCone.isAftRadiusAutomatic()); + assertFalse(noseCone.isBaseRadiusAutomatic()); + assertFalse(noseCone.isForeRadiusAutomatic()); + assertEquals(noseCone.getAftRadius(), noseCone.getAftRadiusNoAutomatic(), EPSILON); + assertEquals(noseCone.getBaseRadius(), noseCone.getBaseRadiusNoAutomatic(), EPSILON); + assertEquals(noseCone.getForeRadius(), noseCone.getForeRadiusNoAutomatic(), EPSILON); + assertEquals(0, noseCone.getAftRadiusNoAutomatic(), EPSILON); + assertEquals(0.01, noseCone.getForeRadiusNoAutomatic(), EPSILON); + + noseCone.setAftRadiusAutomatic(true, true); + assertFalse(noseCone.isAftRadiusAutomatic()); + assertFalse(noseCone.isBaseRadiusAutomatic()); + noseCone.setForeRadiusAutomatic(true, true); + assertFalse(noseCone.isForeRadiusAutomatic()); + assertFalse(noseCone.isBaseRadiusAutomatic()); + + // Test with previous component + stage.addChild(tube1, 0); + + assertFalse(noseCone.usesPreviousCompAutomatic()); + assertFalse(noseCone.usesNextCompAutomatic()); + assertSame(tube1, noseCone.getPreviousComponent()); + assertSame(tube1, noseCone.getPreviousSymmetricComponent()); + assertNull(noseCone.getNextComponent()); + assertNull(noseCone.getNextSymmetricComponent()); + assertFalse(noseCone.isAftRadiusAutomatic()); + assertFalse(noseCone.isBaseRadiusAutomatic()); + assertFalse(noseCone.isForeRadiusAutomatic()); + assertEquals(noseCone.getAftRadius(), noseCone.getAftRadiusNoAutomatic(), EPSILON); + assertEquals(noseCone.getBaseRadius(), noseCone.getBaseRadiusNoAutomatic(), EPSILON); + assertEquals(noseCone.getForeRadius(), noseCone.getForeRadiusNoAutomatic(), EPSILON); + assertEquals(0, noseCone.getAftRadiusNoAutomatic(), EPSILON); + + noseCone.setBaseRadiusAutomatic(true); + assertTrue(noseCone.usesPreviousCompAutomatic()); + assertFalse(noseCone.usesNextCompAutomatic()); + assertSame(tube1, noseCone.getPreviousComponent()); + assertSame(tube1, noseCone.getPreviousSymmetricComponent()); + assertNull(noseCone.getNextComponent()); + assertNull(noseCone.getNextSymmetricComponent()); + assertFalse(noseCone.isAftRadiusAutomatic()); + assertTrue(noseCone.isBaseRadiusAutomatic()); + assertTrue(noseCone.isForeRadiusAutomatic()); + assertEquals(tube1.getAftRadius(), noseCone.getForeRadius(), EPSILON); + assertEquals(0.01, noseCone.getForeRadiusNoAutomatic(), EPSILON); + assertEquals(tube1.getAftRadius(), noseCone.getBaseRadius(), EPSILON); + assertEquals(0.01, noseCone.getBaseRadiusNoAutomatic(), EPSILON); + assertEquals(noseCone.getAftRadius(), noseCone.getAftRadiusNoAutomatic(), EPSILON); + assertEquals(0, noseCone.getAftRadiusNoAutomatic(), EPSILON); + + noseCone.setForeRadiusAutomatic(false, true); + assertEquals(noseCone.getAftRadius(), noseCone.getAftRadiusNoAutomatic(), EPSILON); + assertEquals(noseCone.getBaseRadius(), noseCone.getBaseRadiusNoAutomatic(), EPSILON); + assertEquals(noseCone.getForeRadius(), noseCone.getForeRadiusNoAutomatic(), EPSILON); + assertEquals(0.01, noseCone.getForeRadiusNoAutomatic(), EPSILON); + + noseCone.setBaseRadiusAutomatic(true); + assertEquals(tube1.getAftRadius(), noseCone.getForeRadius(), EPSILON); + assertEquals(0.01, noseCone.getForeRadiusNoAutomatic(), EPSILON); + assertEquals(tube1.getAftRadius(), noseCone.getBaseRadius(), EPSILON); + assertEquals(0.01, noseCone.getBaseRadiusNoAutomatic(), EPSILON); + assertEquals(noseCone.getAftRadius(), noseCone.getAftRadiusNoAutomatic(), EPSILON); + assertEquals(0, noseCone.getAftRadiusNoAutomatic(), EPSILON); + + assertTrue(noseCone.isForeRadiusAutomatic()); + assertTrue(noseCone.isBaseRadiusAutomatic()); + assertFalse(noseCone.isAftRadiusAutomatic()); + + // Test with next component + stage.addChild(tube2); + + assertTrue(noseCone.usesPreviousCompAutomatic()); + assertFalse(noseCone.usesNextCompAutomatic()); + assertSame(tube1, noseCone.getPreviousComponent()); + assertSame(tube1, noseCone.getPreviousSymmetricComponent()); + assertSame(tube2, noseCone.getNextComponent()); + assertSame(tube2, noseCone.getNextSymmetricComponent()); + assertFalse(noseCone.isAftRadiusAutomatic()); + assertTrue(noseCone.isBaseRadiusAutomatic()); + assertTrue(noseCone.isForeRadiusAutomatic()); + assertEquals(tube1.getAftRadius(), noseCone.getForeRadius(), EPSILON); + assertEquals(0.01, noseCone.getForeRadiusNoAutomatic(), EPSILON); + assertEquals(tube1.getForeRadius(), noseCone.getBaseRadius(), EPSILON); + assertEquals(0.01, noseCone.getBaseRadiusNoAutomatic(), EPSILON); + assertEquals(noseCone.getAftRadius(), noseCone.getAftRadiusNoAutomatic(), EPSILON); + assertEquals(0, noseCone.getAftRadius(), EPSILON); + } +} diff --git a/core/test/net/sf/openrocket/simulation/FlightEventsTest.java b/core/test/net/sf/openrocket/simulation/FlightEventsTest.java index 2dc047a22..1069d67f5 100644 --- a/core/test/net/sf/openrocket/simulation/FlightEventsTest.java +++ b/core/test/net/sf/openrocket/simulation/FlightEventsTest.java @@ -24,6 +24,8 @@ import static org.junit.Assert.assertSame; * Tests to verify that simulations contain all the expected flight events. */ public class FlightEventsTest extends BaseTestCase { + private static final double EPSILON = 0.005; + /** * Tests for a single stage design. */ @@ -66,7 +68,7 @@ public class FlightEventsTest extends BaseTestCase { // Test that the event times are correct for (int i = 0; i < expectedEventTimes.length; i++) { assertEquals(" Flight type " + expectedEventTypes[i] + " has wrong time", - expectedEventTimes[i], eventList.get(i).getTime(), 0.001); + expectedEventTimes[i], eventList.get(i).getTime(), EPSILON); } @@ -142,7 +144,7 @@ public class FlightEventsTest extends BaseTestCase { // Test that the event times are correct for (int i = 0; i < expectedEventTimes.length; i++) { assertEquals(" Flight type " + expectedEventTypes[i] + " has wrong time", - expectedEventTimes[i], eventList.get(i).getTime(), 0.001); + expectedEventTimes[i], eventList.get(i).getTime(), EPSILON); } // Test that the event sources are correct diff --git a/fileformat.txt b/fileformat.txt index 2a488f6cc..dafe8745a 100644 --- a/fileformat.txt +++ b/fileformat.txt @@ -58,6 +58,7 @@ The following file format versions exist: Added PhotoStudio settings saving () Added override CD parameter () Added stage activeness remembrance ( under ) + Added parameter for Nose Cones Separated into individual parameters for mass, CG, and CD. Rename to ( remains for backward compatibility) Rename to ( remains for backward compatibility) diff --git a/swing/.classpath b/swing/.classpath index 4cb3e712c..e2bf65e9a 100644 --- a/swing/.classpath +++ b/swing/.classpath @@ -19,7 +19,7 @@ - + diff --git a/swing/lib/jogl/gluegen-rt-natives-macosx-universal.jar b/swing/lib/jogl/gluegen-rt-natives-macosx-universal.jar index a47411ec9..33e73ee5b 100644 Binary files a/swing/lib/jogl/gluegen-rt-natives-macosx-universal.jar and b/swing/lib/jogl/gluegen-rt-natives-macosx-universal.jar differ diff --git a/swing/lib/jogl/jogl-all-natives-macosx-universal.jar b/swing/lib/jogl/jogl-all-natives-macosx-universal.jar index a95f52455..1798a558e 100644 Binary files a/swing/lib/jogl/jogl-all-natives-macosx-universal.jar and b/swing/lib/jogl/jogl-all-natives-macosx-universal.jar differ diff --git a/swing/resources/datafiles/examples/A simple model rocket.ork b/swing/resources/datafiles/examples/A simple model rocket.ork index b20c1d91e..e56383db5 100644 Binary files a/swing/resources/datafiles/examples/A simple model rocket.ork and b/swing/resources/datafiles/examples/A simple model rocket.ork differ diff --git a/swing/resources/datafiles/examples/Airstart timing.ork b/swing/resources/datafiles/examples/Airstart timing.ork new file mode 100644 index 000000000..d2542d766 Binary files /dev/null and b/swing/resources/datafiles/examples/Airstart timing.ork differ diff --git a/swing/resources/datafiles/examples/Apocalypse with decals.ork b/swing/resources/datafiles/examples/Apocalypse with decals.ork deleted file mode 100644 index e059b4a4e..000000000 Binary files a/swing/resources/datafiles/examples/Apocalypse with decals.ork and /dev/null differ diff --git a/swing/resources/datafiles/examples/Boosted Dart.ork b/swing/resources/datafiles/examples/Boosted Dart.ork deleted file mode 100644 index b56c868d6..000000000 Binary files a/swing/resources/datafiles/examples/Boosted Dart.ork and /dev/null differ diff --git a/swing/resources/datafiles/examples/Chute release.ork b/swing/resources/datafiles/examples/Chute release.ork new file mode 100644 index 000000000..aa6705928 Binary files /dev/null and b/swing/resources/datafiles/examples/Chute release.ork differ diff --git a/swing/resources/datafiles/examples/Clustered motors.ork b/swing/resources/datafiles/examples/Clustered motors.ork new file mode 100644 index 000000000..16fdd59a5 Binary files /dev/null and b/swing/resources/datafiles/examples/Clustered motors.ork differ diff --git a/swing/resources/datafiles/examples/Clustered rocket design.ork b/swing/resources/datafiles/examples/Clustered rocket design.ork deleted file mode 100644 index 6cc2f28c9..000000000 Binary files a/swing/resources/datafiles/examples/Clustered rocket design.ork and /dev/null differ diff --git a/swing/resources/datafiles/examples/Dual Deploy.ork b/swing/resources/datafiles/examples/Dual Deploy.ork deleted file mode 100644 index 0d2821e93..000000000 Binary files a/swing/resources/datafiles/examples/Dual Deploy.ork and /dev/null differ diff --git a/swing/resources/datafiles/examples/Dual parachute deployment.ork b/swing/resources/datafiles/examples/Dual parachute deployment.ork new file mode 100644 index 000000000..d23bf96cd Binary files /dev/null and b/swing/resources/datafiles/examples/Dual parachute deployment.ork differ diff --git a/swing/resources/datafiles/examples/High Power Airstart.ork b/swing/resources/datafiles/examples/High Power Airstart.ork deleted file mode 100644 index 479880efb..000000000 Binary files a/swing/resources/datafiles/examples/High Power Airstart.ork and /dev/null differ diff --git a/swing/resources/datafiles/examples/Hybrid rocket with dual parachute deployment.ork b/swing/resources/datafiles/examples/Hybrid rocket with dual parachute deployment.ork deleted file mode 100644 index 574c89c57..000000000 Binary files a/swing/resources/datafiles/examples/Hybrid rocket with dual parachute deployment.ork and /dev/null differ diff --git a/swing/resources/datafiles/examples/Parallel Staging Example.ork b/swing/resources/datafiles/examples/Parallel Staging Example.ork deleted file mode 100644 index 9b4ea51f1..000000000 Binary files a/swing/resources/datafiles/examples/Parallel Staging Example.ork and /dev/null differ diff --git a/swing/resources/datafiles/examples/Parallel booster staging.ork b/swing/resources/datafiles/examples/Parallel booster staging.ork new file mode 100644 index 000000000..7543c2951 Binary files /dev/null and b/swing/resources/datafiles/examples/Parallel booster staging.ork differ diff --git a/swing/resources/datafiles/examples/Pods Example.ork b/swing/resources/datafiles/examples/Pods Example.ork deleted file mode 100644 index 1d0d3f7fc..000000000 Binary files a/swing/resources/datafiles/examples/Pods Example.ork and /dev/null differ diff --git a/swing/resources/datafiles/examples/Pods--airframes and winglets.ork b/swing/resources/datafiles/examples/Pods--airframes and winglets.ork new file mode 100644 index 000000000..ca940caf3 Binary files /dev/null and b/swing/resources/datafiles/examples/Pods--airframes and winglets.ork differ diff --git a/swing/resources/datafiles/examples/Pods--powered with recovery deployment.ork b/swing/resources/datafiles/examples/Pods--powered with recovery deployment.ork new file mode 100644 index 000000000..1656e5144 Binary files /dev/null and b/swing/resources/datafiles/examples/Pods--powered with recovery deployment.ork differ diff --git a/swing/resources/datafiles/examples/Preset Usage.ork b/swing/resources/datafiles/examples/Preset Usage.ork deleted file mode 100644 index afb1cfb00..000000000 Binary files a/swing/resources/datafiles/examples/Preset Usage.ork and /dev/null differ diff --git a/swing/resources/datafiles/examples/Presets.ork b/swing/resources/datafiles/examples/Presets.ork new file mode 100644 index 000000000..5812e0d38 Binary files /dev/null and b/swing/resources/datafiles/examples/Presets.ork differ diff --git a/swing/resources/datafiles/examples/Simulation Extension.ork b/swing/resources/datafiles/examples/Simulation Extension.ork deleted file mode 100644 index 6f8243e9d..000000000 Binary files a/swing/resources/datafiles/examples/Simulation Extension.ork and /dev/null differ diff --git a/swing/resources/datafiles/examples/Simulation extensions and scripting.ork b/swing/resources/datafiles/examples/Simulation extensions and scripting.ork index e84fc0d9d..dc0d416b1 100644 Binary files a/swing/resources/datafiles/examples/Simulation extensions and scripting.ork and b/swing/resources/datafiles/examples/Simulation extensions and scripting.ork differ diff --git a/swing/resources/datafiles/examples/Simulation extensions.ork b/swing/resources/datafiles/examples/Simulation extensions.ork new file mode 100644 index 000000000..1ba1d3c06 Binary files /dev/null and b/swing/resources/datafiles/examples/Simulation extensions.ork differ diff --git a/swing/resources/datafiles/examples/TARC Payloader.ork b/swing/resources/datafiles/examples/TARC Payloader.ork deleted file mode 100644 index 0482ad09a..000000000 Binary files a/swing/resources/datafiles/examples/TARC Payloader.ork and /dev/null differ diff --git a/swing/resources/datafiles/examples/TARC payload rocket.ork b/swing/resources/datafiles/examples/TARC payload rocket.ork new file mode 100644 index 000000000..56731d94f Binary files /dev/null and b/swing/resources/datafiles/examples/TARC payload rocket.ork differ diff --git a/swing/resources/datafiles/examples/Three-stage rocket.ork b/swing/resources/datafiles/examples/Three-stage rocket.ork index e8cb67662..3d90fce6e 100644 Binary files a/swing/resources/datafiles/examples/Three-stage rocket.ork and b/swing/resources/datafiles/examples/Three-stage rocket.ork differ diff --git a/swing/resources/datafiles/examples/Tube Fin.ork b/swing/resources/datafiles/examples/Tube Fin.ork deleted file mode 100644 index c176ce02c..000000000 Binary files a/swing/resources/datafiles/examples/Tube Fin.ork and /dev/null differ diff --git a/swing/resources/datafiles/examples/Tube fin rocket.ork b/swing/resources/datafiles/examples/Tube fin rocket.ork new file mode 100644 index 000000000..e6a56ba32 Binary files /dev/null and b/swing/resources/datafiles/examples/Tube fin rocket.ork differ diff --git a/swing/resources/datafiles/examples/Two-stage rocket.ork b/swing/resources/datafiles/examples/Two-stage rocket.ork new file mode 100644 index 000000000..142cbbbac Binary files /dev/null and b/swing/resources/datafiles/examples/Two-stage rocket.ork differ diff --git a/swing/src/net/sf/openrocket/database/ComponentPresetDatabaseLoader.java b/swing/src/net/sf/openrocket/database/ComponentPresetDatabaseLoader.java index 7ba2ee72f..ea0961462 100644 --- a/swing/src/net/sf/openrocket/database/ComponentPresetDatabaseLoader.java +++ b/swing/src/net/sf/openrocket/database/ComponentPresetDatabaseLoader.java @@ -1,5 +1,6 @@ package net.sf.openrocket.database; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; @@ -77,8 +78,8 @@ public class ComponentPresetDatabaseLoader extends AsynchronousDatabaseLoader { return; } while (iterator.hasNext()) { - Pair f = iterator.next(); - Collection presets = loadFile(f.getU(), f.getV()); + Pair f = iterator.next(); + Collection presets = loadFile(f.getU().getName(), f.getV()); componentPresetDao.addAll(presets); fileCount++; presetCount += presets.size(); @@ -97,8 +98,8 @@ public class ComponentPresetDatabaseLoader extends AsynchronousDatabaseLoader { return; while (iterator.hasNext()) { - Pair f = iterator.next(); - Collection presets = loadFile(f.getU(), f.getV()); + Pair f = iterator.next(); + Collection presets = loadFile(f.getU().getName(), f.getV()); componentPresetDao.addAll(presets); fileCount++; presetCount += presets.size(); diff --git a/swing/src/net/sf/openrocket/database/MotorDatabaseLoader.java b/swing/src/net/sf/openrocket/database/MotorDatabaseLoader.java index b073aee89..279f0cf92 100644 --- a/swing/src/net/sf/openrocket/database/MotorDatabaseLoader.java +++ b/swing/src/net/sf/openrocket/database/MotorDatabaseLoader.java @@ -89,7 +89,7 @@ public class MotorDatabaseLoader extends AsynchronousDatabaseLoader { log.info("Starting reading serialized motor database"); FileIterator iterator = DirectoryIterator.findDirectory(THRUSTCURVE_DIRECTORY, new SimpleFileFilter("", false, "ser")); while (iterator.hasNext()) { - Pair f = iterator.next(); + Pair f = iterator.next(); loadSerialized(f); } log.info("Ending reading serialized motor database, motorCount=" + motorCount); @@ -99,12 +99,12 @@ public class MotorDatabaseLoader extends AsynchronousDatabaseLoader { /** * loads a serailized motor data from an stream * - * @param f the pair of a String with the filename (for logging) and the input stream + * @param f the pair of a File (for logging) and the input stream */ @SuppressWarnings("unchecked") - private void loadSerialized(Pair f) { + private void loadSerialized(Pair f) { try { - log.debug("Reading motors from file " + f.getU()); + log.debug("Reading motors from file " + f.getU().getPath()); ObjectInputStream ois = new ObjectInputStream(f.getV()); List motors = (List) ois.readObject(); addMotors(motors); @@ -124,11 +124,11 @@ public class MotorDatabaseLoader extends AsynchronousDatabaseLoader { log.debug("Loading motors from file " + file); loadFile( loader, - new Pair( - file.getName(), + new Pair<>( + file, new BufferedInputStream(new FileInputStream(file)))); - } catch (IOException e) { - log.warn("IOException while reading " + file + ": " + e, e); + } catch (Exception e) { + log.warn("Exception while reading " + file + ": " + e, e); } } @@ -138,18 +138,17 @@ public class MotorDatabaseLoader extends AsynchronousDatabaseLoader { * @param loader an object to handle the loading * @param f the pair of File name and its input stream */ - private void loadFile(GeneralMotorLoader loader, Pair f) { + private void loadFile(GeneralMotorLoader loader, Pair f) { try { - List motors = loader.load(f.getV(), f.getU()); + List motors = loader.load(f.getV(), f.getU().getName()); try { addMotorsFromBuilders(motors); } catch (IllegalArgumentException e) { Translator trans = Application.getTranslator(); - File thrustCurveDir = ((SwingPreferences) Application.getPreferences()).getDefaultUserThrustCurveFile(); - File fullPath = new File(thrustCurveDir, f.getU()); + String fullPath = f.getU().getPath(); String message = "

" + e.getMessage() + - ".

" + MessageFormat.format( trans.get("MotorDbLoaderDlg.message1"), fullPath.getPath()) + + ".

" + MessageFormat.format( trans.get("MotorDbLoaderDlg.message1"), fullPath) + "
" + trans.get("MotorDbLoaderDlg.message2") + "

"; JOptionPane pane = new JOptionPane(message, JOptionPane.WARNING_MESSAGE); JDialog dialog = pane.createDialog(null, trans.get("MotorDbLoaderDlg.title")); @@ -158,8 +157,8 @@ public class MotorDatabaseLoader extends AsynchronousDatabaseLoader { dialog.setVisible(true); } f.getV().close(); - } catch (IOException e) { - log.warn("IOException while loading file " + f.getU() + ": " + e, e); + } catch (Exception e) { + log.warn("Exception while loading file " + f.getU() + ": " + e, e); try { f.getV().close(); } catch (IOException e1) { @@ -178,7 +177,7 @@ public class MotorDatabaseLoader extends AsynchronousDatabaseLoader { FileIterator iterator; try { iterator = new DirectoryIterator(file, fileFilter, true); - } catch (IOException e) { + } catch (Exception e) { log.warn("Unable to read directory " + file + ": " + e, e); return; } diff --git a/swing/src/net/sf/openrocket/file/motor/MotorLoaderHelper.java b/swing/src/net/sf/openrocket/file/motor/MotorLoaderHelper.java index deacaad41..0719b8c11 100644 --- a/swing/src/net/sf/openrocket/file/motor/MotorLoaderHelper.java +++ b/swing/src/net/sf/openrocket/file/motor/MotorLoaderHelper.java @@ -95,10 +95,10 @@ public final class MotorLoaderHelper { List list = new ArrayList(); while (iterator.hasNext()) { - final Pair input = iterator.next(); + final Pair input = iterator.next(); log.debug("Loading motors from file " + input.getU()); try { - List motors = load(input.getV(), input.getU()); + List motors = load(input.getV(), input.getU().getName()); for (ThrustCurveMotor.Builder m : motors) { list.add(m); } diff --git a/swing/src/net/sf/openrocket/gui/SpinnerEditor.java b/swing/src/net/sf/openrocket/gui/SpinnerEditor.java index d02997e3d..fe6956b71 100644 --- a/swing/src/net/sf/openrocket/gui/SpinnerEditor.java +++ b/swing/src/net/sf/openrocket/gui/SpinnerEditor.java @@ -1,5 +1,7 @@ package net.sf.openrocket.gui; +import net.sf.openrocket.gui.adaptors.TextComponentSelectionKeyListener; + import javax.swing.JSpinner; import javax.swing.JTextField; import javax.swing.SwingUtilities; @@ -7,8 +9,8 @@ import javax.swing.text.DefaultFormatter; import javax.swing.text.DefaultFormatterFactory; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; +import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; -import java.awt.event.MouseListener; /** * Editable editor for a JSpinner. Simply uses JSpinner.DefaultEditor, which has been made @@ -29,58 +31,7 @@ public class SpinnerEditor extends JSpinner.DefaultEditor { DefaultFormatter formatter = (DefaultFormatter) dff.getDefaultFormatter(); formatter.setOverwriteMode(false); - - // Add listeners to select all the text when the field is focussed - { - getTextField().addFocusListener(new FocusListener() { - @Override - public void focusGained(FocusEvent e) { - selectAllText(); - } - - @Override - public void focusLost(FocusEvent e) { - } - }); - - getTextField().addMouseListener(new MouseListener() { - private boolean isFocussed = false; // Checks whether the text field was focussed when it was clicked upon - - @Override - public void mouseClicked(MouseEvent e) { - // If the text field was focussed when it was clicked upon instead of e.g. tab-switching to gain focus, - // then the select all action from the focus listener is ignored (it is replaced by a cursor-click event). - // So if we detect such a focus change, then redo the select all action. - if (!isFocussed) { - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { - JTextField tf = (JTextField) e.getSource(); - tf.selectAll(); - } - }); - } - } - - @Override - public void mousePressed(MouseEvent e) { - JTextField tf = (JTextField) e.getSource(); - isFocussed = tf.hasFocus(); - } - - @Override - public void mouseReleased(MouseEvent e) { - } - - @Override - public void mouseEntered(MouseEvent e) { - } - - @Override - public void mouseExited(MouseEvent e) { - } - }); - } + addListeners(); } /** @@ -93,14 +44,54 @@ public class SpinnerEditor extends JSpinner.DefaultEditor { getTextField().setColumns(cols); } + private void addListeners() { + // Select all the text when the field is focussed + getTextField().addFocusListener(new FocusListener() { + @Override + public void focusGained(FocusEvent e) { + selectAllText(getTextField()); + } + + @Override + public void focusLost(FocusEvent e) { + } + }); + + // Select all the text when the field is first clicked upon + getTextField().addMouseListener(new MouseAdapter() { + private boolean isFocussed = false; // Checks whether the text field was focussed when it was clicked upon + + @Override + public void mouseClicked(MouseEvent e) { + /* + If the text field was focussed when it was clicked upon instead of e.g. tab-switching to gain focus, + then the select all action from the focus listener is ignored (it is replaced by a cursor-click event). + So if we detect such a focus change, then redo the select all action. + */ + if (!isFocussed) { + selectAllText((JTextField) e.getSource()); + } + } + + @Override + public void mousePressed(MouseEvent e) { + JTextField tf = (JTextField) e.getSource(); + isFocussed = tf.hasFocus(); + } + }); + + // Fix key behavior on text selection + getTextField().addKeyListener(new TextComponentSelectionKeyListener(getTextField())); + } + /** * Highlights all the text in the text field. */ - private void selectAllText() { + private void selectAllText(JTextField tf) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { - getTextField().selectAll(); + tf.selectAll(); } }); } diff --git a/swing/src/net/sf/openrocket/gui/adaptors/TextComponentSelectionKeyListener.java b/swing/src/net/sf/openrocket/gui/adaptors/TextComponentSelectionKeyListener.java new file mode 100644 index 000000000..3d0addbf2 --- /dev/null +++ b/swing/src/net/sf/openrocket/gui/adaptors/TextComponentSelectionKeyListener.java @@ -0,0 +1,43 @@ +package net.sf.openrocket.gui.adaptors; + +import javax.swing.text.JTextComponent; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; + +/** + * This key listener fixes a default behavior by Java Swing text components, where if you select a text, pressing the + * left or right arrow key would not bring the text cursor to the beginning or the end of the selection + * (@Java, please fix...). + *

+ * This listener's behavior: + * If some text of the editor is selected, set the caret position to: + * - the end of the selection if the user presses the right arrow key + * - the beginning of the selection if the user presses the left arrow key + */ +public class TextComponentSelectionKeyListener extends KeyAdapter { + private final JTextComponent textField; + + public TextComponentSelectionKeyListener(JTextComponent textField) { + this.textField = textField; + } + + @Override + public void keyPressed(KeyEvent e) { + if (e.isShiftDown()) { + return; + } + if (e.getKeyCode() == KeyEvent.VK_LEFT || e.getKeyCode() == KeyEvent.VK_KP_LEFT) { + int start = textField.getSelectionStart(); + int end = textField.getSelectionEnd(); + if (end > start) { + textField.setCaretPosition(start + 1); + } + } else if (e.getKeyCode() == KeyEvent.VK_RIGHT || e.getKeyCode() == KeyEvent.VK_KP_RIGHT) { + int start = textField.getSelectionStart(); + int end = textField.getSelectionEnd(); + if (end > start) { + textField.setCaretPosition(end - 1); + } + } + } +} diff --git a/swing/src/net/sf/openrocket/gui/components/DescriptionArea.java b/swing/src/net/sf/openrocket/gui/components/DescriptionArea.java index 844c4909d..4061e2ba1 100644 --- a/swing/src/net/sf/openrocket/gui/components/DescriptionArea.java +++ b/swing/src/net/sf/openrocket/gui/components/DescriptionArea.java @@ -58,7 +58,17 @@ public class DescriptionArea extends JScrollPane { public DescriptionArea(String text, int rows, float size) { this(text, rows, size, true); } - + + /** + * Construct an opaque description area with the specified number of rows, size and text, being opaque. + * + * @param text the initial text. + * @param rows the number of rows. + */ + public DescriptionArea(String text, int rows) { + this(text, rows, -1); + } + /** * Constructor with all options. * diff --git a/swing/src/net/sf/openrocket/gui/configdialog/AppearancePanel.java b/swing/src/net/sf/openrocket/gui/configdialog/AppearancePanel.java index f19076c96..f72b616ae 100644 --- a/swing/src/net/sf/openrocket/gui/configdialog/AppearancePanel.java +++ b/swing/src/net/sf/openrocket/gui/configdialog/AppearancePanel.java @@ -436,6 +436,8 @@ public class AppearancePanel extends JPanel { remove(outsideInsidePane); add(outsidePanel, "span 4, growx, wrap"); } + + // Repaint to fit to the new size if (parent != null) { parent.pack(); } else { diff --git a/swing/src/net/sf/openrocket/gui/configdialog/AxialStageConfig.java b/swing/src/net/sf/openrocket/gui/configdialog/AxialStageConfig.java index 611e67920..56d89d8fc 100644 --- a/swing/src/net/sf/openrocket/gui/configdialog/AxialStageConfig.java +++ b/swing/src/net/sf/openrocket/gui/configdialog/AxialStageConfig.java @@ -16,7 +16,6 @@ import net.sf.openrocket.gui.components.StyledLabel; import net.sf.openrocket.gui.components.StyledLabel.Style; import net.sf.openrocket.l10n.Translator; import net.sf.openrocket.rocketcomponent.AxialStage; -import net.sf.openrocket.rocketcomponent.FlightConfiguration; import net.sf.openrocket.rocketcomponent.RocketComponent; import net.sf.openrocket.rocketcomponent.StageSeparationConfiguration; import net.sf.openrocket.rocketcomponent.StageSeparationConfiguration.SeparationEvent; @@ -38,7 +37,9 @@ public class AxialStageConfig extends ComponentAssemblyConfig { } // Apply the custom focus travel policy to this config dialog - order.add(closeButton); // Make sure the close button is the last component + //// Make sure the cancel & ok button is the last component + order.add(cancelButton); + order.add(okButton); CustomFocusTraversalPolicy policy = new CustomFocusTraversalPolicy(order); parent.setFocusTraversalPolicy(policy); } diff --git a/swing/src/net/sf/openrocket/gui/configdialog/BodyTubeConfig.java b/swing/src/net/sf/openrocket/gui/configdialog/BodyTubeConfig.java index 58835dec6..128bde727 100644 --- a/swing/src/net/sf/openrocket/gui/configdialog/BodyTubeConfig.java +++ b/swing/src/net/sf/openrocket/gui/configdialog/BodyTubeConfig.java @@ -121,7 +121,9 @@ public class BodyTubeConfig extends RocketComponentConfig { trans.get("BodyTubecfg.tab.Motormountconf"), 1); // Apply the custom focus travel policy to this config dialog - order.add(closeButton); // Make sure the close button is the last component + //// Make sure the cancel & ok button is the last component + order.add(cancelButton); + order.add(okButton); CustomFocusTraversalPolicy policy = new CustomFocusTraversalPolicy(order); parent.setFocusTraversalPolicy(policy); } diff --git a/swing/src/net/sf/openrocket/gui/configdialog/BulkheadConfig.java b/swing/src/net/sf/openrocket/gui/configdialog/BulkheadConfig.java index 1659a721c..d1bdb5e63 100644 --- a/swing/src/net/sf/openrocket/gui/configdialog/BulkheadConfig.java +++ b/swing/src/net/sf/openrocket/gui/configdialog/BulkheadConfig.java @@ -29,7 +29,9 @@ public class BulkheadConfig extends RingComponentConfig { tabbedPane.setSelectedIndex(0); // Apply the custom focus travel policy to this panel - order.add(closeButton); // Make sure the close button is the last component + //// Make sure the cancel & ok button is the last component + order.add(cancelButton); + order.add(okButton); CustomFocusTraversalPolicy policy = new CustomFocusTraversalPolicy(order); parent.setFocusTraversalPolicy(policy); } diff --git a/swing/src/net/sf/openrocket/gui/configdialog/CenteringRingConfig.java b/swing/src/net/sf/openrocket/gui/configdialog/CenteringRingConfig.java index 548bf4b86..5ac0bcc75 100644 --- a/swing/src/net/sf/openrocket/gui/configdialog/CenteringRingConfig.java +++ b/swing/src/net/sf/openrocket/gui/configdialog/CenteringRingConfig.java @@ -31,7 +31,9 @@ public class CenteringRingConfig extends RingComponentConfig { tabbedPane.setSelectedIndex(0); // Apply the custom focus travel policy to this panel - order.add(closeButton); // Make sure the close button is the last component + //// Make sure the cancel & ok button is the last component + order.add(cancelButton); + order.add(okButton); CustomFocusTraversalPolicy policy = new CustomFocusTraversalPolicy(order); parent.setFocusTraversalPolicy(policy); } diff --git a/swing/src/net/sf/openrocket/gui/configdialog/ComponentConfigDialog.java b/swing/src/net/sf/openrocket/gui/configdialog/ComponentConfigDialog.java index e71f0f1c4..c3c953faf 100644 --- a/swing/src/net/sf/openrocket/gui/configdialog/ComponentConfigDialog.java +++ b/swing/src/net/sf/openrocket/gui/configdialog/ComponentConfigDialog.java @@ -1,15 +1,11 @@ package net.sf.openrocket.gui.configdialog; -import java.awt.GraphicsDevice; -import java.awt.GraphicsEnvironment; -import java.awt.Rectangle; import java.awt.Window; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; -import java.util.Arrays; import java.util.List; import javax.swing.JDialog; @@ -48,16 +44,19 @@ public class ComponentConfigDialog extends JDialog implements ComponentChangeLis private OpenRocketDocument document = null; protected RocketComponent component = null; private RocketComponentConfig configurator = null; - protected static boolean clearConfigListeners = true; + private boolean isModified = false; + private final boolean isNewComponent; + public static boolean clearConfigListeners = true; private static String previousSelectedTab = null; // Name of the previous selected tab private final Window parent; private static final Translator trans = Application.getTranslator(); - private ComponentConfigDialog(Window parent, OpenRocketDocument document, RocketComponent component) { + private ComponentConfigDialog(Window parent, OpenRocketDocument document, RocketComponent component, boolean isNewComponent) { super(parent); this.parent = parent; + this.isNewComponent = isNewComponent; setComponent(document, component); @@ -104,8 +103,10 @@ public class ComponentConfigDialog extends JDialog implements ComponentChangeLis this.document = document; this.component = component; this.document.getRocket().addComponentChangeListener(this); + this.isModified = false; configurator = getDialogContents(); + configurator.setNewComponent(isNewComponent); this.setContentPane(configurator); configurator.updateFields(); @@ -172,10 +173,8 @@ public class ComponentConfigDialog extends JDialog implements ComponentChangeLis @Override public void componentChanged(ComponentChangeEvent e) { if (e.isTreeChange() || e.isUndoChange()) { - // Hide dialog in case of tree or undo change disposeDialog(); - } else { /* * TODO: HIGH: The line below has caused a NullPointerException (without null check) @@ -183,8 +182,13 @@ public class ComponentConfigDialog extends JDialog implements ComponentChangeLis * root cause should be analyzed. * [Openrocket-bugs] 2009-12-12 19:23:22 Automatic bug report for OpenRocket 0.9.5 */ - if (configurator != null) + if (configurator != null) { configurator.updateFields(); + } + if (!this.isModified) { + setTitle("*" + getTitle()); + this.isModified = true; + } } } @@ -261,7 +265,7 @@ public class ComponentConfigDialog extends JDialog implements ComponentChangeLis previousSelectedTab = null; } - dialog = new ComponentConfigDialog(parent, document, component); + dialog = new ComponentConfigDialog(parent, document, component, isNewComponent); dialog.setVisible(true); if (parent instanceof BasicFrame && BasicFrame.getStartupFrame() == parent) { WindowLocationUtil.moveIfOutsideOfParentMonitor(dialog, parent); @@ -335,6 +339,13 @@ public class ComponentConfigDialog extends JDialog implements ComponentChangeLis return (dialog != null) && (dialog.isVisible()); } + /** + * Returns true if the current component has been modified or not. + */ + public boolean isModified() { + return isModified; + } + public int getSelectedTabIndex() { return configurator.getSelectedTabIndex(); } diff --git a/swing/src/net/sf/openrocket/gui/configdialog/EllipticalFinSetConfig.java b/swing/src/net/sf/openrocket/gui/configdialog/EllipticalFinSetConfig.java index add84adfb..6d765b36f 100644 --- a/swing/src/net/sf/openrocket/gui/configdialog/EllipticalFinSetConfig.java +++ b/swing/src/net/sf/openrocket/gui/configdialog/EllipticalFinSetConfig.java @@ -201,7 +201,9 @@ public class EllipticalFinSetConfig extends FinSetConfig { tabbedPane.setSelectedIndex(0); // Apply the custom focus travel policy to this config dialog - order.add(closeButton); // Make sure the close button is the last component + //// Make sure the cancel & ok button is the last component + order.add(cancelButton); + order.add(okButton); CustomFocusTraversalPolicy policy = new CustomFocusTraversalPolicy(order); parent.setFocusTraversalPolicy(policy); } diff --git a/swing/src/net/sf/openrocket/gui/configdialog/FreeformFinSetConfig.java b/swing/src/net/sf/openrocket/gui/configdialog/FreeformFinSetConfig.java index 5bae5b08f..d83c8470d 100644 --- a/swing/src/net/sf/openrocket/gui/configdialog/FreeformFinSetConfig.java +++ b/swing/src/net/sf/openrocket/gui/configdialog/FreeformFinSetConfig.java @@ -107,7 +107,9 @@ public class FreeformFinSetConfig extends FinSetConfig { addFinSetButtons(); // Apply the custom focus travel policy to this panel - order.add(closeButton); // Make sure the close button is the last component + //// Make sure the cancel & ok button is the last component + order.add(cancelButton); + order.add(okButton); CustomFocusTraversalPolicy policy = new CustomFocusTraversalPolicy(order); parent.setFocusTraversalPolicy(policy); } diff --git a/swing/src/net/sf/openrocket/gui/configdialog/InnerTubeConfig.java b/swing/src/net/sf/openrocket/gui/configdialog/InnerTubeConfig.java index 45c984dd5..669b816e7 100644 --- a/swing/src/net/sf/openrocket/gui/configdialog/InnerTubeConfig.java +++ b/swing/src/net/sf/openrocket/gui/configdialog/InnerTubeConfig.java @@ -207,7 +207,9 @@ public class InnerTubeConfig extends RocketComponentConfig { tabbedPane.setSelectedIndex(0); // Apply the custom focus travel policy to this config dialog - order.add(closeButton); // Make sure the close button is the last component + //// Make sure the cancel & ok button is the last component + order.add(cancelButton); + order.add(okButton); CustomFocusTraversalPolicy policy = new CustomFocusTraversalPolicy(order); parent.setFocusTraversalPolicy(policy); } diff --git a/swing/src/net/sf/openrocket/gui/configdialog/LaunchLugConfig.java b/swing/src/net/sf/openrocket/gui/configdialog/LaunchLugConfig.java index 37d93a0fd..22f062436 100644 --- a/swing/src/net/sf/openrocket/gui/configdialog/LaunchLugConfig.java +++ b/swing/src/net/sf/openrocket/gui/configdialog/LaunchLugConfig.java @@ -162,7 +162,9 @@ public class LaunchLugConfig extends RocketComponentConfig { tabbedPane.setSelectedIndex(0); // Apply the custom focus travel policy to this config dialog - order.add(closeButton); // Make sure the close button is the last component + //// Make sure the cancel & ok button is the last component + order.add(cancelButton); + order.add(okButton); CustomFocusTraversalPolicy policy = new CustomFocusTraversalPolicy(order); parent.setFocusTraversalPolicy(policy); } diff --git a/swing/src/net/sf/openrocket/gui/configdialog/MassComponentConfig.java b/swing/src/net/sf/openrocket/gui/configdialog/MassComponentConfig.java index 5bb30f602..3934ee940 100644 --- a/swing/src/net/sf/openrocket/gui/configdialog/MassComponentConfig.java +++ b/swing/src/net/sf/openrocket/gui/configdialog/MassComponentConfig.java @@ -166,7 +166,9 @@ public class MassComponentConfig extends RocketComponentConfig { tabbedPane.setSelectedIndex(0); // Apply the custom focus travel policy to this config dialog - order.add(closeButton); // Make sure the close button is the last component + //// Make sure the cancel & ok button is the last component + order.add(cancelButton); + order.add(okButton); CustomFocusTraversalPolicy policy = new CustomFocusTraversalPolicy(order); parent.setFocusTraversalPolicy(policy); } diff --git a/swing/src/net/sf/openrocket/gui/configdialog/NoseConeConfig.java b/swing/src/net/sf/openrocket/gui/configdialog/NoseConeConfig.java index 9587d4760..828cc96a1 100644 --- a/swing/src/net/sf/openrocket/gui/configdialog/NoseConeConfig.java +++ b/swing/src/net/sf/openrocket/gui/configdialog/NoseConeConfig.java @@ -3,6 +3,8 @@ package net.sf.openrocket.gui.configdialog; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; import javax.swing.JCheckBox; import javax.swing.JComboBox; @@ -40,7 +42,7 @@ public class NoseConeConfig extends RocketComponentConfig { private JLabel shapeLabel; private JSpinner shapeSpinner; private JSlider shapeSlider; - private final JCheckBox checkAutoAftRadius; + private final JCheckBox checkAutoBaseRadius; private static final Translator trans = Application.getTranslator(); // Prepended to the description from NoseCone.DESCRIPTIONS @@ -106,21 +108,21 @@ public class NoseConeConfig extends RocketComponentConfig { panel.add(new JLabel(trans.get("NoseConeCfg.lbl.Basediam"))); - final DoubleModel aftRadiusModel = new DoubleModel(component, "AftRadius", 2.0, UnitGroup.UNITS_LENGTH, 0); // Diameter = 2*Radius - final JSpinner radiusSpinner = new JSpinner(aftRadiusModel.getSpinnerModel()); + final DoubleModel baseRadius = new DoubleModel(component, "BaseRadius", 2.0, UnitGroup.UNITS_LENGTH, 0); // Diameter = 2*Radius + final JSpinner radiusSpinner = new JSpinner(baseRadius.getSpinnerModel()); radiusSpinner.setEditor(new SpinnerEditor(radiusSpinner)); panel.add(radiusSpinner, "growx"); order.add(((SpinnerEditor) radiusSpinner.getEditor()).getTextField()); - panel.add(new UnitSelector(aftRadiusModel), "growx"); - panel.add(new BasicSlider(aftRadiusModel.getSliderModel(0, 0.04, 0.2)), "w 100lp, wrap 0px"); + panel.add(new UnitSelector(baseRadius), "growx"); + panel.add(new BasicSlider(baseRadius.getSliderModel(0, 0.04, 0.2)), "w 100lp, wrap 0px"); - checkAutoAftRadius = new JCheckBox(aftRadiusModel.getAutomaticAction()); + checkAutoBaseRadius = new JCheckBox(baseRadius.getAutomaticAction()); //// Automatic - checkAutoAftRadius.setText(trans.get("NoseConeCfg.checkbox.Automatic")); - panel.add(checkAutoAftRadius, "skip, span 2, wrap"); - order.add(checkAutoAftRadius); - updateCheckboxAutoAftRadius(); + checkAutoBaseRadius.setText(trans.get("NoseConeCfg.checkbox.Automatic")); + panel.add(checkAutoBaseRadius, "skip, span 2, wrap"); + order.add(checkAutoBaseRadius); + updateCheckboxAutoBaseRadius(((NoseCone) component).isFlipped()); } {//// Wall thickness: @@ -142,10 +144,24 @@ public class NoseConeConfig extends RocketComponentConfig { //// Filled filledCheckbox.setText(trans.get("NoseConeCfg.checkbox.Filled")); filledCheckbox.setToolTipText(trans.get("NoseConeCfg.checkbox.Filled.ttip")); - panel.add(filledCheckbox, "skip, span 2, wrap"); + panel.add(filledCheckbox, "skip, span 2, wrap para"); order.add(filledCheckbox); } + {//// Flip to tail cone: + final JCheckBox flipCheckbox = new JCheckBox(new BooleanModel(component, "Flipped")); + flipCheckbox.setText(trans.get("NoseConeCfg.checkbox.Flip")); + flipCheckbox.setToolTipText(trans.get("NoseConeCfg.checkbox.Flip.ttip")); + panel.add(flipCheckbox, "spanx, wrap"); + order.add(flipCheckbox); + flipCheckbox.addItemListener(new ItemListener() { + @Override + public void itemStateChanged(ItemEvent e) { + updateCheckboxAutoBaseRadius(e.getStateChange() == ItemEvent.SELECTED); + } + }); + } + panel.add(new JLabel(""), "growy"); //// Description @@ -172,7 +188,9 @@ public class NoseConeConfig extends RocketComponentConfig { tabbedPane.setSelectedIndex(0); // Apply the custom focus travel policy to this config dialog - order.add(closeButton); // Make sure the close button is the last component + //// Make sure the cancel & ok button is the last component + order.add(cancelButton); + order.add(okButton); CustomFocusTraversalPolicy policy = new CustomFocusTraversalPolicy(order); parent.setFocusTraversalPolicy(policy); } @@ -189,25 +207,29 @@ public class NoseConeConfig extends RocketComponentConfig { * Sets the checkAutoAftRadius checkbox's enabled state and tooltip text, based on the state of its next component. * If there is no next symmetric component or if that component already has its auto checkbox checked, the * checkAutoAftRadius checkbox is disabled. + * + * @param isFlipped whether the nose cone is flipped */ - private void updateCheckboxAutoAftRadius() { - if (component == null || checkAutoAftRadius == null) return; + private void updateCheckboxAutoBaseRadius(boolean isFlipped) { + if (component == null || checkAutoBaseRadius == null) return; // Disable check button if there is no component to get the diameter from - SymmetricComponent nextComp = ((NoseCone) component).getNextSymmetricComponent(); - if (nextComp == null) { - checkAutoAftRadius.setEnabled(false); - ((NoseCone) component).setAftRadiusAutomatic(false); - checkAutoAftRadius.setToolTipText(trans.get("NoseConeCfg.checkbox.ttip.Automatic_noReferenceComponent")); + NoseCone noseCone = ((NoseCone) component); + SymmetricComponent referenceComp = isFlipped ? noseCone.getPreviousSymmetricComponent() : noseCone.getNextSymmetricComponent(); + if (referenceComp == null) { + checkAutoBaseRadius.setEnabled(false); + ((NoseCone) component).setBaseRadiusAutomatic(false); + checkAutoBaseRadius.setToolTipText(trans.get("NoseConeCfg.checkbox.ttip.Automatic_noReferenceComponent")); return; } - if (!nextComp.usesPreviousCompAutomatic()) { - checkAutoAftRadius.setEnabled(true); - checkAutoAftRadius.setToolTipText(trans.get("NoseConeCfg.checkbox.ttip.Automatic")); + if ((!isFlipped&& !referenceComp.usesPreviousCompAutomatic()) || + isFlipped && !referenceComp.usesNextCompAutomatic()) { + checkAutoBaseRadius.setEnabled(true); + checkAutoBaseRadius.setToolTipText(trans.get("NoseConeCfg.checkbox.ttip.Automatic")); } else { - checkAutoAftRadius.setEnabled(false); - ((NoseCone) component).setAftRadiusAutomatic(false); - checkAutoAftRadius.setToolTipText(trans.get("NoseConeCfg.checkbox.ttip.Automatic_alreadyAuto")); + checkAutoBaseRadius.setEnabled(false); + ((NoseCone) component).setBaseRadiusAutomatic(false); + checkAutoBaseRadius.setToolTipText(trans.get("NoseConeCfg.checkbox.ttip.Automatic_alreadyAuto")); } } } diff --git a/swing/src/net/sf/openrocket/gui/configdialog/ParachuteConfig.java b/swing/src/net/sf/openrocket/gui/configdialog/ParachuteConfig.java index 74f9f85e6..62259f074 100644 --- a/swing/src/net/sf/openrocket/gui/configdialog/ParachuteConfig.java +++ b/swing/src/net/sf/openrocket/gui/configdialog/ParachuteConfig.java @@ -290,7 +290,9 @@ public class ParachuteConfig extends RecoveryDeviceConfig { tabbedPane.setSelectedIndex(0); // Apply the custom focus travel policy to this config dialog - order.add(closeButton); // Make sure the close button is the last component + //// Make sure the cancel & ok button is the last component + order.add(cancelButton); + order.add(okButton); CustomFocusTraversalPolicy policy = new CustomFocusTraversalPolicy(order); parent.setFocusTraversalPolicy(policy); } diff --git a/swing/src/net/sf/openrocket/gui/configdialog/RailButtonConfig.java b/swing/src/net/sf/openrocket/gui/configdialog/RailButtonConfig.java index da9c6af8c..1f9b25eef 100644 --- a/swing/src/net/sf/openrocket/gui/configdialog/RailButtonConfig.java +++ b/swing/src/net/sf/openrocket/gui/configdialog/RailButtonConfig.java @@ -35,7 +35,9 @@ public class RailButtonConfig extends RocketComponentConfig { tabbedPane.setSelectedIndex(0); // Apply the custom focus travel policy to this panel - order.add(closeButton); // Make sure the close button is the last component + //// Make sure the cancel & ok button is the last component + order.add(cancelButton); + order.add(okButton); CustomFocusTraversalPolicy policy = new CustomFocusTraversalPolicy(order); parent.setFocusTraversalPolicy(policy); } diff --git a/swing/src/net/sf/openrocket/gui/configdialog/RingComponentConfig.java b/swing/src/net/sf/openrocket/gui/configdialog/RingComponentConfig.java index da346f88a..35c313483 100644 --- a/swing/src/net/sf/openrocket/gui/configdialog/RingComponentConfig.java +++ b/swing/src/net/sf/openrocket/gui/configdialog/RingComponentConfig.java @@ -11,15 +11,12 @@ import javax.swing.JSpinner; import net.miginfocom.swing.MigLayout; import net.sf.openrocket.document.OpenRocketDocument; import net.sf.openrocket.gui.SpinnerEditor; -import net.sf.openrocket.gui.adaptors.CustomFocusTraversalPolicy; import net.sf.openrocket.gui.adaptors.DoubleModel; import net.sf.openrocket.gui.adaptors.EnumModel; import net.sf.openrocket.gui.components.BasicSlider; -import net.sf.openrocket.gui.components.DescriptionArea; import net.sf.openrocket.gui.components.UnitSelector; import net.sf.openrocket.l10n.Translator; import net.sf.openrocket.material.Material; -import net.sf.openrocket.rocketcomponent.EngineBlock; import net.sf.openrocket.rocketcomponent.RocketComponent; import net.sf.openrocket.rocketcomponent.ThicknessRingComponent; import net.sf.openrocket.rocketcomponent.position.AxialMethod; @@ -168,13 +165,6 @@ public class RingComponentConfig extends RocketComponentConfig { //// Material MaterialPanel materialPanel = new MaterialPanel(component, document, Material.Type.BULK, order); - - if (component instanceof EngineBlock) { - final DescriptionArea desc = new DescriptionArea(6); - //// An engine block stops the motor from moving forwards in the motor mount tube.

In order to add a motor, create a body tube or inner tube and mark it as a motor mount in the Motor tab. - desc.setText(trans.get("ringcompcfg.EngineBlock.desc")); - materialPanel.add(desc, "width 1px, growx, wrap"); - } panel.add(materialPanel, "cell 4 0, gapleft paragraph, aligny 0%, spany"); return panel; diff --git a/swing/src/net/sf/openrocket/gui/configdialog/RocketComponentConfig.java b/swing/src/net/sf/openrocket/gui/configdialog/RocketComponentConfig.java index a174f317f..697d9b8fe 100644 --- a/swing/src/net/sf/openrocket/gui/configdialog/RocketComponentConfig.java +++ b/swing/src/net/sf/openrocket/gui/configdialog/RocketComponentConfig.java @@ -5,7 +5,12 @@ import java.awt.Color; import java.awt.Component; import java.awt.Container; import java.awt.Font; -import java.awt.event.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -17,6 +22,7 @@ import javax.swing.JCheckBox; import javax.swing.JComboBox; import javax.swing.JDialog; import javax.swing.JLabel; +import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JSpinner; @@ -35,30 +41,40 @@ import net.sf.openrocket.gui.adaptors.BooleanModel; import net.sf.openrocket.gui.adaptors.DoubleModel; import net.sf.openrocket.gui.adaptors.IntegerModel; import net.sf.openrocket.gui.adaptors.PresetModel; +import net.sf.openrocket.gui.adaptors.TextComponentSelectionKeyListener; import net.sf.openrocket.gui.components.BasicSlider; +import net.sf.openrocket.gui.components.DescriptionArea; import net.sf.openrocket.gui.components.StyledLabel; import net.sf.openrocket.gui.components.StyledLabel.Style; import net.sf.openrocket.gui.components.UnitSelector; import net.sf.openrocket.gui.dialogs.preset.ComponentPresetChooserDialog; import net.sf.openrocket.gui.util.GUIUtil; +import net.sf.openrocket.gui.util.Icons; +import net.sf.openrocket.gui.widgets.IconToggleButton; import net.sf.openrocket.gui.widgets.SelectColorButton; import net.sf.openrocket.l10n.Translator; import net.sf.openrocket.preset.ComponentPreset; import net.sf.openrocket.rocketcomponent.*; import net.sf.openrocket.rocketcomponent.position.AxialMethod; import net.sf.openrocket.startup.Application; +import net.sf.openrocket.startup.Preferences; import net.sf.openrocket.unit.UnitGroup; import net.sf.openrocket.util.Invalidatable; public class RocketComponentConfig extends JPanel { private static final long serialVersionUID = -2925484062132243982L; + // Preference key + private static final String IGNORE_DISCARD_EDITING_WARNING = "IgnoreDiscardEditingWarning"; + private static final Translator trans = Application.getTranslator(); + private static final Preferences preferences = Application.getPreferences(); protected final OpenRocketDocument document; protected final RocketComponent component; protected final JTabbedPane tabbedPane; - protected final JDialog parent; + protected final ComponentConfigDialog parent; + protected boolean isNewComponent = false; // Checks whether this config dialog is editing an existing component, or a new one private final List invalidatables = new ArrayList(); protected final List order = new ArrayList<>(); // Component traversal order @@ -70,9 +86,13 @@ public class RocketComponentConfig extends JPanel { protected final JTextField componentNameField; protected JTextArea commentTextArea; private final TextFieldListener textFieldListener; - + + private DescriptionArea componentInfo; + private IconToggleButton infoBtn; + private JPanel buttonPanel; - protected JButton closeButton; + protected JButton okButton; + protected JButton cancelButton; private AppearancePanel appearancePanel = null; private JLabel infoLabel; @@ -82,11 +102,15 @@ public class RocketComponentConfig extends JPanel { private boolean allMassive; // Checks whether all listener components, and this component, are massive public RocketComponentConfig(OpenRocketDocument document, RocketComponent component, JDialog parent) { - setLayout(new MigLayout("fill, gap 4!, ins panel", "[]:5[]", "[growprio 5]5![fill, grow, growprio 500]5![growprio 5]")); + setLayout(new MigLayout("fill, gap 4!, ins panel, hidemode 3", "[]:5[]", "[growprio 5]5![fill, grow, growprio 500]5![growprio 5]")); this.document = document; this.component = component; - this.parent = parent; + if (parent instanceof ComponentConfigDialog) { + this.parent = (ComponentConfigDialog) parent; + } else { + this.parent = null; + } // Check the listeners for the same type and massive status allSameType = true; @@ -114,6 +138,7 @@ public class RocketComponentConfig extends JPanel { textFieldListener = new TextFieldListener(); componentNameField.addActionListener(textFieldListener); componentNameField.addFocusListener(textFieldListener); + componentNameField.addKeyListener(new TextComponentSelectionKeyListener(componentNameField)); //// The component name. componentNameField.setToolTipText(trans.get("RocketCompCfg.lbl.Componentname.ttip")); this.add(componentNameField, "growx"); @@ -169,18 +194,71 @@ public class RocketComponentConfig extends JPanel { updateFields(); } - + + /** + * Add a section to the component configuration dialog that displays information about the component. + */ + private void addComponentInfo(JPanel buttonPanel) { + // Don't add the info panel if this is a multi-comp edit + List listeners = component.getConfigListeners(); + if (listeners != null && listeners.size() > 0) { + return; + } + + final String helpText; + final String key = "ComponentInfo." + component.getClass().getSimpleName(); + if (!trans.checkIfKeyExists(key)) { + return; + } + helpText = "" + trans.get(key) + ""; + + // Component info + componentInfo = new DescriptionArea(helpText, 5); + componentInfo.setTextFont(null); + componentInfo.setVisible(false); + this.add(componentInfo, "gapleft 10, gapright 10, growx, spanx, wrap"); + + // Component info toggle button + infoBtn = new IconToggleButton(); + infoBtn.setToolTipText(trans.get("RocketCompCfg.btn.ComponentInfo.ttip")); + infoBtn.setIconScale(1.2f); + infoBtn.setSelectedIcon(Icons.HELP_ABOUT); + buttonPanel.add(infoBtn, "split 3, gapright para"); + + infoBtn.addItemListener(new ItemListener() { + @Override + public void itemStateChanged(ItemEvent e) { + // Note: we will display the help text if the infoButton is not selected, and hide it if it is selected. + // This way, the info button is displayed as "enabled" to draw attention to it. + componentInfo.setVisible(e.getStateChange() != ItemEvent.SELECTED); + + // Repaint to fit to the new size + if (parent != null) { + parent.pack(); + } else { + updateUI(); + } + } + }); + + infoBtn.setSelected(true); + } protected void addButtons(JButton... buttons) { + if (componentInfo != null) { + this.remove(componentInfo); + } if (buttonPanel != null) { this.remove(buttonPanel); } - buttonPanel = new JPanel(new MigLayout("fillx, ins 5")); + buttonPanel = new JPanel(new MigLayout("fill, ins 5, hidemode 3")); + + //// Component info + addComponentInfo(buttonPanel); //// Multi-comp edit label multiCompEditLabel = new StyledLabel(" ", -1, Style.BOLD); - //multiCompEditLabel.setFontColor(new Color(0, 0, 239)); multiCompEditLabel.setFontColor(new Color(170, 0, 100)); buttonPanel.add(multiCompEditLabel, "split 2"); @@ -191,21 +269,75 @@ public class RocketComponentConfig extends JPanel { for (JButton b : buttons) { buttonPanel.add(b, "right, gap para"); } - - //// Close button - this.closeButton = new SelectColorButton(trans.get("dlg.but.close")); - closeButton.addActionListener(new ActionListener() { + + //// Cancel button + this.cancelButton = new SelectColorButton(trans.get("dlg.but.cancel")); + this.cancelButton.setToolTipText(trans.get("RocketCompCfg.btn.Cancel.ttip")); + cancelButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent arg0) { + if (preferences.getBoolean(IGNORE_DISCARD_EDITING_WARNING, false)) { + ComponentConfigDialog.clearConfigListeners = false; // Undo action => config listeners of new component will be cleared + ComponentConfigDialog.disposeDialog(); + document.undo(); + return; + } + if (!isNewComponent && parent != null && !parent.isModified()) { + ComponentConfigDialog.disposeDialog(); + return; + } + + // Yes/No dialog: Are you sure you want to discard your changes? + JPanel msg = createCancelOperationContent(); + int resultYesNo = JOptionPane.showConfirmDialog(RocketComponentConfig.this, msg, + trans.get("RocketCompCfg.CancelOperation.title"), JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE); + if (resultYesNo == JOptionPane.YES_OPTION) { + ComponentConfigDialog.clearConfigListeners = false; // Undo action => config listeners of new component will be cleared + ComponentConfigDialog.disposeDialog(); + document.undo(); + } + } + }); + buttonPanel.add(cancelButton, "split 2, right, gapleft 30lp"); + + //// Ok button + this.okButton = new SelectColorButton(trans.get("dlg.but.ok")); + this.okButton.setToolTipText(trans.get("RocketCompCfg.btn.OK.ttip")); + okButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent arg0) { ComponentConfigDialog.disposeDialog(); } }); - buttonPanel.add(closeButton, "right, gap 30lp"); - + buttonPanel.add(okButton); + updateFields(); this.add(buttonPanel, "newline, spanx, growx"); } + + private JPanel createCancelOperationContent() { + JPanel panel = new JPanel(new MigLayout()); + String msg = isNewComponent ? trans.get("RocketCompCfg.CancelOperation.msg.undoAdd") : + trans.get("RocketCompCfg.CancelOperation.msg.discardChanges"); + JLabel msgLabel = new JLabel(msg); + JCheckBox dontAskAgain = new JCheckBox(trans.get("RocketCompCfg.CancelOperation.checkbox.dontAskAgain")); + dontAskAgain.setSelected(false); + dontAskAgain.addItemListener(new ItemListener() { + @Override + public void itemStateChanged(ItemEvent e) { + if (e.getStateChange() == ItemEvent.SELECTED) { + preferences.putBoolean(IGNORE_DISCARD_EDITING_WARNING, true); + } + // Unselected state should be impossible + } + }); + + panel.add(msgLabel, "left, wrap"); + panel.add(dontAskAgain, "left, gaptop para"); + + return panel; + } /** @@ -251,6 +383,11 @@ public class RocketComponentConfig extends JPanel { // Multi-comp edit label if (listeners != null && listeners.size() > 0) { + if (infoBtn != null) { + componentInfo.setVisible(false); + infoBtn.setVisible(false); + } + multiCompEditLabel.setText(trans.get("ComponentCfgDlg.MultiComponentEdit")); StringBuilder components = new StringBuilder(trans.get("ComponentCfgDlg.MultiComponentEdit.ttip")); @@ -264,6 +401,11 @@ public class RocketComponentConfig extends JPanel { } multiCompEditLabel.setToolTipText(components.toString()); } else { + if (infoBtn != null) { + componentInfo.setVisible(false); + infoBtn.setSelected(true); + infoBtn.setVisible(true); + } multiCompEditLabel.setText(""); } } @@ -603,6 +745,7 @@ public class RocketComponentConfig extends JPanel { commentTextArea.setEditable(true); GUIUtil.setTabToFocusing(commentTextArea); commentTextArea.addFocusListener(textFieldListener); + commentTextArea.addKeyListener(new TextComponentSelectionKeyListener(commentTextArea)); panel.add(new JScrollPane(commentTextArea), "grow"); order.add(commentTextArea); @@ -613,157 +756,178 @@ public class RocketComponentConfig extends JPanel { protected JPanel shoulderTab() { JPanel panel = new JPanel(new MigLayout("fill")); - JPanel sub; - DoubleModel m, m2; DoubleModel m0 = new DoubleModel(0); - BooleanModel bm; - JCheckBox check; - JSpinner spin; - //// Fore shoulder, not for NoseCone - if (!(component instanceof NoseCone)) { - sub = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::]", "")); - - //// Fore shoulder - sub.setBorder(BorderFactory.createTitledBorder(trans.get("RocketCompCfg.border.Foreshoulder"))); - - - //// Radius - //// Diameter: - sub.add(new JLabel(trans.get("RocketCompCfg.lbl.Diameter"))); - - m = new DoubleModel(component, "ForeShoulderRadius", 2, UnitGroup.UNITS_LENGTH, 0); - m2 = new DoubleModel(component, "ForeRadius", 2, UnitGroup.UNITS_LENGTH); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - sub.add(spin, "growx"); - order.add(((SpinnerEditor) spin.getEditor()).getTextField()); - - sub.add(new UnitSelector(m), "growx"); - sub.add(new BasicSlider(m.getSliderModel(m0, m2)), "w 100lp, wrap"); - - - //// Length: - sub.add(new JLabel(trans.get("RocketCompCfg.lbl.Length"))); - - m = new DoubleModel(component, "ForeShoulderLength", UnitGroup.UNITS_LENGTH, 0); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - sub.add(spin, "growx"); - order.add(((SpinnerEditor) spin.getEditor()).getTextField()); - - sub.add(new UnitSelector(m), "growx"); - sub.add(new BasicSlider(m.getSliderModel(0, 0.02, 0.2)), "w 100lp, wrap"); - - - //// Thickness: - sub.add(new JLabel(trans.get("RocketCompCfg.lbl.Thickness"))); - - m = new DoubleModel(component, "ForeShoulderThickness", UnitGroup.UNITS_LENGTH, 0); - m2 = new DoubleModel(component, "ForeShoulderRadius", UnitGroup.UNITS_LENGTH); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - sub.add(spin, "growx"); - order.add(((SpinnerEditor) spin.getEditor()).getTextField()); - - sub.add(new UnitSelector(m), "growx"); - sub.add(new BasicSlider(m.getSliderModel(m0, m2)), "w 100lp, wrap"); - - - //// Capped - bm = new BooleanModel(component, "ForeShoulderCapped"); - check = new JCheckBox(bm); - //// End capped - check.setText(trans.get("RocketCompCfg.checkbox.Endcapped")); - check.setToolTipText(trans.get("RocketCompCfg.checkbox.Endcapped.ttip")); - sub.add(check, "spanx"); - order.add(check); - - - panel.add(sub); + addForeShoulderSection(panel, m0); } - - + //// Aft shoulder + addAftShoulderSection(panel, m0); + + return panel; + } + + private void addForeShoulderSection(JPanel panel, DoubleModel m0) { + DoubleModel m; + JCheckBox check; + JPanel sub; + DoubleModel m2; + JSpinner spin; + BooleanModel bm; sub = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::]", "")); - - if (component instanceof NoseCone) - //// Nose cone shoulder - sub.setBorder(BorderFactory.createTitledBorder(trans.get("RocketCompCfg.title.Noseconeshoulder"))); - else - //// Aft shoulder - sub.setBorder(BorderFactory.createTitledBorder(trans.get("RocketCompCfg.title.Aftshoulder"))); - - + + //// Fore shoulder + sub.setBorder(BorderFactory.createTitledBorder(trans.get("RocketCompCfg.border.Foreshoulder"))); + + //// Radius //// Diameter: sub.add(new JLabel(trans.get("RocketCompCfg.lbl.Diameter"))); - - m = new DoubleModel(component, "AftShoulderRadius", 2, UnitGroup.UNITS_LENGTH, 0); - m2 = new DoubleModel(component, "AftRadius", 2, UnitGroup.UNITS_LENGTH); - + + m = new DoubleModel(component, "ForeShoulderRadius", 2, UnitGroup.UNITS_LENGTH, 0); + m2 = new DoubleModel(component, "ForeRadius", 2, UnitGroup.UNITS_LENGTH); + spin = new JSpinner(m.getSpinnerModel()); spin.setEditor(new SpinnerEditor(spin)); sub.add(spin, "growx"); order.add(((SpinnerEditor) spin.getEditor()).getTextField()); - + sub.add(new UnitSelector(m), "growx"); sub.add(new BasicSlider(m.getSliderModel(m0, m2)), "w 100lp, wrap"); - - + + //// Length: sub.add(new JLabel(trans.get("RocketCompCfg.lbl.Length"))); - - m = new DoubleModel(component, "AftShoulderLength", UnitGroup.UNITS_LENGTH, 0); - + + m = new DoubleModel(component, "ForeShoulderLength", UnitGroup.UNITS_LENGTH, 0); + spin = new JSpinner(m.getSpinnerModel()); spin.setEditor(new SpinnerEditor(spin)); sub.add(spin, "growx"); order.add(((SpinnerEditor) spin.getEditor()).getTextField()); - + sub.add(new UnitSelector(m), "growx"); sub.add(new BasicSlider(m.getSliderModel(0, 0.02, 0.2)), "w 100lp, wrap"); - - + + //// Thickness: sub.add(new JLabel(trans.get("RocketCompCfg.lbl.Thickness"))); - - m = new DoubleModel(component, "AftShoulderThickness", UnitGroup.UNITS_LENGTH, 0); - m2 = new DoubleModel(component, "AftShoulderRadius", UnitGroup.UNITS_LENGTH); - + + m = new DoubleModel(component, "ForeShoulderThickness", UnitGroup.UNITS_LENGTH, 0); + m2 = new DoubleModel(component, "ForeShoulderRadius", UnitGroup.UNITS_LENGTH); + spin = new JSpinner(m.getSpinnerModel()); spin.setEditor(new SpinnerEditor(spin)); sub.add(spin, "growx"); order.add(((SpinnerEditor) spin.getEditor()).getTextField()); - + sub.add(new UnitSelector(m), "growx"); sub.add(new BasicSlider(m.getSliderModel(m0, m2)), "w 100lp, wrap"); - - + + //// Capped - bm = new BooleanModel(component, "AftShoulderCapped"); + bm = new BooleanModel(component, "ForeShoulderCapped"); check = new JCheckBox(bm); //// End capped check.setText(trans.get("RocketCompCfg.checkbox.Endcapped")); check.setToolTipText(trans.get("RocketCompCfg.checkbox.Endcapped.ttip")); sub.add(check, "spanx"); order.add(check); - - + panel.add(sub); - - - return panel; } - - - - + + private void addAftShoulderSection(JPanel panel, DoubleModel m0) { + JSpinner spin; + JCheckBox check; + DoubleModel m; + DoubleModel m2; + JPanel sub; + BooleanModel bm; + sub = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::]", "")); + + String valueNameShoulder = "AftShoulder"; + String valueNameRadius = "AftRadius"; + + if (component instanceof NoseCone) { + // Nose cones have a special shoulder method to cope with flipped nose cones + valueNameShoulder = "Shoulder"; + valueNameRadius = "BaseRadius"; + //// Nose cone shoulder + sub.setBorder(BorderFactory.createTitledBorder(trans.get("RocketCompCfg.title.Noseconeshoulder"))); + } else { + //// Aft shoulder + sub.setBorder(BorderFactory.createTitledBorder(trans.get("RocketCompCfg.title.Aftshoulder"))); + } + + + //// Radius + //// Diameter: + sub.add(new JLabel(trans.get("RocketCompCfg.lbl.Diameter"))); + + m = new DoubleModel(component, valueNameShoulder+"Radius", 2, UnitGroup.UNITS_LENGTH, 0); + m2 = new DoubleModel(component, valueNameRadius, 2, UnitGroup.UNITS_LENGTH); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + sub.add(spin, "growx"); + order.add(((SpinnerEditor) spin.getEditor()).getTextField()); + + sub.add(new UnitSelector(m), "growx"); + sub.add(new BasicSlider(m.getSliderModel(m0, m2)), "w 100lp, wrap"); + + + //// Length: + sub.add(new JLabel(trans.get("RocketCompCfg.lbl.Length"))); + + m = new DoubleModel(component, valueNameShoulder+"Length", UnitGroup.UNITS_LENGTH, 0); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + sub.add(spin, "growx"); + order.add(((SpinnerEditor) spin.getEditor()).getTextField()); + + sub.add(new UnitSelector(m), "growx"); + sub.add(new BasicSlider(m.getSliderModel(0, 0.02, 0.2)), "w 100lp, wrap"); + + + //// Thickness: + sub.add(new JLabel(trans.get("RocketCompCfg.lbl.Thickness"))); + + m = new DoubleModel(component, valueNameShoulder+"Thickness", UnitGroup.UNITS_LENGTH, 0); + m2 = new DoubleModel(component, valueNameShoulder+"Radius", UnitGroup.UNITS_LENGTH); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + sub.add(spin, "growx"); + order.add(((SpinnerEditor) spin.getEditor()).getTextField()); + + sub.add(new UnitSelector(m), "growx"); + sub.add(new BasicSlider(m.getSliderModel(m0, m2)), "w 100lp, wrap"); + + + //// Capped + bm = new BooleanModel(component, valueNameShoulder+"Capped"); + check = new JCheckBox(bm); + //// End capped + check.setText(trans.get("RocketCompCfg.checkbox.Endcapped")); + check.setToolTipText(trans.get("RocketCompCfg.checkbox.Endcapped.ttip")); + sub.add(check, "spanx"); + order.add(check); + + panel.add(sub); + } + + /** + * Sets whether this dialog is editing a new component (true), or an existing one (false). + * @param newComponent true if this dialog is editing a new component. + */ + public void setNewComponent(boolean newComponent) { + isNewComponent = newComponent; + } + /* * Private inner class to handle events in componentNameField. */ diff --git a/swing/src/net/sf/openrocket/gui/configdialog/RocketConfig.java b/swing/src/net/sf/openrocket/gui/configdialog/RocketConfig.java index a740bcec8..7ad61a171 100644 --- a/swing/src/net/sf/openrocket/gui/configdialog/RocketConfig.java +++ b/swing/src/net/sf/openrocket/gui/configdialog/RocketConfig.java @@ -35,7 +35,7 @@ public class RocketConfig extends RocketComponentConfig { rocket = (Rocket) c; this.removeAll(); - setLayout(new MigLayout("fill")); + setLayout(new MigLayout("fill, hideMode 3")); //// Design name: this.add(new JLabel(trans.get("RocketCfg.lbl.Designname")), "top, pad 4lp, gapright 10lp"); diff --git a/swing/src/net/sf/openrocket/gui/configdialog/ShockCordConfig.java b/swing/src/net/sf/openrocket/gui/configdialog/ShockCordConfig.java index 40222576f..752d7995e 100644 --- a/swing/src/net/sf/openrocket/gui/configdialog/ShockCordConfig.java +++ b/swing/src/net/sf/openrocket/gui/configdialog/ShockCordConfig.java @@ -142,7 +142,9 @@ public class ShockCordConfig extends RocketComponentConfig { tabbedPane.setSelectedIndex(0); // Apply the custom focus travel policy to this config dialog - order.add(closeButton); // Make sure the close button is the last component + //// Make sure the cancel & ok button is the last component + order.add(cancelButton); + order.add(okButton); CustomFocusTraversalPolicy policy = new CustomFocusTraversalPolicy(order); parent.setFocusTraversalPolicy(policy); } diff --git a/swing/src/net/sf/openrocket/gui/configdialog/SleeveConfig.java b/swing/src/net/sf/openrocket/gui/configdialog/SleeveConfig.java index 4545660c9..d2261dfd6 100644 --- a/swing/src/net/sf/openrocket/gui/configdialog/SleeveConfig.java +++ b/swing/src/net/sf/openrocket/gui/configdialog/SleeveConfig.java @@ -31,7 +31,9 @@ public class SleeveConfig extends RingComponentConfig { tabbedPane.setSelectedIndex(0); // Apply the custom focus travel policy to this panel - order.add(closeButton); // Make sure the close button is the last component + //// Make sure the cancel & ok button is the last component + order.add(cancelButton); + order.add(okButton); CustomFocusTraversalPolicy policy = new CustomFocusTraversalPolicy(order); parent.setFocusTraversalPolicy(policy); } diff --git a/swing/src/net/sf/openrocket/gui/configdialog/StreamerConfig.java b/swing/src/net/sf/openrocket/gui/configdialog/StreamerConfig.java index e05cc0f6f..d3519c75d 100644 --- a/swing/src/net/sf/openrocket/gui/configdialog/StreamerConfig.java +++ b/swing/src/net/sf/openrocket/gui/configdialog/StreamerConfig.java @@ -277,7 +277,9 @@ public class StreamerConfig extends RecoveryDeviceConfig { tabbedPane.setSelectedIndex(0); // Apply the custom focus travel policy to this config dialog - order.add(closeButton); // Make sure the close button is the last component + //// Make sure the cancel & ok button is the last component + order.add(cancelButton); + order.add(okButton); CustomFocusTraversalPolicy policy = new CustomFocusTraversalPolicy(order); parent.setFocusTraversalPolicy(policy); } diff --git a/swing/src/net/sf/openrocket/gui/configdialog/ThicknessRingComponentConfig.java b/swing/src/net/sf/openrocket/gui/configdialog/ThicknessRingComponentConfig.java index b204435a3..66ce8703e 100644 --- a/swing/src/net/sf/openrocket/gui/configdialog/ThicknessRingComponentConfig.java +++ b/swing/src/net/sf/openrocket/gui/configdialog/ThicknessRingComponentConfig.java @@ -33,7 +33,9 @@ public class ThicknessRingComponentConfig extends RingComponentConfig { tabbedPane.setSelectedIndex(0); // Apply the custom focus travel policy to this panel - order.add(closeButton); // Make sure the close button is the last component + //// Make sure the cancel & ok button is the last component + order.add(cancelButton); + order.add(okButton); CustomFocusTraversalPolicy policy = new CustomFocusTraversalPolicy(order); parent.setFocusTraversalPolicy(policy); } diff --git a/swing/src/net/sf/openrocket/gui/configdialog/TransitionConfig.java b/swing/src/net/sf/openrocket/gui/configdialog/TransitionConfig.java index 6487e5059..f9050d396 100644 --- a/swing/src/net/sf/openrocket/gui/configdialog/TransitionConfig.java +++ b/swing/src/net/sf/openrocket/gui/configdialog/TransitionConfig.java @@ -24,7 +24,6 @@ import net.sf.openrocket.gui.components.UnitSelector; import net.sf.openrocket.l10n.Translator; import net.sf.openrocket.material.Material; import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.rocketcomponent.SymmetricComponent; import net.sf.openrocket.rocketcomponent.Transition; import net.sf.openrocket.startup.Application; import net.sf.openrocket.unit.UnitGroup; @@ -206,7 +205,9 @@ public class TransitionConfig extends RocketComponentConfig { tabbedPane.setSelectedIndex(0); // Apply the custom focus travel policy to this config dialog - order.add(closeButton); // Make sure the close button is the last component + //// Make sure the cancel & ok button is the last component + order.add(cancelButton); + order.add(okButton); CustomFocusTraversalPolicy policy = new CustomFocusTraversalPolicy(order); parent.setFocusTraversalPolicy(policy); } @@ -228,18 +229,16 @@ public class TransitionConfig extends RocketComponentConfig { private void updateCheckboxAutoAftRadius() { if (component == null || checkAutoAftRadius == null) return; - // Disable check button if there is no component to get the diameter from - SymmetricComponent nextComp = ((Transition) component).getNextSymmetricComponent(); - if (nextComp == null) { + Transition transition = (Transition) component; + boolean enabled = transition.canUseNextCompAutomatic(); + if (enabled) { // Can use auto radius + checkAutoAftRadius.setEnabled(true); + checkAutoAftRadius.setToolTipText(trans.get("TransitionCfg.checkbox.ttip.Automatic")); + } else if (transition.getNextSymmetricComponent() == null) { // No next component to take the auto radius from checkAutoAftRadius.setEnabled(false); ((Transition) component).setAftRadiusAutomatic(false); checkAutoAftRadius.setToolTipText(trans.get("TransitionCfg.checkbox.ttip.Automatic_noReferenceComponent")); - return; - } - if (!nextComp.usesPreviousCompAutomatic()) { - checkAutoAftRadius.setEnabled(true); - checkAutoAftRadius.setToolTipText(trans.get("TransitionCfg.checkbox.ttip.Automatic")); - } else { + } else { // Next component already has its auto radius checked checkAutoAftRadius.setEnabled(false); ((Transition) component).setAftRadiusAutomatic(false); checkAutoAftRadius.setToolTipText(trans.get("TransitionCfg.checkbox.ttip.Automatic_alreadyAuto")); @@ -254,18 +253,16 @@ public class TransitionConfig extends RocketComponentConfig { private void updateCheckboxAutoForeRadius() { if (component == null || checkAutoForeRadius == null) return; - // Disable check button if there is no component to get the diameter from - SymmetricComponent prevComp = ((Transition) component).getPreviousSymmetricComponent(); - if (prevComp == null) { + Transition transition = (Transition) component; + boolean enabled = transition.canUsePreviousCompAutomatic(); + if (enabled) { // Can use auto radius + checkAutoForeRadius.setEnabled(true); + checkAutoForeRadius.setToolTipText(trans.get("TransitionCfg.checkbox.ttip.Automatic")); + } else if (transition.getPreviousSymmetricComponent() == null) { // No next component to take the auto radius from checkAutoForeRadius.setEnabled(false); ((Transition) component).setForeRadiusAutomatic(false); checkAutoForeRadius.setToolTipText(trans.get("TransitionCfg.checkbox.ttip.Automatic_noReferenceComponent")); - return; - } - if (!prevComp.usesNextCompAutomatic()) { - checkAutoForeRadius.setEnabled(true); - checkAutoForeRadius.setToolTipText(trans.get("TransitionCfg.checkbox.ttip.Automatic")); - } else { + } else { // Next component already has its auto radius checked checkAutoForeRadius.setEnabled(false); ((Transition) component).setForeRadiusAutomatic(false); checkAutoForeRadius.setToolTipText(trans.get("TransitionCfg.checkbox.ttip.Automatic_alreadyAuto")); diff --git a/swing/src/net/sf/openrocket/gui/configdialog/TrapezoidFinSetConfig.java b/swing/src/net/sf/openrocket/gui/configdialog/TrapezoidFinSetConfig.java index df54e7f2f..0744aa169 100644 --- a/swing/src/net/sf/openrocket/gui/configdialog/TrapezoidFinSetConfig.java +++ b/swing/src/net/sf/openrocket/gui/configdialog/TrapezoidFinSetConfig.java @@ -252,7 +252,9 @@ public class TrapezoidFinSetConfig extends FinSetConfig { addFinSetButtons(); // Apply the custom focus travel policy to this config dialog - order.add(closeButton); // Make sure the close button is the last component + //// Make sure the cancel & ok button is the last component + order.add(cancelButton); + order.add(okButton); CustomFocusTraversalPolicy policy = new CustomFocusTraversalPolicy(order); parent.setFocusTraversalPolicy(policy); } diff --git a/swing/src/net/sf/openrocket/gui/configdialog/TubeFinSetConfig.java b/swing/src/net/sf/openrocket/gui/configdialog/TubeFinSetConfig.java index 9ce17a3a9..e0ea77134 100644 --- a/swing/src/net/sf/openrocket/gui/configdialog/TubeFinSetConfig.java +++ b/swing/src/net/sf/openrocket/gui/configdialog/TubeFinSetConfig.java @@ -179,7 +179,9 @@ public class TubeFinSetConfig extends RocketComponentConfig { tabbedPane.setSelectedIndex(0); // Apply the custom focus travel policy to this config dialog - order.add(closeButton); // Make sure the close button is the last component + //// Make sure the cancel & ok button is the last component + order.add(cancelButton); + order.add(okButton); CustomFocusTraversalPolicy policy = new CustomFocusTraversalPolicy(order); parent.setFocusTraversalPolicy(policy); } diff --git a/swing/src/net/sf/openrocket/gui/customexpression/VariableTableModel.java b/swing/src/net/sf/openrocket/gui/customexpression/VariableTableModel.java index 490b8e68f..21147e703 100644 --- a/swing/src/net/sf/openrocket/gui/customexpression/VariableTableModel.java +++ b/swing/src/net/sf/openrocket/gui/customexpression/VariableTableModel.java @@ -31,7 +31,7 @@ public class VariableTableModel extends AbstractTableModel { //Collections.addAll(types, FlightDataType.ALL_TYPES); //for (CustomExpression expression : doc.getCustomExpressions()){ - // types.add(expression.getType()); + // types.add(expression.getCurrentViewType()); //} } diff --git a/swing/src/net/sf/openrocket/gui/dialogs/AboutDialog.java b/swing/src/net/sf/openrocket/gui/dialogs/AboutDialog.java index a6683e2c4..e7d713080 100644 --- a/swing/src/net/sf/openrocket/gui/dialogs/AboutDialog.java +++ b/swing/src/net/sf/openrocket/gui/dialogs/AboutDialog.java @@ -94,7 +94,7 @@ public class AboutDialog extends JDialog { // OpenRocket logo - panel.add(new JLabel(Icons.loadImageIcon("pix/icon/icon-about.png", "OpenRocket")), "top"); + panel.add(new JLabel(Icons.loadImageIcon("pix/icon/icon-128.png", "OpenRocket")), "top"); // OpenRocket version info + copyright diff --git a/swing/src/net/sf/openrocket/gui/dialogs/BugReportDialog.java b/swing/src/net/sf/openrocket/gui/dialogs/BugReportDialog.java index 4683a6a15..763f873b0 100644 --- a/swing/src/net/sf/openrocket/gui/dialogs/BugReportDialog.java +++ b/swing/src/net/sf/openrocket/gui/dialogs/BugReportDialog.java @@ -27,6 +27,7 @@ import net.miginfocom.swing.MigLayout; import net.sf.openrocket.gui.components.StyledLabel; import net.sf.openrocket.gui.components.URLLabel; import net.sf.openrocket.gui.util.GUIUtil; +import net.sf.openrocket.gui.util.SwingPreferences; import net.sf.openrocket.l10n.Translator; import net.sf.openrocket.logging.LogLevelBufferLogger; import net.sf.openrocket.logging.LogLine; @@ -44,6 +45,7 @@ public class BugReportDialog extends JDialog { private static final String REPORT_EMAIL_URL = "mailto:" + REPORT_EMAIL; private static final Translator trans = Application.getTranslator(); + private static final SwingPreferences preferences = (SwingPreferences) Application.getPreferences(); public BugReportDialog(Window parent, String labelText, final String message, final boolean sendIfUnchanged) { @@ -108,31 +110,10 @@ public class BugReportDialog extends JDialog { * @param parent the parent window (may be null). */ public static void showBugReportDialog(Window parent) { - StringBuilder sb = new StringBuilder(); - - sb.append("---------- Bug report ----------\n"); - sb.append('\n'); - sb.append("Include detailed steps on how to trigger the bug:\n"); - sb.append("(You can edit text directly in this window)\n"); - sb.append('\n'); - sb.append("1. \n"); - sb.append("2. \n"); - sb.append("3. \n"); - sb.append('\n'); - - sb.append("What does the software do and what in your opinion should it do in the " + - "case described above:\n"); - sb.append('\n'); - sb.append('\n'); - sb.append('\n'); - - sb.append("Include your email address (optional; it helps if we can " + - "contact you in case we need additional information):\n"); - sb.append('\n'); - sb.append('\n'); - sb.append('\n'); - + + // ---------- Bug report ---------- + addBugReportInformation(sb); sb.append("(Do not modify anything below this line.)\n"); sb.append("---------- System information ----------\n"); @@ -157,28 +138,9 @@ public class BugReportDialog extends JDialog { */ public static void showExceptionDialog(Window parent, Thread t, Throwable e) { StringBuilder sb = new StringBuilder(); - - sb.append("---------- Bug report ----------\n"); - sb.append('\n'); - sb.append("Please include a description about what actions you were " + - "performing when the exception occurred:\n"); - sb.append("(You can edit text directly in this window)\n"); - sb.append('\n'); - sb.append("1. \n"); - sb.append("2. \n"); - sb.append("3. \n"); - sb.append("\n"); - sb.append("If possible, please send us the .ork file that caused the bug.\n"); - sb.append('\n'); - - - sb.append("Include your email address (optional; it helps if we can " + - "contact you in case we need additional information):\n"); - sb.append('\n'); - sb.append('\n'); - sb.append('\n'); - sb.append('\n'); + // ---------- Bug report ---------- + addBugReportInformation(sb); sb.append("(Do not modify anything below this line.)\n"); sb.append("---------- Exception stack trace ----------\n"); @@ -211,12 +173,37 @@ public class BugReportDialog extends JDialog { new BugReportDialog(parent, trans.get("bugreport.reportDialog.txt2"), sb.toString(), true); reportDialog.setVisible(true); } + + private static void addBugReportInformation(StringBuilder sb) { + sb.append("---------- Bug report ----------\n"); + sb.append('\n'); + sb.append("Please include a description about what actions you were " + + "performing when the exception occurred:\n"); + sb.append("(You can edit text directly in this window)\n"); + sb.append('\n'); + sb.append("1. \n"); + sb.append("2. \n"); + sb.append("3. \n"); + + sb.append("\n"); + sb.append("If possible, please send us the .ork file that caused the bug.\n"); + sb.append('\n'); + + + sb.append("Include your email address (optional; it helps if we can " + + "contact you in case we need additional information):\n"); + sb.append('\n'); + sb.append('\n'); + sb.append('\n'); + sb.append('\n'); + } private static void addSystemInformation(StringBuilder sb) { StringBuilder sbTemp = new StringBuilder(); sbTemp.append("OpenRocket version: " + BuildProperties.getVersion() + "\n"); sbTemp.append("OpenRocket source: " + BuildProperties.getBuildSource() + "\n"); sbTemp.append("OpenRocket location: " + JarUtil.getCurrentJarFile() + "\n"); + sbTemp.append("User-defined thrust curves location: " + preferences.getUserThrustCurveFilesAsString() + "\n"); sbTemp.append("JOGL version: " + JoglVersion.getInstance().getImplementationVersion() + "\n"); sbTemp.append("Current default locale: " + Locale.getDefault() + "\n"); sbTemp.append("System properties:\n"); diff --git a/swing/src/net/sf/openrocket/gui/dialogs/ComponentAnalysisDialog.java b/swing/src/net/sf/openrocket/gui/dialogs/ComponentAnalysisDialog.java index b0b120c82..d05c0b039 100644 --- a/swing/src/net/sf/openrocket/gui/dialogs/ComponentAnalysisDialog.java +++ b/swing/src/net/sf/openrocket/gui/dialogs/ComponentAnalysisDialog.java @@ -571,7 +571,12 @@ public class ComponentAnalysisDialog extends JDialog implements StateChangeListe } if (forces.getCP() != null) { - row.cpx = forces.getCP().x; + if ((comp instanceof Rocket) && + (forces.getCP().weight < MathUtil.EPSILON)) { + row.cpx = Double.NaN; + } else { + row.cpx = forces.getCP().x; + } row.cna = forces.getCNa(); } diff --git a/swing/src/net/sf/openrocket/gui/dialogs/LicenseDialog.java b/swing/src/net/sf/openrocket/gui/dialogs/LicenseDialog.java index 31fc47290..831ab6d42 100644 --- a/swing/src/net/sf/openrocket/gui/dialogs/LicenseDialog.java +++ b/swing/src/net/sf/openrocket/gui/dialogs/LicenseDialog.java @@ -30,7 +30,7 @@ public class LicenseDialog extends JDialog { JPanel panel = new JPanel(new MigLayout("fill")); // OpenRocket logo - panel.add(new JLabel(Icons.loadImageIcon("pix/icon/icon-about.png", "OpenRocket")), "top"); + panel.add(new JLabel(Icons.loadImageIcon("pix/icon/icon-128.png", "OpenRocket")), "top"); panel.add(new StyledLabel("Software Licenses", 10), "ax 50%, pushx, wrap para"); diff --git a/swing/src/net/sf/openrocket/gui/dialogs/ScaleDialog.java b/swing/src/net/sf/openrocket/gui/dialogs/ScaleDialog.java index 0fd20a25d..111f16378 100644 --- a/swing/src/net/sf/openrocket/gui/dialogs/ScaleDialog.java +++ b/swing/src/net/sf/openrocket/gui/dialogs/ScaleDialog.java @@ -83,7 +83,7 @@ public class ScaleDialog extends JDialog { // SymmetricComponent addScaler(SymmetricComponent.class, "Thickness", "isFilled", SCALERS_NO_OFFSET); - // Transition + Nose cone + // Transition addScaler(Transition.class, "ForeRadius", "isForeRadiusAutomatic", SCALERS_NO_OFFSET); addScaler(Transition.class, "AftRadius", "isAftRadiusAutomatic", SCALERS_NO_OFFSET); addScaler(Transition.class, "ForeShoulderRadius", SCALERS_NO_OFFSET); @@ -92,6 +92,12 @@ public class ScaleDialog extends JDialog { addScaler(Transition.class, "AftShoulderRadius", SCALERS_NO_OFFSET); addScaler(Transition.class, "AftShoulderThickness", SCALERS_NO_OFFSET); addScaler(Transition.class, "AftShoulderLength", SCALERS_NO_OFFSET); + + // Nose cone + addScaler(NoseCone.class, "BaseRadius", "isBaseRadiusAutomatic", SCALERS_NO_OFFSET); + addScaler(NoseCone.class, "ShoulderRadius", SCALERS_NO_OFFSET); + addScaler(NoseCone.class, "ShoulderThickness", SCALERS_NO_OFFSET); + addScaler(NoseCone.class, "ShoulderLength", SCALERS_NO_OFFSET); // Body tube addScaler(BodyTube.class, "OuterRadius", "isOuterRadiusAutomatic", SCALERS_NO_OFFSET); @@ -559,6 +565,10 @@ public class ScaleDialog extends JDialog { } Collections.reverse(classes); // Always do the super component scales first (can cause problems otherwise in the scale order) for (Class cl : classes) { + // Don't use the super-class methods of transitions for nose cones + if (cl == Transition.class && component instanceof NoseCone) { + continue; + } List list = SCALERS_NO_OFFSET.get(cl); if (list != null && list.size() > 0) { for (Scaler s : list) { diff --git a/swing/src/net/sf/openrocket/gui/dialogs/UpdateInfoDialog.java b/swing/src/net/sf/openrocket/gui/dialogs/UpdateInfoDialog.java index 50b0ee066..d6eda56df 100644 --- a/swing/src/net/sf/openrocket/gui/dialogs/UpdateInfoDialog.java +++ b/swing/src/net/sf/openrocket/gui/dialogs/UpdateInfoDialog.java @@ -58,7 +58,7 @@ public class UpdateInfoDialog extends JDialog { JPanel panel = new JPanel(new MigLayout("insets n n 8px n, fill")); // OpenRocket logo on the left - panel.add(new JLabel(Icons.getScaledIcon(Icons.loadImageIcon("pix/icon/icon-about.png", "OpenRocket"), 0.6)), + panel.add(new JLabel(Icons.loadImageIcon("pix/icon/icon-128.png", "OpenRocket")), "spany, top, gapright 20px, cell 0 0"); // OpenRocket version available! diff --git a/swing/src/net/sf/openrocket/gui/dialogs/WelcomeDialog.java b/swing/src/net/sf/openrocket/gui/dialogs/WelcomeDialog.java index db19183c1..4e4119188 100644 --- a/swing/src/net/sf/openrocket/gui/dialogs/WelcomeDialog.java +++ b/swing/src/net/sf/openrocket/gui/dialogs/WelcomeDialog.java @@ -43,7 +43,7 @@ public class WelcomeDialog extends JDialog { JPanel panel = new JPanel(new MigLayout("insets n n 8px n, fill")); // OpenRocket logo on the left - panel.add(new JLabel(Icons.getScaledIcon(Icons.loadImageIcon("pix/icon/icon-about.png", "OpenRocket"), 0.6)), + panel.add(new JLabel(Icons.loadImageIcon("pix/icon/icon-128.png", "OpenRocket")), "spany, top, gapright 20px, cell 0 0"); // Thank you for downloading! diff --git a/swing/src/net/sf/openrocket/gui/dialogs/preferences/GeneralPreferencesPanel.java b/swing/src/net/sf/openrocket/gui/dialogs/preferences/GeneralPreferencesPanel.java index 9631603a1..de73270f9 100644 --- a/swing/src/net/sf/openrocket/gui/dialogs/preferences/GeneralPreferencesPanel.java +++ b/swing/src/net/sf/openrocket/gui/dialogs/preferences/GeneralPreferencesPanel.java @@ -86,14 +86,7 @@ public class GeneralPreferencesPanel extends PreferencesPanel { //// User-defined thrust curves: this.add(new JLabel(trans.get("pref.dlg.lbl.User-definedthrust")), "spanx, wrap"); final JTextField field = new JTextField(); - List files = preferences.getUserThrustCurveFiles(); - String str = ""; - for (File file : files) { - if (str.length() > 0) { - str += ";"; - } - str += file.getAbsolutePath(); - } + String str = preferences.getUserThrustCurveFilesAsString(); field.setText(str); field.getDocument().addDocumentListener(new DocumentListener() { @Override diff --git a/swing/src/net/sf/openrocket/gui/dialogs/preset/ComponentPresetChooserDialog.java b/swing/src/net/sf/openrocket/gui/dialogs/preset/ComponentPresetChooserDialog.java index 9c0854d24..bdc5230a1 100644 --- a/swing/src/net/sf/openrocket/gui/dialogs/preset/ComponentPresetChooserDialog.java +++ b/swing/src/net/sf/openrocket/gui/dialogs/preset/ComponentPresetChooserDialog.java @@ -35,6 +35,7 @@ import net.sf.openrocket.gui.util.SwingPreferences; import net.sf.openrocket.l10n.Translator; import net.sf.openrocket.preset.ComponentPreset; import net.sf.openrocket.preset.TypedKey; +import net.sf.openrocket.rocketcomponent.NoseCone; import net.sf.openrocket.rocketcomponent.RocketComponent; import net.sf.openrocket.rocketcomponent.SymmetricComponent; import net.sf.openrocket.startup.Application; @@ -257,6 +258,7 @@ public class ComponentPresetChooserDialog extends JDialog { * Add filter by fore diameter */ foreDiameterFilterCheckBox = new JCheckBox(trans.get("ComponentPresetChooserDialog.checkbox.filterForeDiameter")); + foreDiameterFilterCheckBox.setToolTipText(trans.get("ComponentPresetChooserDialog.checkbox.filterForeDiameter.ttip")); final SymmetricComponent prevSym = curSym.getPreviousSymmetricComponent(); if (prevSym != null && foreDiameterColumnIndex >= 0) { foreDiameterFilterCheckBox.setSelected(preferences.isMatchForeDiameter()); @@ -274,8 +276,16 @@ public class ComponentPresetChooserDialog extends JDialog { /* * Add filter by aft diameter */ - aftDiameterFilterCheckBox = new JCheckBox(trans.get("ComponentPresetChooserDialog.checkbox.filterAftDiameter")); - final SymmetricComponent nextSym = curSym.getNextSymmetricComponent(); + final SymmetricComponent nextSym; + if (curSym instanceof NoseCone && ((NoseCone) curSym).isFlipped()) { + aftDiameterFilterCheckBox = new JCheckBox(trans.get("ComponentPresetChooserDialog.checkbox.filterForeDiameter")); + aftDiameterFilterCheckBox.setToolTipText(trans.get("ComponentPresetChooserDialog.checkbox.filterForeDiameter.ttip")); + nextSym = curSym.getPreviousSymmetricComponent(); + } else { + aftDiameterFilterCheckBox = new JCheckBox(trans.get("ComponentPresetChooserDialog.checkbox.filterAftDiameter")); + aftDiameterFilterCheckBox.setToolTipText(trans.get("ComponentPresetChooserDialog.checkbox.filterAftDiameter.ttip")); + nextSym = curSym.getNextSymmetricComponent(); + } if (nextSym != null && aftDiameterColumnIndex >= 0) { aftDiameterFilterCheckBox.setSelected(preferences.isMatchAftDiameter()); aftDiameterFilter = new ComponentPresetRowFilter(nextSym.getForeRadius() * 2.0, aftDiameterColumnIndex); diff --git a/swing/src/net/sf/openrocket/gui/figureelements/RocketInfo.java b/swing/src/net/sf/openrocket/gui/figureelements/RocketInfo.java index 763f759c8..bd0e37bd0 100644 --- a/swing/src/net/sf/openrocket/gui/figureelements/RocketInfo.java +++ b/swing/src/net/sf/openrocket/gui/figureelements/RocketInfo.java @@ -348,7 +348,7 @@ public class RocketInfo implements FigureElement { float y = y2 - line * warnings.size(); - g2.setColor(new Color(255,0,0,130)); + g2.setColor(Color.RED); for (GlyphVector v: texts) { Rectangle2D rect = v.getVisualBounds(); diff --git a/swing/src/net/sf/openrocket/gui/main/DesignPanel.java b/swing/src/net/sf/openrocket/gui/main/DesignPanel.java index dcb300261..b30a605ce 100644 --- a/swing/src/net/sf/openrocket/gui/main/DesignPanel.java +++ b/swing/src/net/sf/openrocket/gui/main/DesignPanel.java @@ -105,6 +105,7 @@ public class DesignPanel extends JSplitPane { // Double-click if ((e.getButton() == MouseEvent.BUTTON1) && (e.getClickCount() == 2) && !ComponentConfigDialog.isDialogVisible()) { RocketComponent component = (RocketComponent) selPath.getLastPathComponent(); + component.clearConfigListeners(); // Multi-component edit if shift/meta key is pressed if ((e.isShiftDown() || e.isMetaDown()) && tree.getSelectionPaths() != null) { diff --git a/swing/src/net/sf/openrocket/gui/main/ExampleDesignFileAction.java b/swing/src/net/sf/openrocket/gui/main/ExampleDesignFileAction.java index 6fc0f39a2..64abf6dc0 100644 --- a/swing/src/net/sf/openrocket/gui/main/ExampleDesignFileAction.java +++ b/swing/src/net/sf/openrocket/gui/main/ExampleDesignFileAction.java @@ -1,6 +1,8 @@ package net.sf.openrocket.gui.main; import java.awt.event.ActionEvent; +import java.util.ArrayList; +import java.util.List; import javax.swing.AbstractAction; import javax.swing.Action; @@ -18,6 +20,35 @@ public final class ExampleDesignFileAction extends JMenu { */ private final BasicFrame parent; + /** + * Order in which the example files should be displayed in the menu. + * A null items means there should be a separator. + *

+ * NOTE: update this list if you add a new example file, or update the name of an existing one!!. + */ + private static final String[] exampleFileOrder = { + // Examples of basic rockets + "A simple model rocket", + "Two-stage rocket", + "Three-stage rocket", + "TARC payload rocket", + "Tube fin rocket", + null, + // Examples demonstrating complex rocket features + "Airstart timing", + "Chute release", + "Dual parachute deployment", + "Clustered motors", + "Parallel booster staging", + "Pods--airframes and winglets", + "Pods--powered with recovery deployment", + null, + // Examples demonstrating customized functionality + "Presets", + "Simulation extensions", + "Simulation extensions and scripting" + }; + /** * Constructor. * @@ -38,11 +69,37 @@ public final class ExampleDesignFileAction extends JMenu { private void updateMenu() { removeAll(); ExampleDesignFile[] examples = ExampleDesignFile.getExampleDesigns(); + List itemList = new ArrayList<>(); + + // First create the menu items for (ExampleDesignFile file : examples) { Action action = createAction(file); action.putValue(Action.NAME, file.toString()); JMenuItem menuItem = new JMenuItem(action); - add(menuItem); + itemList.add(menuItem); + } + + // Then add them according to their order + for (String s : exampleFileOrder) { + if (s == null) { + addSeparator(); + } else { + for (JMenuItem item : itemList) { + if (item.getText().equals(s)) { + add(item); + itemList.remove(item); + break; + } + } + } + } + + // Add the remaining (unordered) items to the end + if (itemList.size() > 0) { + addSeparator(); + for (JMenuItem item : itemList) { + add(item); + } } } diff --git a/swing/src/net/sf/openrocket/gui/main/RocketActions.java b/swing/src/net/sf/openrocket/gui/main/RocketActions.java index 6bce6fbd7..497f30bcb 100644 --- a/swing/src/net/sf/openrocket/gui/main/RocketActions.java +++ b/swing/src/net/sf/openrocket/gui/main/RocketActions.java @@ -25,7 +25,6 @@ import net.sf.openrocket.gui.components.StyledLabel; import net.sf.openrocket.gui.configdialog.ComponentConfigDialog; import net.sf.openrocket.gui.dialogs.ScaleDialog; import net.sf.openrocket.gui.util.Icons; -import net.sf.openrocket.gui.widgets.IconButton; import net.sf.openrocket.l10n.Translator; import net.sf.openrocket.logging.Markers; import net.sf.openrocket.rocketcomponent.ComponentChangeEvent; @@ -985,6 +984,7 @@ public class RocketActions { ComponentConfigDialog.disposeDialog(); RocketComponent component = components.get(0); + component.clearConfigListeners(); if (components.size() > 1) { for (int i = 1; i < components.size(); i++) { RocketComponent listener = components.get(i); diff --git a/swing/src/net/sf/openrocket/gui/main/componenttree/ComponentTreeRenderer.java b/swing/src/net/sf/openrocket/gui/main/componenttree/ComponentTreeRenderer.java index 2dad7795d..a852e94b7 100644 --- a/swing/src/net/sf/openrocket/gui/main/componenttree/ComponentTreeRenderer.java +++ b/swing/src/net/sf/openrocket/gui/main/componenttree/ComponentTreeRenderer.java @@ -1,7 +1,9 @@ package net.sf.openrocket.gui.main.componenttree; import java.awt.Component; +import java.awt.Dimension; import java.awt.FlowLayout; +import java.awt.Font; import java.util.List; import javax.swing.JLabel; @@ -80,6 +82,8 @@ public class ComponentTreeRenderer extends DefaultTreeCellRenderer { p.setToolTipText(getToolTipSingleComponent(c)); } + Font originalFont = tree.getFont(); + p.setFont(originalFont); comp = p; } diff --git a/swing/src/net/sf/openrocket/gui/plot/SimulationPlot.java b/swing/src/net/sf/openrocket/gui/plot/SimulationPlot.java index 1714fbd6f..57f45c09c 100644 --- a/swing/src/net/sf/openrocket/gui/plot/SimulationPlot.java +++ b/swing/src/net/sf/openrocket/gui/plot/SimulationPlot.java @@ -20,9 +20,12 @@ import java.util.regex.Pattern; import net.sf.openrocket.document.Simulation; import net.sf.openrocket.gui.simulation.SimulationPlotPanel; +import net.sf.openrocket.gui.util.SwingPreferences; import net.sf.openrocket.simulation.FlightDataBranch; import net.sf.openrocket.simulation.FlightDataType; import net.sf.openrocket.simulation.FlightEvent; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.startup.Preferences; import net.sf.openrocket.unit.Unit; import net.sf.openrocket.unit.UnitGroup; import net.sf.openrocket.util.LinearInterpolator; @@ -65,6 +68,8 @@ import org.jfree.ui.TextAnchor; */ @SuppressWarnings("serial") public class SimulationPlot { + private static final SwingPreferences preferences = (SwingPreferences) Application.getPreferences(); + private static final float PLOT_STROKE_WIDTH = 1.5f; @@ -79,7 +84,7 @@ public class SimulationPlot { private final LegendItems legendItems; - int branchCount; + private int branchCount; void setShowPoints(boolean showPoints) { for (ModifiedXYItemRenderer r : renderers) { @@ -286,32 +291,19 @@ public class SimulationPlot { } XYSeries ser = collection.getSeries(series); String name = ser.getDescription(); + // Extract the unit from the last part of the series description, between parenthesis Matcher m = Pattern.compile(".*\\((.*?)\\)").matcher(name); - String unit_y = ""; + String unitY = ""; if (m.find()) { - unit_y = m.group(1); + unitY = m.group(1); } - String unit_x = domainUnit.getUnit(); - String ord_end = "th"; // Ordinal number ending (1'st', 2'nd'...) - if (item % 10 == 1) - ord_end = "st"; - else if (item % 10 == 2) - ord_end = "nd"; - else if (item % 10 == 3) - ord_end = "rd"; - double data_y = dataset.getYValue(series, item); - double data_x = dataset.getXValue(series, item); - DecimalFormat df_y = DecimalFormatter.df(data_y, 2, false); - DecimalFormat df_x = DecimalFormatter.df(data_x, 2, false); - return String.format("" + - "%s
" + - "Y: %s %s
" + - "X: %s %s
" + - "%d%s sample" + - "", - name, df_y.format(data_y), unit_y, - df_x.format(data_x), unit_x, item, ord_end); + String unitX = domainUnit.getUnit(); + + double dataY = dataset.getYValue(series, item); + double dataX = dataset.getXValue(series, item); + + return formatSampleTooltip(name, dataX, unitX, dataY, unitY, item); } }; @@ -364,6 +356,42 @@ public class SimulationPlot { return chart; } + private String formatSampleTooltip(String dataName, double dataX, String unitX, double dataY, String unitY, int sampleIdx, boolean addYValue) { + String ord_end = "th"; // Ordinal number ending (1'st', 2'nd'...) + if (sampleIdx % 10 == 1) { + ord_end = "st"; + } else if (sampleIdx % 10 == 2) { + ord_end = "nd"; + } else if (sampleIdx % 10 == 3) { + ord_end = "rd"; + } + + DecimalFormat df_y = DecimalFormatter.df(dataY, 2, false); + DecimalFormat df_x = DecimalFormatter.df(dataX, 2, false); + + StringBuilder sb = new StringBuilder(); + sb.append(String.format("" + + "%s
", dataName)); + + if (addYValue) { + sb.append(String.format("Y: %s %s
", df_y.format(dataY), unitY)); + } + + sb.append(String.format("X: %s %s
" + + "%d%s sample" + + "", df_x.format(dataX), unitX, sampleIdx, ord_end)); + + return sb.toString(); + } + + private String formatSampleTooltip(String dataName, double dataX, String unitX, double dataY, String unitY, int sampleIdx) { + return formatSampleTooltip(dataName, dataX, unitX, dataY, unitY, sampleIdx, true); + } + + private String formatSampleTooltip(String dataName, double dataX, String unitX, int sampleIdx) { + return formatSampleTooltip(dataName, dataX, unitX, 0, "", sampleIdx, false); + } + private String getLabel(FlightDataType type, Unit unit) { String name = type.getName(); if (unit != null && !UnitGroup.UNITS_NONE.contains(unit) && @@ -372,129 +400,166 @@ public class SimulationPlot { return name; } - private void drawDomainMarkers(int stage) { + /** + * Draw the domain markers for a certain branch. Draws all the markers if the branch is -1. + * @param branch branch to draw, or -1 to draw all + */ + private void drawDomainMarkers(int branch) { XYPlot plot = chart.getXYPlot(); - FlightDataBranch mainBranch = simulation.getSimulatedData().getBranch(0); + FlightDataBranch dataBranch = simulation.getSimulatedData().getBranch(Math.max(branch, 0)); - // Clear existing domain markers + // Clear existing domain markers and annotations plot.clearDomainMarkers(); + plot.clearAnnotations(); - // Construct domain marker lists collapsing based on time. + // Store flight event information + List eventTimes = new ArrayList<>(); + List eventLabels = new ArrayList<>(); + List eventColors = new ArrayList<>(); + List eventImages = new ArrayList<>(); - List eventTimes = new ArrayList(); - List eventLabels = new ArrayList(); - List eventColors = new ArrayList(); - List eventImages = new ArrayList(); - { - HashSet typeSet = new HashSet(); - double prevTime = -100; - String text = null; - Color color = null; - Image image = null; - for (EventDisplayInfo info : eventList) { - if (stage >= 0 && stage != info.stage) { - continue; + // Plot the markers + if (config.getDomainAxisType() == FlightDataType.TYPE_TIME && !preferences.getBoolean(Preferences.MARKER_STYLE_ICON, false)) { + fillEventLists(branch, eventTimes, eventLabels, eventColors, eventImages); + plotVerticalLineMarkers(plot, eventTimes, eventLabels, eventColors); + + } else { // Other domains are plotted as image annotations + if (branch == -1) { + // For icon markers, we need to do the plotting separately, otherwise you can have icon markers from e.g. + // branch 1 be plotted on branch 0 + for (int b = 0; b < simulation.getSimulatedData().getBranchCount(); b++) { + fillEventLists(b, eventTimes, eventLabels, eventColors, eventImages); + dataBranch = simulation.getSimulatedData().getBranch(b); + plotIconMarkers(plot, dataBranch, eventTimes, eventLabels, eventImages); + eventTimes.clear(); + eventLabels.clear(); + eventColors.clear(); + eventImages.clear(); } + } else { + fillEventLists(branch, eventTimes, eventLabels, eventColors, eventImages); + plotIconMarkers(plot, dataBranch, eventTimes, eventLabels, eventImages); + } + } + } - double t = info.time; - FlightEvent.Type type = info.event.getType(); + private void fillEventLists(int branch, List eventTimes, List eventLabels, + List eventColors, List eventImages) { + HashSet typeSet = new HashSet<>(); + double prevTime = -100; + String text = null; + Color color = null; + Image image = null; + for (EventDisplayInfo info : eventList) { + if (branch >= 0 && branch != info.stage) { + continue; + } - if (Math.abs(t - prevTime) <= 0.05) { + double t = info.time; + FlightEvent.Type type = info.event.getType(); - if (!typeSet.contains(type)) { - text = text + ", " + type.toString(); - color = EventGraphics.getEventColor(type); - image = EventGraphics.getEventImage(type); - typeSet.add(type); - } - - } else { - - if (text != null) { - eventTimes.add(prevTime); - eventLabels.add(text); - eventColors.add(color); - eventImages.add(image); - } - prevTime = t; - text = type.toString(); + if (Math.abs(t - prevTime) <= 0.05) { + if (!typeSet.contains(type)) { + text = text + ", " + type.toString(); color = EventGraphics.getEventColor(type); image = EventGraphics.getEventImage(type); - typeSet.clear(); typeSet.add(type); } + } else { + if (text != null) { + eventTimes.add(prevTime); + eventLabels.add(text); + eventColors.add(color); + eventImages.add(image); + } + prevTime = t; + text = type.toString(); + color = EventGraphics.getEventColor(type); + image = EventGraphics.getEventImage(type); + typeSet.clear(); + typeSet.add(type); } - if (text != null) { - eventTimes.add(prevTime); - eventLabels.add(text); - eventColors.add(color); - eventImages.add(image); + + } + if (text != null) { + eventTimes.add(prevTime); + eventLabels.add(text); + eventColors.add(color); + eventImages.add(image); + } + } + + private static void plotVerticalLineMarkers(XYPlot plot, List eventTimes, List eventLabels, List eventColors) { + double markerWidth = 0.01 * plot.getDomainAxis().getUpperBound(); + + // Domain time is plotted as vertical lines + for (int i = 0; i < eventTimes.size(); i++) { + double t = eventTimes.get(i); + String event = eventLabels.get(i); + Color color = eventColors.get(i); + + ValueMarker m = new ValueMarker(t); + m.setLabel(event); + m.setPaint(color); + m.setLabelPaint(color); + m.setAlpha(0.7f); + m.setLabelFont(new Font("Dialog", Font.PLAIN, 13)); + plot.addDomainMarker(m); + + if (t > plot.getDomainAxis().getUpperBound() - markerWidth) { + plot.setDomainAxis(new PresetNumberAxis(plot.getDomainAxis().getLowerBound(), t + markerWidth)); } } + } - // Plot the markers - if (config.getDomainAxisType() == FlightDataType.TYPE_TIME) { - double markerWidth = 0.01 * plot.getDomainAxis().getUpperBound(); + private void plotIconMarkers(XYPlot plot, FlightDataBranch dataBranch, List eventTimes, + List eventLabels, List eventImages) { + List time = dataBranch.get(FlightDataType.TYPE_TIME); + List domain = dataBranch.get(config.getDomainAxisType()); - // Domain time is plotted as vertical markers - for (int i = 0; i < eventTimes.size(); i++) { - double t = eventTimes.get(i); - String event = eventLabels.get(i); - Color color = eventColors.get(i); + LinearInterpolator domainInterpolator = new LinearInterpolator(time, domain); - ValueMarker m = new ValueMarker(t); - m.setLabel(event); - m.setPaint(color); - m.setLabelPaint(color); - m.setAlpha(0.7f); - m.setLabelFont(new Font("Dialog", Font.PLAIN, 13)); - plot.addDomainMarker(m); + for (int i = 0; i < eventTimes.size(); i++) { + double t = eventTimes.get(i); + Image image = eventImages.get(i); - if (t > plot.getDomainAxis().getUpperBound() - markerWidth) { - plot.setDomainAxis(new PresetNumberAxis(plot.getDomainAxis().getLowerBound(), t + markerWidth)); - } + if (image == null) { + continue; } - } else { + double xcoord = domainInterpolator.getValue(t); - // Other domains are plotted as image annotations - List time = mainBranch.get(FlightDataType.TYPE_TIME); - List domain = mainBranch.get(config.getDomainAxisType()); + for (int index = 0; index < config.getTypeCount(); index++) { + FlightDataType type = config.getType(index); + List range = dataBranch.get(type); - LinearInterpolator domainInterpolator = new LinearInterpolator(time, domain); - - for (int i = 0; i < eventTimes.size(); i++) { - double t = eventTimes.get(i); - String event = eventLabels.get(i); - Image image = eventImages.get(i); - - if (image == null) + LinearInterpolator rangeInterpolator = new LinearInterpolator(time, range); + // Image annotations are not supported on the right-side axis + // TODO: LOW: Can this be achieved by JFreeChart? + if (filled.getAxis(index) != SimulationPlotPanel.LEFT) { continue; - - double xcoord = domainInterpolator.getValue(t); - for (int index = 0; index < config.getTypeCount(); index++) { - FlightDataType type = config.getType(index); - List range = mainBranch.get(type); - - LinearInterpolator rangeInterpolator = new LinearInterpolator(time, range); - // Image annotations are not supported on the right-side axis - // TODO: LOW: Can this be achieved by JFreeChart? - if (filled.getAxis(index) != SimulationPlotPanel.LEFT) { - continue; - } - - double ycoord = rangeInterpolator.getValue(t); - - // Convert units - xcoord = config.getDomainAxisUnit().toUnit(xcoord); - ycoord = config.getUnit(index).toUnit(ycoord); - - XYImageAnnotation annotation = - new XYImageAnnotation(xcoord, ycoord, image, RectangleAnchor.CENTER); - annotation.setToolTipText(event); - plot.addAnnotation(annotation); } + + double ycoord = rangeInterpolator.getValue(t); + + // Convert units + xcoord = config.getDomainAxisUnit().toUnit(xcoord); + ycoord = config.getUnit(index).toUnit(ycoord); + + // Get the sample index of the flight event. Because this can be an interpolation between two samples, + // take the closest sample. + final int sampleIdx; + Optional closestSample = time.stream() + .min(Comparator.comparingDouble(sample -> Math.abs(sample - t))); + sampleIdx = closestSample.map(time::indexOf).orElse(-1); + + String tooltipText = formatSampleTooltip(eventLabels.get(i), xcoord, config.getDomainAxisUnit().getUnit(), sampleIdx) ; + + XYImageAnnotation annotation = + new XYImageAnnotation(xcoord, ycoord, image, RectangleAnchor.CENTER); + annotation.setToolTipText(tooltipText); + plot.addAnnotation(annotation); } } } diff --git a/swing/src/net/sf/openrocket/gui/scalefigure/RocketFigure.java b/swing/src/net/sf/openrocket/gui/scalefigure/RocketFigure.java index 1c0925ed6..0f62a28f7 100644 --- a/swing/src/net/sf/openrocket/gui/scalefigure/RocketFigure.java +++ b/swing/src/net/sf/openrocket/gui/scalefigure/RocketFigure.java @@ -58,9 +58,10 @@ public class RocketFigure extends AbstractScaleFigure { private static final String ROCKET_FIGURE_PACKAGE = "net.sf.openrocket.gui.rocketfigure"; private static final String ROCKET_FIGURE_SUFFIX = "Shapes"; - - public static final int VIEW_SIDE=0; - public static final int VIEW_BACK=1; + + public static final int VIEW_TOP = 0; + public static final int VIEW_SIDE = 1; + public static final int VIEW_BACK = 2; // Width for drawing normal and selected components public static final double NORMAL_WIDTH = 1.0; @@ -130,10 +131,6 @@ public class RocketFigure extends AbstractScaleFigure { return rotation; } - public Transformation getRotateTransformation() { - return axialRotation; - } - public void setRotation(double rot) { if (MathUtil.equals(rotation, rot)) return; @@ -142,14 +139,22 @@ public class RocketFigure extends AbstractScaleFigure { updateFigure(); fireChangeEvent(); } + + private Transformation getFigureRotation() { + if (currentViewType == RocketPanel.VIEW_TYPE.TopView) { + return this.axialRotation.applyTransformation(Transformation.rotate_x(-Math.PI / 2)); + } else { + return this.axialRotation; + } + } - public RocketPanel.VIEW_TYPE getType() { + public RocketPanel.VIEW_TYPE getCurrentViewType() { return currentViewType; } public void setType(final RocketPanel.VIEW_TYPE type) { - if (type != RocketPanel.VIEW_TYPE.BackView && type != RocketPanel.VIEW_TYPE.SideView) { + if (type != RocketPanel.VIEW_TYPE.BackView && type != RocketPanel.VIEW_TYPE.SideView && type != RocketPanel.VIEW_TYPE.TopView) { throw new IllegalArgumentException("Illegal type: " + type); } if (this.currentViewType == type) @@ -201,7 +206,7 @@ public class RocketFigure extends AbstractScaleFigure { AffineTransform baseTransform = g2.getTransform(); PriorityQueue figureShapes; - if (currentViewType == RocketPanel.VIEW_TYPE.SideView) + if (currentViewType == RocketPanel.VIEW_TYPE.SideView || currentViewType == RocketPanel.VIEW_TYPE.TopView) figureShapes = figureShapes_side; else if (currentViewType == RocketPanel.VIEW_TYPE.BackView) figureShapes = figureShapes_back; @@ -300,11 +305,11 @@ public class RocketFigure extends AbstractScaleFigure { // System.err.println(String.format(" mount instance: %s => %s", curMountLocation.toString(), curMotorLocation.toString() )); // rotate by figure's axial rotation: - curMotorLocation = this.axialRotation.transform(curMotorLocation); + curMotorLocation = getFigureRotation().transform(curMotorLocation); { Shape s; - if (currentViewType == RocketPanel.VIEW_TYPE.SideView) { + if (currentViewType == RocketPanel.VIEW_TYPE.SideView || currentViewType == RocketPanel.VIEW_TYPE.TopView) { s = new Rectangle2D.Double(curMotorLocation.x, (curMotorLocation.y - motorRadius), motorLength, @@ -354,7 +359,7 @@ public class RocketFigure extends AbstractScaleFigure { LinkedHashSet l = new LinkedHashSet(); PriorityQueue figureShapes; - if (currentViewType == RocketPanel.VIEW_TYPE.SideView) + if (currentViewType == RocketPanel.VIEW_TYPE.SideView || currentViewType == RocketPanel.VIEW_TYPE.TopView) figureShapes = figureShapes_side; else if (currentViewType == RocketPanel.VIEW_TYPE.BackView) figureShapes = figureShapes_back; @@ -399,7 +404,7 @@ public class RocketFigure extends AbstractScaleFigure { final ArrayList contextList = entry.getValue(); for (InstanceContext context : contextList) { - final Transformation currentTransform = this.axialRotation.applyTransformation(context.transform); + final Transformation currentTransform = getFigureRotation().applyTransformation(context.transform); allShapes = addThisShape(allShapes, this.currentViewType, comp, currentTransform); } } @@ -432,9 +437,10 @@ public class RocketFigure extends AbstractScaleFigure { // Find the appropriate method switch (viewType) { case SideView: + case TopView: m = Reflection.findMethod(ROCKET_FIGURE_PACKAGE, component, ROCKET_FIGURE_SUFFIX, "getShapesSide", RocketComponent.class, Transformation.class); - break; + break; case BackView: m = Reflection.findMethod(ROCKET_FIGURE_PACKAGE, component, ROCKET_FIGURE_SUFFIX, "getShapesBack", @@ -503,6 +509,7 @@ public class RocketFigure extends AbstractScaleFigure { switch (currentViewType) { case SideView: + case TopView: subjectBounds_m = new Rectangle2D.Double(bounds.min.x, -maxR, bounds.span().x, 2 * maxR); break; case BackView: @@ -528,9 +535,8 @@ public class RocketFigure extends AbstractScaleFigure { if (currentViewType == RocketPanel.VIEW_TYPE.BackView){ final int newOriginX = mid_x; final int newOriginY = borderThickness_px.height + getHeight() / 2; - originLocation_px = new Point(newOriginX, newOriginY); - }else if (currentViewType == RocketPanel.VIEW_TYPE.SideView){ + } else if (currentViewType == RocketPanel.VIEW_TYPE.SideView || currentViewType == RocketPanel.VIEW_TYPE.TopView) { final int newOriginX = mid_x - (subjectWidth / 2) - (int)(subjectBounds_m.getMinX() * scale); final int newOriginY = Math.max(getHeight(), subjectHeight + 2*borderThickness_px.height )/ 2; originLocation_px = new Point(newOriginX, newOriginY); diff --git a/swing/src/net/sf/openrocket/gui/scalefigure/RocketPanel.java b/swing/src/net/sf/openrocket/gui/scalefigure/RocketPanel.java index 5a61fd46e..aa7ef052e 100644 --- a/swing/src/net/sf/openrocket/gui/scalefigure/RocketPanel.java +++ b/swing/src/net/sf/openrocket/gui/scalefigure/RocketPanel.java @@ -2,6 +2,7 @@ package net.sf.openrocket.gui.scalefigure; import java.awt.BorderLayout; +import java.awt.Component; import java.awt.Dimension; import java.awt.Font; import java.awt.Point; @@ -91,9 +92,12 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change private static final Translator trans = Application.getTranslator(); private static final Logger log = LoggerFactory.getLogger(RocketPanel.class); + private static final String VIEW_TYPE_SEPARATOR = "__SEPARATOR__"; // Dummy string to indicate a horizontal separator item in the view type combobox public enum VIEW_TYPE { + TopView(false, RocketFigure.VIEW_TOP), SideView(false, RocketFigure.VIEW_SIDE), BackView(false, RocketFigure.VIEW_BACK), + SEPARATOR(false, -248), // Horizontal combobox separator dummy item Figure3D(true, RocketFigure3d.TYPE_FIGURE), Unfinished(true, RocketFigure3d.TYPE_UNFINISHED), Finished(true, RocketFigure3d.TYPE_FINISHED); @@ -108,9 +112,16 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change @Override public String toString() { + if (type == -248) { + return VIEW_TYPE_SEPARATOR; + } return trans.get("RocketPanel.FigTypeAct." + super.toString()); } + public static VIEW_TYPE getDefaultViewType() { + return SideView; + } + } private boolean is3d; @@ -308,12 +319,16 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change JPanel ribbon = new JPanel(new MigLayout("insets 0, fill")); // View Type drop-down - ComboBoxModel cm = new DefaultComboBoxModel(VIEW_TYPE.values()) { + ComboBoxModel cm = new ViewTypeComboBoxModel(VIEW_TYPE.values(), VIEW_TYPE.getDefaultViewType()) { @Override public void setSelectedItem(Object o) { - super.setSelectedItem(o); VIEW_TYPE v = (VIEW_TYPE) o; + if (v == VIEW_TYPE.SEPARATOR) { + return; + } + + super.setSelectedItem(o); if (v.is3d) { figure3d.setType(v.type); go3D(); @@ -325,7 +340,9 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change } }; ribbon.add(new JLabel(trans.get("RocketPanel.lbl.ViewType")), "cell 0 0"); - ribbon.add(new JComboBox(cm), "cell 0 1"); + final JComboBox viewSelector = new JComboBox(cm); + viewSelector.setRenderer(new SeparatorComboBoxRenderer(viewSelector.getRenderer())); + ribbon.add(viewSelector, "cell 0 1"); // Zoom level selector scaleSelector = new ScaleSelector(scrollPane); @@ -619,6 +636,7 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change if (event.isShiftDown() || event.isMetaDown()) { List paths = new ArrayList<>(Arrays.asList(selectionModel.getSelectionPaths())); RocketComponent component = selectedComponents.get(selectedComponents.size() - 1); + component.clearConfigListeners(); // Make sure the clicked component is selected for (RocketComponent c : clicked) { @@ -773,7 +791,8 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change figure3d.setCP(new Coordinate(Double.NaN, Double.NaN)); } - if (figure.getType() == RocketPanel.VIEW_TYPE.SideView && length > 0) { + if (length > 0 && + ((figure.getCurrentViewType() == RocketPanel.VIEW_TYPE.TopView) || (figure.getCurrentViewType() == RocketPanel.VIEW_TYPE.SideView))) { extraCP.setPosition(cpx, cpy); extraCG.setPosition(cgx, cgy); } else { @@ -1061,40 +1080,34 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change figure3d.setSelection(components); } - // /** - // * An Action that shows whether the figure type is the - // type - // * given in the constructor. - // * - // * @author Sampo Niskanen - // */ - // private class FigureTypeAction extends AbstractAction implements - // StateChangeListener { - // private static final long serialVersionUID = 1L; - // private final VIEW_TYPE type; - // - // public FigureTypeAction(VIEW_TYPE type) { - // this.type = type; - // stateChanged(null); - // figure.addChangeListener(this); - // } - // - // @Override - // public void actionPerformed(ActionEvent e) { - // boolean state = (Boolean) getValue(Action.SELECTED_KEY); - // if (state == true) { - // // This view has been selected - // figure.setType(type); - // go2D(); - // updateExtras(); - // } - // stateChanged(null); - // } - // - // @Override - // public void stateChanged(EventObject e) { - // putValue(Action.SELECTED_KEY, figure.getType() == type && !is3d); - // } - // } + private static class ViewTypeComboBoxModel extends DefaultComboBoxModel { + public ViewTypeComboBoxModel(VIEW_TYPE[] items, VIEW_TYPE initialItem) { + super(items); + super.setSelectedItem(initialItem); + } + } + + /** + * Custom combobox renderer that supports the display of horizontal separators between items. + * ComboBox objects with the text {@link VIEW_TYPE_SEPARATOR} objects in the combobox are replaced by a separator object. + */ + private static class SeparatorComboBoxRenderer extends JLabel implements ListCellRenderer { + private final JSeparator separator; + private final ListCellRenderer defaultRenderer; + + public SeparatorComboBoxRenderer(ListCellRenderer defaultRenderer) { + this.defaultRenderer = defaultRenderer; + this.separator = new JSeparator(JSeparator.HORIZONTAL); + } + + public Component getListCellRendererComponent(JList list, Object value, + int index, boolean isSelected, boolean cellHasFocus) { + String str = (value == null) ? "" : value.toString(); + if (VIEW_TYPE_SEPARATOR.equals(str)) { + return separator; + }; + return defaultRenderer.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + } + } } diff --git a/swing/src/net/sf/openrocket/gui/simulation/SimulationPlotPanel.java b/swing/src/net/sf/openrocket/gui/simulation/SimulationPlotPanel.java index da516379c..c28cc606c 100644 --- a/swing/src/net/sf/openrocket/gui/simulation/SimulationPlotPanel.java +++ b/swing/src/net/sf/openrocket/gui/simulation/SimulationPlotPanel.java @@ -9,12 +9,15 @@ import java.util.Arrays; import java.util.EnumSet; import javax.swing.BorderFactory; +import javax.swing.ButtonGroup; import javax.swing.JButton; import javax.swing.JComboBox; +import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; +import javax.swing.JRadioButton; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.table.AbstractTableModel; @@ -29,11 +32,13 @@ import net.sf.openrocket.gui.plot.PlotConfiguration; import net.sf.openrocket.gui.plot.SimulationPlotDialog; import net.sf.openrocket.gui.util.GUIUtil; import net.sf.openrocket.gui.util.Icons; +import net.sf.openrocket.gui.util.SwingPreferences; import net.sf.openrocket.l10n.Translator; import net.sf.openrocket.simulation.FlightDataBranch; import net.sf.openrocket.simulation.FlightDataType; import net.sf.openrocket.simulation.FlightEvent; import net.sf.openrocket.startup.Application; +import net.sf.openrocket.startup.Preferences; import net.sf.openrocket.unit.Unit; import net.sf.openrocket.util.Utils; import net.sf.openrocket.gui.widgets.SelectColorButton; @@ -47,6 +52,7 @@ public class SimulationPlotPanel extends JPanel { private static final long serialVersionUID = -2227129713185477998L; private static final Translator trans = Application.getTranslator(); + private static final SwingPreferences preferences = (SwingPreferences) Application.getPreferences(); // TODO: LOW: Should these be somewhere else? public static final int AUTO = -1; @@ -201,7 +207,7 @@ public class SimulationPlotPanel extends JPanel { typeSelectorPanel = new JPanel(new MigLayout("gapy rel")); JScrollPane scroll = new JScrollPane(typeSelectorPanel); - this.add(scroll, "spany 2, height 10px, wmin 400lp, grow 100, gapright para"); + this.add(scroll, "spany 3, height 10px, wmin 400lp, grow 100, gapright para"); //// Flight events @@ -244,10 +250,46 @@ public class SimulationPlotPanel extends JPanel { eventTableModel.fireTableDataChanged(); } }); - this.add(button, "gapleft para, gapright para, growx, sizegroup buttons, wrap para"); - - + this.add(button, "gapleft para, gapright para, growx, sizegroup buttons, wrap"); + + //// Style event marker + JLabel styleEventMarker = new JLabel(trans.get("simplotpanel.MarkerStyle.lbl.MarkerStyle")); + JRadioButton radioVerticalMarker = new JRadioButton(trans.get("simplotpanel.MarkerStyle.btn.VerticalMarker")); + JRadioButton radioIcon = new JRadioButton(trans.get("simplotpanel.MarkerStyle.btn.Icon")); + ButtonGroup bg = new ButtonGroup(); + bg.add(radioVerticalMarker); + bg.add(radioIcon); + + boolean useIcon = preferences.getBoolean(Preferences.MARKER_STYLE_ICON, false); + if (useIcon) { + radioIcon.setSelected(true); + } else { + radioVerticalMarker.setSelected(true); + } + + radioIcon.addItemListener(new ItemListener() { + @Override + public void itemStateChanged(ItemEvent e) { + if (modifying > 0) + return; + preferences.putBoolean(Preferences.MARKER_STYLE_ICON, radioIcon.isSelected()); + } + }); + + domainTypeSelector.addItemListener(new ItemListener() { + @Override + public void itemStateChanged(ItemEvent e) { + updateStyleEventWidgets(styleEventMarker, radioVerticalMarker, radioIcon); + } + }); + updateStyleEventWidgets(styleEventMarker, radioVerticalMarker, radioIcon); + + this.add(styleEventMarker, "split 3, growx"); + this.add(radioVerticalMarker); + this.add(radioIcon, "wrap para"); + + //// New Y axis plot type button = new SelectColorButton(trans.get("simplotpanel.but.NewYaxisplottype")); button.addActionListener(new ActionListener() { @@ -322,6 +364,19 @@ public class SimulationPlotPanel extends JPanel { */ updatePlots(); } + + private void updateStyleEventWidgets(JLabel styleEventMarker, JRadioButton radioVerticalMarker, JRadioButton radioIcon) { + if (modifying > 0) + return; + FlightDataType type = (FlightDataType) domainTypeSelector.getSelectedItem(); + boolean isTime = type == FlightDataType.TYPE_TIME; + styleEventMarker.setEnabled(isTime); + radioVerticalMarker.setEnabled(isTime); + radioIcon.setEnabled(isTime); + styleEventMarker.setToolTipText(isTime ? trans.get("simplotpanel.MarkerStyle.lbl.MarkerStyle.ttip") : trans.get("simplotpanel.MarkerStyle.OnlyInTime")); + radioVerticalMarker.setToolTipText(isTime ? null : trans.get("simplotpanel.MarkerStyle.OnlyInTime")); + radioIcon.setToolTipText(isTime ? null : trans.get("simplotpanel.MarkerStyle.OnlyInTime")); + } public JDialog doPlot(Window parent) { if (configuration.getTypeCount() == 0) { diff --git a/swing/src/net/sf/openrocket/gui/util/SwingPreferences.java b/swing/src/net/sf/openrocket/gui/util/SwingPreferences.java index 4a42ffdb9..45c5ce476 100644 --- a/swing/src/net/sf/openrocket/gui/util/SwingPreferences.java +++ b/swing/src/net/sf/openrocket/gui/util/SwingPreferences.java @@ -304,6 +304,24 @@ public class SwingPreferences extends net.sf.openrocket.startup.Preferences { return list; } + + /** + * Returns the files/directories to be loaded as custom thrust curves, formatting as a string. If there are multiple + * locations, they are separated by a semicolon. + * + * @return a list of files to load as thrust curves, formatted as a semicolon separated string. + */ + public String getUserThrustCurveFilesAsString() { + List files = getUserThrustCurveFiles(); + StringBuilder sb = new StringBuilder(); + for (File file : files) { + if (sb.length() > 0) { + sb.append(";"); + } + sb.append(file.getAbsolutePath()); + } + return sb.toString(); + } public File getDefaultUserThrustCurveFile() { File appdir = SystemInfo.getUserApplicationDirectory(); diff --git a/swing/src/net/sf/openrocket/gui/widgets/IconToggleButton.java b/swing/src/net/sf/openrocket/gui/widgets/IconToggleButton.java new file mode 100644 index 000000000..55e779f28 --- /dev/null +++ b/swing/src/net/sf/openrocket/gui/widgets/IconToggleButton.java @@ -0,0 +1,119 @@ +package net.sf.openrocket.gui.widgets; + +import net.sf.openrocket.gui.util.Icons; + +import javax.swing.Action; +import javax.swing.Icon; +import javax.swing.UIManager; +import java.awt.Insets; + +/** + * Toggle button specifically for displaying an icon. + * The button does not have any borders and only displays the icon. + * When setting the selected icon, the button will automatically generate a version of that icon + * for the unselected state (a greyed out version). + * You can also set the scale of the icon using the ICON_SCALE variable. + * + * @author Sibo Van Gool + */ +public class IconToggleButton extends SelectColorToggleButton { + private double ICON_SCALE = 1; + + public IconToggleButton(Action a) { + super(a); + initSettings(); + } + + public IconToggleButton(String text) { + super(text); + initSettings(); + } + + public IconToggleButton() { + initSettings(); + } + + public IconToggleButton(Icon icon) { + super(icon); + initSettings(); + } + + public IconToggleButton(Icon icon, boolean selected) { + super(icon, selected); + initSettings(); + } + + public IconToggleButton(String text, boolean selected) { + super(text, selected); + initSettings(); + } + + public IconToggleButton(String text, Icon icon) { + super(text, icon); + initSettings(); + } + + public IconToggleButton(String text, Icon icon, boolean selected) { + super(text, icon, selected); + initSettings(); + } + + public void setIconScale(double iconScale) { + ICON_SCALE = iconScale; + } + + private void initSettings() { + setBorderPainted(false); + setBorder(null); + setFocusable(false); + setMargin(new Insets(0, 0, 0, 0)); + setContentAreaFilled(false); + } + + @Override + public void setSelectedIcon(Icon selectedIcon) { + super.setSelectedIcon(selectedIcon); + setUnselectedIcon(selectedIcon); + // There is a bug where the normal override of the pressed icon does not work, so we have to assign it here. + setPressedIcon(Icons.getScaledIcon(selectedIcon, ICON_SCALE)); + } + + /** + * Generates and sets an unselected icon based on the current icon. + * @param icon the icon of the button when it is selected + */ + private void setUnselectedIcon(Icon icon) { + Icon unselectedIcon = UIManager.getLookAndFeel().getDisabledIcon(null, icon); + setIcon(unselectedIcon); + } + + @Override + public Icon getIcon() { + return Icons.getScaledIcon(super.getIcon(), ICON_SCALE); + } + + @Override + public Icon getSelectedIcon() { + return Icons.getScaledIcon(super.getSelectedIcon(), ICON_SCALE); + } + + @Override + public Icon getDisabledIcon() { + return Icons.getScaledIcon(super.getDisabledIcon(), ICON_SCALE); + } + + @Override + public Icon getDisabledSelectedIcon() { + return Icons.getScaledIcon(super.getDisabledSelectedIcon(), ICON_SCALE); + } + + @Override + public Icon getRolloverIcon() { + return Icons.getScaledIcon(super.getRolloverIcon(), ICON_SCALE); + } + + @Override + public Icon getRolloverSelectedIcon() { + return Icons.getScaledIcon(super.getRolloverSelectedIcon(), ICON_SCALE); + } +} diff --git a/swing/src/net/sf/openrocket/utils/SerializePresets.java b/swing/src/net/sf/openrocket/utils/SerializePresets.java index d872fc223..a47e7d768 100644 --- a/swing/src/net/sf/openrocket/utils/SerializePresets.java +++ b/swing/src/net/sf/openrocket/utils/SerializePresets.java @@ -50,8 +50,8 @@ public class SerializePresets extends BasicApplication { } while (iterator.hasNext()) { - Pair f = iterator.next(); - String fileName = f.getU(); + Pair f = iterator.next(); + String fileName = f.getU().getName(); InputStream is = f.getV(); OpenRocketComponentLoader loader = new OpenRocketComponentLoader(); diff --git a/swing/test/net/sf/openrocket/IntegrationTest.java b/swing/test/net/sf/openrocket/IntegrationTest.java index e0a280f09..a1fd04a58 100644 --- a/swing/test/net/sf/openrocket/IntegrationTest.java +++ b/swing/test/net/sf/openrocket/IntegrationTest.java @@ -122,7 +122,7 @@ public class IntegrationTest { // Compute cg+cp + altitude // double cgx, double mass, double cpx, double cna) checkCgCp(0.248, 0.0645, 0.320, 12.0); - checkAlt(49.0); + checkAlt(48.8); // Mass modification document.addUndoPosition("Modify mass"); @@ -132,7 +132,7 @@ public class IntegrationTest { // Check cg+cp + altitude checkCgCp(0.230, 0.0745, 0.320, 12.0); - checkAlt(37.2); + checkAlt(37.4); // Non-change document.addUndoPosition("No change"); @@ -164,7 +164,7 @@ public class IntegrationTest { // Check cg+cp + altitude checkCgCp(0.230, 0.0745, 0.320, 12.0); - checkAlt(37.2); + checkAlt(37.4); // Undo "Name change" change undoAction.actionPerformed(new ActionEvent(this, 0, "foo")); @@ -190,7 +190,7 @@ public class IntegrationTest { // Check cg+cp + altitude checkCgCp(0.230, 0.0745, 0.320, 12.0); - checkAlt(37.2); + checkAlt(37.4); // Mass modification document.addUndoPosition("Modify mass2"); @@ -200,7 +200,7 @@ public class IntegrationTest { // Check cg+cp + altitude checkCgCp(0.223, 0.0795, 0.320, 12.0); - checkAlt(32.7); + checkAlt(33); // Perform component movement document.startUndo("Move component"); @@ -217,7 +217,7 @@ public class IntegrationTest { // Check cg+cp + altitude checkCgCp(0.221, 0.0797, 0.320, 12.0); - checkAlt(32.7); + checkAlt(33); // Modify mass without setting undo description massComponent().setComponentMass(0.020); @@ -234,7 +234,7 @@ public class IntegrationTest { // Check cg+cp + altitude checkCgCp(0.221, 0.0797, 0.320, 12.0); - checkAlt(32.7); + checkAlt(33); // Undo "Move component" change undoAction.actionPerformed(new ActionEvent(this, 0, "foo")); @@ -243,7 +243,7 @@ public class IntegrationTest { // Check cg+cp + altitude checkCgCp(0.223, 0.0795, 0.320, 12.0); - checkAlt(32.7); + checkAlt(33); // Redo "Move component" change redoAction.actionPerformed(new ActionEvent(this, 0, "foo")); @@ -252,7 +252,7 @@ public class IntegrationTest { // Check cg+cp + altitude checkCgCp(0.221, 0.0797, 0.320, 12.0); - checkAlt(32.7); + checkAlt(33); }