// ---------------------- // OpenGL GPS datastream plotter. // // Written by and Copyright Chris Halsall (chalsall@chalsall.com). // First published on the O'Reilly Network on Linux.com // (oreilly.linux.com). December 2000. All rights reserved. // // This code is licensed under the GNU GPL Version 2.0. // See (URL: http://www.gnu.org/copyleft/gpl.html ) for details. // // Coded to the strange waves of Laurie Anderson: Bright Red. // #define PROGRAM_TITLE "O'Reilly Net: GPS plot -- C.Halsall" #include // Always a good idea. #include // Ditto. #include // For isdigit(). #include // For our FPS stats. #include // For our FPS stats. #include // OpenGL itself -- may need to add "mesa". #include // GLU support library. #include // GLUT support library. #include #include #define max(a,b) (((a)>(b))?(a):(b)) // Structure for data for each point. typedef struct { float X, Y, Z; double Lat, Long; float Alt; double LatT, LongT; char LatH, LongH; float Err; int Time, Date; } Point; // Structure to abstract point-array book keeping vars. typedef struct { Point *P; int Alloc; int Used; } Points; #define POINT_ARRAYS 10 static Points PointsArray[ POINT_ARRAYS ]; int ArrayColors[ POINT_ARRAYS + 1] = { 0x000000, 0x000000, 0xFFFFAA, 0xFFAAFF, 0xAAFFFF, 0xCCFFCC, 0xCCCCFF, 0xFFCCCC, 0xEEEEEE, 0xFFFF88, 0xFF88FF }; int NumPointsA = 0; // A shortcut to our first array of points. Point *Points1; int NumPoints = 0; int Curr = 1; int Step = 10; int Range = 100; // A default filename to try. char *FName1 = "sample.gps"; // Maximum ranges. double AMin, AMinT, OMin, OMinT, HMin; double AMax, AMaxT, OMax, OMaxT, HMax; float AMet, OMet, HMet; // Time range (for color fountains). unsigned int TMin, TMax, TRange; // Some global variables. // Window and texture IDs, window width and height. int Window_ID; int Window_Width = 600; int Window_Height = 400; // Our display mode settings. int AAliasing_On = 0; int Ground_Track = 1; int On_Path=0; // Object and scene global variables. // When not on-path, current observer location. float Obs[3] = {0.0, -1.4, 1.4}; float View_Azimuth=0; float View_Elevation=-13.0; // Calculated point of interest. float PoI[3]; // Calculated point of interest vector. float PoIV[3]; // Variables used for mouse movement tracking. int MouseLX, MouseLY; int MouseActive = -1; // ------ // Frames per second (FPS) statistic variables and routine. #define FRAME_RATE_SAMPLES 50 int FrameCount=0; float FrameRate=0; static void ourDoFPS( ) { static clock_t last=0; clock_t now; float delta; if (++FrameCount >= FRAME_RATE_SAMPLES) { now = times(NULL); // Migrating from clock(); if (last) { delta= (now - last) / (float) CLK_TCK; FrameRate = FRAME_RATE_SAMPLES / delta; } last = now; FrameCount = 0; } } // ------ // String rendering routine; leverages on GLUT routine. static void ourPrintString( void *font, char *str ) { int i,l=strlen(str); for(i=0;iP = realloc(Ps->P, sizeof(*Ps->P) * Num); if (!Ps->P) return 0; if (Ps->Alloc < Num) memset( &Ps->P[Ps->Alloc], 0,sizeof(*Ps->P) * (Num - Ps->Alloc)); if (Ps->Used > Num) Ps->Used = Num; Ps->Alloc = Num; return 1; } // ------ // Function to return a random floating number between 0 and the passed // parameter. float ourRand( float Max ) { return( (Max * rand()) / RAND_MAX ); } // ------ // Function to build a display list for the ground-tracks. void ourBuildDL( Points *Ps, int List ) { int Cnt, T; float Ri,Gi,Bi; float Rt,Gt,Bt; Point *P = Ps->P; int Count = Ps->Used; printf("Building Display List for ground track %d.\n", List); if (List == 1) { Ri = (1.0 - 0.0) / TRange; Gi = (0.0 - 0.0) / TRange; Bi = (0.0 - 1.0) / TRange; } else { Rt = ( ArrayColors[List] >> 16) / 0xFF; Gt = ((ArrayColors[List] >> 8) & 0xFF) / 0xFF; Bt = ( ArrayColors[List] & 0xFF) / 0xFF; } glNewList(List, GL_COMPILE); glLineWidth(1.0); glBegin(GL_LINE_STRIP); for (Cnt = 0; Cnt < Count; Cnt++) { T = P[Cnt].Time; if (T == 999999) { glEnd(); glBegin(GL_LINE_STRIP); continue; } if (List == 1) { Rt = 1.0 - Ri * (T-TMin); Gt = 0.0 - Gi * (T-TMin); Bt = 0.0 - Bi * (T-TMin); Gt = .25 + (T % 100) / 90.0; } glColor4f( Rt, Gt, Bt, .70); // Line color glVertex3d( P[Cnt].X, P[Cnt].Y, 0.0); } glEnd(); glEndList(); } // ------ // Routine to draw the position cross. void ourDrawCross( int Type, Point *P ) { float size = 0.01; float Z = P->Z - size; if (Type) Z = 0; glVertex3d(P->X,P->Y-size,P->Z); glVertex3d(P->X,P->Y+size,P->Z); glVertex3d(P->X,P->Y+size,P->Z); glVertex3d(P->X+size/2,P->Y+size/2,P->Z+size/2); glVertex3d(P->X,P->Y+size,P->Z); glVertex3d(P->X-size/2,P->Y+size/2,P->Z+size/2); glVertex3d(P->X,P->Y+size,P->Z); glVertex3d(P->X-size/2,P->Y+size/2,P->Z-size/2); glVertex3d(P->X,P->Y+size,P->Z); glVertex3d(P->X+size/2,P->Y+size/2,P->Z-size/2); glVertex3d(P->X,P->Y,0); glVertex3d(P->X,P->Y,-.05); } // ------ // Routine to draw a horizontal plane. void ourDrawPlane( float Z ) { glBegin(GL_LINE_STRIP); glColor4f(0.9,0.2,0.2,1.0); // Red glVertex3d(1.0, -1.0, Z); glColor4f(0.2,0.9,0.2,1.0); // North side is Green. glVertex3d(1.0, 1.0, Z); glVertex3d(-1.0, 1.0, Z); glColor4f(0.9,0.2,0.2,1.0); // Back to Red. glVertex3d(-1.0, -1.0, Z); glVertex3d(1.0, -1.0, Z); glEnd(); } // ------ // Angle to Radian conversion. float ourA2R( float Angle ) { return Angle * M_PI/180; } // ------ // Calculates the observer's XYZ position of interest from current // position and their viewing andles. static void ourCalcPoI(void) { PoIV[0] = sin(ourA2R(View_Azimuth)) / 10.0; PoIV[1] = cos(ourA2R(View_Azimuth)) / 10.0; PoIV[2] = View_Elevation / 100.0; PoI[0]=Obs[0] + PoIV[0]; PoI[1]=Obs[1] + PoIV[1]; PoI[2]=Obs[2] + PoIV[2]; } // ------ // Renders the text labels using Stroke characters. void ourRenderLables( void ) { char buf[80]; // For our strings. float X_Rot = -45; glColor4f(0.2,0.9,0.9,1.0); glPushMatrix(); glTranslatef(1.0f,1.0f,0.0f); glScalef(0.0004, 0.0004, 0.0004); glRotatef(90,0.0f,0.0f,1.0f); glRotatef(X_Rot,-1.0f,0.0f,0.0f); sprintf(buf,"<= %.4f", OMax); ourStrokeString(GLUT_STROKE_ROMAN, buf); glPopMatrix(); glPushMatrix(); glTranslatef(-1.0f,1.0f,0.0f); glScalef(0.0004, 0.0004, 0.0004); glRotatef(90,0.0f,0.0f,1.0f); glRotatef(X_Rot,-1.0f,0.0f,0.0f); sprintf(buf,"<= %.4f", OMin); ourStrokeString(GLUT_STROKE_ROMAN, buf); glPopMatrix(); glPushMatrix(); glTranslatef(-0.3f,1.2f,0.0f); glScalef(0.0006, 0.0006, 0.0006); glRotatef(X_Rot,-1.0f,0.0f,0.0f); sprintf(buf,"<--- %.1f M --->", OMet); ourStrokeString(GLUT_STROKE_ROMAN, buf); glPopMatrix(); glPushMatrix(); glTranslatef(1.0f,-1.0f,0.0f); glScalef(0.0004, 0.0004, 0.0004); glRotatef(X_Rot,-1.0f,0.0f,0.0f); sprintf(buf,"<= %.4f", AMin); ourStrokeString(GLUT_STROKE_ROMAN, buf); glPopMatrix(); glPushMatrix(); glTranslatef(1.0f,1.0f,0.0f); glScalef(0.0004, 0.0004, 0.0004); glRotatef(X_Rot,-1.0f,0.0f,0.0f); sprintf(buf,"<= %.4f", AMax); ourStrokeString(GLUT_STROKE_ROMAN, buf); glPopMatrix(); } // ------ // Routine which actuall does the drawing. void cbRenderScene( void ) { int Cnt, T; char buf[80]; // For our strings. Cnt = max(Curr-Range,1); // Enables, disables or otherwise adjusts as // appropriate for our current settings. if (AAliasing_On) { glEnable(GL_LINE_SMOOTH); glEnable(GL_BLEND); } else { glDisable(GL_LINE_SMOOTH); glDisable(GL_BLEND); } glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA); glEnable(GL_DEPTH_TEST); // Need to manipulate the ModelView matrix to move our model around. glMatrixMode(GL_MODELVIEW); // Reset to 0,0,0; no rotation, no scaling. glLoadIdentity(); // Places the camera at the right point, looking in the right // direction, depending on the mode. if (On_Path) { gluLookAt( Points1[Cnt].X, Points1[Cnt].Y, Points1[Cnt].Z + .1, Points1[Curr].X, Points1[Curr].Y, Points1[Curr].Z, 0.0,0.0,1.0); } else { gluLookAt( Obs[0], Obs[1], Obs[2], PoI[0], PoI[1], PoI[2], 0.0,0.0,1.0); } // Clear the color and depth buffers. glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glLineWidth(1.0); ourDrawPlane(0.0); ourDrawPlane(0.4); if (Ground_Track) { for (T = 0; T < NumPointsA; T++) glCallList(T+1); } glLineWidth(1.5); glBegin(GL_LINES); glColor4f(0.9,0.9,0.2,1); ourDrawCross(1, &Points1[Cnt++]); for (; Cnt <= Curr; Cnt++) { T = Points1[Cnt].Time; glColor4f( .5 + (T / 10000) / 48.0, .5 + ((T / 100)%100) / 120.0, .5 + (T % 100) / 120.0, .75); // Basic polygon color glVertex3d(Points1[Cnt-1].X,Points1[Cnt-1].Y,Points1[Cnt-1].Z); glVertex3d(Points1[Cnt-0].X,Points1[Cnt-0].Y,Points1[Cnt-0].Z); } glColor4f(0.2,0.9,0.9,1); ourDrawCross(1, &Points1[Curr]); Curr += Step; if (Curr > NumPoints) Curr = 1; // All lines have been drawn. glEnd(); ourRenderLables(); // Move back to the origin (for the text, below). glLoadIdentity(); // We need to change the projection matrix for the text rendering. glMatrixMode(GL_PROJECTION); // But we like our current view too; so we save it here. glPushMatrix(); // Now we set up a new projection for the text. glLoadIdentity(); glOrtho(0,Window_Width,0,Window_Height,-1.0,1.0); // Lit or textured text looks awful. glDisable(GL_TEXTURE_2D); glDisable(GL_LIGHTING); // We don't want depth-testing either. glDisable(GL_DEPTH_TEST); // But, for fun, let's make the text partially transparent too. glColor4f(0.6,1.0,0.6,.75); // Render our various display mode settings. sprintf(buf,"UTC: %06d Curr: %d", Points1[Curr].Time, Curr); glRasterPos2i(2,14); ourPrintString(GLUT_BITMAP_HELVETICA_12,buf); sprintf(buf,"%f %f", Points1[Curr].Lat, Points1[Curr].Long); glRasterPos2i(2,2); ourPrintString(GLUT_BITMAP_HELVETICA_12,buf); // Now we want to render the calulated FPS at the top. // To ease, simply translate up. Note we're working in screen // pixels in this projection. glTranslatef(6.0f,Window_Height - 14,0.0f); // Make sure we can read the FPS section by first placing a // dark, mostly opaque backdrop rectangle. glColor4f(0.2,0.2,0.2,0.75); glBegin(GL_QUADS); glVertex3f( 0.0f, -2.0f, 0.0f); glVertex3f( 0.0f, 12.0f, 0.0f); glVertex3f(140.0f, 12.0f, 0.0f); glVertex3f(140.0f, -2.0f, 0.0f); glEnd(); glColor4f(0.9,0.2,0.2,.75); sprintf(buf,"FPS: %f F: %2d", FrameRate, FrameCount); glRasterPos2i(6,0); ourPrintString(GLUT_BITMAP_HELVETICA_12,buf); // Done with this special projection matrix. Throw it away. glPopMatrix(); // All done drawing. Let's show it. glutSwapBuffers(); // And collect our statistics. ourDoFPS(); } // ------ // Callback function called when a normal key is pressed. void cbKeyPressed( unsigned char key, int x, int y ) { switch (key) { case 113: case 81: case 27: // Q (Escape) - We're outta here. glutDestroyWindow(Window_ID); exit(1); break; // exit doesn't return, but anyway... case 'a': case 'A': // A - Antialiased Lines AAliasing_On = AAliasing_On ? 0 : 1; break; case 'g': case 'G': // Ground-track. Ground_Track = Ground_Track ? 0 : 1; break; case 'r': case 'R': // Reset! Obs[0] = 0; Obs[1] = -3.0; Obs[2] = 2.0; View_Azimuth=0; View_Elevation=-5.0; ourCalcPoI(); break; // Toggle being on-path. case 'o': case 'O': On_Path = ! On_Path; break; case '<': case ',': if (Step > 0) Step--; break; case '>': case '.': Step++; break; case ' ': Step = 0; break; case '-': case '_': if (Range > 2) Range--; break; case '=': case '+': Range++; break; default: printf ("KP: No action for %d.\n", key); break; } } // ------ // Callback Function called when a special key is pressed. void cbSpecialKeyPressed( int key, int x, int y ) { switch (key) { case GLUT_KEY_PAGE_UP: Obs[2] += 0.01; break; case GLUT_KEY_PAGE_DOWN: Obs[2] -= 0.01; break; case GLUT_KEY_UP: Obs[0] += PoIV[0]; Obs[1] += PoIV[1]; break; case GLUT_KEY_DOWN: Obs[0] -= PoIV[0]; Obs[1] -= PoIV[1]; break; case GLUT_KEY_LEFT: break; case GLUT_KEY_RIGHT: break; default: printf ("SKP: No action for %d.\n", key); break; } ourCalcPoI(); } // ------ // Callback function called for mouse button events. void cbMouse( int button, int state, int x, int y ) { switch (button) { case GLUT_LEFT_BUTTON: case GLUT_MIDDLE_BUTTON: case GLUT_RIGHT_BUTTON: if (state == GLUT_DOWN) { MouseActive = button; MouseLX = x; MouseLY = y; } else { MouseActive = -1; } return; break; case 3: // Special -- for wheel mice. Obs[0] += PoIV[0]; Obs[1] += PoIV[1]; break; case 4: // Special -- for wheel mice. Obs[0] -= PoIV[0]; Obs[1] -= PoIV[1]; break; } ourCalcPoI(); // printf("%d %d (%d, %d)\n", button, state, x, y); } // ------ // Callback function called for mouse move events. void cbMotion( int x, int y ) { int DX, DY; if (MouseActive > -1) { DX = x - MouseLX; MouseLX = x; DY = y - MouseLY; MouseLY = y; switch (MouseActive) { case GLUT_LEFT_BUTTON: View_Azimuth -= DX / 10.0; View_Elevation += DY / 20.0; break; case GLUT_RIGHT_BUTTON: Obs[0] -= PoIV[1] * DX / 10; Obs[1] -= PoIV[2] * DX / 10; Obs[0] += PoIV[0] * DY / 10; Obs[1] += PoIV[1] * DY / 10; break; case GLUT_MIDDLE_BUTTON: Obs[2] += DY / 10.0; break; } } ourCalcPoI(); // printf("(%d, %d)\n", x, y); // printf("%d : (%f, %f)\n", MouseActive, View_Azimuth, View_Elevation); } // ------ // Callback routine executed whenever our window is resized. Lets us // request the newly appropriate perspective projection matrix for // our needs. Try removing the gluPerspective() call to see what happens. void cbResizeScene( int Width, int Height ) { // Let's not core dump, no matter what. if (Height == 0) Height = 1; glViewport(0, 0, Width, Height); glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective(45.0f,(GLfloat)Width/(GLfloat)Height,0.001f,100.0f); glMatrixMode(GL_MODELVIEW); Window_Width = Width; Window_Height = Height; } // ------ // Very simple XOR checksum function. Note that the checksum isn't // absolutely required in the NMEA 0183 standard, but almost always is. // If parcing a stream without the checksum, remove the commented out // line below returning 1 if no "*" is found. int ourChecksumValid( unsigned char *S ) { unsigned char CS = 0; unsigned int SCS = 0; S++; // The leading "$" is not included in the calculation. while (*S && *S != '*') { CS = CS ^ *S++; } if (*S == '*') { S++; if (sscanf(S, "%x", &SCS)) { if (SCS == (unsigned int)CS) return 1; } printf("Checksum failure: Calc: %x Passed: (%x) %s", CS, SCS, S); } else { // Checksum isn't absolutely required in spec -- uncomment for such streams. // return 1; } return 0; } #define FRACTION_RANGE 100000 // ------ // Parces a string of digits, assuming it is a decimal portion of a // value, of up to 5 digits. Returns an integer which should be // divided by FRACTION_RANGE to get the real value. // // This is needed because sscanf() et al do not handle double values, // and floats do not preserve the resolution of the values. int ourParceFraction( char *S ) { int Mult = FRACTION_RANGE; int Base = 0; if (!S) return 0; while (isdigit(*S)) { Base = Base * 10 + *S - '0'; Mult /= 10; S++; } return Base * Mult; } int ourLoadNMEA ( char *FName, Points *Ps ) { FILE *File; char *Read; char Buf[80]; int Max = 0; int LastTime=0, Day=0; Point *P; int Fix, Birds; float HDoP; char C4; char LatH, LongH; int Dump; int LaI, LaF; int LoI, LoF; char *S; printf("Loading NMEA from \"%s\"\n", FName); ourAllocPoints(Ps, 1000); File = fopen(FName, "r"); if (File == 0) return 0; while ((Read = fgets(Buf, sizeof(Buf), File))) { // Very crude checksum -- don't expect it to catch all errors. if (!ourChecksumValid(Buf)) continue; if (!(strncmp(Buf, "$GPGGA", 6))) { Dump = 0; // if (Max < 10) Dump =1; P = &Ps->P[Max]; sscanf(Buf, "$GPGGA,%d,%d.%d,%c,%d.%d,%c,%d,%d,%f,%f,%c", &P->Time, &LaI, &LaF, &LatH, &LoI, &LoF, &LongH, &Fix, &Birds, &HDoP, &P->Alt, &C4); if ((S = strchr(Buf, '.'))) { S++; LaF = ourParceFraction(S); if ((S = strchr(S, '.'))) { S++; LoF = ourParceFraction(S); } else Dump = 1; // Parce Error. } P->Long = LoI + LoF / (double)FRACTION_RANGE; P->Lat = LaI + LaF / (double)FRACTION_RANGE; P->LongT = ((int)LoI/100) * 100 + ((LoI%100) + LoF / (double)FRACTION_RANGE) / 60 * 100; P->LatT = (LaI/100) * 100 + ((LaI%100) + LaF / (double)FRACTION_RANGE) / 60 * 100; if (Fix == 0) { continue; } // if (P->Lat < 1000) Dump = 1; // if (P->Long < 12323) Dump = 1; // -- needed for gps2_1124a.txt // if (P->Long > 16000) Dump = 1; if (LatH == 'S' || LatH == 's') { P->Lat = -P->Lat; P->LatT = -P->LatT; } if (LongH == 'W' || LongH == 'w') { P->Long = -P->Long; P->LongT = -P->LongT; } if (P->Time != 999999) { // This is so we can handle multi-day datasets correctly. if (P->Time < LastTime) { printf("Dayroll -- P->Time == %d, LT==%d\n", P->Time, LastTime); Day += 1000000; } LastTime = P->Time; P->Time += Day; if (AMin>P->Lat) { AMin = P->Lat; AMinT = P->LatT; } if (AMaxLat) { AMax = P->Lat; AMaxT = P->LatT; } if (OMin>P->Long) { OMin = P->Long; OMinT = P->LongT; } if (OMaxLong) { OMax = P->Long; OMaxT = P->LongT; } if (HMin>P->Alt) HMin = P->Alt; if (HMaxAlt) HMax = P->Alt; if (TMin>P->Time) TMin = P->Time; if (TMaxTime) TMax = P->Time; } if (++Max >= Ps->Alloc) ourAllocPoints(Ps, Ps->Alloc + 1000); } } return Ps->Used = Max; } int Normalize( Points *Ps ) { float Scale; int Count; Point *P; static int Note = 1; // Meters per hundredth of a degree -- at equator only. OMet = (OMaxT - OMinT) * 1113.19; TRange = (TMax - TMin); Scale = OMaxT - OMinT; if (AMaxT - AMinT > Scale) Scale = AMaxT - AMinT; if (Note) { Note = 0; printf("\nDoing normalization.\n"); printf("Lat : %f -> %f\n", AMin, AMax); printf("Long : %f -> %f\n", OMin, OMax); printf("LongT: %f -> %f\n", OMinT, OMaxT); printf("Alt : %f -> %f\n", HMin, HMax); printf("Time : %d -> %d\n", TMin, TMax); printf("Scale : %f, OMin : %f, OMet: %f\n\n", Scale, OMinT, OMet); } Scale /= 2; for (Count = 0; CountUsed; Count++) { P = &Ps->P[Count]; if (P->Time != 999999) { P->X = -1 + (P->LongT - OMinT) / Scale; P->Y = -1 + (P->LatT - AMinT) / Scale; P->Z = (P->Alt - HMin) / ((HMax - HMin) * 2.5); } } printf("Normalization (of %d points) complete.\n", Ps->Used); return Ps->Used; } // ------ // Does everything needed before losing control to the main // OpenGL event loop. void ourOGLInit( int Width, int Height ) { int Count; // Color to clear color buffer to. glClearColor(0.1f, 0.1f, 0.1f, 0.0f); // Depth to clear depth buffer to; type of test. glClearDepth(1.0); glDepthFunc(GL_LESS); glHint(GL_LINE_SMOOTH_HINT, GL_NICEST); // Load up the correct perspective matrix; using a callback directly. cbResizeScene(Width,Height); for (Count = 0; Count < NumPointsA; Count++) { ourBuildDL(&PointsArray[Count], Count + 1); } ourCalcPoI(); } int ourLoadFiles( int argc, char **argv) { int Count; AMin = OMin = HMin = 999999.0; AMax = OMax = HMax = -999999.0; if (argc == 1) { printf("No NMEA datafile passed -- trying %s\n", FName1); } else { for (Count = 2; Count < argc; Count++) { ourLoadNMEA(argv[Count], &PointsArray[Count-1]); } FName1 = argv[1]; } TMin = -1; TMax = 0; ourLoadNMEA(FName1, &PointsArray[0]); NumPointsA = argc - 1; for (Count = 0; Count < NumPointsA; Count++) Normalize(&PointsArray[Count]); Points1 = PointsArray[0].P; NumPoints = PointsArray[0].Used; return 1; } // ------ // The main() function. Inits OpenGL. Calls our own init function, // then passes control onto OpenGL. int main( int argc, char **argv ) { glutInit(&argc, argv); ourLoadFiles(argc, argv); // To see OpenGL drawing, take out the GLUT_DOUBLE request. glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH); glutInitWindowSize(Window_Width, Window_Height); // Open a window Window_ID = glutCreateWindow( PROGRAM_TITLE ); // Register the callback function to do the drawing. glutDisplayFunc(&cbRenderScene); // If there's nothing to do, draw. glutIdleFunc(&cbRenderScene); // It's a good idea to know when our window's resized. glutReshapeFunc(&cbResizeScene); // And let's get some keyboard input. glutKeyboardFunc(&cbKeyPressed); glutSpecialFunc(&cbSpecialKeyPressed); glutMouseFunc(&cbMouse); glutMotionFunc(&cbMotion); // OK, OpenGL's ready to go. Let's call our own init function. ourOGLInit(Window_Width, Window_Height); // Print out a bit of help dialog. printf("\n" PROGRAM_TITLE "\n\n\ Use arrow keys to move forward or backwards and to rotate.\n\ Page up/down moves observer vertically. Mouse buttons work as\n\ well; left looks around, right moves horizontally, middle moves\n\ vertically. Mouse wheel, if available, moves forwards and backwards.\n\ \n\ '<' and '>' control speed of stepping through first GPS track,\n\ '-' and '+' effect number of points to show behind current point.\n\ 'R' resets observer position, 'A' toggles antialiased lines, and\n\ 'G' toggles ground-tracks. [Space] freezes path motion.\n\ 'Q' or [Esc] to quit; OpenGL window must have focus for input.\n\ \n\ Important: This program should not be used for serious navigation or\n\ measurement. Vertical scale is exaggerated.\n"); // Pass off control to OpenGL. // Above functions are called as appropriate. glutMainLoop(); return 1; }