1 using System; 2 using System.Collections.Generic; 3 using System.Collections.ObjectModel; 4 using System.Globalization; 5 using System.Linq; 6 using OpenTK; 7 using Prism.Mvvm; 8 using TrainEditor2.Extensions; 9 using TrainEditor2.Models.Dialogs; 10 using TrainEditor2.Models.Others; 11 using TrainManager.Motor; 12 13 namespace TrainEditor2.Models.Trains 14 { 15 internal partial class Motor : BindableBase, ICloneable 16 { 17 internal class Vertex : BindableBase, ICloneable 18 { 19 private double x; 20 private double y; 21 private bool selected; 22 private bool isOrigin; 23 24 internal double X 25 { 26 get 27 { 28 return x; 29 } 30 set 31 { 32 SetProperty(ref x, value); 33 } 34 } 35 36 internal double Y 37 { 38 get 39 { 40 return y; 41 } 42 set 43 { 44 SetProperty(ref y, value); 45 } 46 } 47 48 internal bool Selected 49 { 50 get 51 { 52 return selected; 53 } 54 set 55 { 56 SetProperty(ref selected, value); 57 } 58 } 59 60 internal bool IsOrigin 61 { 62 get 63 { 64 return isOrigin; 65 } 66 set 67 { 68 SetProperty(ref isOrigin, value); 69 } 70 } 71 Vertex(double x, double y)72 internal Vertex(double x, double y) 73 { 74 X = x; 75 Y = y; 76 Selected = false; 77 IsOrigin = false; 78 } 79 Clone()80 public object Clone() 81 { 82 return MemberwiseClone(); 83 } 84 } 85 86 internal class Line : BindableBase, ICloneable 87 { 88 private int leftID; 89 private int rightID; 90 private bool selected; 91 92 internal int LeftID 93 { 94 get 95 { 96 return leftID; 97 } 98 private set 99 { 100 SetProperty(ref leftID, value); 101 } 102 } 103 104 internal int RightID 105 { 106 get 107 { 108 return rightID; 109 } 110 private set 111 { 112 SetProperty(ref rightID, value); 113 } 114 } 115 116 internal bool Selected 117 { 118 get 119 { 120 return selected; 121 } 122 set 123 { 124 SetProperty(ref selected, value); 125 } 126 } 127 Line(int leftID, int rightID)128 internal Line(int leftID, int rightID) 129 { 130 LeftID = leftID; 131 RightID = rightID; 132 Selected = false; 133 } 134 Clone()135 public object Clone() 136 { 137 return MemberwiseClone(); 138 } 139 } 140 141 internal class Area : BindableBase, ICloneable 142 { 143 private double leftX; 144 private double rightX; 145 private int index; 146 private bool tbd; 147 148 internal double LeftX 149 { 150 get 151 { 152 return leftX; 153 } 154 set 155 { 156 SetProperty(ref leftX, value); 157 } 158 } 159 160 internal double RightX 161 { 162 get 163 { 164 return rightX; 165 } 166 set 167 { 168 SetProperty(ref rightX, value); 169 } 170 } 171 172 internal int Index 173 { 174 get 175 { 176 return index; 177 } 178 private set 179 { 180 SetProperty(ref index, value); 181 } 182 } 183 184 internal bool TBD 185 { 186 get 187 { 188 return tbd; 189 } 190 set 191 { 192 SetProperty(ref tbd, value); 193 } 194 } 195 Area(double leftX, double rightX, int index)196 internal Area(double leftX, double rightX, int index) 197 { 198 LeftX = leftX; 199 RightX = rightX; 200 Index = index; 201 TBD = false; 202 } 203 Clone()204 public object Clone() 205 { 206 return MemberwiseClone(); 207 } 208 } 209 210 internal class VertexLibrary : ObservableDictionary<int, Vertex>, ICloneable 211 { 212 private int lastID; 213 VertexLibrary()214 internal VertexLibrary() 215 { 216 lastID = -1; 217 } 218 Add(Vertex vertex)219 internal void Add(Vertex vertex) 220 { 221 if (this.Any(v => v.Value.X == vertex.X)) 222 { 223 int id = this.First(v => v.Value.X == vertex.X).Key; 224 base[id] = vertex; 225 } 226 else 227 { 228 Add(++lastID, vertex); 229 } 230 } 231 Clone()232 public object Clone() 233 { 234 VertexLibrary vertices = new VertexLibrary 235 { 236 lastID = lastID 237 }; 238 239 foreach (KeyValuePair<int, Vertex> vertex in this) 240 { 241 vertices.Add(vertex.Key, (Vertex)vertex.Value.Clone()); 242 } 243 244 return vertices; 245 } 246 } 247 248 internal class Track : ICloneable 249 { 250 internal VertexLibrary PitchVertices; 251 internal ObservableCollection<Line> PitchLines; 252 253 internal VertexLibrary VolumeVertices; 254 internal ObservableCollection<Line> VolumeLines; 255 256 internal ObservableCollection<Area> SoundIndices; 257 Track()258 internal Track() 259 { 260 PitchVertices = new VertexLibrary(); 261 PitchLines = new ObservableCollection<Line>(); 262 263 VolumeVertices = new VertexLibrary(); 264 VolumeLines = new ObservableCollection<Line>(); 265 266 SoundIndices = new ObservableCollection<Area>(); 267 } 268 Clone()269 public object Clone() 270 { 271 Track track = (Track)MemberwiseClone(); 272 273 track.PitchVertices = (VertexLibrary)PitchVertices.Clone(); 274 track.PitchLines = new ObservableCollection<Line>(PitchLines.Select(l => (Line)l.Clone())); 275 276 track.VolumeVertices = (VertexLibrary)VolumeVertices.Clone(); 277 track.VolumeLines = new ObservableCollection<Line>(VolumeLines.Select(l => (Line)l.Clone())); 278 279 track.SoundIndices = new ObservableCollection<Area>(SoundIndices.Select(a => (Area)a.Clone())); 280 281 return track; 282 } 283 EntriesToTrack(BVEMotorSoundTableEntry[] entries)284 internal static Track EntriesToTrack(BVEMotorSoundTableEntry[] entries) 285 { 286 Track track = new Track(); 287 288 for (int i = 0; i < entries.Length; i++) 289 { 290 double velocity = 0.2 * i; 291 292 if (track.PitchVertices.Count >= 2) 293 { 294 KeyValuePair<int, Vertex>[] leftVertices = new KeyValuePair<int, Vertex>[] { track.PitchVertices.ElementAt(track.PitchVertices.Count - 2), track.PitchVertices.Last() }; 295 Func<double, double> f = x => leftVertices[0].Value.Y + (leftVertices[1].Value.Y - leftVertices[0].Value.Y) / (leftVertices[1].Value.X - leftVertices[0].Value.X) * (x - leftVertices[0].Value.X); 296 297 if (f(velocity) == entries[i].Pitch) 298 { 299 track.PitchVertices.Remove(leftVertices[1].Key); 300 } 301 } 302 303 track.PitchVertices.Add(new Vertex(velocity, 0.01 * Math.Round(100.0 * entries[i].Pitch))); 304 305 if (track.VolumeVertices.Count >= 2) 306 { 307 KeyValuePair<int, Vertex>[] leftVertices = new KeyValuePair<int, Vertex>[] { track.VolumeVertices.ElementAt(track.VolumeVertices.Count - 2), track.VolumeVertices.Last() }; 308 Func<double, double> f = x => leftVertices[0].Value.Y + (leftVertices[1].Value.Y - leftVertices[0].Value.Y) / (leftVertices[1].Value.X - leftVertices[0].Value.X) * (x - leftVertices[0].Value.X); 309 310 if (f(velocity) == entries[i].Gain) 311 { 312 track.VolumeVertices.Remove(leftVertices[1].Key); 313 } 314 } 315 316 track.VolumeVertices.Add(new Vertex(velocity, 0.01 * Math.Round(100.0 * entries[i].Gain))); 317 318 if (track.SoundIndices.Any()) 319 { 320 Area leftArea = track.SoundIndices.Last(); 321 322 if (entries[i].SoundIndex != leftArea.Index) 323 { 324 leftArea.RightX = velocity - 0.2; 325 track.SoundIndices.Add(new Area(velocity, velocity, entries[i].SoundIndex)); 326 } 327 else 328 { 329 leftArea.RightX = velocity; 330 } 331 } 332 else 333 { 334 track.SoundIndices.Add(new Area(velocity, velocity, entries[i].SoundIndex)); 335 } 336 } 337 338 for (int j = 0; j < track.PitchVertices.Count - 1; j++) 339 { 340 track.PitchLines.Add(new Line(track.PitchVertices.ElementAt(j).Key, track.PitchVertices.ElementAt(j + 1).Key)); 341 } 342 343 for (int j = 0; j < track.VolumeVertices.Count - 1; j++) 344 { 345 track.VolumeLines.Add(new Line(track.VolumeVertices.ElementAt(j).Key, track.VolumeVertices.ElementAt(j + 1).Key)); 346 } 347 348 if (track.SoundIndices.Any()) 349 { 350 Area lastArea = track.SoundIndices.Last(); 351 352 if (lastArea.LeftX == lastArea.RightX) 353 { 354 lastArea.RightX += 0.2; 355 } 356 } 357 358 return track; 359 } 360 TrackToEntries(Track track)361 internal static BVEMotorSoundTableEntry[] TrackToEntries(Track track) 362 { 363 int n = 0; 364 365 if (track.PitchVertices.Any()) 366 { 367 n = Math.Max(n, (int)Math.Round(5.0 * track.PitchVertices.Last().Value.X)); 368 } 369 370 if (track.VolumeVertices.Any()) 371 { 372 n = Math.Max(n, (int)Math.Round(5.0 * track.VolumeVertices.Last().Value.X)); 373 } 374 375 BVEMotorSoundTableEntry[] entries = Enumerable.Repeat(new BVEMotorSoundTableEntry { SoundIndex = -1, Pitch = 100.0f, Gain = 128.0f }, n + 1).ToArray(); 376 377 for (int i = 0; i < entries.Length; i++) 378 { 379 double velocity = 0.2 * i; 380 381 Line pitchLine = track.PitchLines.FirstOrDefault(l => track.PitchVertices[l.LeftID].X <= velocity && track.PitchVertices[l.RightID].X >= velocity); 382 383 if (pitchLine != null) 384 { 385 Vertex left = track.PitchVertices[pitchLine.LeftID]; 386 Vertex right = track.PitchVertices[pitchLine.RightID]; 387 388 Func<double, double> f = x => left.Y + (right.Y - left.Y) / (right.X - left.X) * (x - left.X); 389 390 entries[i].Pitch = (float)(0.01 * Math.Round(100.0 * Math.Max(f(velocity), 0.0))); 391 } 392 393 Line volumeLine = track.VolumeLines.FirstOrDefault(l => track.VolumeVertices[l.LeftID].X <= velocity && track.VolumeVertices[l.RightID].X >= velocity); 394 395 if (volumeLine != null) 396 { 397 Vertex left = track.VolumeVertices[volumeLine.LeftID]; 398 Vertex right = track.VolumeVertices[volumeLine.RightID]; 399 400 Func<double, double> f = x => left.Y + (right.Y - left.Y) / (right.X - left.X) * (x - left.X); 401 402 entries[i].Gain = (float)(0.01 * Math.Round(100.0 * Math.Max(f(velocity), 0.0))); 403 } 404 405 Area area = track.SoundIndices.FirstOrDefault(a => a.LeftX <= velocity && a.RightX >= velocity); 406 407 if (area != null) 408 { 409 entries[i].SoundIndex = area.Index; 410 } 411 } 412 413 return entries; 414 } 415 EntriesToMotorSoundTable(BVEMotorSoundTableEntry[] entries)416 internal static BVEMotorSoundTable EntriesToMotorSoundTable(BVEMotorSoundTableEntry[] entries) 417 { 418 BVEMotorSoundTable table = new BVEMotorSoundTable 419 { 420 Entries = new BVEMotorSoundTableEntry[entries.Length] 421 }; 422 423 for (int i = 0; i < entries.Length; i++) 424 { 425 table.Entries[i].Pitch = (float)(0.01 * entries[i].Pitch); 426 table.Entries[i].Gain = (float)Math.Pow(0.0078125 * entries[i].Gain, 0.25); 427 table.Entries[i].SoundIndex = entries[i].SoundIndex; 428 } 429 430 return table; 431 } 432 } 433 434 internal enum TrackInfo 435 { 436 Power1, 437 Power2, 438 Brake1, 439 Brake2 440 } 441 442 internal enum InputMode 443 { 444 Pitch, 445 Volume, 446 SoundIndex 447 } 448 449 internal enum ToolMode 450 { 451 Select, 452 Move, 453 Dot, 454 Line 455 } 456 457 internal enum SimulationState 458 { 459 Disable, 460 Stopped, 461 Paused, 462 Started 463 } 464 465 internal class TrackState : ICloneable 466 { 467 internal TrackInfo Info 468 { 469 get; 470 private set; 471 } 472 473 internal Track Track 474 { 475 get; 476 private set; 477 } 478 TrackState(TrackInfo info, Track track)479 internal TrackState(TrackInfo info, Track track) 480 { 481 Info = info; 482 Track = track; 483 } 484 Clone()485 public object Clone() 486 { 487 TrackState state = (TrackState)MemberwiseClone(); 488 state.Track = (Track)Track.Clone(); 489 return state; 490 } 491 } 492 493 internal class SelectedRange 494 { 495 internal Box2d Range 496 { 497 get; 498 private set; 499 } 500 501 internal Vertex[] SelectedVertices 502 { 503 get; 504 private set; 505 } 506 507 internal Line[] SelectedLines 508 { 509 get; 510 private set; 511 } 512 SelectedRange(Box2d range, Vertex[] selectedVertices, Line[] selectedLines)513 private SelectedRange(Box2d range, Vertex[] selectedVertices, Line[] selectedLines) 514 { 515 Range = range; 516 SelectedVertices = selectedVertices; 517 SelectedLines = selectedLines; 518 } 519 CreateSelectedRange(VertexLibrary vertices, ObservableCollection<Line> lines, double leftX, double rightX, double topY, double bottomY)520 internal static SelectedRange CreateSelectedRange(VertexLibrary vertices, ObservableCollection<Line> lines, double leftX, double rightX, double topY, double bottomY) 521 { 522 Func<Vertex, bool> conditionVertex = v => v.X >= leftX && v.X <= rightX && v.Y >= bottomY && v.Y <= topY; 523 524 Vertex[] selectedVertices = vertices.Values.Where(v => conditionVertex(v)).ToArray(); 525 Line[] selectedLines = lines.Where(l => selectedVertices.Any(v => v.X == vertices[l.LeftID].X) && selectedVertices.Any(v => v.X == vertices[l.RightID].X)).ToArray(); 526 527 return new SelectedRange(new Box2d(leftX, topY, rightX, bottomY), selectedVertices, selectedLines); 528 } 529 } 530 531 private readonly CultureInfo culture; 532 533 private MessageBox messageBox; 534 private ToolTipModel toolTipVertexPitch; 535 private ToolTipModel toolTipVertexVolume; 536 private InputEventModel.ModifierKeys currentModifierKeys; 537 private InputEventModel.CursorType currentCursorType; 538 539 private TrackInfo selectedTrackInfo; 540 private int selectedSoundIndex; 541 private Track copyTrack; 542 private double minVelocity; 543 private double maxVelocity; 544 private double minPitch; 545 private double maxPitch; 546 private double minVolume; 547 private double maxVolume; 548 private double nowVelocity; 549 private double nowPitch; 550 private double nowVolume; 551 552 private InputMode currentInputMode; 553 private ToolMode currentToolMode; 554 private SimulationState currentSimState; 555 556 private double lastMousePosX; 557 private double lastMousePosY; 558 559 private bool isMoving; 560 private Area previewArea; 561 private SelectedRange selectedRange; 562 private Vertex hoveredVertexPitch; 563 private Vertex hoveredVertexVolume; 564 565 private int runIndex; 566 private bool isPlayTrack1; 567 private bool isPlayTrack2; 568 private bool isLoop; 569 private bool isConstant; 570 private double acceleration; 571 private double startSpeed; 572 private double endSpeed; 573 574 private DateTime startTime; 575 private double oldElapsedTime; 576 private double nowSpeed; 577 578 private int glControlWidth; 579 private int glControlHeight; 580 private bool isRefreshGlControl; 581 582 internal ObservableCollection<Track> Tracks; 583 internal ObservableCollection<TrackState> PrevTrackStates; 584 internal ObservableCollection<TrackState> NextTrackStates; 585 586 internal MessageBox MessageBox 587 { 588 get 589 { 590 return messageBox; 591 } 592 set 593 { 594 SetProperty(ref messageBox, value); 595 } 596 } 597 598 internal ToolTipModel ToolTipVertexPitch 599 { 600 get 601 { 602 return toolTipVertexPitch; 603 } 604 set 605 { 606 SetProperty(ref toolTipVertexPitch, value); 607 } 608 } 609 610 internal ToolTipModel ToolTipVertexVolume 611 { 612 get 613 { 614 return toolTipVertexVolume; 615 } 616 set 617 { 618 SetProperty(ref toolTipVertexVolume, value); 619 } 620 } 621 622 internal InputEventModel.ModifierKeys CurrentModifierKeys 623 { 624 get 625 { 626 return currentModifierKeys; 627 } 628 set 629 { 630 SetProperty(ref currentModifierKeys, value); 631 } 632 } 633 634 internal InputEventModel.CursorType CurrentCursorType 635 { 636 get 637 { 638 return currentCursorType; 639 } 640 set 641 { 642 SetProperty(ref currentCursorType, value); 643 } 644 } 645 646 internal TrackInfo SelectedTrackInfo 647 { 648 get 649 { 650 return selectedTrackInfo; 651 } 652 set 653 { 654 SetProperty(ref selectedTrackInfo, value); 655 } 656 } 657 658 internal Track SelectedTrack 659 { 660 get 661 { 662 return Tracks[(int)SelectedTrackInfo]; 663 } 664 set 665 { 666 Tracks[(int)SelectedTrackInfo] = value; 667 } 668 } 669 670 internal int SelectedSoundIndex 671 { 672 get 673 { 674 return selectedSoundIndex; 675 } 676 set 677 { 678 SetProperty(ref selectedSoundIndex, value); 679 } 680 } 681 682 internal Track CopyTrack 683 { 684 get 685 { 686 return copyTrack; 687 } 688 set 689 { 690 SetProperty(ref copyTrack, value); 691 } 692 } 693 694 internal double MinVelocity 695 { 696 get 697 { 698 return minVelocity; 699 } 700 set 701 { 702 SetProperty(ref minVelocity, value); 703 } 704 } 705 706 internal double MaxVelocity 707 { 708 get 709 { 710 return maxVelocity; 711 } 712 set 713 { 714 SetProperty(ref maxVelocity, value); 715 } 716 } 717 718 internal double MinPitch 719 { 720 get 721 { 722 return minPitch; 723 } 724 set 725 { 726 SetProperty(ref minPitch, value); 727 } 728 } 729 730 internal double MaxPitch 731 { 732 get 733 { 734 return maxPitch; 735 } 736 set 737 { 738 SetProperty(ref maxPitch, value); 739 } 740 } 741 742 internal double MinVolume 743 { 744 get 745 { 746 return minVolume; 747 } 748 set 749 { 750 SetProperty(ref minVolume, value); 751 } 752 } 753 754 internal double MaxVolume 755 { 756 get 757 { 758 return maxVolume; 759 } 760 set 761 { 762 SetProperty(ref maxVolume, value); 763 } 764 } 765 766 internal double NowVelocity 767 { 768 get 769 { 770 return nowVelocity; 771 } 772 set 773 { 774 SetProperty(ref nowVelocity, value); 775 } 776 } 777 778 internal double NowPitch 779 { 780 get 781 { 782 return nowPitch; 783 } 784 set 785 { 786 SetProperty(ref nowPitch, value); 787 } 788 } 789 790 internal double NowVolume 791 { 792 get 793 { 794 return nowVolume; 795 } 796 set 797 { 798 SetProperty(ref nowVolume, value); 799 } 800 } 801 802 internal InputMode CurrentInputMode 803 { 804 get 805 { 806 return currentInputMode; 807 } 808 set 809 { 810 SetProperty(ref currentInputMode, value); 811 } 812 } 813 814 internal ToolMode CurrentToolMode 815 { 816 get 817 { 818 return currentToolMode; 819 } 820 set 821 { 822 SetProperty(ref currentToolMode, value); 823 } 824 } 825 826 internal SimulationState CurrentSimState 827 { 828 get 829 { 830 return currentSimState; 831 } 832 set 833 { 834 SetProperty(ref currentSimState, value); 835 } 836 } 837 838 internal int RunIndex 839 { 840 get 841 { 842 return runIndex; 843 } 844 set 845 { 846 SetProperty(ref runIndex, value); 847 } 848 } 849 850 internal bool IsPlayTrack1 851 { 852 get 853 { 854 return isPlayTrack1; 855 } 856 set 857 { 858 SetProperty(ref isPlayTrack1, value); 859 } 860 } 861 862 internal bool IsPlayTrack2 863 { 864 get 865 { 866 return isPlayTrack2; 867 } 868 set 869 { 870 SetProperty(ref isPlayTrack2, value); 871 } 872 } 873 874 internal bool IsLoop 875 { 876 get 877 { 878 return isLoop; 879 } 880 set 881 { 882 SetProperty(ref isLoop, value); 883 } 884 } 885 886 internal bool IsConstant 887 { 888 get 889 { 890 return isConstant; 891 } 892 set 893 { 894 SetProperty(ref isConstant, value); 895 } 896 } 897 898 internal double Acceleration 899 { 900 get 901 { 902 return acceleration; 903 } 904 set 905 { 906 SetProperty(ref acceleration, value); 907 } 908 } 909 910 internal double StartSpeed 911 { 912 get 913 { 914 return startSpeed; 915 } 916 set 917 { 918 SetProperty(ref startSpeed, value); 919 } 920 } 921 922 internal double EndSpeed 923 { 924 get 925 { 926 return endSpeed; 927 } 928 set 929 { 930 SetProperty(ref endSpeed, value); 931 } 932 } 933 934 internal int GlControlWidth 935 { 936 get 937 { 938 return glControlWidth; 939 } 940 set 941 { 942 SetProperty(ref glControlWidth, value); 943 } 944 } 945 946 internal int GlControlHeight 947 { 948 get 949 { 950 return glControlHeight; 951 } 952 set 953 { 954 SetProperty(ref glControlHeight, value); 955 } 956 } 957 958 internal bool IsRefreshGlControl 959 { 960 get 961 { 962 return isRefreshGlControl; 963 } 964 set 965 { 966 SetProperty(ref isRefreshGlControl, value); 967 } 968 } 969 Motor()970 internal Motor() 971 { 972 culture = CultureInfo.InvariantCulture; 973 974 MessageBox = new MessageBox(); 975 ToolTipVertexPitch = new ToolTipModel(); 976 ToolTipVertexVolume = new ToolTipModel(); 977 CurrentCursorType = InputEventModel.CursorType.Arrow; 978 979 Tracks = new ObservableCollection<Track>(); 980 981 for (int i = 0; i < 4; i++) 982 { 983 Tracks.Add(new Track()); 984 } 985 986 SelectedTrackInfo = TrackInfo.Power1; 987 SelectedSoundIndex = -1; 988 989 PrevTrackStates = new ObservableCollection<TrackState>(); 990 NextTrackStates = new ObservableCollection<TrackState>(); 991 992 MinVelocity = 0.0; 993 MaxVelocity = 40.0; 994 995 MinPitch = 0.0; 996 MaxPitch = 400.0; 997 998 MinVolume = 0.0; 999 MaxVolume = 256.0; 1000 1001 CurrentSimState = SimulationState.Stopped; 1002 RunIndex = -1; 1003 IsPlayTrack1 = IsPlayTrack2 = true; 1004 Acceleration = 2.6; 1005 StartSpeed = 0.0; 1006 EndSpeed = 160.0; 1007 1008 GlControlWidth = 568; 1009 GlControlHeight = 593; 1010 } 1011 Clone()1012 public object Clone() 1013 { 1014 Motor motor = (Motor)MemberwiseClone(); 1015 motor.MessageBox = new MessageBox(); 1016 ToolTipVertexPitch = new ToolTipModel(); 1017 ToolTipVertexVolume = new ToolTipModel(); 1018 motor.previewArea = null; 1019 motor.selectedRange = null; 1020 motor.Tracks = new ObservableCollection<Track>(Tracks.Select(x => (Track)x.Clone())); 1021 motor.PrevTrackStates = new ObservableCollection<TrackState>(PrevTrackStates.Select(x => (TrackState)x.Clone())); 1022 motor.NextTrackStates = new ObservableCollection<TrackState>(NextTrackStates.Select(x => (TrackState)x.Clone())); 1023 return motor; 1024 } 1025 } 1026 } 1027