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