forked from omega13a/stargen
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdisplay.cpp
More file actions
3419 lines (3085 loc) · 132 KB
/
Copy pathdisplay.cpp
File metadata and controls
3419 lines (3085 loc) · 132 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#include <variant>
#include "display.h"
#include <cmath>
#include <filesystem>
#include <format>
#include <iostream>
#include <nlohmann/json.hpp>
#include <sstream>
#include <stdexcept>
#include "PerformanceMonitor.h"
#include "SimulationContext.h"
#include "const.h"
#include "units.hh" // au-backed exact length conversions (sgu::)
#include "elements.h"
#include "enviro.h"
#include "stargen.h"
#include "utils.h"
#include "PlanetRow.h"
void ensure_directory_exists(const std::string& path) {
std::filesystem::path dir_path(path);
if (!std::filesystem::exists(dir_path)) {
std::filesystem::create_directories(dir_path);
std::cout << "Created directory: " << path << std::endl;
}
}
int random_numberInt(int min, int max) {
// Draw from the active seeded RandomContext (see utils.cpp) rather than a
// static std::random_device generator. The old static RNG was (a) seeded
// from system entropy -> Celestia (-c) texture indices differed every run,
// (b) a shared-mutable static -> a latent data race if ever reached from the
// parallel path, and (c) buggy: the static uniform_int_distribution was
// constructed once and ignored min/max on later calls. This routes through
// the same deterministic path as random_number() and respects min/max.
return random_number_int(min, max);
}
/**
* @brief Output system to text
*
* @param innermost_planet
* @param do_gases
* @param seed
* @param do_moons
*/
/**
* @brief Get planet classification symbol for text output
*/
static auto get_planet_symbol(planet* the_planet) -> char {
if (the_planet->getGreenhouseEffect() && the_planet->getSurfPressure() > 0.0) {
return '+'; // Greenhouse effect
}
if ((the_planet->getHydrosphere() > 0.05) && (the_planet->getHydrosphere() < 0.8)) {
return '*'; // Water world
}
if ((the_planet->getMass() * SUN_MASS_IN_EARTH_MASSES) > 0.1) {
return 'o'; // Significant mass
}
return '.'; // Small body
}
/**
* @brief Print system characteristics header
*/
static void text_print_system_header(sun& the_sun, long int seed) {
std::cout << std::format("Stargen - V{}; seed={}\n", stargen_revision, seed)
<< " SYSTEM CHARACTERISTICS\n";
if (!the_sun.getIsCircumbinary()) {
std::cout << std::format("Stellar mass: {} solar masses\n", the_sun.getMass())
<< std::format("Stellar luminosity: {}\n", the_sun.getLuminosity());
} else {
std::cout << std::format("Mass of Primary: {} solar masses\n", the_sun.getMass())
<< std::format("Luminosity of Primary: {}\n", the_sun.getLuminosity())
<< std::format("Mass of Secondary: {} solar masses\n", the_sun.getSecondaryMass())
<< std::format("Luminosity of Secondary: {}\n", the_sun.getSecondaryLuminosity());
}
std::cout << std::format("Stellar metallicity: [Fe/H] = {} dex\n", the_sun.getMetallicity());
std::cout << std::format("Age: {} billion years\t({} billion left on main sequence)\n",
the_sun.getAge() / 1.0E9,
(the_sun.getLife() - the_sun.getAge()) / 1.0E9)
<< std::format("Habitable ecosphere radius: {} AU\n\n",
AVE(habitable_zone_distance(the_sun, RECENT_VENUS, 1.0),
habitable_zone_distance(the_sun, EARLY_MARS, 1.0)));
}
/**
* @brief Print compact planet list
*/
static void text_print_planet_list() {
std::cout << "Planets present at:\n";
int counter = 1;
for (planet* the_planet : g_sim_context.planets) {
std::cout << std::format(
"{}\t{} AU\t{} EM\t{}\n",
counter,
the_planet->getA(),
the_planet->getMass() * SUN_MASS_IN_EARTH_MASSES,
get_planet_symbol(the_planet));
counter++;
}
std::cout << "\n\n\n";
}
/**
* @brief Print detailed planet properties
*/
static void text_print_planet_details(planet* the_planet, int counter) {
std::cout << std::format("Planet {}", counter);
if (is_gas_planet(the_planet)) {
std::cout << "\t*gas giant*";
}
std::cout << '\n';
// Tidal locking status
if ((int)the_planet->getDay() == (int)(the_planet->getOrbPeriod() * 24.0)) {
std::cout << "Planet is tidally locked with one face to star.\n";
} else if (the_planet->getResonantPeriod()) {
std::cout << "Planet's rotation is in a resonant spin lock with the star.\n";
}
// Habitability caveats (notably for M-dwarf hosts); see set_habitability_flags.
if (the_planet->getPmsDesiccationRisk()) {
std::cout << "Likely desiccated during the host M-dwarf's pre-main-sequence "
"phase (possible abiotic-O2 \"mirage Earth\").\n";
}
if (the_planet->getHighXuvEscapeRisk()) {
std::cout << "High atmospheric-escape risk from M-dwarf XUV/flare "
"irradiation.\n";
}
if (the_planet->getCo2CollapseRisk()) {
std::cout << "Cold for the habitable zone: outgassed CO2 may freeze out, "
"risking irreversible glaciation.\n";
}
// Orbital properties
std::cout << std::format(" Distance from primary star:\t{} AU\n", the_planet->getA())
<< std::format(" Eccentricity of orbit:\t{}\n", the_planet->getE())
<< std::format(" Length of year:\t\t{} days\n", the_planet->getOrbPeriod())
<< std::format(" Length of day:\t\t{} hours\n", the_planet->getDay())
<< std::format(" Mass:\t\t\t{} Earth masses\n",
the_planet->getMass() * SUN_MASS_IN_EARTH_MASSES);
// Surface properties (rocky planets only)
if (!is_gas_planet(the_planet)) {
std::cout << std::format(" Surface gravity:\t\t{} Earth gees\n", the_planet->getSurfGrav())
<< std::format(" Surface pressure:\t\t{} Earth atmospheres",
the_planet->getSurfPressure() / EARTH_SURF_PRES_IN_MILLIBARS);
if (the_planet->getGreenhouseEffect() && the_planet->getSurfPressure() > 0.0) {
std::cout << " GREENHOUSE EFFECT";
}
std::cout << '\n'
<< std::format(" Surface temperature:\t\t{} degrees Celcius\n",
the_planet->getSurfTemp() - FREEZING_POINT_OF_WATER)
<< std::format(" Boiling point of water:\t{} degrees Celcius\n",
the_planet->getBoilPoint() - FREEZING_POINT_OF_WATER)
<< std::format(" Hydrosphere percentage:\t{}%\n", the_planet->getHydrosphere() * 100.0)
<< std::format(" Cloud cover percentage:\t{}%\n", the_planet->getCloudCover() * 100.0)
<< std::format(" Ice cover percentage:\t{}%\n", the_planet->getIceCover() * 100.0);
if (the_planet->getClimateModelTemp() > 0.0) {
std::cout << std::format(
" 1D-climate surface temp:\t{} degrees Celcius (Lehmer et al. 2020)\n",
the_planet->getClimateModelTemp() - FREEZING_POINT_OF_WATER);
}
}
// Physical properties (all planets)
std::cout << std::format(" Equatorial radius:\t\t{} Km\n", the_planet->getRadius())
<< std::format(" Density:\t\t\t{} grams/cc\n", the_planet->getDensity())
<< std::format(" Escape Velocity:\t\t{} Km/sec\n",
sgu::cm_to_km(the_planet->getEscVelocity()))
<< std::format(" Molecular weight retained:\t{} and above\n",
the_planet->getMolecWeight())
<< std::format(" Surface acceleration:\t{} cm/sec2\n", the_planet->getSurfAccel())
<< std::format(" Axial tilt:\t\t\t{} degrees\n", the_planet->getAxialTilt())
<< std::format(" Planetary albedo:\t\t{}\n", the_planet->getAlbedo()) << "\n\n";
}
void text_describe_system(planet* innermost_planet, bool do_gases, long int seed, bool do_moons) {
performanceMonitor.recordFileOperation("text_output");
do_gases = (flags_arg_clone & fDoGases) != 0;
sun the_sun = innermost_planet->getTheSun();
// Print system header
text_print_system_header(the_sun, seed);
// Print compact planet list
text_print_planet_list();
// Print detailed planet information
int counter = 1;
for (planet* the_planet : g_sim_context.planets) {
text_print_planet_details(the_planet, counter);
counter++;
}
}
/**
* @brief Output to csv file
*
* @param the_file
* @param innermost_planet
* @param do_gases
* @param seed
* @param do_moons
*/
void csv_describe_system(std::fstream& the_file, planet* innermost_planet, bool do_gases, long int seed,
bool do_moons) {
performanceMonitor.recordFileOperation("csv_output");
do_gases = (flags_arg_clone & fDoGases) != 0;
planet* the_planet;
sun the_sun = innermost_planet->getTheSun();
int counter;
std::stringstream ss;
std::string id;
planet* moon;
int moons;
if (!the_sun.getIsCircumbinary()) {
the_file << "'Seed', 'Star Name', 'Luminosity', 'Mass', 'Temperature', "
"'Spectral Type', 'Total Time on Main Sequence', 'Age', "
"'Earth-like Distance'\n";
the_file << seed << ", '" << escapeCsvField(the_sun.getName()) << "', " << toString(the_sun.getLuminosity())
<< ", " << toString(the_sun.getMass()) << ", " << toString(the_sun.getEffTemp())
<< ", '" << escapeCsvField(the_sun.getSpecType()) << "', " << toString(the_sun.getLife()) << ", "
<< toString(the_sun.getAge()) << ", " << toString(the_sun.getREcosphere(1.0)) << "\n";
} else {
the_file << "'Seed', 'Star Name', 'Luminosity of Primary', 'Mass of Primary', "
"'Temperature of Primary', 'Spectral Type of Primary', 'Luminosity "
"of Secondary', 'Mass of Secondary', 'Temperature of Secondary', "
"'Spectral Type of Secondary', 'Seperation', 'Eccentricity', "
"'Combined Temperature', 'Primary's Total Time on Main Sequence', "
"'Age', 'Earth-like Distance'\n";
the_file << seed << ", '" << escapeCsvField(the_sun.getName()) << "', " << toString(the_sun.getLuminosity())
<< ", " << toString(the_sun.getMass()) << ", " << toString(the_sun.getEffTemp())
<< ", '" << escapeCsvField(the_sun.getSpecType()) << "', "
<< toString(the_sun.getSecondaryLuminosity()) << ", "
<< toString(the_sun.getSecondaryMass()) << ", "
<< toString(the_sun.getSecondaryEffTemp()) << ", '" << escapeCsvField(the_sun.getSecondarySpecType())
<< "', " << toString(the_sun.getSeperation()) << ", "
<< toString(the_sun.getEccentricity()) << ", "
<< toString(the_sun.getCombinedEffTemp()) << ", " << toString(the_sun.getLife())
<< ", " << toString(the_sun.getAge()) << ", " << toString(the_sun.getREcosphere(1.0))
<< "\n";
}
the_file << emit_csv_header();
counter = 1;
for (planet* the_planet : g_sim_context.planets) {
ss << the_sun.getName() << " " << counter;
id = ss.str();
ss.str("");
csv_row(the_file, the_planet, do_gases, false, id, ss);
if (do_moons && !the_planet->moons.empty()) {
moons = 1;
for (planet* moon : the_planet->moons) {
ss << the_sun.getName() << " " << counter << "." << moons;
id = ss.str();
ss.str("");
csv_row(the_file, moon, do_gases, true, id, ss);
moons++;
}
}
counter++;
}
ZoneScoped;
}
/**
* @brief Output to JSON
*
* @param the_file
* @param innermost_planet
* @param do_gases
* @param seed
* @param do_moons
*/
void jsonDescribeSystem(std::fstream& the_file, planet* innermost_planet, bool do_gases, long int seed,
bool do_moons) {
performanceMonitor.recordFileOperation("json_output");
do_gases = (flags_arg_clone & fDoGases) != 0;
planet* the_planet;
sun the_sun = innermost_planet->getTheSun();
int counter;
std::stringstream ss;
std::string id;
planet* moon;
int moons;
nlohmann::json header;
if (!the_sun.getIsCircumbinary()) {
header = {
{"schema_version", "1.0.0" },
{"generator", "stargen" },
{"Body Type", "Star" },
{"seed", seed },
{"Star Name", the_sun.getName() },
{"Luminosity", the_sun.getLuminosity() },
{"Mass", the_sun.getMass() },
{"Temperature", the_sun.getEffTemp() },
{"Spectral Type", the_sun.getSpecType() },
{"Metallicity", the_sun.getMetallicity() },
{"Total Time on Main Sequence", the_sun.getLife() },
{"Age", the_sun.getAge() },
{"Earth-like Distance", the_sun.getREcosphere(1.0)}
};
} else {
header = {
{"schema_version", "1.0.0" },
{"generator", "stargen" },
{"Body Type", "Binary Star" },
{"seed", seed },
{"Star Name", the_sun.getName() },
{"Luminosity of Primary", the_sun.getLuminosity() },
{"Mass of Primary", the_sun.getMass() },
{"Temperature of Primary", the_sun.getEffTemp() },
{"Spectral Type of Primary", the_sun.getSpecType() },
{"Luminosity of Secondary", the_sun.getSecondaryLuminosity()},
{"Mass of Secondary", the_sun.getSecondaryMass() },
{"Temperature of Secondary", the_sun.getSecondaryEffTemp() },
{"Spectral Type of Secondary", the_sun.getSecondarySpecType() },
{"Seperation", the_sun.getSeperation() },
{"Eccentricity", the_sun.getEccentricity() },
{"Combined Temperature", the_sun.getCombinedEffTemp() },
{"Metallicity", the_sun.getMetallicity() },
{"Total Time on Main Sequence", the_sun.getLife() },
{"Age", the_sun.getAge() },
{"Earth-like Distance", the_sun.getREcosphere(1.0) }
};
}
nlohmann::json planets = nlohmann::json::array();
counter = 1;
for (planet* the_planet : g_sim_context.planets) {
ss << the_sun.getName() << " " << counter;
id = ss.str();
ss.str("");
nlohmann::json planet_json = jsonRow(the_planet, do_gases, false, id, ss);
if (do_moons && !the_planet->moons.empty()) {
nlohmann::json moon_array = nlohmann::json::array();
moons = 1;
for (planet* moon : the_planet->moons) {
ss << the_sun.getName() << " " << counter << "." << moons;
id = ss.str();
ss.str("");
moon_array.push_back(jsonRow(moon, do_gases, true, id, ss));
moons++;
}
planet_json["Moons"] = std::move(moon_array);
}
planets.push_back(std::move(planet_json));
counter++;
}
nlohmann::json document;
document["star"] = std::move(header);
document["planets"] = std::move(planets);
the_file << document.dump(4) << "\n";
ZoneScoped;
}
/**
* @brief Create a "row" for cvs output. A row is basically a planetoid
*
* @param the_file
* @param the_planet
* @param do_gases
* @param is_moon
* @param id
* @param ss
*/
// Single population point for the per-planet output row consumed by the
// reflective JSON/CSV emitters (PlanetRow.h). Members hold RAW getter values.
// Two atmosphere strings are computed because JSON and CSV format gas pressures
// differently: JSON via toString() (legacy jsonRow formatting), CSV via {:.2f}
// (generate_atmosphere_string).
auto to_row(planet* the_planet, bool is_moon, const std::string& id, bool do_gases) -> PlanetRow {
do_gases = (flags_arg_clone & fDoGases) != 0;
// JSON-style atmosphere string, byte-for-byte the legacy jsonRow inline build.
std::string atmosphere_json;
if (do_gases) {
std::stringstream ss;
for (int i = 0; i < the_planet->getNumGases(); i++) {
int index = gases.count();
bool poisonous = false;
for (int n = 0; n < gases.count(); n++) {
if (gases[n].getNum() == the_planet->getGas(i).getNum()) {
index = n;
break;
}
}
long double ipp = inspired_partial_pressure(the_planet->getSurfPressure(),
the_planet->getGas(i).getSurfPressure());
if (ipp < 0.0) {
ipp = 0.0;
}
if (ipp > gases[index].getMaxIpp()) {
poisonous = true;
}
ss << gases[index].getSymbol() << " "
<< toString(100.0 *
(the_planet->getGas(i).getSurfPressure() / the_planet->getSurfPressure()))
<< " " << toString(the_planet->getGas(i).getSurfPressure()) << " (" << toString(ipp) << ")";
if (poisonous) {
ss << " poisonous";
}
ss << ";";
}
atmosphere_json = ss.str();
}
PlanetRow r;
r.planet_no = id;
r.distance = is_moon ? the_planet->getMoonA() : the_planet->getA();
r.eccentricity = is_moon ? the_planet->getMoonE() : the_planet->getE();
r.inclination = the_planet->getInclination();
r.ascending_node = the_planet->getAscendingNode();
r.longitude_of_pericenter = the_planet->getLongitudeOfPericenter();
r.mean_longitude = the_planet->getMeanLongitude();
r.axial_tilt = the_planet->getAxialTilt();
r.ice_mass_fraction = the_planet->getImf();
r.rock_mass_fraction = the_planet->getRmf();
r.carbon_mass_fraction = the_planet->getCmf();
r.total_mass = the_planet->getMass();
r.is_gas_giant = the_planet->getGasGiant();
r.dust_mass = the_planet->getDustMass();
r.gas_mass = the_planet->getGasMass();
r.radius_of_core = the_planet->getCoreRadius();
r.total_radius = the_planet->getRadius();
r.orbit_zone = the_planet->getOrbitZone();
r.density = the_planet->getDensity();
r.orbit_period = the_planet->getOrbPeriod();
r.rotation_period = the_planet->getDay();
r.has_spin_orbit_resonance = the_planet->getResonantPeriod();
r.escape_velocity = the_planet->getEscVelocity();
r.surface_acceleration = the_planet->getSurfAccel();
r.surface_gravity = the_planet->getSurfGrav();
r.rms_velocity = the_planet->getRmsVelocity();
r.minimum_molecular_weight = the_planet->getMolecWeight();
r.volatile_gas_inventory = the_planet->getVolatileGasInventory();
r.surface_pressure = the_planet->getSurfPressure();
r.greenhouse_effect = the_planet->getGreenhouseEffect();
r.boiling_point = the_planet->getBoilPoint();
r.albedo = the_planet->getAlbedo();
r.exospheric_temperature = the_planet->getExosphericTemp();
r.estimated_temperature = the_planet->getEstimatedTemp();
r.estimated_terran_temperature = the_planet->getEstimatedTerrTemp();
r.surface_temperature = the_planet->getSurfTemp();
r.greenhouse_rise = the_planet->getGreenhsRise();
r.high_temperature = the_planet->getHighTemp();
r.low_temperature = the_planet->getLowTemp();
r.maximum_temperature = the_planet->getMaxTemp();
r.minimum_temperature = the_planet->getMinTemp();
r.hydrosphere = the_planet->getHydrosphere();
r.cloud_cover = the_planet->getCloudCover();
r.ice_cover = the_planet->getIceCover();
r.atmosphere = generate_atmosphere_string(the_planet, do_gases); // CSV-style
r.type = type_string(the_planet);
r.minor_moons = the_planet->getMinorMoons();
r.tidally_locked = the_planet->getTidallyLocked();
r.pms_desiccation_risk = the_planet->getPmsDesiccationRisk();
r.high_xuv_escape_risk = the_planet->getHighXuvEscapeRisk();
r.co2_collapse_risk = the_planet->getCo2CollapseRisk();
r.climate_model_temp = the_planet->getClimateModelTemp();
r.atmosphere_json = atmosphere_json;
r.is_moon = is_moon;
return r;
}
void csv_row(std::fstream& the_file, planet* the_planet, bool do_gases, bool is_moon,
const std::string& id, std::stringstream& /*ss*/) {
ZoneScoped;
the_file << emit_csv_row(to_row(the_planet, is_moon, id, do_gases));
}
std::string generate_atmosphere_string(planet* the_planet, bool do_gases) {
if (!do_gases) return "";
std::string atmosphere;
for (int i = 0; i < the_planet->getNumGases(); i++) {
auto gas = the_planet->getGas(i);
auto gas_info = gases[find_gas_index(gas.getNum())];
long double ipp = std::max(
0.0L, inspired_partial_pressure(the_planet->getSurfPressure(), gas.getSurfPressure()));
bool poisonous = ipp > gas_info.getMaxIpp();
atmosphere += std::format("{} {:.2f} {:.2f} ({:.2f}){};", gas_info.getSymbol(),
100.0 * (gas.getSurfPressure() / the_planet->getSurfPressure()),
gas.getSurfPressure(), ipp, poisonous ? " poisonous" : "");
}
return atmosphere;
}
auto jsonRow(planet* the_planet, bool do_gases, bool is_moon, std::string id,
std::stringstream& /*ss*/) -> nlohmann::json {
ZoneScoped;
return emit_planet_json(to_row(the_planet, is_moon, id, do_gases));
}
/**
* @brief type string
*
* @param the_planet
* @return string
*/
std::string base_type_string(planet* the_planet) {
switch (the_planet->getType()) {
case tUnknown:
return "Unknown";
case tRock:
return "Rock";
case tVenusian:
return "Venusian";
case tTerrestrial:
return "Terrestrial";
case tSubSubGasGiant:
return std::format("{} Gas Dwarf", cloud_type_string(the_planet));
case tSubGasGiant:
return std::format("{} Neptunian", cloud_type_string(the_planet));
case tGasGiant:
return std::format("{} Jovian", cloud_type_string(the_planet));
case tMartian:
return "Martian";
case tWater:
return "Water";
case tIce:
return "Ice";
case tAsteroids:
return "Asteroids";
case t1Face:
return "1Face";
case tBrownDwarf:
return "Brown Dwarf";
case tIron:
return "Iron";
case tCarbon:
return "Carbon";
case tOil:
return "Oil";
default:
return "Unknown";
}
}
/**
* @brief Full display name for a planet: state modifiers ("Tidally Locked",
* "Hot", "Super-Earth", ...) composed on top of the base composition type.
*
* Icons must NOT use this (they derive from base_type_string()); see
* image_type_string(). The base/modifier split mirrors the existing
* "{cloud} Jovian" gas-naming pattern.
*/
// Orthogonal STATE modifiers composed in front of the base composition type.
// Read at display time from already-computed planet state; they never change the
// base type, the icon, or any generated number. Extended in later phases
// (thermal / size / lava / hycean).
std::string modifier_prefix(planet* the_planet) {
std::string prefix;
const bool rocky = !is_gas_planet(the_planet);
const planet_type t = the_planet->getType();
// State: synchronous / spin-orbit-resonant rotation.
if (the_planet->getTidallyLocked()) {
prefix += "Tidally Locked ";
}
// Thermal (rocky only -- gas giants already carry a temperature-derived cloud
// class, so a "Hot" prefix there would just duplicate it). Lava = surface hot
// enough to melt silicate rock; Hot = strongly irradiated but below melt. Skip
// the types whose own name already implies their thermal state.
if (rocky && t != tIce && t != tMartian) {
const long double ts = the_planet->getSurfTemp();
if (ts >= LAVA_SURFACE_TEMP_K) {
prefix += "Lava ";
} else if (ts >= HOT_SURFACE_TEMP_K) {
prefix += "Hot ";
}
}
// Size class for rocky worlds: super-Earth band, below the sub-Neptune valley.
if (rocky && (t == tRock || t == tTerrestrial || t == tIron || t == tWater ||
t == tCarbon)) {
const long double r_earth = convert_km_to_eu(the_planet->getRadius());
if (r_earth >= SUPER_EARTH_MIN_REARTH && r_earth <= SUPER_EARTH_MAX_REARTH) {
prefix += "Super-Earth ";
}
}
// Hycean: a temperate, water/volatile-rich gas dwarf / small Neptunian -- a
// water world beneath a H2/He envelope (Madhusudhan et al. 2021). StarGen
// gives rocky worlds no envelope, so this is recognized on the EXISTING small
// gas-dwarf population in the habitable-temperature range (see const.h).
if (!rocky && (t == tSubSubGasGiant || t == tSubGasGiant)) {
const long double et = the_planet->getEstimatedTemp();
const long double m_e = the_planet->getMass() * SUN_MASS_IN_EARTH_MASSES;
const long double r_e = convert_km_to_eu(the_planet->getRadius());
if (et >= HYCEAN_TEMP_MIN_K && et <= HYCEAN_TEMP_MAX_K &&
m_e >= HYCEAN_MASS_MIN_REARTH && m_e <= HYCEAN_MASS_MAX_REARTH &&
r_e <= HYCEAN_RADIUS_MAX_REARTH) {
prefix += "Hycean ";
}
}
return prefix;
}
std::string type_string(planet* the_planet) {
return modifier_prefix(the_planet) + base_type_string(the_planet);
}
/**
* @brief cloud type string
*
* @param the_planet
* @return string
*/
std::string cloud_type_string(planet* the_planet) {
long double temp = the_planet->getEstimatedTemp();
ZoneScoped;
if (temp > 2240) {
return "Carbon";
} else if (temp > 1400) {
return "Silicate";
} else if (temp > 900) {
return "Alkali";
} else if (temp > 360) {
return "Cloudless";
} else if (temp > 320) {
return "Sulfur";
} else if (temp > 150) {
return "Water";
} else if (temp > 81) {
return "Ammonia";
} else {
return "Methane";
}
}
/**
* @brief Create a svg file
*
* @param innermost_planet
* @param path
* @param file_name
* @param svg_ext
* @param prognam
* @param do_moons
*/
/**
* @brief SVG scaling parameters
*/
struct SVGScaleParams {
long double max_x;
long double max_y;
long double margin;
long double min_log;
long double max_log;
long double mult;
long double offset;
long double em_scale;
int floor;
int ceiling;
};
/**
* @brief Calculate SVG scaling parameters for logarithmic distance display
*/
static auto calculate_svg_scale(planet* innermost_planet, planet* outermost_planet) -> SVGScaleParams {
SVGScaleParams params;
params.max_x = 760.0;
params.max_y = 120.0;
params.margin = 20.0;
params.em_scale = 5.0;
long double inner_edge = innermost_planet->getA() * (1.0 - innermost_planet->getE());
long double outer_edge = outermost_planet->getA() * (1.0 + outermost_planet->getE());
params.floor = (int)(log10(inner_edge) - 1.0);
params.min_log = params.floor;
params.ceiling = (int)(log10(outer_edge) + 1.0);
params.max_log = 0.0;
// Find precise min and max log values
for (int x = params.floor; x <= params.ceiling; x++) {
for (float n = 1.0; n < 9.9; n++) {
if (inner_edge > std::pow(10.0, x + log10(n))) {
params.min_log = x + log10(n);
}
if (params.max_log == 0.0 && outer_edge < std::pow(10.0, x + log10(n))) {
params.max_log = x + log10(n);
}
}
}
params.mult = params.max_x / (params.max_log - params.min_log);
params.offset = -params.mult * (1.0 + params.min_log);
return params;
}
/**
* @brief Write SVG header and document setup
*/
static void svg_write_header(std::fstream& output, planet* innermost_planet,
const std::string& prognam, const SVGScaleParams& params) {
output << "<?xml version='1.0' standalone='no'?>\n";
output << "<!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' \n";
output << " 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'>\n";
output << "\n";
output << "<svg width='100%' height='100%' version='1.1'\n";
output << " xmlns='http://www.w3.org/2000/svg'\n";
output << " viewBox='-" << params.margin << " -" << params.margin << " "
<< (params.max_x + (params.margin * 2.0)) << " "
<< (params.max_y + (params.margin * 2.0)) << "' >\n";
output << "\n";
output << "<title>" << escapeXmlText(innermost_planet->getTheSun().getName()) << "</title>\n";
output << "<desc>Created by: " << escapeXmlText(prognam) << " - " << stargen_revision << "</desc>\n";
output << "\n";
}
/**
* @brief Draw axis and tick marks
*/
static void svg_draw_axis(std::fstream& output, const SVGScaleParams& params) {
output << "<g stroke='#8893b5' stroke-width='1'>\n";
output << " <line x1='" << ((params.offset + params.mult) + (params.min_log * params.mult))
<< "' y1='" << (params.max_y - params.margin) << "' x2='"
<< ((params.offset + params.mult) + (params.max_log * params.mult)) << "' y2='"
<< (params.max_y - params.margin) << "' />\n";
for (int x = params.floor; x <= params.ceiling; x++) {
for (float n = 1.0; n < 9.9; n++) {
if (params.min_log <= (x + log10(n)) && params.max_log >= (x + log10(n))) {
float value = (n == 1) ? 10 : 5;
output << " <line x1='" << ((params.offset + params.mult) + ((x + log10(n)) * params.mult))
<< "' y1='" << (params.max_y - params.margin - value) << "' x2='"
<< ((params.offset + params.mult) + ((x + log10(n)) * params.mult)) << "' y2='"
<< (params.max_y - params.margin + value) << "' />\n";
}
}
}
output << "</g>\n\n";
}
/**
* @brief Draw habitable zone indicators
*/
static void svg_draw_habitable_zone(std::fstream& output, sun& the_sun, const SVGScaleParams& params) {
long double min_r_ecosphere = habitable_zone_distance(the_sun, RECENT_VENUS, 1.0);
long double max_r_ecosphere = habitable_zone_distance(the_sun, EARLY_MARS, 1.0);
// Center line of habitable zone
output << "<line x1='" << ((params.offset + params.mult) + (log10(the_sun.getREcosphere(1.0)) * params.mult))
<< "' y1='" << ((params.max_y - params.margin) - 5) << "' x2='"
<< ((params.offset + params.mult) + (log10(the_sun.getREcosphere(1.0)) * params.mult))
<< "' y2='" << ((params.max_y - params.margin) + 5)
<< "' stroke='#9fe6b4' stroke-width='1' />\n";
// Habitable zone range
output << "<line x1='" << ((params.offset + params.mult) + (log10(min_r_ecosphere) * params.mult))
<< "' y1='" << (params.max_y - params.margin) << "' x2='"
<< ((params.offset + params.mult) + (log10(max_r_ecosphere) * params.mult)) << "' y2='"
<< (params.max_y - params.margin)
<< "' stroke='#3ddc84' stroke-width='10' stroke-opacity='0.30' />\n";
}
/**
* @brief Draw axis labels (AU distances)
*/
static void svg_draw_axis_labels(std::fstream& output, const SVGScaleParams& params) {
output << "<g font-family='Arial' font-size='10' \n";
output << " font-style='normal' font-weight='normal'\n";
output << " fill='#aab2cc' text-anchor='middle'>\n";
for (int x = params.floor; x <= params.ceiling; x++) {
if (params.min_log <= x && params.max_log >= x) {
output << " <text x='" << ((params.offset + params.mult) + (x * params.mult))
<< "' y='120'> " << std::pow(10.0, x) << " AU </text>\n";
}
}
output << "\n";
}
/**
* @brief Draw all planets in the system
*/
static void svg_draw_planets(std::fstream& output, planet* innermost_planet, const SVGScaleParams& params) {
for (planet* a_planet : g_sim_context.planets) {
long double x = (params.offset + params.mult) + (log10(a_planet->getA()) * params.mult);
long double r = std::pow((a_planet->getMass() * SUN_MASS_IN_EARTH_MASSES), 1.0 / 3.0) * params.em_scale;
long double x1 = (params.offset + params.mult) + (log10(a_planet->getA() * (1.0 - a_planet->getE())) * params.mult);
long double x2 = (params.offset + params.mult) + (log10(a_planet->getA() * (1.0 + a_planet->getE())) * params.mult);
output << " <circle cx='" << x << "' cy='30' r='" << r
<< "' fill='none' stroke='#cdd6ee' stroke-width='1' />\n";
output << " <line x1='" << x1 << "' y1='" << ((params.max_y - params.margin) - 15.0)
<< "' x2='" << x2 << "' y2='" << ((params.max_y - params.margin) - 15.0)
<< "' stroke='#cdd6ee' stroke-width='8' stroke-opacity='0.35'/>\n";
output << " <text x='" << x << "' y='" << (params.max_y - (params.margin * 2.0)) << "'>";
output << (a_planet->getMass() * SUN_MASS_IN_EARTH_MASSES);
output << "</text>\n\n";
}
}
/**
* @brief Build the hover-title string for a body in the icon strip.
*/
static auto strip_icon_title(planet* body, const std::string& label) -> std::string {
std::ostringstream info;
info << label << " " << type_string(body) << ": " << (body->getMass() * SUN_MASS_IN_EARTH_MASSES)
<< " EM, " << toString(body->getA(), 3) << " AU";
if (!is_gas_planet(body) && body->getType() != tUnknown) {
info << ", " << (body->getSurfTemp() - FREEZING_POINT_OF_WATER) << " C";
}
return info.str();
}
/**
* @brief Emit one <a>-wrapped type icon (image) centered at (cx, cy).
*/
static void strip_icon_image(std::fstream& output, planet* body, double cx, double cy, double w,
const std::string& href, const std::string& url_path,
const std::string& title) {
output << "<a href='" << href << "'>";
output << "<title>" << escapeXmlText(title) << "</title>";
output << "<image href='" << escapeXmlAttr(url_path) << "ref/" << image_type_string(body)
<< "Planet.webp' x='" << (cx - (w / 2.0)) << "' y='" << (cy - (w / 2.0)) << "' width='" << w
<< "' height='" << w << "' />";
output << "</a>\n";
}
// Icon side lengths (in viewBox units), shared by the layout pre-pass and the
// draw pass so the two can never diverge.
static auto planet_icon_width(planet* p) -> double {
if (p->getType() == tAsteroids) {
return 11.0;
}
double w = sqrt(convert_km_to_eu(p->getRadius())) * 14.0;
if (w < 9.0) {
w = 9.0;
}
if (w > 40.0) {
w = 40.0;
}
return w;
}
static auto moon_icon_width(planet* m) -> double {
double mw = sqrt(convert_km_to_eu(m->getRadius())) * 18.0;
if (mw < 5.0) {
mw = 5.0;
}
if (mw > 14.0) {
mw = 14.0;
}
return mw;
}
/**
* @brief Draw the whole system as ONE continuous SVG: type icons positioned by
* log-distance, each connected straight down to its place on the AU axis,
* with the habitable-zone band and distance labels in the same figure.
*
* Because the icons and the scale live in a single coordinate system there is no
* separator or gap between them — a planet's icon sits directly above its tick on
* the axis and its eccentricity bar, and the figure reads top-to-bottom as one
* unit. Moons (when do_moons) stack vertically above their parent planet at the
* same x. Sets the three habitable-category flags so the caller can decide
* whether to emit the terrestrials table.
*/
static void svg_draw_system_map(std::fstream& output, const SVGScaleParams& params,
const std::string& url_path, const std::string& system_url,
bool int_link, bool do_gases, bool do_moons, sun& the_sun,
bool& terrestrials_seen, bool& habitable_jovians_seen,
bool& potentialy_habitables_seen) {
const double view_w = (double)(params.max_x + (params.margin * 2.0));
const double view_bottom = (double)(params.max_y + params.margin); // keeps the AU labels (y=120) in view
const double axis_y = (double)(params.max_y - params.margin); // where svg_draw_axis puts the line
const double baseline = 50.0; // y of planet icon centers
const double ecc_y = axis_y - 12.0; // eccentricity bars sit just above the axis
const double moon_gap = 2.0;
// Pre-pass: find the highest point any icon/moon reaches so the viewBox can
// grow upward to fit the tallest moon stack (nothing clips) yet stay snug when
// there are few moons. Seed with the sun icon's top edge.
double min_top = baseline - 16.0;
for (planet* p : g_sim_context.planets) {
double w = planet_icon_width(p);
if ((baseline - (w / 2.0)) < min_top) {
min_top = baseline - (w / 2.0);
}
if (do_moons) {
double cursor = (baseline - (w / 2.0)) - 3.0;
for (planet* moon : p->moons) {
double mw = moon_icon_width(moon);
double top = cursor - mw; // top edge = (cursor - mw/2) - mw/2
if (top < min_top) {
min_top = top;
}
cursor = top - moon_gap;
}
}
}
const double view_top = min_top - 4.0;
const double view_h = view_bottom - view_top;
// Full-width, single figure. Generous height cap so it is not cramped; because
// everything is one SVG. No height cap: width fills the cell and the height
// follows from the viewBox aspect ratio (height:auto), so it spans the full
// page width like the old GIF overview and grows as tall as it needs.
output << "<svg xmlns='http://www.w3.org/2000/svg' version='1.1' width='100%' "
"preserveAspectRatio='xMidYMid meet'\n";
output << " viewBox='-" << params.margin << " " << view_top << " " << view_w << " " << view_h
<< "' style='display:block;width:100%;height:auto'>\n";
// Scale first, so icons/connectors paint on top of it.
svg_draw_axis(output, params);
svg_draw_habitable_zone(output, the_sun, params);
// The sun has no log-distance position; pin it at the far left edge.
output << "<image href='" << escapeXmlAttr(url_path) << "ref/Sun.gif' x='"
<< (-params.margin + 2.0) << "' y='" << (baseline - 16.0) << "' width='10' height='32' />\n";
int counter = 1;
for (planet* p : g_sim_context.planets) {
if ((p->getSurfPressure() > 0 || p->getGasGiant()) && p->getNumGases() == 0 && do_gases) {
std::string pid = std::to_string(counter);
calculate_gases(the_sun, p, pid);
}
double x = (double)((params.offset + params.mult) + (log10(p->getA()) * params.mult));
double w = planet_icon_width(p);
// Eccentricity bar + a connector from the icon straight down to the axis, so
// the icon and its position on the scale read as one continuous mark.
double x1 = (double)((params.offset + params.mult) + (log10(p->getA() * (1.0 - p->getE())) * params.mult));
double x2 = (double)((params.offset + params.mult) + (log10(p->getA() * (1.0 + p->getE())) * params.mult));
output << "<line x1='" << x1 << "' y1='" << ecc_y << "' x2='" << x2 << "' y2='" << ecc_y
<< "' stroke='#cdd6ee' stroke-width='8' stroke-opacity='0.35' />\n";
output << "<line x1='" << x << "' y1='" << (baseline + (w / 2.0)) << "' x2='" << x << "' y2='" << axis_y
<< "' stroke='#cdd6ee' stroke-width='1' stroke-opacity='0.30' />\n";
std::string p_href = (int_link ? "" : escapeXmlAttr(system_url)) + "#" + std::to_string(counter);
strip_icon_image(output, p, x, baseline, w, p_href, url_path,
strip_icon_title(p, "#" + std::to_string(counter)));
if (is_terrestrial(p)) {
terrestrials_seen = true;
}
if (is_habitable_jovian(p)) {
habitable_jovians_seen = true;
}
if (is_potentialy_habitable(p)) {
potentialy_habitables_seen = true;
}
// Moons: stack upward from just above the planet icon, at the same x. The
// pre-pass already sized the viewBox so the whole stack fits without clipping.
if (do_moons) {
double cursor = (baseline - (w / 2.0)) - 3.0; // top edge of planet, minus a gap
int midx = 1;
for (planet* moon : p->moons) {
if (is_terrestrial(moon)) {
terrestrials_seen = true;
}
if (is_habitable_jovian(moon)) {
habitable_jovians_seen = true;
}
if (is_potentialy_habitable(moon)) {
potentialy_habitables_seen = true;
}
double mw = moon_icon_width(moon);
double mcy = cursor - (mw / 2.0);
std::string m_label = "#" + std::to_string(counter) + "." + std::to_string(midx);
std::string m_href = (int_link ? "" : escapeXmlAttr(system_url)) + m_label;
strip_icon_image(output, moon, x, mcy, mw, m_href, url_path, strip_icon_title(moon, m_label));
cursor = (mcy - (mw / 2.0)) - moon_gap;
midx++;
}
}
counter++;
}
// AU distance labels last; this opens a <g> that the closing </g> wraps.
svg_draw_axis_labels(output, params);
output << "</g>\n</svg>\n";