1 /* -*-c++-*- */
2 /* osgEarth - Geospatial SDK for OpenSceneGraph
3 * Copyright 2019 Pelican Mapping
4 * http://osgearth.org
5 *
6 * osgEarth is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU Lesser General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
12 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
13 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
14 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
15 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
16 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
17 * IN THE SOFTWARE.
18 *
19 * You should have received a copy of the GNU Lesser General Public License
20 * along with this program.  If not, see <http://www.gnu.org/licenses/>
21 */
22 
23 /**
24  * Experiment with using a J2000/ECI reference frame as the root of the scene,
25  * with the MapNode under an ECI-to-ECEF transform.
26  */
27 #include <osgEarth/MapNode>
28 #include <osgEarth/DateTime>
29 #include <osgEarth/NodeUtils>
30 #include <osgEarth/PointDrawable>
31 #include <osgEarth/CullingUtils>
32 #include <osgEarth/LineDrawable>
33 #include <osgEarth/Lighting>
34 #include <osgEarthUtil/EarthManipulator>
35 #include <osgEarthUtil/ExampleResources>
36 #include <osgEarthUtil/Sky>
37 #include <osgEarthSymbology/Color>
38 #include <osgEarthAnnotation/LabelNode>
39 #include <osgViewer/Viewer>
40 #include <iostream>
41 
42 #define LC "[eci] "
43 
44 using namespace osgEarth;
45 using namespace osgEarth::Util;
46 using namespace osgEarth::Symbology;
47 using namespace osgEarth::Annotation;
48 namespace ui = osgEarth::Util::Controls;
49 
50 int
usage(const char * name,const char * msg)51 usage(const char* name, const char* msg)
52 {
53     OE_NOTICE
54         << "\nUsage: " << name << " [file.earth]\n"
55         << "     --tle <filename>    : Load a NORAD TLE file\n"
56         << "     --maxpoints <num>   : Limit the track size to <num> points\n"
57         << "     --ecef              : View the track in ECEF space instead of ECI\n"
58         << "     --tessellate        : Add interpolated points to the track data\n"
59         << "\nDownload NORAD TLE files from https://www.celestrak.com/NORAD/archives\n\n"
60         << msg << std::endl;
61 
62     return 0;
63 }
64 
65 // Reference time for the J2000 ECI coordinate frame
66 static DateTime J2000Epoch(2000, 1, 1, 12.00);
67 
68 // Transform that takes us from a J2000 ECI reference frame
69 // to an ECEF reference frame (i.e. MapNode)
70 class J2000ToECEFTransform : public osg::MatrixTransform
71 {
72 public:
setDateTime(const DateTime & dt)73     void setDateTime(const DateTime& dt)
74     {
75         osg::Matrix matrix = createMatrix(dt);
76         setMatrix(matrix);
77     }
78 
createMatrix(const DateTime & dt)79     static osg::Matrix createMatrix(const DateTime& dt)
80     {
81         // Earth's rotation rate: International Astronomical Union (IAU) GRS 67
82         const double IAU_EARTH_ANGULAR_VELOCITY = 7292115.1467e-11; // (rad/sec)
83 
84         double secondsElapsed = (double)(dt.asTimeStamp() - J2000Epoch.asTimeStamp());
85         const double rotation = IAU_EARTH_ANGULAR_VELOCITY * secondsElapsed;
86 
87         osg::Matrix matrix;
88         matrix.makeRotate(rotation, 0, 0, 1);
89         return matrix;
90     }
91 };
92 
93 // Code to read TLE track data files from https://celestrak.com/NORAD
94 struct ECILocation
95 {
96     DateTime timestamp;     // point time
97     Angle incl;             // inclination
98     Angle raan;             // right ascencion of ascending node
99     Distance alt;           // altitude
100     osg::Vec3d eci;         // ECI coordinate
101     osg::Vec3d ecef;        // ECEF coordinate
102 
computeECIAndECEFECILocation103     void computeECIAndECEF()
104     {
105         eci =
106             osg::Quat(raan.as(Units::RADIANS), osg::Vec3d(0, 0, 1)) *
107             osg::Quat(incl.as(Units::RADIANS), osg::Vec3d(1, 0, 0)) *
108             osg::Vec3d(alt.as(Units::METERS), 0, 0);
109 
110         osg::Matrix eci2ecef = J2000ToECEFTransform::createMatrix(timestamp);
111         ecef = eci * eci2ecef;
112     }
113 };
114 
115 struct ECITrack : public std::vector<ECILocation>
116 {
117     // interpolate points for a smoother track
tessellateECITrack118     void tessellate()
119     {
120         ECITrack newTrack;
121         for(unsigned k=0; k<size()-1; ++k)
122         {
123             for(float t=0; t<1.0f; t+=0.1)
124             {
125                 const ECILocation& p0 = at(k);
126                 const ECILocation& p1 = at(k+1);
127 
128                 newTrack.push_back(ECILocation());
129                 ECILocation& loc = newTrack.back();
130 
131                 loc.timestamp = DateTime(p0.timestamp.asTimeStamp() + (p1.timestamp.asTimeStamp()-p0.timestamp.asTimeStamp())*t);
132                 loc.raan.set(p0.raan.as(Units::RADIANS) + (p1.raan.as(Units::RADIANS)-p0.raan.as(Units::RADIANS))*t, Units::RADIANS);
133                 loc.incl.set(p0.incl.as(Units::RADIANS) + (p1.incl.as(Units::RADIANS)-p0.incl.as(Units::RADIANS))*t, Units::RADIANS);
134                 loc.alt = p0.alt;
135                 loc.computeECIAndECEF();
136             }
137         }
138         swap(newTrack);
139     }
140 };
141 
142 class TLEReader
143 {
144 public:
145     // https://celestrak.com/NORAD/documentation/tle-fmt.php
read(const std::string & filename,ECITrack & track) const146     bool read(const std::string& filename, ECITrack& track) const
147     {
148         std::ifstream fin(filename.c_str());
149         while(!fin.eof())
150         {
151             std::string line1, line2;
152 
153             std::getline(fin, line1);
154             std::getline(fin, line2);
155             if (line1.empty() || line2.empty())
156                 break;
157 
158             track.push_back(ECILocation());
159             ECILocation& loc = track.back();
160 
161             // read timestamp
162             int year2digit = osgEarth::as<int>(line1.substr(18, 2), 99);
163             int year = year2digit > 50? 1900+year2digit : 2000+year2digit;
164             double dayOfYear = osgEarth::as<double>(line1.substr(20, 12), 0);
165             loc.timestamp = DateTime(year, dayOfYear);
166 
167             // read ra/decl
168             loc.incl.set(osgEarth::as<double>(line2.substr(8,8),0), Units::DEGREES);
169             loc.raan.set(osgEarth::as<double>(line2.substr(17,8),0), Units::DEGREES);
170             loc.alt.set(6371 + 715, Units::KILOMETERS);
171 
172             loc.computeECIAndECEF();
173         }
174         OE_INFO << "Read " << track.size() << " track points" << std::endl;
175         return true;
176     }
177 };
178 
179 // If the "global" coordinate system is ECI, you can put this transform
180 // under the MapNode (in ECEF space) to "revert" to that global ECI frame.
181 // Useful if you want to put ECI-space data under the MapNode.
182 class ECIReferenceFrame : public osg::Group
183 {
184 public:
ECIReferenceFrame()185     ECIReferenceFrame()
186     {
187         Lighting::set(getOrCreateStateSet(), osg::StateAttribute::OFF);
188     }
189 
traverse(osg::NodeVisitor & nv)190     void traverse(osg::NodeVisitor& nv)
191     {
192         osgUtil::CullVisitor* cv = Culling::asCullVisitor(nv);
193         if (cv)
194         {
195             const osg::Camera* cam = cv->getRenderStage()->getCamera();
196             cv->pushModelViewMatrix(new osg::RefMatrix(cam->getViewMatrix()), osg::Transform::ABSOLUTE_RF);
197             osg::Group::traverse(nv);
198             cv->popModelViewMatrix();
199         }
200         else osg::Group::traverse(nv);
201     }
202 };
203 
204 // Loads up an ECITrack for display as a series of points.
205 class ECITrackDrawable : public LineDrawable //public PointDrawable
206 {
207 public:
ECITrackDrawable()208     ECITrackDrawable() : LineDrawable(GL_LINE_STRIP)
209     {
210         Lighting::set(getOrCreateStateSet(), 0);
211         //setPointSmooth(true);
212         //setPointSize(4.0f);
213     }
214 
setDateTime(const DateTime & dt)215     void setDateTime(const DateTime& dt)
216     {
217         osg::FloatArray* times = dynamic_cast<osg::FloatArray*>(getVertexAttribArray(6));
218         unsigned i;
219         for (i = 0; i < getNumVerts(); ++i)
220         {
221             if (dt.asTimeStamp() < getVertexAttrib(times, i))
222                 break;
223         }
224         setCount(i);
225     }
226 
load(const ECITrack & track,bool drawECEF)227     void load(const ECITrack& track, bool drawECEF)
228     {
229         osg::FloatArray* times = new osg::FloatArray();
230         times->setBinding(osg::Array::BIND_PER_VERTEX);
231         setVertexAttribArray(6, times);
232 
233         osg::Vec4f HSLA;
234         Color color;
235 
236         for(unsigned i=0; i<track.size(); ++i)
237         {
238             const ECILocation& loc = track[i];
239             pushVertex(drawECEF? loc.ecef : loc.eci);
240             pushVertexAttrib(times, (float)loc.timestamp.asTimeStamp());
241 
242             // simple color ramp
243             HSLA.set((float)i/(float)(track.size()-1), 1.0f, 1.0f, 1.0f);
244             color.fromHSL(HSLA);
245             setColor(i, color);
246         }
247         finish();
248     }
249 };
250 
createECIAxes()251 osg::Node* createECIAxes()
252 {
253     const float R = 10e6;
254     LineDrawable* d = new LineDrawable(GL_LINES);
255     d->allocate(6);
256 
257     d->setVertex(0, osg::Vec3(0,0,0));
258     d->setColor(0, osg::Vec4(1,0,0,1));
259     d->setVertex(1, osg::Vec3(R,0,0));
260     d->setColor(1, osg::Vec4(1,0,0,1));
261 
262     d->setVertex(2, osg::Vec3(0,0,0));
263     d->setColor(2, osg::Vec4(0,1,0,1));
264     d->setVertex(3, osg::Vec3(0,R,0));
265     d->setColor(3, osg::Vec4(0,1,0,1));
266 
267     d->setVertex(4, osg::Vec3(0,0,0));
268     d->setColor(4, osg::Vec4(0,0,1,1));
269     d->setVertex(5, osg::Vec3(0,0,R));
270     d->setColor(5, osg::Vec4(0,0,1,1));
271 
272     d->setLineWidth(10);
273     return d;
274 }
275 
276 // Application-wide data and control structure
277 struct App
278 {
279     DateTime start, end;
280     HSliderControl* time;
281     LabelControl* timeLabel;
282     SkyNode* sky;
283     J2000ToECEFTransform* ecef;
284     osg::Group* eci;
285     ECITrackDrawable* trackDrawable;
286     ECITrack track;
287 
AppApp288     App()
289     {
290         trackDrawable = 0L;
291         start = J2000Epoch;
292         end = start + 24.0;
293     }
294 
setTimeApp295     void setTime()
296     {
297         DateTime newTime(time->getValue());
298 
299         if (sky)
300             sky->setDateTime(newTime);
301 
302         if (ecef)
303             ecef->setDateTime(newTime);
304 
305         if (trackDrawable)
306             trackDrawable->setDateTime(newTime);
307 
308         timeLabel->setText(newTime.asRFC1123());
309     }
310 };
311 
312 OE_UI_HANDLER(setTime);
313 
314 
315 int
main(int argc,char ** argv)316 main(int argc, char** argv)
317 {
318     osg::ArgumentParser arguments(&argc,argv);
319     if ( arguments.read("--help") )
320         return usage(argv[0], "");
321 
322     App app;
323 
324     // Read in an optiona TLE track data file
325     std::string tlefile;
326     if (arguments.read("--tle", tlefile))
327     {
328         TLEReader().read(tlefile, app.track);
329         if (!app.track.empty())
330         {
331             int maxPoints;
332             if (arguments.read("--maxpoints", maxPoints) && app.track.size() > maxPoints)
333                 app.track.resize(maxPoints);
334             if (arguments.read("--tessellate"))
335                 app.track.tessellate();
336             app.start = app.track.front().timestamp;
337             app.end   = app.track.back().timestamp;
338         }
339     }
340 
341     osgViewer::Viewer viewer(arguments);
342     viewer.setCameraManipulator( new EarthManipulator(arguments) );
343 
344     ui::VBox* container = new ui::VBox();
345     container->setChildSpacing(3);
346     container->addControl(new ui::LabelControl("ECI COORDINATE SYSTEM EXAMPLE", Color::Yellow));
347 
348     // UI control to modify the time of day.
349     ui::HBox* h = container->addControl(new ui::HBox());
350     h->addControl(new ui::LabelControl("Time:"));
351     app.time = h->addControl(new HSliderControl(
352         app.start.asTimeStamp(), app.end.asTimeStamp(), app.start.asTimeStamp(),
353         new setTime(app)));
354     app.time->setWidth(500);
355     app.timeLabel = container->addControl(new LabelControl());
356 
357     // Load an earth file
358     osg::Node* earth = MapNodeHelper().load(arguments, &viewer, container);
359     if (earth)
360     {
361         // New scene graph root
362         osg::Group* root = new osg::Group();
363 
364         // First create a Sky which we will place in the (default) ECI frame.
365         SkyOptions skyOptions;
366         skyOptions.coordinateSystem() = SkyOptions::COORDSYS_ECI;
367         app.sky = SkyNode::create(MapNode::get(earth));
368         app.sky->attach(&viewer);
369         app.sky->getSunLight()->setAmbient(osg::Vec4(0.5,0.5,0.5,1.0));
370         root->addChild(app.sky);
371 
372         // A special transform takes us from the ECI into an ECEF frame
373         // based on the current date and time.
374         // The earth (MapNode) lives here since it is ECEF.
375         app.ecef = new J2000ToECEFTransform();
376         app.sky->addChild(app.ecef);
377         app.ecef->addChild(earth);
378 
379         // This group holds data in the ECI frame.
380         app.eci = new ECIReferenceFrame();
381         app.eci->addChild(createECIAxes());
382         MapNode::get(earth)->addChild(app.eci);
383 
384         // Track data
385         if (!app.track.empty())
386         {
387             app.trackDrawable = new ECITrackDrawable();
388 
389             bool drawECEF = arguments.read("--ecef");
390             if (drawECEF)
391             {
392                 app.trackDrawable->load(app.track, true);
393                 MapNode::get(earth)->addChild(app.trackDrawable);
394             }
395             else
396             {
397                 app.trackDrawable->load(app.track, false);
398                 app.eci->addChild(app.trackDrawable);
399             }
400         }
401 
402         viewer.realize();
403         app.time->setWidth(viewer.getCamera()->getViewport()->width()-40);
404 
405         app.setTime();
406         viewer.setSceneData(root);
407         viewer.run();
408     }
409     else
410     {
411         return usage(argv[0], "Bad earth file");
412     }
413 
414     return 0;
415 }