1 // winstarbrowser.cpp
2 //
3 // Copyright (C) 2001, Chris Laurel <claurel@shatters.net>
4 //
5 // Star browser tool for Windows.
6 //
7 // This program is free software; you can redistribute it and/or
8 // modify it under the terms of the GNU General Public License
9 // as published by the Free Software Foundation; either version 2
10 // of the License, or (at your option) any later version.
11 
12 #include <string>
13 #include <algorithm>
14 #include <set>
15 #include <windows.h>
16 #include <commctrl.h>
17 #include <cstring>
18 #include "winstarbrowser.h"
19 #include "celutil/winutil.h"
20 
21 #include "res/resource.h"
22 
23 extern void SetMouseCursor(LPCTSTR lpCursor);
24 
25 using namespace std;
26 
27 static const int MinListStars = 10;
28 static const int MaxListStars = 500;
29 static const int DefaultListStars = 100;
30 
31 
32 // TODO: More of the functions in this module should be converted to
33 // methods of the StarBrowser class.
34 
35 enum {
36     BrightestStars = 0,
37     NearestStars = 1,
38     StarsWithPlanets = 2,
39 };
40 
toMicroLY(const Point3f & p)41 static Point3f toMicroLY(const Point3f& p)
42 {
43     return Point3f(p.x * 1e6f, p.y * 1e6f, p.z * 1e6f);
44 }
45 
fromMicroLY(const Point3f & p)46 static Point3f fromMicroLY(const Point3f& p)
47 {
48     return Point3f(p.x * 1e-6f, p.y * 1e-6f, p.z * 1e-6f);
49 }
50 
51 
InitStarBrowserColumns(HWND listView)52 bool InitStarBrowserColumns(HWND listView)
53 {
54     LVCOLUMN lvc;
55     LVCOLUMN columns[5];
56 
57     lvc.mask = LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM;
58     lvc.fmt = LVCFMT_LEFT;
59     lvc.cx = 60;
60     lvc.pszText = "";
61 
62     int nColumns = sizeof(columns) / sizeof(columns[0]);
63     int i;
64 
65     for (i = 0; i < nColumns; i++)
66         columns[i] = lvc;
67 
68     bind_textdomain_codeset("celestia", CurrentCP());
69 
70     columns[0].pszText = _("Name");
71     columns[0].cx = 100;
72     columns[1].pszText = _("Distance (ly)");
73     columns[1].fmt = LVCFMT_RIGHT;
74     columns[1].cx = 75;
75     columns[2].pszText = _("App. mag");
76     columns[2].fmt = LVCFMT_RIGHT;
77     columns[3].pszText = _("Abs. mag");
78     columns[3].fmt = LVCFMT_RIGHT;
79     columns[4].pszText = _("Type");
80 
81     bind_textdomain_codeset("celestia", "UTF8");
82 
83     for (i = 0; i < nColumns; i++)
84     {
85         columns[i].iSubItem = i;
86         if (ListView_InsertColumn(listView, i, &columns[i]) == -1)
87             return false;
88     }
89 
90     return true;
91 }
92 
93 
94 struct CloserStarPredicate
95 {
96     Point3f pos;
operator ()CloserStarPredicate97     bool operator()(const Star* star0, const Star* star1) const
98     {
99         return ((pos - star0->getPosition()).lengthSquared() <
100                 (pos - star1->getPosition()).lengthSquared());
101 
102     }
103 };
104 
105 struct BrighterStarPredicate
106 {
107     Point3f pos;
108     UniversalCoord ucPos;
operator ()BrighterStarPredicate109     bool operator()(const Star* star0, const Star* star1) const
110     {
111         float d0 = pos.distanceTo(star0->getPosition());
112         float d1 = pos.distanceTo(star1->getPosition());
113 
114         // If the stars are closer than one light year, use
115         // a more precise distance estimate.
116         if (d0 < 1.0f)
117             d0 = (toMicroLY(star0->getPosition()) - ucPos).length() * 1e-6f;
118         if (d1 < 1.0f)
119             d1 = (toMicroLY(star1->getPosition()) - ucPos).length() * 1e-6f;
120 
121         return (star0->getApparentMagnitude(d0) <
122                 star1->getApparentMagnitude(d1));
123     }
124 };
125 
126 struct SolarSystemPredicate
127 {
128     Point3f pos;
129     SolarSystemCatalog* solarSystems;
130 
operator ()SolarSystemPredicate131     bool operator()(const Star* star0, const Star* star1) const
132     {
133         SolarSystemCatalog::iterator iter;
134 
135         iter = solarSystems->find(star0->getCatalogNumber());
136         bool hasPlanets0 = (iter != solarSystems->end());
137         iter = solarSystems->find(star1->getCatalogNumber());
138         bool hasPlanets1 = (iter != solarSystems->end());
139         if (hasPlanets1 == hasPlanets0)
140         {
141             return ((pos - star0->getPosition()).lengthSquared() <
142                     (pos - star1->getPosition()).lengthSquared());
143         }
144         else
145         {
146             return hasPlanets0;
147         }
148     }
149 };
150 
151 
152 // Find the nearest/brightest/X-est N stars in a database.  The
153 // supplied predicate determines which of two stars is a better match.
154 template<class Pred> vector<const Star*>*
FindStars(const StarDatabase & stardb,Pred pred,int nStars)155 FindStars(const StarDatabase& stardb, Pred pred, int nStars)
156 {
157     vector<const Star*>* finalStars = new vector<const Star*>();
158     if (nStars == 0)
159         return finalStars;
160 
161     typedef multiset<const Star*, Pred> StarSet;
162     StarSet firstStars(pred);
163 
164     int totalStars = stardb.size();
165     if (totalStars < nStars)
166         nStars = totalStars;
167 
168     // We'll need at least nStars in the set, so first fill
169     // up the list indiscriminately.
170     int i = 0;
171     for (i = 0; i < nStars; i++)
172     {
173         Star* star = stardb.getStar(i);
174         if (star->getVisibility())
175             firstStars.insert(star);
176     }
177 
178     // From here on, only add a star to the set if it's
179     // a better match than the worst matching star already
180     // in the set.
181     const Star* lastStar = *--firstStars.end();
182     for (; i < totalStars; i++)
183     {
184         Star* star = stardb.getStar(i);
185         if (star->getVisibility() && pred(star, lastStar))
186         {
187             firstStars.insert(star);
188             firstStars.erase(--firstStars.end());
189             lastStar = *--firstStars.end();
190         }
191     }
192 
193     // Move the best matching stars into the vector
194     finalStars->reserve(nStars);
195     for (StarSet::const_iterator iter = firstStars.begin();
196          iter != firstStars.end(); iter++)
197     {
198         finalStars->insert(finalStars->end(), *iter);
199     }
200 
201     return finalStars;
202 }
203 
204 
InitStarBrowserLVItems(HWND listView,vector<const Star * > & stars)205 bool InitStarBrowserLVItems(HWND listView, vector<const Star*>& stars)
206 {
207     LVITEM lvi;
208 
209     lvi.mask = LVIF_TEXT | LVIF_PARAM | LVIF_STATE;
210     lvi.state = 0;
211     lvi.stateMask = 0;
212     lvi.pszText = LPSTR_TEXTCALLBACK;
213 
214     for (unsigned int i = 0; i < stars.size(); i++)
215     {
216         lvi.iItem = i;
217         lvi.iSubItem = 0;
218         lvi.lParam = (LPARAM) stars[i];
219         ListView_InsertItem(listView, &lvi);
220     }
221 
222     return true;
223 }
224 
225 
InitStarBrowserItems(HWND listView,StarBrowser * browser)226 bool InitStarBrowserItems(HWND listView, StarBrowser* browser)
227 {
228     Universe* univ = browser->appCore->getSimulation()->getUniverse();
229     StarDatabase* stardb = univ->getStarCatalog();
230     SolarSystemCatalog* solarSystems = univ->getSolarSystemCatalog();
231 
232     vector<const Star*>* stars = NULL;
233     switch (browser->predicate)
234     {
235     case BrightestStars:
236         {
237             BrighterStarPredicate brighterPred;
238             brighterPred.pos = browser->pos;
239             brighterPred.ucPos = browser->ucPos;
240             stars = FindStars(*stardb, brighterPred, browser->nStars);
241         }
242         break;
243 
244     case NearestStars:
245         {
246             CloserStarPredicate closerPred;
247             closerPred.pos = browser->pos;
248             stars = FindStars(*stardb, closerPred, browser->nStars);
249         }
250         break;
251 
252     case StarsWithPlanets:
253         {
254             if (solarSystems == NULL)
255                 return false;
256             SolarSystemPredicate solarSysPred;
257             solarSysPred.pos = browser->pos;
258             solarSysPred.solarSystems = solarSystems;
259             stars = FindStars(*stardb, solarSysPred,
260                               min((unsigned int) browser->nStars, solarSystems->size()));
261         }
262         break;
263 
264     default:
265         return false;
266     }
267 
268     bool succeeded = InitStarBrowserLVItems(listView, *stars);
269     delete stars;
270 
271     return succeeded;
272 }
273 
274 
275 // Crud used for the list item display callbacks
276 static string starNameString("");
277 static char callbackScratch[256];
278 
279 struct StarBrowserSortInfo
280 {
281     int subItem;
282     Point3f pos;
283     UniversalCoord ucPos;
284 };
285 
StarBrowserCompareFunc(LPARAM lParam0,LPARAM lParam1,LPARAM lParamSort)286 int CALLBACK StarBrowserCompareFunc(LPARAM lParam0, LPARAM lParam1,
287                                     LPARAM lParamSort)
288 {
289     StarBrowserSortInfo* sortInfo = reinterpret_cast<StarBrowserSortInfo*>(lParamSort);
290     Star* star0 = reinterpret_cast<Star*>(lParam0);
291     Star* star1 = reinterpret_cast<Star*>(lParam1);
292 
293     switch (sortInfo->subItem)
294     {
295     case 0:
296         return 0;
297 
298     case 1:
299         {
300             float d0 = sortInfo->pos.distanceTo(star0->getPosition());
301             float d1 = sortInfo->pos.distanceTo(star1->getPosition());
302             return (int) sign(d0 - d1);
303         }
304 
305     case 2:
306         {
307             float d0 = sortInfo->pos.distanceTo(star0->getPosition());
308             float d1 = sortInfo->pos.distanceTo(star1->getPosition());
309             if (d0 < 1.0f)
310                 d0 = (toMicroLY(star0->getPosition()) - sortInfo->ucPos).length() * 1e-6f;
311             if (d1 < 1.0f)
312                 d1 = (toMicroLY(star1->getPosition()) - sortInfo->ucPos).length() * 1e-6f;
313             return (int) sign(astro::absToAppMag(star0->getAbsoluteMagnitude(), d0) -
314                               astro::absToAppMag(star1->getAbsoluteMagnitude(), d1));
315         }
316 
317     case 3:
318         return (int) sign(star0->getAbsoluteMagnitude() - star1->getAbsoluteMagnitude());
319 
320     case 4:
321         return strcmp(star0->getSpectralType(), star1->getSpectralType());
322 
323     default:
324         return 0;
325     }
326 }
327 
328 
StarBrowserDisplayItem(LPNMLVDISPINFOA nm,StarBrowser * browser)329 void StarBrowserDisplayItem(LPNMLVDISPINFOA nm, StarBrowser* browser)
330 {
331     double tdb = browser->appCore->getSimulation()->getTime();
332 
333     Star* star = reinterpret_cast<Star*>(nm->item.lParam);
334     if (star == NULL)
335     {
336         nm->item.pszText = "";
337         return;
338     }
339 
340     switch (nm->item.iSubItem)
341     {
342     case 0:
343         {
344             Universe* u = browser->appCore->getSimulation()->getUniverse();
345             starNameString = UTF8ToCurrentCP(u->getStarCatalog()->getStarName(*star));
346             nm->item.pszText = const_cast<char*>(starNameString.c_str());
347         }
348         break;
349 
350     case 1:
351         {
352             Vec3d r = star->getPosition(tdb) - browser->ucPos;
353             sprintf(callbackScratch, "%.4g", r.length() * 1.0e-6);
354             nm->item.pszText = callbackScratch;
355         }
356         break;
357 
358     case 2:
359         {
360             Vec3d r = star->getPosition(tdb) - browser->ucPos;
361             double appMag = astro::absToAppMag((double) star->getAbsoluteMagnitude(),
362                                                (r.length() * 1e-6));
363             sprintf(callbackScratch, "%.2f", appMag);
364             nm->item.pszText = callbackScratch;
365         }
366         break;
367 
368     case 3:
369         sprintf(callbackScratch, "%.2f", star->getAbsoluteMagnitude());
370         nm->item.pszText = callbackScratch;
371         break;
372 
373     case 4:
374         strncpy(callbackScratch, star->getSpectralType(),
375                 sizeof(callbackScratch));
376         callbackScratch[sizeof(callbackScratch) - 1] = '\0';
377         nm->item.pszText = callbackScratch;
378         break;
379     }
380 }
381 
RefreshItems(HWND hDlg,StarBrowser * browser)382 void RefreshItems(HWND hDlg, StarBrowser* browser)
383 {
384     SetMouseCursor(IDC_WAIT);
385 
386     Simulation* sim = browser->appCore->getSimulation();
387     browser->ucPos = sim->getObserver().getPosition();
388     browser->pos = fromMicroLY((Point3f) browser->ucPos);
389     HWND hwnd = GetDlgItem(hDlg, IDC_STARBROWSER_LIST);
390     if (hwnd != 0)
391     {
392         ListView_DeleteAllItems(hwnd);
393         InitStarBrowserItems(hwnd, browser);
394     }
395 
396     SetMouseCursor(IDC_ARROW);
397 }
398 
StarBrowserProc(HWND hDlg,UINT message,UINT wParam,LONG lParam)399 BOOL APIENTRY StarBrowserProc(HWND hDlg,
400                               UINT message,
401                               UINT wParam,
402                               LONG lParam)
403 {
404     StarBrowser* browser = reinterpret_cast<StarBrowser*>(GetWindowLong(hDlg, DWL_USER));
405 
406     switch (message)
407     {
408     case WM_INITDIALOG:
409         {
410             StarBrowser* browser = reinterpret_cast<StarBrowser*>(lParam);
411             if (browser == NULL)
412                 return EndDialog(hDlg, 0);
413             SetWindowLong(hDlg, DWL_USER, lParam);
414 
415             HWND hwnd = GetDlgItem(hDlg, IDC_STARBROWSER_LIST);
416             InitStarBrowserColumns(hwnd);
417             InitStarBrowserItems(hwnd, browser);
418             CheckRadioButton(hDlg, IDC_RADIO_NEAREST, IDC_RADIO_WITHPLANETS, IDC_RADIO_NEAREST);
419 
420             //Initialize Max Stars edit box
421             char val[16];
422             hwnd = GetDlgItem(hDlg, IDC_MAXSTARS_EDIT);
423             sprintf(val, "%d", DefaultListStars);
424             SetWindowText(hwnd, val);
425             SendMessage(hwnd, EM_LIMITTEXT, 3, 0);
426 
427             //Initialize Max Stars Slider control
428             SendDlgItemMessage(hDlg, IDC_MAXSTARS_SLIDER, TBM_SETRANGE,
429                 (WPARAM)TRUE, (LPARAM)MAKELONG(MinListStars, MaxListStars));
430             SendDlgItemMessage(hDlg, IDC_MAXSTARS_SLIDER, TBM_SETPOS,
431                 (WPARAM)TRUE, (LPARAM)DefaultListStars);
432 
433             return(TRUE);
434         }
435 
436     case WM_DESTROY:
437         if (browser != NULL && browser->parent != NULL)
438         {
439             SendMessage(browser->parent, WM_COMMAND, IDCLOSE,
440                         reinterpret_cast<LPARAM>(browser));
441         }
442         break;
443 
444     case WM_COMMAND:
445         switch (LOWORD(wParam))
446         {
447         case IDOK:
448         case IDCANCEL:
449             if (browser != NULL && browser->parent != NULL)
450             {
451                 SendMessage(browser->parent, WM_COMMAND, IDCLOSE,
452                             reinterpret_cast<LPARAM>(browser));
453             }
454             EndDialog(hDlg, 0);
455             return TRUE;
456 
457         case IDC_BUTTON_CENTER:
458             browser->appCore->charEntered('c');
459             break;
460 
461         case IDC_BUTTON_GOTO:
462             browser->appCore->charEntered('G');
463             break;
464 
465         case IDC_RADIO_BRIGHTEST:
466             browser->predicate = BrightestStars;
467             RefreshItems(hDlg, browser);
468             break;
469 
470         case IDC_RADIO_NEAREST:
471             browser->predicate = NearestStars;
472             RefreshItems(hDlg, browser);
473             break;
474 
475         case IDC_RADIO_WITHPLANETS:
476             browser->predicate = StarsWithPlanets;
477             RefreshItems(hDlg, browser);
478             break;
479 
480         case IDC_BUTTON_REFRESH:
481             RefreshItems(hDlg, browser);
482             break;
483 
484         case IDC_MAXSTARS_EDIT:
485             // TODO: browser != NULL check should be in a lot more places
486             if (HIWORD(wParam) == EN_KILLFOCUS && browser != NULL)
487             {
488                 char val[16];
489                 DWORD nNewStars;
490                 DWORD minRange, maxRange;
491                 GetWindowText((HWND) lParam, val, sizeof(val));
492                 nNewStars = atoi(val);
493 
494                 // Check if new value is different from old. Don't want to
495                 // cause a refresh to occur if not necessary.
496                 if (nNewStars != browser->nStars)
497                 {
498                     minRange = SendDlgItemMessage(hDlg, IDC_MAXSTARS_SLIDER, TBM_GETRANGEMIN, 0, 0);
499                     maxRange = SendDlgItemMessage(hDlg, IDC_MAXSTARS_SLIDER, TBM_GETRANGEMAX, 0, 0);
500                     if (nNewStars < minRange)
501                         nNewStars = minRange;
502                     else if (nNewStars > maxRange)
503                         nNewStars = maxRange;
504 
505                     // If new value has been adjusted from what was entered,
506                     // reflect new value back in edit control.
507                     if (atoi(val) != nNewStars)
508                     {
509                         sprintf(val, "%d", nNewStars);
510                         SetWindowText((HWND)lParam, val);
511                     }
512 
513                     // Recheck value if different from original.
514                     if (nNewStars != browser->nStars)
515                     {
516                         browser->nStars = nNewStars;
517                         SendDlgItemMessage(hDlg,
518                                            IDC_MAXSTARS_SLIDER,
519                                            TBM_SETPOS,
520                                            (WPARAM) TRUE,
521                                            (LPARAM) browser->nStars);
522                         RefreshItems(hDlg, browser);
523                     }
524                 }
525             }
526             break;
527         }
528         break;
529 
530     case WM_NOTIFY:
531         {
532             LPNMHDR hdr = (LPNMHDR) lParam;
533 
534             if (hdr->idFrom == IDC_STARBROWSER_LIST && browser != NULL)
535             {
536                 switch(hdr->code)
537                 {
538                 case LVN_GETDISPINFO:
539                     StarBrowserDisplayItem((LPNMLVDISPINFOA) lParam, browser);
540                     break;
541                 case LVN_ITEMCHANGED:
542                     {
543                         LPNMLISTVIEW nm = (LPNMLISTVIEW) lParam;
544                         if ((nm->uNewState & LVIS_SELECTED) != 0)
545                         {
546                             Simulation* sim = browser->appCore->getSimulation();
547                             Star* star = reinterpret_cast<Star*>(nm->lParam);
548                             if (star != NULL)
549                                 sim->setSelection(Selection(star));
550                         }
551                         break;
552                     }
553                 case LVN_COLUMNCLICK:
554                     {
555                         HWND hwnd = GetDlgItem(hDlg, IDC_STARBROWSER_LIST);
556                         if (hwnd != 0)
557                         {
558                             LPNMLISTVIEW nm = (LPNMLISTVIEW) lParam;
559                             StarBrowserSortInfo sortInfo;
560                             sortInfo.subItem = nm->iSubItem;
561                             sortInfo.ucPos = browser->ucPos;
562                             sortInfo.pos = browser->pos;
563                             ListView_SortItems(hwnd, StarBrowserCompareFunc,
564                                                reinterpret_cast<LPARAM>(&sortInfo));
565                         }
566                     }
567 
568                 }
569             }
570         }
571         break;
572 
573     case WM_HSCROLL:
574         {
575             WORD sbValue = LOWORD(wParam);
576             switch(sbValue)
577             {
578                 case SB_THUMBTRACK:
579                 {
580                     char val[16];
581                     HWND hwnd = GetDlgItem(hDlg, IDC_MAXSTARS_EDIT);
582                     sprintf(val, "%d", HIWORD(wParam));
583                     SetWindowText(hwnd, val);
584                     break;
585                 }
586                 case SB_THUMBPOSITION:
587                 {
588                     browser->nStars = (int)HIWORD(wParam);
589                     RefreshItems(hDlg, browser);
590                     break;
591                 }
592             }
593         }
594         break;
595     }
596 
597     return FALSE;
598 }
599 
600 
StarBrowser(HINSTANCE appInstance,HWND _parent,CelestiaCore * _appCore)601 StarBrowser::StarBrowser(HINSTANCE appInstance,
602                          HWND _parent,
603                          CelestiaCore* _appCore) :
604     appCore(_appCore),
605     parent(_parent)
606 {
607     ucPos = appCore->getSimulation()->getObserver().getPosition();
608     pos = fromMicroLY((Point3f) ucPos);
609 
610     predicate = NearestStars;
611     nStars = DefaultListStars;
612 
613     hwnd = CreateDialogParam(appInstance,
614                              MAKEINTRESOURCE(IDD_STARBROWSER),
615                              parent,
616                              StarBrowserProc,
617                              reinterpret_cast<LONG>(this));
618 }
619 
620 
~StarBrowser()621 StarBrowser::~StarBrowser()
622 {
623     SetWindowLong(hwnd, DWL_USER, 0);
624 }
625