1 /*
2  *  gear_simulator.cpp
3  *  PHD Guiding
4  *
5  *  Created by Craig Stark.
6  *  Reimplemented for PHD2 by Andy Galasso.
7  *  Copyright (c) 2006-2010 Craig Stark
8  *  Copyright (c) 2015-2018 Andy Galasso
9  *  All rights reserved.
10  *
11  *  This source code is distributed under the following "BSD" license
12  *  Redistribution and use in source and binary forms, with or without
13  *  modification, are permitted provided that the following conditions are met:
14  *    Redistributions of source code must retain the above copyright notice,
15  *     this list of conditions and the following disclaimer.
16  *    Redistributions in binary form must reproduce the above copyright notice,
17  *     this list of conditions and the following disclaimer in the
18  *     documentation and/or other materials provided with the distribution.
19  *    Neither the name of openphdguiding.org nor the names of its
20  *     contributors may be used to endorse or promote products derived from
21  *     this software without specific prior written permission.
22  *
23  *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
24  *  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
25  *  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
26  *  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
27  *  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
28  *  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
29  *  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
30  *  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
31  *  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
32  *  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
33  *  POSSIBILITY OF SUCH DAMAGE.
34  *
35  */
36 
37 #include "phd.h"
38 
39 #ifdef SIMULATOR
40 
41 #include "camera.h"
42 #include "gear_simulator.h"
43 #include "image_math.h"
44 
45 #include <wx/dir.h>
46 #include <wx/gdicmn.h>
47 #include <wx/stopwatch.h>
48 #include <wx/radiobut.h>
49 
50 #include <wx/wfstream.h>
51 #include <wx/txtstrm.h>
52 #include <wx/tokenzr.h>
53 
54 #define SIMMODE 3   // 1=FITS, 2=BMP, 3=Generate
55 // #define SIMDEBUG
56 
57 /* simulation parameters for SIMMODE = 3*/
58 // #define SIM_FILE_DISPLACEMENTS          // subset of SIMMODE = 3, reading raw star displacements from a file
59 
60 struct SimCamParams
61 {
62     static unsigned int width;
63     static unsigned int height;
64     static unsigned int border;
65     static unsigned int nr_stars;
66     static unsigned int nr_hot_pixels;
67     static double noise_multiplier;
68     static double dec_backlash;
69     static double pe_scale;
70     static double dec_drift_rate;
71     static double seeing_scale;
72     static double cam_angle;
73     static double guide_rate;
74     static PierSide pier_side;
75     static bool reverse_dec_pulse_on_west_side;
76     static unsigned int clouds_inten;
77     static double clouds_opacity;                   // UI has percentage, internally 0-1.0
78     static double image_scale; // arc-sec per pixel
79     static bool use_pe;
80     static bool use_stiction;
81     static bool use_default_pe_params;
82     static double custom_pe_amp;
83     static double custom_pe_period;
84     static bool show_comet;
85     static double comet_rate_x;
86     static double comet_rate_y;
87     static bool allow_async_st4;
88     static unsigned int frame_download_ms;
89 };
90 
91 unsigned int SimCamParams::width = 752;          // simulated camera image width
92 unsigned int SimCamParams::height = 580;         // simulated camera image height
93 unsigned int SimCamParams::border = 12;          // do not place any stars within this size border
94 unsigned int SimCamParams::nr_stars;             // number of stars to generate
95 unsigned int SimCamParams::nr_hot_pixels;        // number of hot pixels to generate
96 double SimCamParams::noise_multiplier;           // noise factor, increase to increase noise
97 double SimCamParams::dec_backlash;               // dec backlash amount (pixels)
98 double SimCamParams::pe_scale;                   // scale factor controlling magnitude of simulated periodic error
99 double SimCamParams::dec_drift_rate;             // dec drift rate (pixels per second)
100 double SimCamParams::seeing_scale;               // simulated seeing scale factor
101 double SimCamParams::cam_angle;                  // simulated camera angle (degrees)
102 double SimCamParams::guide_rate;                 // guide rate, pixels per second
103 PierSide SimCamParams::pier_side;                // side of pier
104 bool SimCamParams::reverse_dec_pulse_on_west_side; // reverse dec pulse on west side of pier, like ASCOM pulse guided equatorial mounts
105 unsigned int SimCamParams::clouds_inten = 50;           // seed brightness for cloud contribution
106 double SimCamParams::clouds_opacity;
107 double SimCamParams::image_scale;                // arc-sec per pixel
108 bool SimCamParams::use_pe;
109 bool SimCamParams::use_stiction;
110 bool SimCamParams::use_default_pe_params;
111 double SimCamParams::custom_pe_amp;
112 double SimCamParams::custom_pe_period;
113 bool SimCamParams::show_comet;
114 double SimCamParams::comet_rate_x;
115 double SimCamParams::comet_rate_y;
116 bool SimCamParams::allow_async_st4 = true;
117 unsigned int SimCamParams::frame_download_ms;    // frame download time, ms
118 
119 // Note: these are all in units appropriate for the UI
120 #define NR_STARS_DEFAULT 20
121 #define NR_HOT_PIXELS_DEFAULT 8
122 #define NOISE_DEFAULT 2.0
123 #define NOISE_MAX 5.0
124 #define DEC_BACKLASH_DEFAULT 5.0                  // arc-sec
125 #define DEC_BACKLASH_MAX 100.0
126 #define DEC_DRIFT_DEFAULT 5.0                     // arc-sec per minute
127 #define DEC_DRIFT_MAX 30.0
128 #define SEEING_DEFAULT 2.0                        // arc-sec FWHM
129 #define SEEING_MAX 5.0
130 #define CAM_ANGLE_DEFAULT 15.0
131 #define CAM_ANGLE_MAX 360.0
132 #define GUIDE_RATE_DEFAULT (1.0 * 15.0)           // multiples of sidereal rate, a-s/sec
133 #define GUIDE_RATE_MAX (1.0 * 15.0)
134 #define PIER_SIDE_DEFAULT PIER_SIDE_EAST
135 #define REVERSE_DEC_PULSE_ON_WEST_SIDE_DEFAULT true
136 #define CLOUDS_OPACITY_DEFAULT 0
137 #define USE_PE_DEFAULT true
138 #define USE_STICTION_DEFAULT false
139 #define PE_SCALE_DEFAULT 5.0                    // amplitude arc-sec
140 #define PE_SCALE_MAX 30.0
141 #define USE_PE_DEFAULT_PARAMS true
142 #define PE_CUSTOM_AMP_DEFAULT 2.0               // Give them a trivial 2 a-s 4 min smooth curve
143 #define PE_CUSTOM_PERIOD_DEFAULT 240.0
144 #define SHOW_COMET_DEFAULT false
145 #define COMET_RATE_X_DEFAULT 555.0              // pixels per hour
146 #define COMET_RATE_Y_DEFAULT -123.4              // pixels per hour
147 #define SIM_FILE_DISPLACEMENTS_DEFAULT "star_displacements.csv"
148 
149 // Needed to handle legacy registry values that may no longer be in correct units or range
range_check(double thisval,double minval,double maxval)150 static double range_check(double thisval, double minval, double maxval)
151 {
152     return wxMin(wxMax(thisval, minval), maxval);
153 }
154 
load_sim_params()155 static void load_sim_params()
156 {
157     SimCamParams::image_scale = pFrame->GetCameraPixelScale();
158 
159     SimCamParams::nr_stars = pConfig->Profile.GetInt("/SimCam/nr_stars", NR_STARS_DEFAULT);
160     SimCamParams::nr_hot_pixels = pConfig->Profile.GetInt("/SimCam/nr_hot_pixels", NR_HOT_PIXELS_DEFAULT);
161     SimCamParams::noise_multiplier = pConfig->Profile.GetDouble("/SimCam/noise", NOISE_DEFAULT);
162     SimCamParams::use_pe = pConfig->Profile.GetBoolean("/SimCam/use_pe", USE_PE_DEFAULT);
163     SimCamParams::use_stiction = pConfig->Profile.GetBoolean("/SimCam/use_stiction", USE_STICTION_DEFAULT);
164     SimCamParams::use_default_pe_params = pConfig->Profile.GetBoolean("/SimCam/use_default_pe", USE_PE_DEFAULT_PARAMS);
165     SimCamParams::custom_pe_amp = pConfig->Profile.GetDouble("/SimCam/pe_cust_amp", PE_CUSTOM_AMP_DEFAULT);
166     SimCamParams::custom_pe_period = pConfig->Profile.GetDouble("/SimCam/pe_cust_period", PE_CUSTOM_PERIOD_DEFAULT);
167 
168     double dval = pConfig->Profile.GetDouble("/SimCam/dec_drift", DEC_DRIFT_DEFAULT);
169     SimCamParams::dec_drift_rate = range_check(dval, -DEC_DRIFT_MAX, DEC_DRIFT_MAX) / (SimCamParams::image_scale * 60.0);  //a-s per min is saved
170     // backlash is in arc-secs in UI - map to px for internal use
171     dval = pConfig->Profile.GetDouble("/SimCam/dec_backlash", DEC_BACKLASH_DEFAULT);
172     SimCamParams::dec_backlash = range_check(dval, 0, DEC_BACKLASH_MAX) / SimCamParams::image_scale;
173     SimCamParams::pe_scale = range_check(pConfig->Profile.GetDouble("/SimCam/pe_scale", PE_SCALE_DEFAULT), 0, PE_SCALE_MAX);
174 
175     SimCamParams::seeing_scale = range_check(pConfig->Profile.GetDouble("/SimCam/seeing_scale", SEEING_DEFAULT), 0, SEEING_MAX);       // FWHM a-s
176     SimCamParams::cam_angle = pConfig->Profile.GetDouble("/SimCam/cam_angle", CAM_ANGLE_DEFAULT);
177     SimCamParams::clouds_opacity = pConfig->Profile.GetDouble("/SimCam/clouds_opacity", CLOUDS_OPACITY_DEFAULT);
178     SimCamParams::guide_rate = range_check(pConfig->Profile.GetDouble("/SimCam/guide_rate", GUIDE_RATE_DEFAULT), 0, GUIDE_RATE_MAX);
179     SimCamParams::pier_side = (PierSide) pConfig->Profile.GetInt("/SimCam/pier_side", PIER_SIDE_DEFAULT);
180     SimCamParams::reverse_dec_pulse_on_west_side = pConfig->Profile.GetBoolean("/SimCam/reverse_dec_pulse_on_west_side", REVERSE_DEC_PULSE_ON_WEST_SIDE_DEFAULT);
181 
182     SimCamParams::show_comet = pConfig->Profile.GetBoolean("/SimCam/show_comet", SHOW_COMET_DEFAULT);
183     SimCamParams::comet_rate_x = pConfig->Profile.GetDouble("/SimCam/comet_rate_x", COMET_RATE_X_DEFAULT);
184     SimCamParams::comet_rate_y = pConfig->Profile.GetDouble("/SimCam/comet_rate_y", COMET_RATE_Y_DEFAULT);
185 
186     SimCamParams::frame_download_ms = pConfig->Profile.GetInt("/SimCam/frame_download_ms", 50);
187 }
188 
save_sim_params()189 static void save_sim_params()
190 {
191     pConfig->Profile.SetInt("/SimCam/nr_stars", SimCamParams::nr_stars);
192     pConfig->Profile.SetInt("/SimCam/nr_hot_pixels", SimCamParams::nr_hot_pixels);
193     pConfig->Profile.SetDouble("/SimCam/noise", SimCamParams::noise_multiplier);
194     pConfig->Profile.SetDouble("/SimCam/dec_backlash", SimCamParams::dec_backlash * SimCamParams::image_scale);
195     pConfig->Profile.SetBoolean("/SimCam/use_pe", SimCamParams::use_pe);
196     pConfig->Profile.SetBoolean("/SimCam/use_stiction", SimCamParams::use_stiction);
197     pConfig->Profile.SetBoolean("/SimCam/use_default_pe", SimCamParams::use_default_pe_params);
198     pConfig->Profile.SetDouble("/SimCam/pe_scale", SimCamParams::pe_scale);
199     pConfig->Profile.SetDouble("/SimCam/pe_cust_amp", SimCamParams::custom_pe_amp);
200     pConfig->Profile.SetDouble("/SimCam/pe_cust_period", SimCamParams::custom_pe_period);
201     pConfig->Profile.SetDouble("/SimCam/dec_drift", SimCamParams::dec_drift_rate * 60.0 * SimCamParams::image_scale);
202     pConfig->Profile.SetDouble("/SimCam/seeing_scale", SimCamParams::seeing_scale);
203     pConfig->Profile.SetDouble("/SimCam/clouds_opacity", SimCamParams::clouds_opacity);
204     pConfig->Profile.SetDouble("/SimCam/cam_angle", SimCamParams::cam_angle);
205     pConfig->Profile.SetDouble("/SimCam/guide_rate", SimCamParams::guide_rate);
206     pConfig->Profile.SetInt("/SimCam/pier_side", (int) SimCamParams::pier_side);
207     pConfig->Profile.SetBoolean("/SimCam/reverse_dec_pulse_on_west_side", SimCamParams::reverse_dec_pulse_on_west_side);
208     pConfig->Profile.SetBoolean("/SimCam/show_comet", SimCamParams::show_comet);
209     pConfig->Profile.SetDouble("/SimCam/comet_rate_x", SimCamParams::comet_rate_x);
210     pConfig->Profile.SetDouble("/SimCam/comet_rate_y", SimCamParams::comet_rate_y);
211     pConfig->Profile.SetInt("/SimCam/frame_download_ms", SimCamParams::frame_download_ms);
212 }
213 
214 #ifdef STEPGUIDER_SIMULATOR
215 
216 struct SimAoParams
217 {
218     static unsigned int max_position; // max position in steps
219     static double scale;           // arcsec per step
220     static double angle;           // angle relative to camera (degrees)
221 };
222 
223 unsigned int SimAoParams::max_position = 45;
224 double SimAoParams::scale = 0.10;
225 double SimAoParams::angle = 35.0;
226 
227 class StepGuiderSimulator : public StepGuider
228 {
229 public:
230     StepGuiderSimulator();
231     virtual ~StepGuiderSimulator();
232 
233     bool Connect() override;
234     bool Disconnect() override;
235 
236 private:
237     bool Center() override;
238     STEP_RESULT Step(GUIDE_DIRECTION direction, int steps) override;
239     int MaxPosition(GUIDE_DIRECTION direction) const override;
240     bool SetMaxPosition(int steps) override;
241     bool HasNonGuiMove() override;
242 };
243 
244 static StepGuiderSimulator *s_sim_ao;
245 
StepGuiderSimulator()246 StepGuiderSimulator::StepGuiderSimulator()
247 {
248     m_Name = _("AO-Simulator");
249     SimAoParams::max_position = pConfig->Profile.GetInt("/SimAo/max_steps", 45);
250 }
251 
~StepGuiderSimulator()252 StepGuiderSimulator::~StepGuiderSimulator()
253 {
254 }
255 
Connect()256 bool StepGuiderSimulator::Connect()
257 {
258     if (StepGuider::Connect())
259         return true;
260 
261     ZeroCurrentPosition();
262 
263     s_sim_ao = this;
264 
265     Debug.AddLine("AO Simulator Connected");
266 
267     if (!pCamera || pCamera->Name != _T("Simulator"))
268     {
269         pFrame->Alert(_("The AO Simulator only works with the Camera Simulator. You should either disconnect the AO Simulator or connect the Camera Simulator."));
270     }
271 
272     return false;
273 }
274 
Disconnect()275 bool StepGuiderSimulator::Disconnect()
276 {
277     if (StepGuider::Disconnect())
278         return true;
279 
280     if (s_sim_ao == this) {
281         Debug.AddLine("AO Simulator Disconnected");
282         s_sim_ao = 0;
283     }
284 
285     return false;
286 }
287 
Center()288 bool StepGuiderSimulator::Center()
289 {
290     ZeroCurrentPosition();
291     return false;
292 }
293 
Step(GUIDE_DIRECTION direction,int steps)294 StepGuider::STEP_RESULT StepGuiderSimulator::Step(GUIDE_DIRECTION direction, int steps)
295 {
296 #if 0 // enable to test step failure
297     wxPoint pos = GetAoPos();
298     if (direction == LEFT && pos.x - steps < -35)
299     {
300         Debug.Write("simulate step failure\n");
301         return STEP_LIMIT_REACHED;
302     }
303 #endif
304 
305     // parent class maintains x/y offsets, so nothing to do here. Just simulate a delay.
306     enum { LATENCY_MS_PER_STEP = 5 };
307     wxMilliSleep(steps * LATENCY_MS_PER_STEP);
308     return STEP_OK;
309 }
310 
MaxPosition(GUIDE_DIRECTION direction) const311 int StepGuiderSimulator::MaxPosition(GUIDE_DIRECTION direction) const
312 {
313     return SimAoParams::max_position;
314 }
315 
SetMaxPosition(int steps)316 bool StepGuiderSimulator::SetMaxPosition(int steps)
317 {
318     SimAoParams::max_position = (unsigned int) steps;
319     pConfig->Profile.SetInt("/SimAo/max_steps", steps);
320     return false;
321 }
322 
HasNonGuiMove()323 bool StepGuiderSimulator::HasNonGuiMove()
324 {
325     return true;
326 }
327 
328 #endif // STEPGUIDER_SIMULATOR
329 
330 #ifdef ROTATOR_SIMULATOR
331 
332 class RotatorSimulator : public Rotator
333 {
334 public:
335     RotatorSimulator();
336     virtual ~RotatorSimulator();
337 
338     bool Connect() override;
339     bool Disconnect() override;
340 
341     // get the display name of the rotator device
342     wxString Name() const override;
343 
344     // get the rotator position in degrees
345     float Position() const override;
346 };
347 
RotatorSimulator()348 RotatorSimulator::RotatorSimulator()
349 {
350 }
351 
~RotatorSimulator()352 RotatorSimulator::~RotatorSimulator()
353 {
354 }
355 
Connect()356 bool RotatorSimulator::Connect()
357 {
358     if (!pCamera || pCamera->Name != _T("Simulator"))
359     {
360         pFrame->Alert(_("The Rotator Simulator only works with the Camera Simulator. You must either disconnect the Rotator Simulator or connect the Camera Simulator."));
361         return true;
362     }
363 
364     Rotator::Connect();
365     return false;
366 }
367 
Disconnect()368 bool RotatorSimulator::Disconnect()
369 {
370     Rotator::Disconnect();
371     return false;
372 }
373 
Name() const374 wxString RotatorSimulator::Name() const
375 {
376     return _T("Simulator");
377 }
378 
Position() const379 float RotatorSimulator::Position() const
380 {
381     assert(IsConnected());
382     return SimCamParams::cam_angle;
383 }
384 
385 #endif // ROTATOR_SIMULATOR
386 
387 // value with backlash
388 //   There is an index value, and a lower and upper limit separated by the
389 //   backlash amount. When the index moves past the upper limit, it carries
390 //   both limits along, likewise for the lower limit. The current value is
391 //   the value of the upper limit.
392 struct BacklashVal
393 {
394     double cur;    // current index value
395     double upper;  // upper limit
396     double amount; // backlash amount (lower limit is upper - amount)
397 
BacklashValBacklashVal398     BacklashVal() { }
399 
BacklashValBacklashVal400     BacklashVal(double backlash_amount)
401         : cur(0), upper(backlash_amount), amount(backlash_amount) { }
402 
valBacklashVal403     double val() const { return upper; }
404 
incrBacklashVal405     void incr(double d) {
406         cur += d;
407         if (d > 0.) {
408             if (cur > upper)
409                 upper = cur;
410         }
411         else if (d < 0.) {
412             if (cur < upper - amount)
413                 upper = cur + amount;
414         }
415     }
416 };
417 
418 struct SimStar
419 {
420     wxRealPoint pos;
421     double inten;
422 };
423 
424 struct StictionSim
425 {
426     int lastDirection;
427     bool pending;
428     double adjustment;
429 
StictionSimStictionSim430     StictionSim()
431     {
432         lastDirection = NONE;
433         pending = false;
434         adjustment = 0.;
435     }
436 
GetAdjustmentStictionSim437     double GetAdjustment(int Direction, int Duration, double Distance)
438     {
439         double rslt = 0;
440         if (lastDirection != NONE)
441         {
442             if (Duration > 300 && Direction != lastDirection && !pending)
443             {
444                 adjustment = Distance / 3.0;
445                 pending = true;
446                 Debug.Write(wxString::Format("Stiction: reduced distance by %0.2f\n", adjustment));
447                 rslt = -adjustment;
448             }
449             else
450             {
451                 if (pending)
452                 {
453                     if (Direction == lastDirection)
454                     {
455                         rslt = adjustment;
456                         Debug.Write(wxString::Format("Stiction: increased distance by %0.2f\n", adjustment));
457                         adjustment = 0;
458                     }
459                     pending = false;
460                 }
461             }
462         }
463         lastDirection = Direction;
464         return rslt;
465     }
466 };
467 
468 static const double AMBIENT_TEMP = 15.;
469 static const double MIN_COOLER_TEMP = -15.;
470 
471 struct Cooler
472 {
473     bool on;
474     double startTemp;
475     double endTemp;
476     double setTemp;
477     time_t endTime;
478     double rate; // degrees per second
479     double direction; // -1 = cooling, +1 = warming
480 
CoolerCooler481     Cooler() : on(false), startTemp(AMBIENT_TEMP), endTemp(AMBIENT_TEMP), setTemp(AMBIENT_TEMP), endTime(0), rate(1. / 8.), direction(0.) { }
482 
CurrentTempCooler483     double CurrentTemp() const
484     {
485         time_t now = wxDateTime::GetTimeNow();
486 
487         if (now >= endTime)
488             return endTemp;
489 
490         return endTemp - rate * direction * (double)(endTime - now);
491     }
492 
TurnOnCooler493     void TurnOn()
494     {
495         if (!on)
496         {
497             _SetTemp(CurrentTemp());
498             on = true;
499         }
500     }
501 
TurnOffCooler502     void TurnOff()
503     {
504         if (on)
505         {
506             _SetTemp(AMBIENT_TEMP);
507             on = false;
508         }
509     }
510 
_SetTempCooler511     void _SetTemp(double newtemp)
512     {
513         startTemp = CurrentTemp();
514         endTemp = std::max(std::min(newtemp, AMBIENT_TEMP), MIN_COOLER_TEMP);
515         double dt = ceil(fabs(endTemp - startTemp) / rate);
516         endTime = wxDateTime::GetTimeNow() + (time_t) dt;
517         direction = endTemp < startTemp ? -1. : +1.;
518     }
519 
SetTempCooler520     void SetTemp(double newtemp)
521     {
522         assert(on);
523         _SetTemp(newtemp);
524         setTemp = newtemp;
525     }
526 };
527 
528 struct SimCamState
529 {
530     unsigned int width;
531     unsigned int height;
532     wxVector<SimStar> stars; // star positions and intensities (ra, dec)
533     wxVector<wxPoint> hotpx; // hot pixels
534     double ra_ofs;           // assume no backlash in RA
535     BacklashVal dec_ofs;     // simulate backlash in DEC
536     double cum_dec_drift;    // cumulative dec drift
537     wxStopWatch timer;       // platform-independent timer
538     long last_exposure_time; // last expoure time, milliseconds
539     Cooler cooler;           // simulated cooler
540     StictionSim stictionSim;
541 
542 #ifdef SIMDEBUG
543     wxFFile DebugFile;
544     double last_ra_move;
545     double last_dec_move;
546 #endif
547 
548 #ifdef SIM_FILE_DISPLACEMENTS
549     wxFileInputStream* pIStream;
550     wxTextInputStream* pText;
551     double scaleConversion;
552     void ReadDisplacements(double& cumX, double& cumY);
553 #endif
554 
555 #if SIMMODE == 1
556     wxDir dir;
557     bool dirStarted;
558     bool ReadNextImage(usImage& img, const wxRect& subframe);
559 #endif
560 
561     void Initialize();
562     void FillImage(usImage& img, const wxRect& subframe, int exptime, int gain, int offset);
563 };
564 
Initialize()565 void SimCamState::Initialize()
566 {
567     width = SimCamParams::width;
568     height = SimCamParams::height;
569     // generate stars at random positions but no closer than 12 pixels from any edge
570     unsigned int const nr_stars = SimCamParams::nr_stars;
571     stars.resize(nr_stars);
572     unsigned int const border = SimCamParams::border;
573 
574     srand(2); // always generate the same stars
575     for (unsigned int i = 0; i < nr_stars; i++)
576     {
577         // generate stars in ra/dec coordinates
578         stars[i].pos.x = (double)(rand() % (width - 2 * border)) - 0.5 * width;
579         stars[i].pos.y = (double)(rand() % (height - 2 * border)) - 0.5 * height;
580         double r = (double) (rand() % 90) / 3.0; // 0..30
581         if (i == 10)
582             stars[i].inten = 30.1;                              // Always have one saturated star
583         else
584             stars[i].inten = 0.1 + (double) (r * r * r) / 9000.0;
585 
586         // force a couple stars to be close together. This is a useful test for Star::AutoFind
587         if (i == 3)
588         {
589             stars[i].pos.x = stars[i - 1].pos.x + 8;
590             stars[i].pos.y = stars[i - 1].pos.y + 8;
591             stars[i].inten = stars[i - 1].inten;
592         }
593     }
594 
595     // generate hot pixels
596     unsigned int const nr_hot = SimCamParams::nr_hot_pixels;
597     hotpx.resize(nr_hot);
598     for (unsigned int i = 0; i < nr_hot; i++) {
599         hotpx[i].x = rand() % width;
600         hotpx[i].y = rand() % height;
601     }
602     srand(clock());
603     ra_ofs = 0.;
604     dec_ofs = BacklashVal(SimCamParams::dec_backlash);
605     cum_dec_drift = 0.;
606     last_exposure_time = 0;
607 
608 #if SIMMODE == 1
609     dirStarted = false;
610 #endif
611 
612 #ifdef SIM_FILE_DISPLACEMENTS
613     pIStream = nullptr;
614     wxString csvName = Debug.GetLogDir() + PATHSEPSTR + SIM_FILE_DISPLACEMENTS_DEFAULT;
615     if (wxFile::Exists(csvName))
616         pIStream = new wxFileInputStream(csvName);
617     else
618     {
619         wxFileDialog dlg(pFrame, _("Choose a star displacements file"), wxEmptyString, wxEmptyString,
620             wxT("Comma-separated files (*.csv)|*.csv"), wxFD_OPEN | wxFD_FILE_MUST_EXIST);
621         dlg.SetDirectory(Debug.GetLogDir());
622         if (dlg.ShowModal() == wxID_OK)
623         {
624             pIStream = new wxFileInputStream(dlg.GetPath());
625             if (!pIStream->IsOk())
626             {
627                 wxMessageBox(_("Can't use this file for star displacements"));
628             }
629         }
630         else
631             wxMessageBox(_("Can't simulate any star movement without a displacement file"));
632     }
633     if (pIStream && pIStream->IsOk())
634         pText = new wxTextInputStream(*pIStream);
635     else
636         pText = nullptr;                   // User cancelled open dialog or file is useless
637     scaleConversion = 1.0;          // safe default
638 #endif
639 
640 #ifdef SIMDEBUG
641     DebugFile.Open ("Sim_Debug.txt", "w");
642 #ifdef SIM_FILE_DISPLACEMENTS
643     DebugFile.Write ("Total_X, Total_Y, RA_Ofs, Dec_Ofs \n");
644 #else
645     DebugFile.Write ("PE, Drift, RA_Seeing, Dec_Seeing, Total_X, Total_Y, RA_Ofs, Dec_Ofs, \n");
646 #endif
647 #endif
648 }
649 
650 #if SIMMODE == 1
ReadNextImage(usImage & img,const wxRect & subframe)651 bool SimCamState::ReadNextImage(usImage& img, const wxRect& subframe)
652 {
653     wxString filename;
654 
655     if (!dir.IsOpened())
656     {
657         dir.Open(wxFileName(Debug.GetLogDir(), "sim_images").GetFullPath());
658     }
659     if (dir.IsOpened())
660     {
661         if (!dirStarted)
662         {
663             dir.GetFirst(&filename, "*.fit", wxDIR_FILES);
664             dirStarted = true;
665         }
666         else
667         {
668             if (!dir.GetNext(&filename))
669                 dir.GetFirst(&filename, "*.fit", wxDIR_FILES);
670         }
671     }
672     if (filename.IsEmpty())
673     {
674         return true;
675     }
676 
677     Debug.Write("Sim file opened: " + filename + "\n");
678     fitsfile *fptr;  // FITS file pointer
679     int status = 0;  // CFITSIO status value MUST be initialized to zero!
680 
681     if (PHD_fits_open_diskfile(&fptr, wxFileName(dir.GetName(), filename).GetFullPath(), READONLY, &status))
682         return true;
683 
684     int hdutype;
685     if (fits_get_hdu_type(fptr, &hdutype, &status) || hdutype != IMAGE_HDU)
686     {
687         pFrame->Alert(_("FITS file is not of an image"));
688         PHD_fits_close_file(fptr);
689         return true;
690     }
691 
692     int naxis;
693     fits_get_img_dim(fptr, &naxis, &status);
694 
695     int nhdus;
696     fits_get_num_hdus(fptr, &nhdus, &status);
697     if ((nhdus != 1) || (naxis != 2)) {
698         pFrame->Alert(_("Unsupported type or read error loading FITS file"));
699         PHD_fits_close_file(fptr);
700         return true;
701     }
702 
703     long fits_size[2];
704     fits_get_img_size(fptr, 2, fits_size, &status);
705 
706     int xsize = (int) fits_size[0];
707     int ysize = (int) fits_size[1];
708 
709     if (img.Init(xsize, ysize)) {
710         pFrame->Alert(_("Memory allocation error"));
711         PHD_fits_close_file(fptr);
712         return true;
713     }
714 
715     unsigned short *buf = new unsigned short[img.NPixels];
716     bool useSubframe = !subframe.IsEmpty();
717     wxRect frame;
718     if (useSubframe)
719         frame = subframe;
720     else
721         frame = wxRect(0, 0, xsize, ysize);
722 
723     long inc[] = { 1, 1 };
724     long fpixel[] = { frame.GetLeft() + 1, frame.GetTop() + 1 };
725     long lpixel[] = { frame.GetRight() + 1, frame.GetBottom() + 1 };
726     if (fits_read_subset(fptr, TUSHORT, fpixel, lpixel, inc, nullptr, buf, nullptr, &status))
727     {
728         pFrame->Alert(_("Error reading data"));
729         PHD_fits_close_file(fptr);
730         return true;
731     }
732 
733     if (useSubframe)
734     {
735         img.Subframe = subframe;
736 
737         // Clear out the image
738         img.Clear();
739 
740         int i = 0;
741         for (int y = 0; y < subframe.height; y++)
742         {
743             unsigned short *dst = img.ImageData + (y + subframe.y) * xsize + subframe.x;
744             for (int x = 0; x < subframe.width; x++, i++)
745                 *dst++ = (unsigned short) buf[i];
746         }
747     }
748     else
749     {
750         for (unsigned int i = 0; i < img.NPixels; i++)
751             img.ImageData[i] = (unsigned short) buf[i];
752     }
753 
754     delete[] buf;
755 
756     PHD_fits_close_file(fptr);
757 
758     return false;
759 }
760 #endif // SIMMODE == 1
761 
762 // get a pair of normally-distributed independent random values - Box-Muller algorithm, sigma=1
rand_normal(double r[2])763 static void rand_normal(double r[2])
764 {
765     double u = (double) rand() / RAND_MAX;
766     double v = (double) rand() / RAND_MAX;
767     double const a = sqrt(-2.0 * log(u));
768     double const p = 2 * M_PI * v;
769     r[0] = a * cos(p);
770     r[1] = a * sin(p);
771 }
772 
pixel_addr(usImage & img,int x,int y)773 inline static unsigned short *pixel_addr(usImage& img, int x, int y)
774 {
775     if (x < 0 || x >= img.Size.x)
776         return 0;
777     if (y < 0 || y >= img.Size.y)
778         return 0;
779     return &img.Pixel(x, y);
780 }
781 
set_pixel(usImage & img,int x,int y,unsigned short val)782 inline static void set_pixel(usImage& img, int x, int y, unsigned short val)
783 {
784     unsigned short *const addr = pixel_addr(img, x, y);
785     if (addr)
786         *addr = val;
787 }
788 
incr_pixel(usImage & img,int x,int y,unsigned int val)789 inline static void incr_pixel(usImage& img, int x, int y, unsigned int val)
790 {
791     unsigned short *const addr = pixel_addr(img, x, y);
792     if (addr) {
793         unsigned int t = *addr;
794         t += val;
795         if (t > (unsigned int)(unsigned short)-1)
796             *addr = (unsigned short)-1;
797         else
798             *addr = (unsigned short)t;
799     }
800 }
801 
render_comet(usImage & img,int binning,const wxRect & subframe,const wxRealPoint & p,double inten)802 static void render_comet(usImage& img, int binning, const wxRect& subframe, const wxRealPoint& p, double inten)
803 {
804     enum { WIDTH = 5 };
805     double STAR[][WIDTH] = { { 0.0, 0.8, 2.2, 0.8, 0.0, },
806                              { 0.8, 16.6, 46.1, 16.6, 0.8, },
807                              { 2.2, 46.1, 128.0, 46.1, 2.2, },
808                              { 0.8, 16.6, 46.1, 16.6, 0.8, },
809                              { 0.0, 0.8, 2.2, 0.8, 0.0, },
810                             };
811 
812     wxRealPoint intpart;
813     double fx = modf(p.x / (double) binning, &intpart.x);
814     double fy = modf(p.y / (double) binning, &intpart.y);
815     double f00 = (1.0 - fx) * (1.0 - fy);
816     double f01 = (1.0 - fx) * fy;
817     double f10 = fx * (1.0 - fy);
818     double f11 = fx * fy;
819 
820     double d[WIDTH + 1][WIDTH + 1] = { { 0.0 } };
821     for (unsigned int i = 0; i < WIDTH; i++)
822     for (unsigned int j = 0; j < WIDTH; j++)
823     {
824         double s = STAR[i][j];
825         if (s > 0.0)
826         {
827             s *= inten / 256.0;
828             d[i][j] += f00 * s;
829             d[i + 1][j] += f10 * s;
830             d[i][j + 1] += f01 * s;
831             d[i + 1][j + 1] += f11 * s;
832         }
833     }
834 
835     wxPoint c((int)intpart.x - (WIDTH - 1) / 2,
836         (int)intpart.y - (WIDTH - 1) / 2);
837 
838     for (unsigned int x_inc = 0; x_inc < 10; x_inc++)
839     {
840         for (double y = -1; y < 1.5; y += 0.5)
841         {
842             int const cx = c.x + x_inc;
843             int const cy = c.y + y * x_inc;
844             if (cx < subframe.GetRight() && cy < subframe.GetBottom() && cy > subframe.GetTop())
845                 incr_pixel(img, cx, cy, (int)d[2][2]);
846         }
847 
848     }
849 
850 }
851 
render_star(usImage & img,int binning,const wxRect & subframe,const wxRealPoint & p,double inten)852 static void render_star(usImage& img, int binning, const wxRect& subframe, const wxRealPoint& p, double inten)
853 {
854     enum { WIDTH = 5 };
855     double STAR[][WIDTH] = {{ 0.0,  0.8,   2.2,  0.8, 0.0, },
856                             { 0.8, 16.6,  46.1, 16.6, 0.8, },
857                             { 2.2, 46.1, 128.0, 46.1, 2.2, },
858                             { 0.8, 16.6,  46.1, 16.6, 0.8, },
859                             { 0.0,  0.8,   2.2,  0.8, 0.0, },
860                            };
861 
862     wxRealPoint intpart;
863     double fx = modf(p.x / (double) binning, &intpart.x);
864     double fy = modf(p.y / (double) binning, &intpart.y);
865     double f00 = (1.0 - fx) * (1.0 - fy);
866     double f01 = (1.0 - fx) * fy;
867     double f10 = fx * (1.0 - fy);
868     double f11 = fx * fy;
869 
870     double d[WIDTH + 1][WIDTH + 1] = { { 0.0 } };
871     for (unsigned int i = 0; i < WIDTH; i++)
872         for (unsigned int j = 0; j < WIDTH; j++)
873         {
874             double s = STAR[i][j];
875             if (s > 0.0)
876             {
877                 s *= inten / 256.0;
878                 d[i][j] += f00 * s;
879                 d[i+1][j] += f10 * s;
880                 d[i][j+1] += f01 * s;
881                 d[i+1][j+1] += f11 * s;
882             }
883         }
884 
885     wxPoint c((int) intpart.x - (WIDTH - 1) / 2,
886               (int) intpart.y - (WIDTH - 1) / 2);
887 
888     for (unsigned int i = 0; i < WIDTH + 1; i++)
889     {
890         int const cx = c.x + i;
891         if (cx < subframe.GetLeft() || cx > subframe.GetRight())
892             continue;
893         for (unsigned int j = 0; j < WIDTH + 1; j++)
894         {
895             int const cy = c.y + j;
896             if (cy < subframe.GetTop() || cy > subframe.GetBottom())
897                 continue;
898             int incr = (int) d[i][j];
899             if (incr > (unsigned short)-1)
900                 incr = (unsigned short)-1;
901             incr_pixel(img, cx, cy, incr);
902         }
903     }
904 }
905 
render_clouds(usImage & img,const wxRect & subframe,int exptime,int gain,int offset)906 static void render_clouds(usImage& img, const wxRect& subframe, int exptime, int gain, int offset)
907 {
908     unsigned short cloud_amt;
909     unsigned short *p0 = &img.Pixel(subframe.GetLeft(), subframe.GetTop());
910     for (int r = 0; r < subframe.GetHeight(); r++, p0 += img.Size.GetWidth())
911     {
912         unsigned short *const end = p0 + subframe.GetWidth();
913         for (unsigned short *p = p0; p < end; p++)
914         {
915             // Compute a randomized brightness contribution from clouds, then overlay that on the guide frame
916              cloud_amt = (unsigned short)(SimCamParams::clouds_inten * ((double)gain / 10.0 * offset * exptime / 100.0 + ((rand() % (gain * 100)) / 30.0)));
917              *p = (unsigned short) (SimCamParams::clouds_opacity * cloud_amt + (1 - SimCamParams::clouds_opacity) * *p);
918 
919         }
920     }
921 }
922 
923 #ifdef SIM_FILE_DISPLACEMENTS
924 // Get raw star displacements from a file generated by using the CAPTURE_DEFLECTIONS
925 // compile-time option in guider.cpp to record them
ReadDisplacements(double & incX,double & incY)926 void SimCamState::ReadDisplacements(double& incX, double& incY)
927 {
928     wxStringTokenizer tok;
929 
930     // If we reach the EOF, just start over - we don't want to suddenly reverse direction on linear drifts, and the
931     // underlying seeing behavior is sufficiently random that a simple replay is warranted
932     if (pIStream->Eof())
933         pIStream->SeekI(wxFileOffset(0));
934 
935     if (!pIStream->Eof())
936     {
937         wxString line = pText->ReadLine();
938         line.Trim(false); // trim leading whitespace
939 
940         if (line.StartsWith("DeltaRA"))
941         {
942             // Get the image scale of the underlying raw data stream
943             tok.SetString(line, ", =");
944             wxString tk = tok.GetNextToken();
945             while (tk != "Scale")
946                 tk = tok.GetNextToken();
947             tk = tok.GetNextToken();            // numeric image scale a-s/p
948             double realImageScale;
949             if (tk.ToDouble(&realImageScale))
950             {
951                 // Will use this to scale subsequent raw star displacements to match simulator image scale
952                 scaleConversion = realImageScale / SimCamParams::image_scale;
953             }
954             line = pText->ReadLine();
955             line.Trim(false);
956         }
957 
958         tok.SetString(line, ", ");
959         wxString s1 = tok.GetNextToken();
960         wxString s2 = tok.GetNextToken();
961         double x, y;
962         if (s1.ToDouble(&x) && s2.ToDouble(&y))
963         {
964             incX = x * scaleConversion;
965             incY = y * scaleConversion;
966         }
967         else
968         {
969             Debug.AddLine(wxString::Format("Star_deflections file: bad input starting with %s", line));
970         }
971     }
972 }
973 #endif
974 
FillImage(usImage & img,const wxRect & subframe,int exptime,int gain,int offset)975 void SimCamState::FillImage(usImage& img, const wxRect& subframe, int exptime, int gain, int offset)
976 {
977     unsigned int const nr_stars = stars.size();
978 
979 #ifdef SIMDEBUG
980     static int CountUp (0);
981     if (CountUp == 0)
982     {
983         // Changes in the setup dialog are hard to track - just make sure we are using the params we think we are
984         Debug.AddLine (wxString::Format("SimDebug: img_scale: %.3f, seeing_scale: %.3f", SimCamParams::image_scale, SimCamParams::seeing_scale));
985     }
986     CountUp++;
987 #endif
988 
989     // start with original star positions
990     wxVector<wxRealPoint> pos(nr_stars);
991     for (unsigned int i = 0; i < nr_stars; i++)
992         pos[i] = stars[i].pos;
993 
994     double total_shift_x = 0;
995     double total_shift_y = 0;
996 
997 #ifdef SIM_FILE_DISPLACEMENTS
998 
999     double inc_x;
1000     double inc_y;
1001     if (pText)
1002     {
1003         ReadDisplacements(inc_x, inc_y);
1004         total_shift_x = ra_ofs + inc_x;
1005         total_shift_y = dec_ofs.val() + inc_y;
1006         // If user has disabled guiding, let him see the raw behavior of the displacement data - the
1007         // ra_ofs and dec_ofs variables are normally updated in the ST-4 guide function
1008         if (!pMount->GetGuidingEnabled())
1009         {
1010             ra_ofs += inc_x;
1011             dec_ofs.incr(inc_y);
1012         }
1013     }
1014 
1015 #else // SIM_FILE_DISPLACEMENTS
1016 
1017     long const cur_time = timer.Time();
1018     long const delta_time_ms = last_exposure_time - cur_time;
1019     last_exposure_time = cur_time;
1020 
1021     // simulate worm phase changing with RA slew
1022     double dec, st, ra = 0.;
1023     if (pPointingSource)
1024         pPointingSource->GetCoordinates(&ra, &dec, &st);
1025 
1026     static double s_prev_ra;
1027     double dra = norm(ra - s_prev_ra, -12.0, 12.0);
1028     s_prev_ra = ra;
1029 
1030     // convert RA hours to SI seconds
1031     const double SECONDS_PER_HOUR = 60. * 60.;
1032     const double SIDEREAL_SECONDS_PER_SEC = 0.9973;
1033     dra *= SECONDS_PER_HOUR / SIDEREAL_SECONDS_PER_SEC;
1034     static double s_ra_offset;
1035     s_ra_offset += dra;
1036 
1037     // an increase in RA means the worm moved backwards
1038     double const now = cur_time / 1000. - s_ra_offset;
1039 
1040     // Compute PE - canned PE terms create some "steep" sections of the curve
1041     static double const max_amp = 4.85;         // max amplitude of canned PE
1042     double pe = 0.;
1043 
1044     if (SimCamParams::use_pe)
1045     {
1046         if (SimCamParams::use_default_pe_params)
1047         {
1048             static double const period[] = { 230.5, 122.0, 49.4, 9.56, 76.84, };
1049             static double const amp[] =    {2.02, 0.69, 0.22, 0.137, 0.14};   // in a-s
1050             static double const phase[] =  { 0.0,     1.4, 98.8, 35.9, 150.4, };
1051 
1052             for (unsigned int i = 0; i < WXSIZEOF(period); i++)
1053                 pe += amp[i] * cos((now - phase[i]) / period[i] * 2. * M_PI);
1054 
1055             pe *= (SimCamParams::pe_scale / (max_amp * SimCamParams::image_scale));      // modulated PE in px
1056         }
1057         else
1058         {
1059             pe = SimCamParams::custom_pe_amp * cos(now / SimCamParams::custom_pe_period * 2.0 * M_PI) / SimCamParams::image_scale;
1060         }
1061     }
1062 
1063     // simulate drift in DEC
1064     cum_dec_drift += (double) delta_time_ms * SimCamParams::dec_drift_rate / 1000.;
1065 
1066     // Compute total movements from all sources - ra_ofs and dec_ofs are cumulative sums of all guider movements relative to zero-point
1067     total_shift_x = pe + ra_ofs;
1068     total_shift_y = cum_dec_drift + dec_ofs.val();
1069 
1070     double seeing[2] = { 0.0 };
1071 
1072     // simulate seeing
1073     if (SimCamParams::seeing_scale > 0.0)
1074     {
1075         rand_normal(seeing);
1076         static const double seeing_adjustment = (2.345 * 1.4 * 2.4);        //FWHM, geometry, empirical
1077         double sigma = SimCamParams::seeing_scale / (seeing_adjustment * SimCamParams::image_scale);
1078         seeing[0] *= sigma;
1079         seeing[1] *= sigma;
1080         total_shift_x += seeing[0];
1081         total_shift_y += seeing[1];
1082     }
1083 
1084 #endif // SIM_FILE_DISPLACEMENTS
1085 
1086     for (unsigned int i = 0; i < nr_stars; i++)
1087     {
1088         pos[i].x += total_shift_x;
1089         pos[i].y += total_shift_y;
1090     }
1091 
1092 #ifdef SIMDEBUG
1093 #ifdef SIM_FILE_DISPLACEMENTS
1094     DebugFile.Write(wxString::Format("%.3f, %.3f, %.3f, %.3f\n", total_shift_x, total_shift_y,
1095         ra_ofs, dec_ofs.val()));
1096 #else
1097     DebugFile.Write(wxString::Format( "%.3f, %.3f, %.3f, %.3f, %.3f, %.3f, %.3f, %.3f\n",
1098         pe, drift, seeing[0], seeing[1], total_shift_x, total_shift_y,
1099         ra_ofs, dec_ofs.val()));
1100 #endif
1101 #endif
1102 
1103     // check for pier-flip
1104     if (pPointingSource)
1105     {
1106         PierSide new_side = pPointingSource->SideOfPier();
1107         if (new_side != SimCamParams::pier_side)
1108         {
1109             Debug.Write(wxString::Format("Cam simulator: pointing source pier side changed from %d to %d\n", SimCamParams::pier_side, new_side));
1110             SimCamParams::pier_side = new_side;
1111         }
1112     }
1113 
1114     // convert to camera coordinates
1115     wxVector<wxRealPoint> cc(nr_stars);
1116     double angle = radians(SimCamParams::cam_angle);
1117 
1118     if (SimCamParams::pier_side == PIER_SIDE_WEST)
1119         angle += M_PI;
1120     double const cos_t = cos(angle);
1121     double const sin_t = sin(angle);
1122     for (unsigned int i = 0; i < nr_stars; i++) {
1123         cc[i].x = pos[i].x * cos_t - pos[i].y * sin_t + width / 2.0;
1124         cc[i].y = pos[i].x * sin_t + pos[i].y * cos_t + height / 2.0;
1125     }
1126 
1127 #ifdef STEPGUIDER_SIMULATOR
1128     // add-in AO offset
1129     if (s_sim_ao) {
1130         double const ao_angle = radians(SimAoParams::angle);
1131         double const cos_a = cos(ao_angle);
1132         double const sin_a = sin(ao_angle);
1133         double const ao_x = (double) s_sim_ao->CurrentPosition(RIGHT) * SimAoParams::scale;
1134         double const ao_y = (double) s_sim_ao->CurrentPosition(UP) * SimAoParams::scale;
1135         double const dx = ao_x * cos_a - ao_y * sin_a;
1136         double const dy = ao_x * sin_a + ao_y * cos_a;
1137         for (unsigned int i = 0; i < nr_stars; i++) {
1138             cc[i].x += dx;
1139             cc[i].y += dy;
1140         }
1141     }
1142 #endif // STEPGUIDER_SIMULATOR
1143 
1144     // render each star
1145     if (!pCamera->ShutterClosed)
1146     {
1147         for (unsigned int i = 0; i < nr_stars; i++)
1148         {
1149             double star = stars[i].inten * exptime * gain;
1150             double dark = (double) gain / 10.0 * offset * exptime / 100.0;
1151             double noise = (double)(rand() % (gain * 100));
1152             double inten = star + dark + noise;
1153 
1154             render_star(img, pCamera->Binning, subframe, cc[i], inten);
1155         }
1156 
1157 #ifndef SIM_FILE_DISPLACEMENTS
1158         if (SimCamParams::show_comet)
1159         {
1160             double x = total_shift_x + now * SimCamParams::comet_rate_x / 3600.;
1161             double y = total_shift_y + now * SimCamParams::comet_rate_y / 3600.;
1162             double cx = x * cos_t - y * sin_t + width / 2.0;
1163             double cy = x * sin_t + y * cos_t + height / 2.0;
1164 
1165             double inten = 3.0;
1166             double star = inten * exptime * gain;
1167             double dark = (double) gain / 10.0 * offset * exptime / 100.0;
1168             double noise = (double)(rand() % (gain * 100));
1169             inten = star + dark + noise;
1170 
1171             render_comet(img, pCamera->Binning, subframe, wxRealPoint(cx, cy), inten);
1172         }
1173 #endif
1174     }
1175 
1176     if (SimCamParams::clouds_opacity > 0)
1177         render_clouds(img, subframe, exptime, gain, offset);
1178 
1179     // render hot pixels
1180     for (unsigned int i = 0; i < hotpx.size(); i++)
1181     {
1182         wxPoint p(hotpx[i]);
1183         p.x /= pCamera->Binning;
1184         p.y /= pCamera->Binning;
1185         if (subframe.Contains(p))
1186             set_pixel(img, p.x, p.y, (unsigned short)-1);
1187     }
1188 }
1189 
1190 class CameraSimulator : public GuideCamera
1191 {
1192     SimCamState sim;
1193 public:
1194     CameraSimulator();
1195     ~CameraSimulator();
1196     bool     Capture(int duration, usImage& img, int options, const wxRect& subframe) override;
1197     bool     Connect(const wxString& camId) override;
1198     bool     Disconnect() override;
1199     void     ShowPropertyDialog() override;
HasNonGuiCapture()1200     bool     HasNonGuiCapture() override { return true; }
1201     wxByte   BitsPerPixel() override;
1202     bool     SetCoolerOn(bool on) override;
1203     bool     SetCoolerSetpoint(double temperature) override;
1204     bool     GetCoolerStatus(bool *on, double *setpoint, double *power, double *temperature) override;
1205     bool     GetSensorTemperature(double *temperature) override;
ST4HasNonGuiMove()1206     bool     ST4HasNonGuiMove() override { return true; }
1207     bool     ST4SynchronousOnly() override;
1208     bool     ST4PulseGuideScope(int direction, int duration) override;
1209     PierSide SideOfPier() const;
1210     void     FlipPierSide();
1211 };
1212 
CameraSimulator()1213 CameraSimulator::CameraSimulator()
1214 {
1215     Connected = false;
1216     Name = _T("Simulator");
1217     m_hasGuideOutput = true;
1218     HasShutter = true;
1219     HasGainControl = true;
1220     HasSubframes = true;
1221     PropertyDialogType = PROPDLG_WHEN_CONNECTED;
1222     MaxBinning = 3;
1223     HasCooler = true;
1224 }
1225 
BitsPerPixel()1226 wxByte CameraSimulator::BitsPerPixel()
1227 {
1228 #if 1
1229     return 16;
1230 #else
1231     return 8;
1232 #endif
1233 }
1234 
Connect(const wxString & camId)1235 bool CameraSimulator::Connect(const wxString& camId)
1236 {
1237     load_sim_params();
1238     sim.Initialize();
1239 
1240     struct ConnectInBg : public ConnectCameraInBg
1241     {
1242         CameraSimulator *cam;
1243         ConnectInBg(CameraSimulator *cam_) : cam(cam_) { }
1244         bool Entry()
1245         {
1246 //#define TEST_SLOW_CONNECT
1247 #ifdef TEST_SLOW_CONNECT
1248             for (int i = 0; i < 100; i++)
1249             {
1250                 wxMilliSleep(100);
1251                 if (IsCanceled())
1252                     return true;
1253             }
1254 #endif
1255             return false;
1256         }
1257     };
1258 
1259     bool err = ConnectInBg(this).Run();
1260     if (!err)
1261         Connected = true;
1262 
1263     return err;
1264 }
1265 
Disconnect()1266 bool CameraSimulator::Disconnect()
1267 {
1268     Connected = false;
1269     return false;
1270 }
1271 
~CameraSimulator()1272 CameraSimulator::~CameraSimulator()
1273 {
1274 #ifdef SIMDEBUG
1275     sim.DebugFile.Close();
1276 #endif
1277 #ifdef SIM_FILE_DISPLACEMENTS
1278     if (sim.pText)
1279         delete sim.pText;
1280     if (sim.pIStream)
1281         delete sim.pIStream;
1282 #endif
1283 }
1284 
1285 #if SIMMODE==2
Capture(int duration,usImage & img,int options,const wxRect & subframe)1286 bool CameraSimulator::Capture(int duration, usImage& img, int options, const wxRect& subframe)
1287 {
1288     int xsize, ysize;
1289     wxImage disk_image;
1290     unsigned short *dataptr;
1291     unsigned char *imgptr;
1292 
1293     bool retval = disk_image.LoadFile("/Users/stark/dev/PHD/simimage.bmp");
1294     if (!retval) {
1295         pFrame->Alert(_("Cannot load simulated image"));
1296         return true;
1297     }
1298     xsize = disk_image.GetWidth();
1299     ysize = disk_image.GetHeight();
1300     if (img.Init(xsize,ysize)) {
1301         pFrame->Alert(_("Memory allocation error"));
1302         return true;
1303     }
1304 
1305     dataptr = img.ImageData;
1306     imgptr = disk_image.GetData();
1307     for (unsigned int i = 0; i < img.NPixels; i++, dataptr++, imgptr++) {
1308         *dataptr = (unsigned short) *imgptr;
1309         imgptr++; imgptr++;
1310     }
1311     QuickLRecon(img);
1312     return false;
1313 
1314 }
1315 #endif
1316 
1317 #if SIMMODE == 3
fill_noise(usImage & img,const wxRect & subframe,int exptime,int gain,int offset)1318 static void fill_noise(usImage& img, const wxRect& subframe, int exptime, int gain, int offset)
1319 {
1320     unsigned short *p0 = &img.Pixel(subframe.GetLeft(), subframe.GetTop());
1321     for (int r = 0; r < subframe.GetHeight(); r++, p0 += img.Size.GetWidth())
1322     {
1323         unsigned short *const end = p0 + subframe.GetWidth();
1324         for (unsigned short *p = p0; p < end; p++)
1325             *p = (unsigned short) (SimCamParams::noise_multiplier * ((double) gain / 10.0 * offset * exptime / 100.0 + (rand() % (gain * 100))));
1326     }
1327 }
1328 #endif // SIMMODE == 3
1329 
Capture(int duration,usImage & img,int options,const wxRect & subframeArg)1330 bool CameraSimulator::Capture(int duration, usImage& img, int options, const wxRect& subframeArg)
1331 {
1332     wxRect subframe(subframeArg);
1333     CameraWatchdog watchdog(duration, GetTimeoutMs());
1334 
1335     // sleep before rendering the image so that any changes made in the middle of a long exposure (e.g. manual guide pulse) shows up in the image
1336 
1337     if (duration > 5)
1338     {
1339         if (WorkerThread::MilliSleep(duration - 5, WorkerThread::INT_ANY))
1340             return true;
1341         if (watchdog.Expired())
1342         {
1343             DisconnectWithAlert(CAPT_FAIL_TIMEOUT);
1344             return true;
1345         }
1346     }
1347 
1348 #if SIMMODE == 1
1349 
1350     if (!UseSubframes)
1351         subframe = wxRect();
1352 
1353     if (sim.ReadNextImage(img, subframe))
1354         return true;
1355 
1356     FullSize = img.Size;
1357 
1358 #else
1359 
1360     int width = sim.width / Binning;
1361     int height = sim.height / Binning;
1362     FullSize = wxSize(width, height);
1363 
1364     bool usingSubframe = UseSubframes;
1365     if (subframe.width <= 0 || subframe.height <= 0 || subframe.GetRight() >= width || subframe.GetBottom() >= height)
1366         usingSubframe = false;
1367     if (!usingSubframe)
1368         subframe = wxRect(0, 0, FullSize.GetWidth(), FullSize.GetHeight());
1369 
1370     int const exptime = duration;
1371     int const gain = 30;
1372     int const offset = 100;
1373 
1374     if (img.Init(FullSize))
1375     {
1376         pFrame->Alert(_("Memory allocation error"));
1377         return true;
1378     }
1379 
1380     if (usingSubframe)
1381         img.Clear();
1382 
1383     fill_noise(img, subframe, exptime, gain, offset);
1384 
1385     sim.FillImage(img, subframe, exptime, gain, offset);
1386 
1387     if (usingSubframe)
1388         img.Subframe = subframe;
1389 
1390     if (options & CAPTURE_SUBTRACT_DARK) SubtractDark(img);
1391 
1392 #endif // SIMMODE == 1
1393 
1394     unsigned int tot_dur = duration + SimCamParams::frame_download_ms;
1395     long elapsed = watchdog.Time();
1396     if (elapsed < tot_dur)
1397     {
1398         if (WorkerThread::MilliSleep(tot_dur - elapsed, WorkerThread::INT_ANY))
1399             return true;
1400         if (watchdog.Expired())
1401         {
1402             DisconnectWithAlert(CAPT_FAIL_TIMEOUT);
1403             return true;
1404         }
1405     }
1406 
1407     return false;
1408 }
1409 
1410 
1411 
ST4PulseGuideScope(int direction,int duration)1412 bool CameraSimulator::ST4PulseGuideScope(int direction, int duration)
1413 {
1414     // Following must take into account how the render_star function works.  Render_star uses camera binning explicitly, so
1415     // relying only on image scale in computing d creates distances that are too small by a factor of <binning>
1416     double d = SimCamParams::guide_rate * Binning * duration / (1000.0 * SimCamParams::image_scale);
1417 
1418     // simulate RA motion scaling according to declination
1419     if (direction == WEST || direction == EAST)
1420     {
1421         double dec = pPointingSource->GetDeclination();
1422         if (dec == UNKNOWN_DECLINATION)
1423             dec = radians(25.0); // some arbitrary declination
1424         d *= cos(dec);
1425     }
1426 
1427     // simulate stiction if option selected
1428     if (SimCamParams::use_stiction && (direction == NORTH || direction == SOUTH))
1429         d += sim.stictionSim.GetAdjustment(direction, duration, d);
1430 
1431     if (SimCamParams::pier_side == PIER_SIDE_WEST && SimCamParams::reverse_dec_pulse_on_west_side)
1432     {
1433         // after pier flip, North/South have opposite affect on declination
1434         switch (direction) {
1435         case NORTH: direction = SOUTH; break;
1436         case SOUTH: direction = NORTH; break;
1437         }
1438     }
1439 
1440     switch (direction) {
1441     case WEST:    sim.ra_ofs += d;      break;
1442     case EAST:    sim.ra_ofs -= d;      break;
1443     case NORTH:   sim.dec_ofs.incr(d);  break;
1444     case SOUTH:   sim.dec_ofs.incr(-d); break;
1445     default: return true;
1446     }
1447     WorkerThread::MilliSleep(duration, WorkerThread::INT_ANY);
1448     return false;
1449 }
1450 
SetCoolerOn(bool on)1451 bool CameraSimulator::SetCoolerOn(bool on)
1452 {
1453     if (on)
1454         sim.cooler.TurnOn();
1455     else
1456         sim.cooler.TurnOff();
1457 
1458     return false; // no error
1459 }
1460 
SetCoolerSetpoint(double temperature)1461 bool CameraSimulator::SetCoolerSetpoint(double temperature)
1462 {
1463     if (!sim.cooler.on)
1464         return true; // error
1465 
1466     sim.cooler.SetTemp(temperature);
1467     return false;
1468 }
1469 
GetCoolerStatus(bool * onp,double * setpoint,double * power,double * temperature)1470 bool CameraSimulator::GetCoolerStatus(bool *onp, double *setpoint, double *power, double *temperature)
1471 {
1472     bool on = sim.cooler.on;
1473     double cur = sim.cooler.CurrentTemp();
1474 
1475     *onp = on;
1476 
1477     if (on)
1478     {
1479         *setpoint = sim.cooler.setTemp;
1480         *power = cur < MIN_COOLER_TEMP ? 100. : cur >= AMBIENT_TEMP ? 0. : (AMBIENT_TEMP - cur) * 100. / (AMBIENT_TEMP - MIN_COOLER_TEMP);
1481         *temperature = cur;
1482     }
1483     else
1484     {
1485         *temperature = cur;
1486     }
1487 
1488     return false;
1489 }
1490 
GetSensorTemperature(double * temperature)1491 bool CameraSimulator::GetSensorTemperature(double *temperature)
1492 {
1493     bool on;
1494     double setpt, powr;
1495     return GetCoolerStatus(&on, &setpt, &powr, temperature);
1496 }
1497 
SideOfPier() const1498 PierSide CameraSimulator::SideOfPier() const
1499 {
1500     return SimCamParams::pier_side;
1501 }
1502 
ST4SynchronousOnly()1503 bool CameraSimulator::ST4SynchronousOnly()
1504 {
1505     return !SimCamParams::allow_async_st4;
1506 }
1507 
OtherSide(PierSide side)1508 static PierSide OtherSide(PierSide side)
1509 {
1510     return side == PIER_SIDE_EAST ? PIER_SIDE_WEST : PIER_SIDE_EAST;
1511 }
1512 
FlipPierSide()1513 void CameraSimulator::FlipPierSide()
1514 {
1515     SimCamParams::pier_side = OtherSide(SimCamParams::pier_side);
1516     Debug.Write(wxString::Format("CamSimulator FlipPierSide: side = %d  cam_angle = %.1f\n", SimCamParams::pier_side, SimCamParams::cam_angle));
1517 }
1518 
1519 #if SIMMODE == 4
Capture(int duration,usImage & img,int options,const wxRect & subframe)1520 bool CameraSimulator::Capture(int duration, usImage& img, int options, const wxRect& subframe)
1521 {
1522     int xsize, ysize;
1523     //  unsigned short *dataptr;
1524     //  int i;
1525     fitsfile *fptr;  // FITS file pointer
1526     int status = 0;  // CFITSIO status value MUST be initialized to zero!
1527     int hdutype, naxis;
1528     int nhdus=0;
1529     long fits_size[2];
1530     long fpixel[3] = {1,1,1};
1531     //  char keyname[15];
1532     //  char keystrval[80];
1533     static int frame = 0;
1534     static int step = 1;
1535     char fname[256];
1536     sprintf(fname,"/Users/stark/dev/PHD/simimg/DriftSim_%d.fit",frame);
1537     if (!PHD_fits_open_diskfile(&fptr, fname, READONLY, &status))
1538     {
1539         if (fits_get_hdu_type(fptr, &hdutype, &status) || hdutype != IMAGE_HDU) {
1540             pFrame->Alert(_("FITS file is not of an image"));
1541             PHD_fits_close_file(fptr);
1542             return true;
1543         }
1544 
1545         // Get HDUs and size
1546         fits_get_img_dim(fptr, &naxis, &status);
1547         fits_get_img_size(fptr, 2, fits_size, &status);
1548         xsize = (int) fits_size[0];
1549         ysize = (int) fits_size[1];
1550         fits_get_num_hdus(fptr,&nhdus,&status);
1551         if ((nhdus != 1) || (naxis != 2)) {
1552             pFrame->Alert(wxString::Format(_("Unsupported type or read error loading FITS file %d %d"),nhdus,naxis));
1553             PHD_fits_close_file(fptr);
1554             return true;
1555         }
1556         if (img.Init(xsize,ysize)) {
1557             pFrame->Alert(_("Memory allocation error"));
1558             PHD_fits_close_file(fptr);
1559             return true;
1560         }
1561         if (fits_read_pix(fptr, TUSHORT, fpixel, xsize*ysize, nullptr, img.ImageData, nullptr, &status) ) { // Read image
1562             pFrame->Alert(_("Error reading data"));
1563             PHD_fits_close_file(fptr);
1564             return true;
1565         }
1566         PHD_fits_close_file(fptr);
1567         frame = frame + step;
1568         if (frame > 440) {
1569             step = -1;
1570             frame = 439;
1571         }
1572         else if (frame < 0) {
1573             step = 1;
1574             frame = 1;
1575         }
1576 
1577     }
1578     return false;
1579 
1580 }
1581 #endif // SIMMODE == 4
1582 
1583 struct SimCamDialog : public wxDialog
1584 {
1585     wxSlider *pStarsSlider;
1586     wxSlider *pHotpxSlider;
1587     wxSlider *pNoiseSlider;
1588     wxSlider *pCloudSlider;
1589     wxSpinCtrlDouble *pBacklashSpin;
1590     wxSpinCtrlDouble *pDriftSpin;
1591     wxSpinCtrlDouble *pGuideRateSpin;
1592     wxSpinCtrlDouble *pCameraAngleSpin;
1593     wxSpinCtrlDouble *pSeeingSpin;
1594     wxCheckBox* showComet;
1595     wxCheckBox *pUsePECbx;
1596     wxCheckBox *pUseStiction;
1597     wxCheckBox *pReverseDecPulseCbx;
1598     PierSide pPierSide;
1599     wxStaticText *pPiersideLabel;
1600     wxRadioButton *pPEDefaultRb;
1601     wxSpinCtrlDouble *pPEDefScale;
1602     wxRadioButton *pPECustomRb;
1603     wxTextCtrl *pPECustomAmp;
1604     wxTextCtrl *pPECustomPeriod;
1605     wxButton *pPierFlip;
1606     wxButton *pResetBtn;
1607 
1608     SimCamDialog(wxWindow *parent);
~SimCamDialogSimCamDialog1609     ~SimCamDialog() { }
1610     void OnReset(wxCommandEvent& event);
1611     void OnPierFlip(wxCommandEvent& event);
1612     void UpdatePierSideLabel();
1613     void OnRbDefaultPE(wxCommandEvent& evt);
1614     void OnRbCustomPE(wxCommandEvent& evt);
1615     void OnOkClick(wxCommandEvent& evt);
1616 
1617     DECLARE_EVENT_TABLE()
1618 };
1619 
BEGIN_EVENT_TABLE(SimCamDialog,wxDialog)1620 BEGIN_EVENT_TABLE(SimCamDialog, wxDialog)
1621     EVT_BUTTON(wxID_RESET, SimCamDialog::OnReset)
1622     EVT_BUTTON(wxID_CONVERT, SimCamDialog::OnPierFlip)
1623 END_EVENT_TABLE()
1624 
1625 // Utility functions for adding controls with specified properties
1626 static wxSlider *NewSlider(wxWindow *parent, int val, int minval, int maxval, const wxString& tooltip)
1627 {
1628     wxSlider *pNewCtrl = new wxSlider(parent, wxID_ANY, val, minval, maxval, wxDefaultPosition, wxDefaultSize, wxSL_HORIZONTAL | wxSL_VALUE_LABEL);
1629     pNewCtrl->SetToolTip(tooltip);
1630     return pNewCtrl;
1631 }
1632 
NewSpinner(wxWindow * parent,double val,double minval,double maxval,double inc,const wxString & tooltip)1633 static wxSpinCtrlDouble *NewSpinner(wxWindow *parent, double val, double minval, double maxval, double inc,
1634     const wxString& tooltip)
1635 {
1636     wxSize sz = pFrame->GetTextExtent(wxString::Format("%.2f", maxval * 10.));
1637     wxSpinCtrlDouble *pNewCtrl = pFrame->MakeSpinCtrlDouble(parent, wxID_ANY, wxEmptyString, wxDefaultPosition,
1638         sz, wxSP_ARROW_KEYS, minval, maxval, val, inc);
1639     pNewCtrl->SetDigits(2);
1640     pNewCtrl->SetToolTip(tooltip);
1641     return pNewCtrl;
1642 }
1643 
NewCheckBox(wxWindow * parent,bool val,const wxString & label,const wxString & tooltip)1644 static wxCheckBox *NewCheckBox(wxWindow *parent, bool val, const wxString& label, const wxString& tooltip)
1645 {
1646     wxCheckBox *pNewCtrl = new wxCheckBox(parent, wxID_ANY, label);
1647     pNewCtrl->SetValue(val);
1648     pNewCtrl->SetToolTip(tooltip);
1649     return pNewCtrl;
1650 }
1651 
1652 // Utility function to add the <label, input> pairs to a grid including tool-tips
AddTableEntryPair(wxWindow * parent,wxFlexGridSizer * pTable,const wxString & label,wxWindow * pControl)1653 static void AddTableEntryPair(wxWindow *parent, wxFlexGridSizer *pTable, const wxString& label, wxWindow *pControl)
1654 {
1655     wxStaticText *pLabel = new wxStaticText(parent, wxID_ANY, label + _(": "), wxPoint(-1,-1), wxSize(-1,-1));
1656     pTable->Add(pLabel, 1, wxALL, 5);
1657     pTable->Add(pControl, 1, wxALL, 5);
1658 }
1659 
AddCustomPEField(wxWindow * parent,wxFlexGridSizer * pTable,const wxString & label,const wxString & tip,double val)1660 static wxTextCtrl *AddCustomPEField(wxWindow *parent, wxFlexGridSizer *pTable, const wxString& label, const wxString& tip, double val)
1661 {
1662     int width;
1663     int height;
1664 
1665     parent->GetTextExtent(_T("999.9"), &width, &height);
1666     wxTextCtrl *pCtrl = new wxTextCtrl(parent, wxID_ANY, _T("    "), wxDefaultPosition, wxSize(width+30, -1));
1667     pCtrl->SetValue(wxString::Format("%.1f", val));
1668     pCtrl->SetToolTip(tip);
1669     AddTableEntryPair(parent, pTable, label, pCtrl);
1670     return pCtrl;
1671 }
1672 
SetRBState(SimCamDialog * dlg,bool using_defaults)1673 static void SetRBState(SimCamDialog *dlg, bool using_defaults)
1674 {
1675     dlg->pPEDefScale->Enable(using_defaults);
1676     dlg->pPECustomAmp->Enable(!using_defaults);
1677     dlg->pPECustomPeriod->Enable(!using_defaults);
1678 }
1679 
SetControlStates(SimCamDialog * dlg,bool captureActive)1680 static void SetControlStates(SimCamDialog *dlg, bool captureActive)
1681 {
1682     bool enable = !captureActive;
1683 
1684     dlg->pBacklashSpin->Enable(enable);
1685     dlg->pGuideRateSpin->Enable(enable);
1686     dlg->pCameraAngleSpin->Enable(enable);
1687     dlg->pPEDefaultRb->Enable(enable);
1688     dlg->pPEDefScale->Enable(enable);
1689     dlg->pPECustomAmp->Enable(enable);
1690     dlg->pPECustomPeriod->Enable(enable);
1691     dlg->pPECustomRb->Enable(enable);
1692     dlg->pUsePECbx->Enable(enable);
1693     dlg->pUseStiction->Enable(false);                           // no good for end-users
1694     dlg->pPierFlip->Enable(enable);
1695     dlg->pReverseDecPulseCbx->Enable(enable);
1696     dlg->pResetBtn->Enable(enable);
1697 }
1698 
1699 // Event handlers
OnRbDefaultPE(wxCommandEvent & evt)1700 void SimCamDialog::OnRbDefaultPE(wxCommandEvent& evt)
1701 {
1702     SetRBState(this, true);
1703 }
1704 
OnRbCustomPE(wxCommandEvent & evt)1705 void SimCamDialog::OnRbCustomPE(wxCommandEvent& evt)
1706 {
1707     SetRBState(this, false);
1708 }
1709 
1710 // Need to enforce semantics on free-form user input
OnOkClick(wxCommandEvent & evt)1711 void SimCamDialog::OnOkClick(wxCommandEvent& evt)
1712 {
1713     bool bOk = true;
1714 
1715     if (pPECustomRb->GetValue())
1716     {
1717         wxString sAmp = pPECustomAmp->GetValue();
1718         wxString sPeriod = pPECustomPeriod->GetValue();
1719         double amp;
1720         double period;
1721         if (sAmp.ToDouble(&amp) && sPeriod.ToDouble(&period))
1722         {
1723             if (amp <= 0.0 || period <= 0.0)
1724             {
1725                 wxMessageBox(_("PE amplitude and period must be > 0"), "Error", wxOK | wxICON_ERROR);
1726                 bOk = false;
1727             }
1728         }
1729         else
1730         {
1731             wxMessageBox(_("PE amplitude and period must be numbers > 0"), "Error", wxOK | wxICON_ERROR);
1732             bOk = false;
1733         }
1734     }
1735 
1736     if (bOk)
1737         wxDialog::EndModal(wxID_OK);
1738 }
1739 
SimCamDialog(wxWindow * parent)1740 SimCamDialog::SimCamDialog(wxWindow *parent)
1741     : wxDialog(parent, wxID_ANY, _("Camera Simulator"))
1742 {
1743     wxBoxSizer *pVSizer = new wxBoxSizer(wxVERTICAL);
1744     double imageScale = pFrame->GetCameraPixelScale();
1745 
1746     SimCamParams::image_scale = imageScale;
1747 
1748     // Camera group controls
1749     wxStaticBoxSizer *pCamGroup = new wxStaticBoxSizer(wxVERTICAL, this, _("Camera"));
1750     wxFlexGridSizer *pCamTable = new wxFlexGridSizer(1, 6, 15, 15);
1751     pStarsSlider = NewSlider(this, SimCamParams::nr_stars, 1, 100, _("Number of simulated stars"));
1752     AddTableEntryPair(this, pCamTable, _("Stars"), pStarsSlider);
1753     pHotpxSlider = NewSlider(this, SimCamParams::nr_hot_pixels, 0, 50, _("Number of hot pixels"));
1754     AddTableEntryPair(this, pCamTable, _("Hot pixels"), pHotpxSlider);
1755     pNoiseSlider = NewSlider(this, (int)floor(SimCamParams::noise_multiplier * 100 / NOISE_MAX), 0, 100,
1756         /* xgettext:no-c-format */ _("% Simulated noise"));
1757     AddTableEntryPair(this, pCamTable, _("Noise"), pNoiseSlider);
1758     pCamGroup->Add(pCamTable);
1759 
1760     // Mount group controls
1761     wxStaticBoxSizer *pMountGroup = new wxStaticBoxSizer(wxVERTICAL, this, _("Mount"));
1762     wxFlexGridSizer *pMountTable = new wxFlexGridSizer(2, 6, 5, 15);
1763     pBacklashSpin = NewSpinner(this, SimCamParams::dec_backlash * imageScale, 0, DEC_BACKLASH_MAX, 0.1, _("Dec backlash, arc-secs"));
1764     AddTableEntryPair(this, pMountTable, _("Dec backlash"), pBacklashSpin);
1765     pDriftSpin = NewSpinner(this, SimCamParams::dec_drift_rate * 60.0 * imageScale, -DEC_DRIFT_MAX, DEC_DRIFT_MAX, 0.5, _("Dec drift, arc-sec/min"));
1766     AddTableEntryPair(this, pMountTable, _("Dec drift"), pDriftSpin);
1767     pGuideRateSpin = NewSpinner(this, SimCamParams::guide_rate / 15.0, 0.25, GUIDE_RATE_MAX, 0.25, _("Guide rate, x sidereal"));
1768     AddTableEntryPair(this, pMountTable, _("Guide rate"), pGuideRateSpin);
1769     pUseStiction = NewCheckBox(this, SimCamParams::use_stiction, _("Apply stiction"), _("Simulate dec axis stiction"));
1770     pUseStiction->Enable(false);                            // too crude to put in hands of users
1771     pMountTable->Add(pUseStiction, 1, wxBOTTOM, 15);
1772     pMountGroup->Add(pMountTable);
1773 
1774     // Add embedded group for PE info (still within mount group)
1775     wxStaticBoxSizer *pPEGroup = new wxStaticBoxSizer(wxVERTICAL, this, _("PE"));
1776     pUsePECbx = NewCheckBox(this, SimCamParams::use_pe, _("Apply PE"), _("Simulate periodic error"));
1777     wxBoxSizer *pPEHorSizer = new wxBoxSizer(wxHORIZONTAL);
1778     // Default PE parameters
1779     wxFlexGridSizer *pPEDefaults = new wxFlexGridSizer(1, 3, 10, 10);
1780     pPEDefaultRb = new wxRadioButton(this, wxID_ANY, _("Default curve"), wxDefaultPosition, wxDefaultSize, wxRB_GROUP);
1781     pPEDefaultRb->SetValue(SimCamParams::use_default_pe_params);
1782     pPEDefaultRb->SetToolTip(_("Use a built-in PE curve that has some steep and smooth sections."));
1783     pPEDefaultRb->Bind(wxEVT_COMMAND_RADIOBUTTON_SELECTED, &SimCamDialog::OnRbDefaultPE, this);                // Event handler binding
1784     wxStaticText *pSliderLabel = new wxStaticText(this, wxID_ANY, _("Amplitude: "),wxPoint(-1,-1),wxSize(-1,-1));
1785     pPEDefScale = NewSpinner(this, SimCamParams::pe_scale, 0, PE_SCALE_MAX, 0.5, _("PE Amplitude, arc-secs"));
1786 
1787     int hor_spacing = StringWidth(this, "9");
1788     pPEDefaults->Add(pPEDefaultRb);
1789     pPEDefaults->Add(pSliderLabel, wxSizerFlags().Border(wxLEFT, hor_spacing));
1790     pPEDefaults->Add(pPEDefScale, wxSizerFlags().Border(wxLEFT, hor_spacing + 1));
1791     // Custom PE parameters
1792     wxFlexGridSizer *pPECustom = new wxFlexGridSizer(1, 5, 10, 10);
1793     pPECustomRb = new wxRadioButton(this, wxID_ANY, _("Custom curve"), wxDefaultPosition, wxDefaultSize);
1794     pPECustomRb->SetValue(!SimCamParams::use_default_pe_params);
1795     pPECustomRb->SetToolTip(_("Use a simple sinusoidal curve. You can specify the amplitude and period."));
1796     pPECustomRb->Bind(wxEVT_COMMAND_RADIOBUTTON_SELECTED, &SimCamDialog::OnRbCustomPE, this);              // Event handler binding
1797     pPECustom->Add(pPECustomRb, wxSizerFlags().Border(wxTOP, 4));
1798     pPECustomAmp = AddCustomPEField(this, pPECustom, _("Amplitude"), _("Amplitude, arc-secs"), SimCamParams::custom_pe_amp);
1799     pPECustomPeriod = AddCustomPEField(this, pPECustom, _("Period"), _("Period, seconds"), SimCamParams::custom_pe_period);
1800     // VSizer for default and custom controls
1801     wxBoxSizer *pPEVSizer = new wxBoxSizer(wxVERTICAL);
1802     pPEVSizer->Add(pPEDefaults, wxSizerFlags().Border(wxLEFT, 60));
1803     pPEVSizer->Add(pPECustom, wxSizerFlags().Border(wxLEFT, 60));
1804     // Finish off the whole PE group
1805     pPEHorSizer->Add(pUsePECbx);
1806     pPEHorSizer->Add(pPEVSizer);
1807     pPEGroup->Add(pPEHorSizer);
1808 
1809     // Now add some miscellaneous mount-related stuff (still within mount group)
1810     wxBoxSizer *pMiscSizer = new wxBoxSizer(wxHORIZONTAL);
1811     pReverseDecPulseCbx = NewCheckBox(this, SimCamParams::reverse_dec_pulse_on_west_side, _("Reverse Dec pulse on West side of pier"),
1812         _("Simulate a mount that reverses guide pulse direction after a meridian flip, like an ASCOM pulse-guided mount."));
1813     pPierSide = SimCamParams::pier_side;
1814     pPiersideLabel = new wxStaticText(this, wxID_ANY, _("Side of Pier: MMMMM"));
1815     pMiscSizer->Add(pReverseDecPulseCbx, wxSizerFlags().Border(10).Expand());
1816     pPierFlip = new wxButton(this, wxID_CONVERT, _("Pier Flip"));
1817     pMiscSizer->Add(pPierFlip, wxSizerFlags().Border(wxLEFT, 30).Expand());
1818     pMiscSizer->Add(pPiersideLabel , wxSizerFlags().Border(wxLEFT, 30).Expand());
1819     pMountGroup->Add(pPEGroup, wxSizerFlags().Center().Border(10).Expand());
1820     pMountGroup->Add(pMiscSizer, wxSizerFlags().Border(wxTOP, 10).Expand());
1821 
1822     // Session group controls
1823     wxStaticBoxSizer *pSessionGroup = new wxStaticBoxSizer(wxVERTICAL, this, _("Session"));
1824     wxFlexGridSizer *pSessionTable = new wxFlexGridSizer(1, 6, 15, 15);
1825     pCameraAngleSpin = NewSpinner(this, SimCamParams::cam_angle, 0, CAM_ANGLE_MAX, 10, _("Camera angle, degrees"));
1826     AddTableEntryPair(this, pSessionTable, _("Camera angle"), pCameraAngleSpin);
1827     pSeeingSpin = NewSpinner(this, SimCamParams::seeing_scale, 0, SEEING_MAX, 0.5, _("Seeing, FWHM arc-sec"));
1828     AddTableEntryPair(this, pSessionTable, _("Seeing"), pSeeingSpin);
1829     pCloudSlider = NewSlider(this, (int)(100 * SimCamParams::clouds_opacity), 0, 100, _("% cloud opacity"));
1830     AddTableEntryPair(this, pSessionTable, _("Cloud %"), pCloudSlider);
1831     showComet = new wxCheckBox(this, wxID_ANY, _("Comet"));
1832     showComet->SetValue(SimCamParams::show_comet);
1833     pSessionGroup->Add(pSessionTable);
1834     pSessionGroup->Add(showComet);
1835 
1836     pVSizer->Add(pCamGroup, wxSizerFlags().Border(wxALL, 10).Expand());
1837     pVSizer->Add(pMountGroup, wxSizerFlags().Border(wxRIGHT | wxLEFT, 10));
1838     pVSizer->Add(pSessionGroup, wxSizerFlags().Border(wxRIGHT | wxLEFT, 10).Expand());
1839 
1840     // Now deal with the buttons
1841     wxBoxSizer *pButtonSizer = new wxBoxSizer( wxHORIZONTAL );
1842     pResetBtn = new wxButton(this, wxID_RESET, _("Reset"));
1843     pResetBtn->SetToolTip(_("Reset all values to application defaults"));
1844     pButtonSizer->Add(
1845         pResetBtn,
1846         wxSizerFlags(0).Align(0).Border(wxALL, 10));
1847     // Need to handle the OK event ourselves to validate text input fields
1848     wxButton *pBtn = new wxButton(this, wxID_OK, _("OK"));
1849     pBtn->Bind(wxEVT_COMMAND_BUTTON_CLICKED, &SimCamDialog::OnOkClick, this);
1850     pButtonSizer->Add(
1851         pBtn,
1852         wxSizerFlags(0).Align(0).Border(wxALL, 10));
1853     pButtonSizer->Add(
1854         new wxButton( this, wxID_CANCEL, _("Cancel") ),
1855         wxSizerFlags(0).Align(0).Border(wxALL, 10));
1856 
1857      //position the buttons centered with no border
1858     pVSizer->Add(
1859         pButtonSizer,
1860         wxSizerFlags(0).Center() );
1861 
1862     SetSizerAndFit(pVSizer);
1863     SetControlStates(this, pFrame->CaptureActive);
1864     if (!pFrame->CaptureActive)
1865         SetRBState(this, pPEDefaultRb->GetValue());        // Enable matching PE-related controls
1866     UpdatePierSideLabel();
1867 }
1868 
OnReset(wxCommandEvent & event)1869 void SimCamDialog::OnReset(wxCommandEvent& event)
1870 {
1871     pStarsSlider->SetValue(NR_STARS_DEFAULT);
1872     pHotpxSlider->SetValue(NR_HOT_PIXELS_DEFAULT);
1873     pNoiseSlider->SetValue((int)floor(NOISE_DEFAULT * 100.0 / NOISE_MAX));
1874     pBacklashSpin->SetValue(DEC_BACKLASH_DEFAULT);
1875     pCloudSlider->SetValue(0);
1876 
1877     pDriftSpin->SetValue(DEC_DRIFT_DEFAULT);
1878     pSeeingSpin->SetValue(SEEING_DEFAULT);
1879     pCameraAngleSpin->SetValue(CAM_ANGLE_DEFAULT);
1880     pGuideRateSpin->SetValue(GUIDE_RATE_DEFAULT / GUIDE_RATE_MAX);
1881     pReverseDecPulseCbx->SetValue(REVERSE_DEC_PULSE_ON_WEST_SIDE_DEFAULT);
1882     pUsePECbx->SetValue(USE_PE_DEFAULT);
1883     pUseStiction->SetValue(USE_STICTION_DEFAULT);
1884     pPEDefaultRb->SetValue(USE_PE_DEFAULT_PARAMS);
1885     pPECustomRb->SetValue(!USE_PE_DEFAULT_PARAMS);
1886     pPEDefScale->SetValue(PE_SCALE_DEFAULT);
1887     pPECustomAmp->SetValue(wxString::Format("%0.1f",PE_CUSTOM_AMP_DEFAULT));
1888     pPECustomPeriod->SetValue(wxString::Format("%0.1f", PE_CUSTOM_PERIOD_DEFAULT));
1889     pPierSide = PIER_SIDE_DEFAULT;
1890     SetRBState( this, USE_PE_DEFAULT_PARAMS);
1891     UpdatePierSideLabel();
1892     showComet->SetValue(SHOW_COMET_DEFAULT);
1893 }
1894 
OnPierFlip(wxCommandEvent & event)1895 void SimCamDialog::OnPierFlip(wxCommandEvent& event)
1896 {
1897     int angle = pCameraAngleSpin->GetValue();
1898     angle += 180;
1899     if (angle >= 360)
1900         angle -= 360;
1901     pCameraAngleSpin->SetValue(angle);
1902     pPierSide= OtherSide(pPierSide);
1903     UpdatePierSideLabel();
1904 }
1905 
UpdatePierSideLabel()1906 void SimCamDialog::UpdatePierSideLabel()
1907 {
1908     pPiersideLabel->SetLabel(wxString::Format(_("Side of pier: %s"), pPierSide == PIER_SIDE_EAST ? _("East") : _("West")));
1909 }
1910 
1911 struct UpdateChecker
1912 {
1913     bool updated;
UpdateCheckerUpdateChecker1914     UpdateChecker() : updated(false) { }
1915     template<typename T, typename U>
UpdateUpdateChecker1916     void Update(T& val, const U& newval) {
1917         if (val != newval)
1918         {
1919             val = newval;
1920             updated = true;
1921         }
1922     }
WasModifiedUpdateChecker1923     bool WasModified() const { return updated; }
1924 };
1925 
ShowPropertyDialog()1926 void CameraSimulator::ShowPropertyDialog()
1927 {
1928     SimCamDialog dlg(pFrame);
1929     double imageScale = pFrame->GetCameraPixelScale();              // arc-sec/pixel, defaults to 1.0 if no user specs
1930     SimCamParams::image_scale = imageScale;                         // keep current - might have gotten changed in brain dialog
1931     if (dlg.ShowModal() == wxID_OK)
1932     {
1933         UpdateChecker upd; // keep track of whether any values changed
1934         upd.Update(SimCamParams::nr_stars, dlg.pStarsSlider->GetValue());
1935         upd.Update(SimCamParams::nr_hot_pixels, dlg.pHotpxSlider->GetValue());
1936         SimCamParams::noise_multiplier = (double) dlg.pNoiseSlider->GetValue() * NOISE_MAX / 100.0;
1937         upd.Update(SimCamParams::dec_backlash, dlg.pBacklashSpin->GetValue() / imageScale);    // a-s -> px
1938 
1939         bool use_pe = dlg.pUsePECbx->GetValue();
1940         SimCamParams::use_pe = use_pe;
1941         SimCamParams::use_stiction = dlg.pUseStiction->GetValue();
1942         bool use_default_pe_params = dlg.pPEDefaultRb->GetValue();
1943         SimCamParams::use_default_pe_params = use_default_pe_params;
1944         if (SimCamParams::use_default_pe_params)
1945         {
1946             SimCamParams::pe_scale = dlg.pPEDefScale->GetValue();
1947         }
1948         else
1949         {
1950             dlg.pPECustomAmp->GetValue().ToDouble(&SimCamParams::custom_pe_amp);
1951             dlg.pPECustomPeriod->GetValue().ToDouble(&SimCamParams::custom_pe_period);
1952         }
1953         SimCamParams::dec_drift_rate =   dlg.pDriftSpin->GetValue() / (imageScale * 60.0);  // a-s per min to px per second
1954         SimCamParams::seeing_scale =     dlg.pSeeingSpin->GetValue();                      // already in a-s
1955         upd.Update(SimCamParams::cam_angle, dlg.pCameraAngleSpin->GetValue());
1956         SimCamParams::guide_rate =       dlg.pGuideRateSpin->GetValue() * 15.0;
1957         SimCamParams::pier_side = dlg.pPierSide;
1958         SimCamParams::reverse_dec_pulse_on_west_side = dlg.pReverseDecPulseCbx->GetValue();
1959         SimCamParams::show_comet = dlg.showComet->GetValue();
1960         SimCamParams::clouds_opacity = dlg.pCloudSlider->GetValue() / 100.0;
1961         save_sim_params();
1962 
1963         if (upd.WasModified())
1964             sim.Initialize();
1965     }
1966 }
1967 
MakeCamSimulator()1968 GuideCamera *GearSimulator::MakeCamSimulator()
1969 {
1970     return new CameraSimulator();
1971 }
1972 
FlipPierSide(GuideCamera * camera)1973 void GearSimulator::FlipPierSide(GuideCamera *camera)
1974 {
1975     if (camera && camera->Name == _T("Simulator"))
1976     {
1977         CameraSimulator *simcam = static_cast<CameraSimulator *>(camera);
1978         simcam->FlipPierSide();
1979     }
1980 }
1981 
MakeAOSimulator()1982 StepGuider *GearSimulator::MakeAOSimulator()
1983 {
1984     return new StepGuiderSimulator();
1985 }
1986 
MakeRotatorSimulator()1987 Rotator *GearSimulator::MakeRotatorSimulator()
1988 {
1989     return new RotatorSimulator();
1990 }
1991 
1992 #endif // SIMULATOR
1993