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 }