1 #include "gui/windows/PaletteDialog.h"
2 #include "gui/Factory.h"
3 #include "gui/objects/Colorizer.h"
4 #include "gui/objects/RenderContext.h"
5 #include "gui/renderers/ParticleRenderer.h"
6 #include "gui/renderers/Spectrum.h"
7 #include "gui/windows/Widgets.h"
8 #include "io/FileSystem.h"
9 #include "post/Plot.h"
10 #include "post/Point.h"
11 #include <wx/button.h>
12 #include <wx/combobox.h>
13 #include <wx/dcbuffer.h>
14 #include <wx/panel.h>
15 #include <wx/settings.h>
16 #include <wx/sizer.h>
17 #include <wx/stattext.h>
18 
19 NAMESPACE_SPH_BEGIN
20 
21 class FlippedRenderContext : public IRenderContext {
22 private:
23     AutoPtr<IRenderContext> context;
24 
25 public:
FlippedRenderContext(AutoPtr<IRenderContext> && context)26     explicit FlippedRenderContext(AutoPtr<IRenderContext>&& context)
27         : context(std::move(context)) {}
28 
size() const29     virtual Pixel size() const override {
30         const Pixel p = context->size();
31         return Pixel(p.y, p.x);
32     }
33 
setColor(const Rgba & color,const Flags<ColorFlag> flags)34     virtual void setColor(const Rgba& color, const Flags<ColorFlag> flags) override {
35         context->setColor(color, flags);
36     }
37 
setThickness(const float thickness)38     virtual void setThickness(const float thickness) override {
39         context->setThickness(thickness);
40     }
41 
setFontSize(const int fontSize)42     virtual void setFontSize(const int fontSize) override {
43         context->setFontSize(fontSize);
44     }
45 
fill(const Rgba & color)46     virtual void fill(const Rgba& color) override {
47         context->fill(color);
48     }
49 
drawLine(const Coords p1,const Coords p2)50     virtual void drawLine(const Coords p1, const Coords p2) override {
51         context->drawLine(this->transform(p1), this->transform(p2));
52     }
53 
drawCircle(const Coords center,const float radius)54     virtual void drawCircle(const Coords center, const float radius) override {
55         context->drawCircle(this->transform(center), radius);
56     }
57 
drawTriangle(const Coords,const Coords,const Coords)58     virtual void drawTriangle(const Coords, const Coords, const Coords) override {
59         NOT_IMPLEMENTED;
60     }
61 
drawBitmap(const Coords,const Bitmap<Rgba> &)62     virtual void drawBitmap(const Coords, const Bitmap<Rgba>&) override {
63         NOT_IMPLEMENTED;
64     }
65 
drawText(const Coords p,const Flags<TextAlign>,const String & s)66     virtual void drawText(const Coords p, const Flags<TextAlign>, const String& s) override {
67         context->drawText(this->transform(p), TextAlign::VERTICAL_CENTER | TextAlign::HORIZONTAL_CENTER, s);
68     }
69 
70 private:
transform(const Pixel & p)71     Pixel transform(const Pixel& p) {
72         return Pixel(context->size().x - p.y, p.x);
73     }
74 
transform(const Coords & p)75     Coords transform(const Coords& p) {
76         return Coords(context->size().x - p.y, p.x);
77     }
78 };
79 
drawPalette(IRenderContext & context,const Pixel origin,const Pixel size,const Rgba & lineColor,const Palette & palette)80 void drawPalette(IRenderContext& context,
81     const Pixel origin,
82     const Pixel size,
83     const Rgba& lineColor,
84     const Palette& palette) {
85 
86     // draw palette
87     for (int i = 0; i < size.y; ++i) {
88         const float value = palette.relativeToPalette(float(i) / (size.y - 1));
89         context.setColor(palette(value), ColorFlag::LINE);
90         context.drawLine(Coords(origin.x, origin.y - i), Coords(origin.x + size.x, origin.y - i));
91     }
92 
93     // draw tics
94     const Interval interval = palette.getInterval();
95     const PaletteScale scale = palette.getScale();
96 
97     Array<Float> tics;
98     switch (scale) {
99     case PaletteScale::LINEAR:
100         tics = getLinearTics(interval, 4);
101         break;
102     case PaletteScale::LOGARITHMIC:
103         tics = getLogTics(interval, 4);
104         break;
105     case PaletteScale::HYBRID: {
106         const Size ticsCnt = 5;
107         // tics currently not implemented, so just split the range to equidistant steps
108         for (Size i = 0; i < ticsCnt; ++i) {
109             tics.push(palette.relativeToPalette(float(i) / (ticsCnt - 1)));
110         }
111         break;
112     }
113     default:
114         NOT_IMPLEMENTED;
115     }
116     context.setColor(lineColor, ColorFlag::LINE | ColorFlag::TEXT);
117     for (Float tic : tics) {
118         const float value = palette.paletteToRelative(float(tic));
119         const int i = int(value * size.y);
120         context.drawLine(Coords(origin.x, origin.y - i), Coords(origin.x + 6, origin.y - i));
121         context.drawLine(
122             Coords(origin.x + size.x - 6, origin.y - i), Coords(origin.x + size.x, origin.y - i));
123 
124         String text = toPrintableString(tic, 1, 1000);
125         context.drawText(
126             Coords(origin.x - 15, origin.y - i), TextAlign::LEFT | TextAlign::VERTICAL_CENTER, text);
127     }
128 }
129 
130 class PaletteCanvas : public wxPanel {
131 private:
132     Palette palette;
133 
134 public:
PaletteCanvas(wxWindow * parent,const Palette palette)135     PaletteCanvas(wxWindow* parent, const Palette palette)
136         : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize)
137         , palette(palette) {
138         this->Connect(wxEVT_PAINT, wxPaintEventHandler(PaletteCanvas::onPaint));
139         this->SetMinSize(wxSize(320, 100));
140     }
141 
setPalette(const Palette newPalette)142     void setPalette(const Palette newPalette) {
143         palette = newPalette;
144         this->Refresh();
145     }
146 
147 private:
onPaint(wxPaintEvent & UNUSED (evt))148     void onPaint(wxPaintEvent& UNUSED(evt)) {
149         wxPaintDC dc(this);
150         wxFont font = wxSystemSettings::GetFont(wxSystemFont::wxSYS_DEFAULT_GUI_FONT);
151         font.SetPointSize(10);
152         SPH_ASSERT(font.IsOk());
153         dc.SetFont(font);
154         FlippedRenderContext context(makeAuto<WxRenderContext>(dc));
155         context.setFontSize(9);
156         Rgba background(dc.GetBackground().GetColour());
157         drawPalette(context, Pixel(40, 310), Pixel(40, 300), background.inverse(), palette);
158     }
159 };
160 
PalettePanel(wxWindow * parent,wxSize size,const Palette palette)161 PalettePanel::PalettePanel(wxWindow* parent, wxSize size, const Palette palette)
162     : wxPanel(parent, wxID_ANY)
163     , initial(palette)
164     , selected(palette) {
165 
166     this->SetMinSize(size);
167 
168     wxBoxSizer* mainSizer = new wxBoxSizer(wxVERTICAL);
169 
170     canvas = new PaletteCanvas(this, initial);
171     mainSizer->Add(canvas, 0, wxALIGN_CENTER_HORIZONTAL);
172 
173     wxBoxSizer* selectionSizer = new wxBoxSizer(wxHORIZONTAL);
174     paletteBox = new ComboBox(this, "Select palette ...");
175     selectionSizer->Add(paletteBox);
176 
177     wxButton* loadButton = new wxButton(this, wxID_ANY, "Load");
178     selectionSizer->Add(loadButton);
179 
180     wxButton* resetButton = new wxButton(this, wxID_ANY, "Reset");
181     selectionSizer->Add(resetButton);
182 
183     mainSizer->Add(selectionSizer, 0, wxALIGN_CENTER_HORIZONTAL);
184     mainSizer->AddSpacer(5);
185 
186     wxBoxSizer* rangeSizer = new wxBoxSizer(wxHORIZONTAL);
187 
188     wxStaticText* text = new wxStaticText(this, wxID_ANY, "From ");
189     rangeSizer->Add(text, 0, wxALIGN_CENTER_VERTICAL);
190     lowerCtrl = new FloatTextCtrl(this, double(initial.getInterval().lower()));
191     rangeSizer->Add(lowerCtrl);
192     rangeSizer->AddSpacer(30);
193 
194     text = new wxStaticText(this, wxID_ANY, "To ");
195     rangeSizer->Add(text, 0, wxALIGN_CENTER_VERTICAL);
196     upperCtrl = new FloatTextCtrl(this, double(initial.getInterval().upper()));
197     rangeSizer->Add(upperCtrl);
198 
199     upperCtrl->onValueChanged = [this](const Float value) {
200         /// \todo deduplicate
201         const Float lower = selected.getInterval().lower();
202         if (lower >= value) {
203             return false;
204         }
205         selected.setInterval(Interval(lower, value));
206         canvas->setPalette(selected);
207         onPaletteChanged.callIfNotNull(selected);
208         return true;
209     };
210     lowerCtrl->onValueChanged = [this](const Float value) {
211         const Float upper = selected.getInterval().upper();
212         if (value >= upper) {
213             return false;
214         }
215         if (selected.getScale() == PaletteScale::LOGARITHMIC && value <= 0.f) {
216             return false;
217         }
218         selected.setInterval(Interval(value, upper));
219         canvas->setPalette(selected);
220         onPaletteChanged.callIfNotNull(selected);
221         return true;
222     };
223 
224     mainSizer->Add(rangeSizer, 0, wxALIGN_CENTER_HORIZONTAL);
225 
226     this->SetSizerAndFit(mainSizer);
227 
228     // setup palette box
229     paletteBox->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent& UNUSED(evt)) { this->update(); });
230 
231     loadButton->Bind(wxEVT_BUTTON, [this](wxCommandEvent& UNUSED(evt)) {
232         Optional<Path> path = doOpenFileDialog("Load palette", { { "Palette files", "csv" } });
233         if (!path) {
234             return;
235         }
236         this->loadPalettes(path.value());
237     });
238     resetButton->Bind(wxEVT_BUTTON, [this](wxCommandEvent& UNUSED(evt)) { this->setDefaultPaletteList(); });
239 
240     this->setDefaultPaletteList();
241 }
242 
setPalette(const Palette & palette)243 void PalettePanel::setPalette(const Palette& palette) {
244     selected = initial = palette;
245     paletteMap.insert("Current", 0, initial);
246     paletteBox->SetSelection(0);
247     canvas->setPalette(selected);
248     lowerCtrl->setValue(initial.getInterval().lower());
249     upperCtrl->setValue(initial.getInterval().upper());
250 }
251 
252 static UnorderedMap<ExtColorizerId, String> PALETTE_ID_LIST = {
253     { ColorizerId::VELOCITY, "Magnitude 1" },
254     { QuantityId::DEVIATORIC_STRESS, "Magnitude 2" },
255     { ColorizerId::TEMPERATURE, "Temperature" },
256     { QuantityId::DAMAGE, "Grayscale" },
257     { ColorizerId::MOVEMENT_DIRECTION, "Periodic" },
258     { ColorizerId::DENSITY_PERTURBATION, "Diverging 1" },
259     { QuantityId::DENSITY, "Diverging 2" },
260     { QuantityId::VELOCITY_DIVERGENCE, "Diverging 3" },
261     { QuantityId::ANGULAR_FREQUENCY, "Extremes" },
262 };
263 
264 // some extra palettes
265 namespace Palettes {
266 
267 const Palette GALAXY({ { 0.001f, Rgba(0.f, 0.02f, 0.09f) },
268                          { 0.01f, Rgba(0.4f, 0.106f, 0.38f) },
269                          { 0.1f, Rgba(0.78f, 0.18f, 0.38f) },
270                          { 1.f, Rgba(0.91f, 0.56f, 0.81f) },
271                          { 10.f, Rgba(0.29f, 0.69f, 0.93f) } },
272     PaletteScale::LOGARITHMIC);
273 
274 const Palette ACCRETION({ { 0.001f, Rgba(0.43f, 0.70f, 1.f) },
275                             { 0.01f, Rgba(0.5f, 0.5f, 0.5f) },
276                             { 0.1f, Rgba(0.65f, 0.12f, 0.01f) },
277                             { 1.f, Rgba(0.79f, 0.38f, 0.02f) },
278                             { 10.f, Rgba(0.93f, 0.83f, 0.34f) },
279                             { 100.f, Rgba(0.94f, 0.90f, 0.84f) } },
280     PaletteScale::LOGARITHMIC);
281 
282 const Palette STELLAR({ { 0.01f, Rgba(1.f, 0.75f, 0.1f) },
283                           { 0.1f, Rgba(0.75f, 0.25f, 0.1f)},
284                           { 1.f, Rgba(0.4f, 0.7f, 1.f) },
285                           { 10.f, Rgba(0.2f, 0.4f, 0.8f) } },
286     PaletteScale::LOGARITHMIC);
287 
288 } // namespace Palettes
289 
setDefaultPaletteList()290 void PalettePanel::setDefaultPaletteList() {
291     paletteMap = {
292         { "Current", initial },
293         { "Blackbody", getBlackBodyPalette(Interval(300, 12000)) },
294         { "Galaxy", Palettes::GALAXY },
295         { "Accretion", Palettes::ACCRETION },
296         { "Stellar", Palettes::STELLAR },
297     };
298     for (auto& pair : PALETTE_ID_LIST) {
299         paletteMap.insert(pair.value(), Factory::getPalette(pair.key()));
300     }
301 
302     wxArrayString items;
303     for (const auto& e : paletteMap) {
304         items.Add(e.key().toUnicode());
305     }
306     paletteBox->Set(items);
307     paletteBox->SetSelection(0);
308     this->update();
309 }
310 
loadPalettes(const Path & path)311 void PalettePanel::loadPalettes(const Path& path) {
312     paletteMap.clear();
313     for (Path file : FileSystem::iterateDirectory(path.parentPath())) {
314         if (file.extension().string() == "csv") {
315             Palette palette = initial;
316             if (palette.loadFromFile(path.parentPath() / file)) {
317                 paletteMap.insert(file.string(), palette);
318             }
319         }
320     }
321     wxArrayString items;
322     int selectionIdx = 0, idx = 0;
323     for (const auto& e : paletteMap) {
324         items.Add(e.key().toUnicode());
325         if (e.key() == path.fileName().string()) {
326             // this is the palette we selected
327             selectionIdx = idx;
328         }
329         ++idx;
330     }
331     paletteBox->Set(items);
332     paletteBox->SetSelection(selectionIdx);
333 
334     this->update();
335 }
336 
update()337 void PalettePanel::update() {
338     const int idx = paletteBox->GetSelection();
339     const Interval range = selected.getInterval();
340     selected = (paletteMap.begin() + idx)->value();
341     selected.setInterval(range);
342     canvas->setPalette(selected);
343     onPaletteChanged.callIfNotNull(selected);
344 }
345 
346 NAMESPACE_SPH_END
347