]> git.mxchange.org Git - simgear.git/blob - simgear/scene/tsync/terrasync.cxx
Replace OpenThreads with SGThread to avoid useless OSG dependency.
[simgear.git] / simgear / scene / tsync / terrasync.cxx
1 // terrasync.cxx -- scenery fetcher
2 //
3 // Started by Curtis Olson, November 2002.
4 //
5 // Copyright (C) 2002  Curtis L. Olson  - http://www.flightgear.org/~curt
6 // Copyright (C) 2008  Alexander R. Perry <alex.perry@ieee.org>
7 // Copyright (C) 2011  Thorsten Brehm <brehmt@gmail.com>
8 //
9 // This program is free software; you can redistribute it and/or
10 // modify it under the terms of the GNU General Public License as
11 // published by the Free Software Foundation; either version 2 of the
12 // License, or (at your option) any later version.
13 //
14 // This program is distributed in the hope that it will be useful, but
15 // WITHOUT ANY WARRANTY; without even the implied warranty of
16 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
17 // General Public License for more details.
18 //
19 // You should have received a copy of the GNU General Public License
20 // along with this program; if not, write to the Free Software
21 // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
22 //
23 // $Id$
24
25 #ifdef HAVE_CONFIG_H
26 #  include <simgear_config.h>
27 #endif
28
29 #include <simgear/compiler.h>
30
31 #ifdef HAVE_WINDOWS_H
32 #include <windows.h>
33 #endif
34
35 #ifdef __MINGW32__
36 #include <time.h>
37 #include <unistd.h>
38 #elif defined(_MSC_VER)
39 #   include <io.h>
40 #   ifndef HAVE_SVN_CLIENT_H
41 #       include <time.h>
42 #       include <process.h>
43 #   endif
44 #endif
45
46 #include <stdlib.h>             // atoi() atof() abs() system()
47 #include <signal.h>             // signal()
48 #include <string.h>
49
50 #include <iostream>
51 #include <fstream>
52 #include <string>
53 #include <map>
54
55 #include <simgear/compiler.h>
56
57 #include "terrasync.hxx"
58 #include <simgear/bucket/newbucket.hxx>
59 #include <simgear/misc/sg_path.hxx>
60 #include <simgear/misc/strutils.hxx>
61 #include <simgear/threads/SGQueue.hxx>
62 #include <simgear/misc/sg_dir.hxx>
63
64 #ifdef HAVE_SVN_CLIENT_H
65 #  ifdef HAVE_LIBSVN_CLIENT_1
66 #    include <svn_version.h>
67 #    include <svn_auth.h>
68 #    include <svn_client.h>
69 #    include <svn_cmdline.h>
70 #    include <svn_pools.h>
71 #  else
72 #    undef HAVE_SVN_CLIENT_H
73 #  endif
74 #endif
75
76 #ifdef HAVE_SVN_CLIENT_H
77     static const svn_version_checklist_t mysvn_checklist[] = {
78         { "svn_subr",   svn_subr_version },
79         { "svn_client", svn_client_version },
80         { NULL, NULL }
81     };
82     static const bool svn_built_in_available = true;
83 #else
84     static const bool svn_built_in_available = false;
85 #endif
86
87 using namespace simgear;
88 using namespace std;
89
90 const char* rsync_cmd = 
91         "rsync --verbose --archive --delete --perms --owner --group";
92
93 const char* svn_options =
94         "checkout -q";
95
96 namespace UpdateInterval
97 {
98     // interval in seconds to allow an update to repeat after a successful update (=daily)
99     static const double SuccessfulAttempt = 24*60*60;
100     // interval in seconds to allow another update after a failed attempt (10 minutes)
101     static const double FailedAttempt     = 10*60;
102 };
103
104 typedef map<string,time_t> CompletedTiles;
105
106 ///////////////////////////////////////////////////////////////////////////////
107 // helper functions ///////////////////////////////////////////////////////////
108 ///////////////////////////////////////////////////////////////////////////////
109 string stripPath(string path)
110 {
111     // svn doesn't like trailing white-spaces or path separators - strip them!
112     path = simgear::strutils::strip(path);
113     int slen = path.length();
114     while ((slen>0)&&
115             ((path[slen-1]=='/')||(path[slen-1]=='\\')))
116     {
117         slen--;
118     }
119     return path.substr(0,slen);
120 }
121
122 bool hasWhitespace(string path)
123 {
124     return path.find(' ')!=string::npos;
125 }
126
127 ///////////////////////////////////////////////////////////////////////////////
128 // WaitingTile ////////////////////////////////////////////////////////////////
129 ///////////////////////////////////////////////////////////////////////////////
130 class  WaitingTile
131 {
132 public:
133     WaitingTile(string dir,bool refresh) :
134         _dir(dir), _refreshScenery(refresh) {}
135     string _dir;
136     bool _refreshScenery;
137 };
138
139 ///////////////////////////////////////////////////////////////////////////////
140 // SGTerraSync::SvnThread /////////////////////////////////////////////////////
141 ///////////////////////////////////////////////////////////////////////////////
142 class SGTerraSync::SvnThread : public SGThread
143 {
144 public:
145    SvnThread();
146    virtual ~SvnThread( ) { stop(); }
147
148    void stop();
149    bool start();
150
151    bool isIdle() {return waitingTiles.empty();}
152    void request(const WaitingTile& dir) {waitingTiles.push_front(dir);}
153    bool isDirty() { bool r = _is_dirty;_is_dirty = false;return r;}
154    bool hasNewTiles() { return !_freshTiles.empty();}
155    WaitingTile getNewTile() { return _freshTiles.pop_front();}
156
157    void   setSvnServer(string server)       { _svn_server   = stripPath(server);}
158    void   setExtSvnUtility(string svn_util) { _svn_command  = simgear::strutils::strip(svn_util);}
159    void   setRsyncServer(string server)     { _rsync_server = simgear::strutils::strip(server);}
160    void   setLocalDir(string dir)           { _local_dir    = stripPath(dir);}
161    string getLocalDir()                     { return _local_dir;}
162    void   setUseSvn(bool use_svn)           { _use_svn = use_svn;}
163    void   setAllowedErrorCount(int errors)  {_allowed_errors = errors;}
164
165 #ifdef HAVE_SVN_CLIENT_H
166    void setUseBuiltin(bool built_in) { _use_built_in = built_in;}
167 #endif
168
169    volatile bool _active;
170    volatile bool _running;
171    volatile bool _busy;
172    volatile bool _stalled;
173    volatile int  _fail_count;
174    volatile int  _updated_tile_count;
175    volatile int  _success_count;
176    volatile int  _consecutive_errors;
177    volatile int  _allowed_errors;
178
179 private:
180    virtual void run();
181    bool syncTree(const char* dir, bool& isNewDirectory);
182    bool syncTreeExternal(const char* dir);
183
184 #ifdef HAVE_SVN_CLIENT_H
185    static int svnClientSetup(void);
186    bool syncTreeInternal(const char* dir);
187
188    bool _use_built_in;
189
190    // Things we need for doing subversion checkout - often
191    static apr_pool_t *_svn_pool;
192    static svn_client_ctx_t *_svn_ctx;
193    static svn_opt_revision_t *_svn_rev;
194    static svn_opt_revision_t *_svn_rev_peg;
195 #endif
196
197    volatile bool _is_dirty;
198    volatile bool _stop;
199    SGBlockingDeque <WaitingTile> waitingTiles;
200    CompletedTiles _completedTiles;
201    SGBlockingDeque <WaitingTile> _freshTiles;
202    bool _use_svn;
203    string _svn_server;
204    string _svn_command;
205    string _rsync_server;
206    string _local_dir;
207 };
208
209 #ifdef HAVE_SVN_CLIENT_H
210     apr_pool_t* SGTerraSync::SvnThread::_svn_pool = NULL;
211     svn_client_ctx_t* SGTerraSync::SvnThread::_svn_ctx = NULL;
212     svn_opt_revision_t* SGTerraSync::SvnThread::_svn_rev = NULL;
213     svn_opt_revision_t* SGTerraSync::SvnThread::_svn_rev_peg = NULL;
214 #endif
215
216 SGTerraSync::SvnThread::SvnThread() :
217     _active(false),
218     _running(false),
219     _busy(false),
220     _stalled(false),
221     _fail_count(0),
222     _updated_tile_count(0),
223     _success_count(0),
224     _consecutive_errors(0),
225     _allowed_errors(6),
226 #ifdef HAVE_SVN_CLIENT_H
227     _use_built_in(true),
228 #endif
229     _is_dirty(false),
230     _stop(false),
231     _use_svn(true)
232 {
233 #ifdef HAVE_SVN_CLIENT_H
234     int errCode = SGTerraSync::SvnThread::svnClientSetup();
235     if (errCode != EXIT_SUCCESS)
236     {
237         SG_LOG(SG_TERRAIN,SG_ALERT,
238                "Failed to initialize built-in SVN client, error = " << errCode);
239     }
240 #endif
241 }
242
243 void SGTerraSync::SvnThread::stop()
244 {
245     // drop any pending requests
246     waitingTiles.clear();
247
248     if (!_running)
249         return;
250
251     // set stop flag and wake up the thread with an empty request
252     _stop = true;
253     WaitingTile w("",false);
254     request(w);
255     join();
256     _running = false;
257 }
258
259 bool SGTerraSync::SvnThread::start()
260 {
261     if (_running)
262         return false;
263
264     if (_local_dir=="")
265     {
266         SG_LOG(SG_TERRAIN,SG_ALERT,
267                "Cannot start scenery download. Local cache directory is undefined.");
268         _fail_count++;
269         _stalled = true;
270         return false;
271     }
272
273     SGPath path(_local_dir);
274     if (!path.exists())
275     {
276         SG_LOG(SG_TERRAIN,SG_ALERT,
277                "Cannot start scenery download. Directory '" << _local_dir <<
278                "' does not exist. Set correct directory path or create directory folder.");
279         _fail_count++;
280         _stalled = true;
281         return false;
282     }
283
284     path.append("version");
285     if (path.exists())
286     {
287         SG_LOG(SG_TERRAIN,SG_ALERT,
288                "Cannot start scenery download. Directory '" << _local_dir <<
289                "' contains the base package. Use a separate directory.");
290         _fail_count++;
291         _stalled = true;
292         return false;
293     }
294
295 #ifdef HAVE_SVN_CLIENT_H
296     _use_svn |= _use_built_in;
297 #endif
298
299     if ((_use_svn)&&(_svn_server==""))
300     {
301         SG_LOG(SG_TERRAIN,SG_ALERT,
302                "Cannot start scenery download. Subversion scenery server is undefined.");
303         _fail_count++;
304         _stalled = true;
305         return false;
306     }
307     if ((!_use_svn)&&(_rsync_server==""))
308     {
309         SG_LOG(SG_TERRAIN,SG_ALERT,
310                "Cannot start scenery download. Rsync scenery server is undefined.");
311         _fail_count++;
312         _stalled = true;
313         return false;
314     }
315
316     _fail_count = 0;
317     _updated_tile_count = 0;
318     _success_count = 0;
319     _consecutive_errors = 0;
320     _stop = false;
321     _stalled = false;
322     _running = true;
323
324     string status;
325 #ifdef HAVE_SVN_CLIENT_H
326     if (_use_svn && _use_built_in)
327         status = "Using built-in SVN support. ";
328     else
329 #endif
330     if (_use_svn)
331     {
332         status = "Using external SVN utility '";
333         status += _svn_command;
334         status += "'. ";
335     }
336     else
337     {
338         status = "Using RSYNC. ";
339     }
340
341     // not really an alert - but we want to (always) see this message, so user is
342     // aware we're downloading scenery (and using bandwidth).
343     SG_LOG(SG_TERRAIN,SG_ALERT,
344            "Starting automatic scenery download/synchronization. "
345            << status
346            << "Directory: '" << _local_dir << "'.");
347
348     SGThread::start();
349     return true;
350 }
351
352 // sync one directory tree
353 bool SGTerraSync::SvnThread::syncTree(const char* dir, bool& isNewDirectory)
354 {
355     int rc;
356     SGPath path( _local_dir );
357
358     path.append( dir );
359     isNewDirectory = !path.exists();
360     if (isNewDirectory)
361     {
362         rc = path.create_dir( 0755 );
363         if (rc)
364         {
365             SG_LOG(SG_TERRAIN,SG_ALERT,
366                    "Cannot create directory '" << dir << "', return code = " << rc );
367             return false;
368         }
369     }
370
371 #ifdef HAVE_SVN_CLIENT_H
372     if (_use_built_in)
373         return syncTreeInternal(dir);
374     else
375 #endif
376     {
377         return syncTreeExternal(dir);
378     }
379 }
380
381
382 #ifdef HAVE_SVN_CLIENT_H
383 bool SGTerraSync::SvnThread::syncTreeInternal(const char* dir)
384 {
385     SG_LOG(SG_TERRAIN,SG_DEBUG, "Synchronizing scenery directory " << dir);
386     if (!_svn_pool)
387     {
388         SG_LOG(SG_TERRAIN,SG_ALERT,
389                "Built-in SVN client failed to initialize.");
390         return false;
391     }
392
393     ostringstream command;
394     command << _svn_server << "/" << dir;
395
396     ostringstream dest_base_dir;
397     dest_base_dir << _local_dir << "/" << dir;
398
399     apr_pool_t *subpool = svn_pool_create(_svn_pool);
400
401     svn_error_t *err = NULL;
402 #if (SVN_VER_MINOR >= 5)
403     err = svn_client_checkout3(NULL,
404             command.str().c_str(),
405             dest_base_dir.str().c_str(),
406             _svn_rev_peg,
407             _svn_rev,
408             svn_depth_infinity,
409             0, // ignore-externals = false
410             0, // allow unver obstructions = false
411             _svn_ctx,
412             subpool);
413 #else
414     // version 1.4 API
415     err = svn_client_checkout2(NULL,
416             command.str().c_str(),
417             dest_base_dir.str().c_str(),
418             _svn_rev_peg,
419             _svn_rev,
420             1, // recurse=true - same as svn_depth_infinity for checkout3 above
421             0, // ignore externals = false
422             _svn_ctx,
423             subpool);
424 #endif
425
426     bool ReturnValue = true;
427     if (err)
428     {
429         // Report errors from the checkout attempt
430         if (err->apr_err == SVN_ERR_RA_ILLEGAL_URL)
431         {
432             // ignore errors when remote path doesn't exist (no scenery data for ocean areas)
433         }
434         else
435         {
436             SG_LOG(SG_TERRAIN,SG_ALERT,
437                     "Failed to synchronize directory '" << dir << "', " <<
438                     err->message << " (code " << err->apr_err << ").");
439             svn_error_clear(err);
440             // try to clean up
441             err = svn_client_cleanup(dest_base_dir.str().c_str(),
442                     _svn_ctx,subpool);
443             if (!err)
444             {
445                 SG_LOG(SG_TERRAIN,SG_ALERT,
446                        "SVN repository cleanup successful for '" << dir << "'.");
447             }
448             ReturnValue = false;
449         }
450     } else
451     {
452         SG_LOG(SG_TERRAIN,SG_DEBUG, "Done with scenery directory " << dir);
453     }
454     svn_pool_destroy(subpool);
455     return ReturnValue;
456 }
457 #endif
458
459 bool SGTerraSync::SvnThread::syncTreeExternal(const char* dir)
460 {
461     ostringstream buf;
462     SGPath localPath( _local_dir );
463     localPath.append( dir );
464
465     if (_use_svn)
466     {
467         buf << "\"" << _svn_command << "\" "
468             << svn_options << " "
469             << "\"" << _svn_server << "/" << dir << "\" "
470             << "\"" << localPath.str_native() << "\"";
471     } else {
472         buf << rsync_cmd << " "
473             << "\"" << _rsync_server << "/" << dir << "/\" "
474             << "\"" << localPath.str_native() << "/\"";
475     }
476
477     string command;
478 #ifdef SG_WINDOWS
479         // windows command line parsing is just lovely...
480         // to allow white spaces, the system call needs this:
481         // ""C:\Program Files\something.exe" somearg "some other arg""
482         // Note: whitespace strings quoted by a pair of "" _and_ the 
483         //       entire string needs to be wrapped by "" too.
484         // The svn url needs forward slashes (/) as a path separator while
485         // the local path needs windows-native backslash as a path separator.
486     command = "\"" + buf.str() + "\"";
487 #else
488     command = buf.str();
489 #endif
490     SG_LOG(SG_TERRAIN,SG_DEBUG, "sync command '" << command << "'");
491
492 #ifdef SG_WINDOWS
493     // tbd: does Windows support "popen"?
494     int rc = system( command.c_str() );
495 #else
496     FILE* pipe = popen( command.c_str(), "r");
497     int rc=-1;
498     // wait for external process to finish
499     if (pipe)
500         rc = pclose(pipe);
501 #endif
502
503     if (rc)
504     {
505         SG_LOG(SG_TERRAIN,SG_ALERT,
506                "Failed to synchronize directory '" << dir << "', " <<
507                "error code= " << rc);
508         return false;
509     }
510     return true;
511 }
512
513 void SGTerraSync::SvnThread::run()
514 {
515     _active = true;
516     while (!_stop)
517     {
518         WaitingTile next = waitingTiles.pop_front();
519         if (_stop)
520            break;
521
522         CompletedTiles::iterator ii =
523             _completedTiles.find( next._dir );
524         time_t now = time(0);
525         if ((ii == _completedTiles.end())||
526             (ii->second < now ))
527         {
528             bool isNewDirectory = false;
529
530             _busy = true;
531             if (!syncTree(next._dir.c_str(),isNewDirectory))
532             {
533                 _consecutive_errors++;
534                 _fail_count++;
535                 _completedTiles[ next._dir ] = now + UpdateInterval::FailedAttempt;
536             }
537             else
538             {
539                 _consecutive_errors = 0;
540                 _success_count++;
541                 SG_LOG(SG_TERRAIN,SG_INFO,
542                        "Successfully synchronized directory '" << next._dir << "'");
543                 if (next._refreshScenery)
544                 {
545                     // updated a tile
546                     _updated_tile_count++;
547                     if (isNewDirectory)
548                     {
549                         // for now only report new directories to refresh display
550                         // (i.e. only when ocean needs to be replaced with actual data)
551                         _freshTiles.push_back(next);
552                         _is_dirty = true;
553                     }
554                 }
555                 _completedTiles[ next._dir ] = now + UpdateInterval::SuccessfulAttempt;
556             }
557             _busy = false;
558         }
559
560         if ((_allowed_errors >= 0)&&
561             (_consecutive_errors >= _allowed_errors))
562         {
563             _stalled = true;
564             _stop = true;
565         }
566     }
567
568     _active = false;
569     _running = false;
570     _is_dirty = true;
571 }
572
573 #ifdef HAVE_SVN_CLIENT_H
574 // Configure our subversion session
575 int SGTerraSync::SvnThread::svnClientSetup(void)
576 {
577     // Are we already prepared?
578     if (_svn_pool) return EXIT_SUCCESS;
579     // No, so initialize svn internals generally
580
581 #ifdef _MSC_VER
582     // there is a segfault when providing an error stream.
583     //  Apparently, calling setvbuf with a nul buffer is
584     //  not supported under msvc 7.1 ( code inside svn_cmdline_init )
585     if (svn_cmdline_init("terrasync", 0) != EXIT_SUCCESS)
586         return EXIT_FAILURE;
587
588     // revert locale setting
589     setlocale(LC_ALL,"C");
590 #else
591     /* svn_cmdline_init configures the locale. Setup environment to ensure the
592      * default "C" locale remains active, since fgfs isn't locale aware - especially
593      * requires "." as decimal point in strings containing floating point varibales. */
594     setenv("LC_ALL", "C", 1);
595
596     if (svn_cmdline_init("terrasync", stderr) != EXIT_SUCCESS)
597         return EXIT_FAILURE;
598 #endif
599
600     apr_pool_t *pool;
601     apr_pool_create(&pool, NULL);
602     svn_error_t *err = NULL;
603     SVN_VERSION_DEFINE(_svn_version);
604     err = svn_ver_check_list(&_svn_version, mysvn_checklist);
605     if (err)
606         return svn_cmdline_handle_exit_error(err, pool, "fgfs: ");
607     err = svn_ra_initialize(pool);
608     if (err)
609         return svn_cmdline_handle_exit_error(err, pool, "fgfs: ");
610     char *config_dir = NULL;
611     err = svn_config_ensure(config_dir, pool);
612     if (err)
613         return svn_cmdline_handle_exit_error(err, pool, "fgfs: ");
614     err = svn_client_create_context(&_svn_ctx, pool);
615     if (err)
616         return svn_cmdline_handle_exit_error(err, pool, "fgfs: ");
617     err = svn_config_get_config(&(_svn_ctx->config),
618         config_dir, pool);
619     if (err)
620         return svn_cmdline_handle_exit_error(err, pool, "fgfs: ");
621     svn_config_t *cfg;
622     cfg = ( svn_config_t*) apr_hash_get(
623         _svn_ctx->config,
624         SVN_CONFIG_CATEGORY_CONFIG,
625         APR_HASH_KEY_STRING);
626     if (err)
627         return svn_cmdline_handle_exit_error(err, pool, "fgfs: ");
628
629     svn_auth_baton_t *ab=NULL;
630
631 #if (SVN_VER_MINOR >= 6)
632     err = svn_cmdline_create_auth_baton  (&ab,
633             TRUE, NULL, NULL, config_dir, TRUE, FALSE, cfg,
634             _svn_ctx->cancel_func, _svn_ctx->cancel_baton, pool);
635 #else
636     err = svn_cmdline_setup_auth_baton(&ab,
637         TRUE, NULL, NULL, config_dir, TRUE, cfg,
638         _svn_ctx->cancel_func, _svn_ctx->cancel_baton, pool);
639 #endif
640
641     if (err)
642         return svn_cmdline_handle_exit_error(err, pool, "fgfs: ");
643     
644     _svn_ctx->auth_baton = ab;
645 #if (SVN_VER_MINOR >= 5)
646     _svn_ctx->conflict_func = NULL;
647     _svn_ctx->conflict_baton = NULL;
648 #endif
649
650     // Now our magic revisions
651     _svn_rev = (svn_opt_revision_t*) apr_palloc(pool, 
652         sizeof(svn_opt_revision_t));
653     if (!_svn_rev)
654         return EXIT_FAILURE;
655     _svn_rev_peg = (svn_opt_revision_t*) apr_palloc(pool, 
656         sizeof(svn_opt_revision_t));
657     if (!_svn_rev_peg)
658         return EXIT_FAILURE;
659     _svn_rev->kind = svn_opt_revision_head;
660     _svn_rev_peg->kind = svn_opt_revision_unspecified;
661     // Success if we got this far
662     _svn_pool = pool;
663     return EXIT_SUCCESS;
664 }
665 #endif
666
667 ///////////////////////////////////////////////////////////////////////////////
668 // SGTerraSync ////////////////////////////////////////////////////////////////
669 ///////////////////////////////////////////////////////////////////////////////
670 SGTerraSync::SGTerraSync(SGPropertyNode_ptr root) :
671     _svnThread(NULL),
672     last_lat(NOWHERE),
673     last_lon(NOWHERE),
674     _terraRoot(root->getNode("/sim/terrasync",true)),
675     _refreshCb(NULL),
676     _userCbData(NULL)
677 {
678     _svnThread = new SvnThread();
679 }
680
681 SGTerraSync::~SGTerraSync()
682 {
683     _tiedProperties.Untie();
684     delete _svnThread;
685     _svnThread = NULL;
686 }
687
688 void SGTerraSync::init()
689 {
690     _refreshDisplay = _terraRoot->getNode("refresh-display",true);
691     _terraRoot->getNode("built-in-svn-available",true)->setBoolValue(svn_built_in_available);
692     reinit();
693 }
694
695 void SGTerraSync::reinit()
696 {
697     // do not reinit when enabled and we're already up and running
698     if ((_terraRoot->getBoolValue("enabled",false))&&
699          (_svnThread->_active && _svnThread->_running))
700         return;
701
702     _svnThread->stop();
703
704     if (_terraRoot->getBoolValue("enabled",false))
705     {
706         _svnThread->setSvnServer(_terraRoot->getStringValue("svn-server",""));
707         _svnThread->setRsyncServer(_terraRoot->getStringValue("rsync-server",""));
708         _svnThread->setLocalDir(_terraRoot->getStringValue("scenery-dir",""));
709         _svnThread->setAllowedErrorCount(_terraRoot->getIntValue("max-errors",5));
710
711     #ifdef HAVE_SVN_CLIENT_H
712         _svnThread->setUseBuiltin(_terraRoot->getBoolValue("use-built-in-svn",true));
713     #else
714         _terraRoot->getNode("use-built-in-svn",true)->setBoolValue(false);
715     #endif
716         _svnThread->setUseSvn(_terraRoot->getBoolValue("use-svn",true));
717         _svnThread->setExtSvnUtility(_terraRoot->getStringValue("ext-svn-utility","svn"));
718
719         if (_svnThread->start())
720         {
721             syncAirportsModels();
722             if (last_lat != NOWHERE && last_lon != NOWHERE)
723             {
724                 // reschedule most recent position
725                 int lat = last_lat;
726                 int lon = last_lon;
727                 last_lat = NOWHERE;
728                 last_lon = NOWHERE;
729                 schedulePosition(lat, lon);
730             }
731         }
732     }
733
734     _stalledNode->setBoolValue(_svnThread->_stalled);
735 }
736
737 void SGTerraSync::bind()
738 {
739     _tiedProperties.Tie( _terraRoot->getNode("busy", true), (bool*) &_svnThread->_busy );
740     _tiedProperties.Tie( _terraRoot->getNode("active", true), (bool*) &_svnThread->_active );
741     _tiedProperties.Tie( _terraRoot->getNode("update-count", true), (int*) &_svnThread->_success_count );
742     _tiedProperties.Tie( _terraRoot->getNode("error-count", true), (int*) &_svnThread->_fail_count );
743     _tiedProperties.Tie( _terraRoot->getNode("tile-count", true), (int*) &_svnThread->_updated_tile_count );
744     _terraRoot->getNode("busy", true)->setAttribute(SGPropertyNode::WRITE,false);
745     _terraRoot->getNode("active", true)->setAttribute(SGPropertyNode::WRITE,false);
746     _terraRoot->getNode("update-count", true)->setAttribute(SGPropertyNode::WRITE,false);
747     _terraRoot->getNode("error-count", true)->setAttribute(SGPropertyNode::WRITE,false);
748     _terraRoot->getNode("tile-count", true)->setAttribute(SGPropertyNode::WRITE,false);
749     _terraRoot->getNode("use-built-in-svn", true)->setAttribute(SGPropertyNode::USERARCHIVE,false);
750     _terraRoot->getNode("use-svn", true)->setAttribute(SGPropertyNode::USERARCHIVE,false);
751     // stalled is used as a signal handler (to connect listeners triggering GUI pop-ups)
752     _stalledNode = _terraRoot->getNode("stalled", true);
753     _stalledNode->setBoolValue(_svnThread->_stalled);
754     _stalledNode->setAttribute(SGPropertyNode::PRESERVE,true);
755 }
756
757 void SGTerraSync::unbind()
758 {
759     _svnThread->stop();
760     _tiedProperties.Untie();
761 }
762
763 void SGTerraSync::update(double)
764 {
765     static SGBucket bucket;
766     if (_svnThread->isDirty())
767     {
768         if (!_svnThread->_active)
769         {
770             if (_svnThread->_stalled)
771             {
772                 SG_LOG(SG_TERRAIN,SG_ALERT,
773                        "Automatic scenery download/synchronization stalled. Too many errors.");
774             }
775             else
776             {
777                 // not really an alert - just always show this message
778                 SG_LOG(SG_TERRAIN,SG_ALERT,
779                         "Automatic scenery download/synchronization has stopped.");
780             }
781             _stalledNode->setBoolValue(_svnThread->_stalled);
782         }
783
784         if (!_refreshDisplay->getBoolValue())
785             return;
786
787         while (_svnThread->hasNewTiles())
788         {
789             WaitingTile next = _svnThread->getNewTile();
790             if (next._refreshScenery)
791             {
792                 refreshScenery(_svnThread->getLocalDir(),next._dir);
793             }
794         }
795     }
796 }
797
798 void SGTerraSync::refreshScenery(SGPath path,const string& relativeDir)
799 {
800     // find tiles to be refreshed
801     if (_refreshCb)
802     {
803         path.append(relativeDir);
804         if (path.exists())
805         {
806             simgear::Dir dir(path);
807             //TODO need to be smarter here. only update tiles which actually
808             // changed recently. May also be possible to use information from the
809             // built-in SVN client directly (instead of checking directory contents).
810             PathList tileList = dir.children(simgear::Dir::TYPE_FILE, ".stg");
811             for (unsigned int i=0; i<tileList.size(); ++i)
812             {
813                 // reload scenery tile
814                 long index = atoi(tileList[i].file().c_str());
815                 _refreshCb(_userCbData, index);
816             }
817         }
818     }
819 }
820
821 bool SGTerraSync::isIdle() {return _svnThread->isIdle();}
822
823 void SGTerraSync::setTileRefreshCb(SGTerraSyncCallback refreshCb, void* userCbData)
824 {
825     _refreshCb = refreshCb;
826     _userCbData = userCbData;
827 }
828
829 void SGTerraSync::syncAirportsModels()
830 {
831     static const char* bounds = "MZAJKL"; // airport sync order: K-L, A-J, M-Z
832     // note "request" method uses LIFO order, i.e. processes most recent request first
833     for( unsigned i = 0; i < strlen(bounds)/2; i++ )
834     {
835         for ( char synced_other = bounds[2*i]; synced_other <= bounds[2*i+1]; synced_other++ )
836         {
837             ostringstream dir;
838             dir << "Airports/" << synced_other;
839             WaitingTile w(dir.str(),false);
840             _svnThread->request( w );
841         }
842     }
843     WaitingTile w("Models",false);
844     _svnThread->request( w );
845 }
846
847
848 void SGTerraSync::syncArea( int lat, int lon )
849 {
850     if ( lat < -90 || lat > 90 || lon < -180 || lon > 180 )
851         return;
852     char NS, EW;
853     int baselat, baselon;
854
855     if ( lat < 0 ) {
856         int base = (int)(lat / 10);
857         if ( lat == base * 10 ) {
858             baselat = base * 10;
859         } else {
860             baselat = (base - 1) * 10;
861         }
862         NS = 's';
863     } else {
864         baselat = (int)(lat / 10) * 10;
865         NS = 'n';
866     }
867     if ( lon < 0 ) {
868         int base = (int)(lon / 10);
869         if ( lon == base * 10 ) {
870             baselon = base * 10;
871         } else {
872             baselon = (base - 1) * 10;
873         }
874         EW = 'w';
875     } else {
876         baselon = (int)(lon / 10) * 10;
877         EW = 'e';
878     }
879
880     const char* terrainobjects[3] = { "Terrain", "Objects",  0 };
881     bool refresh=true;
882     for (const char** tree = &terrainobjects[0]; *tree; tree++)
883     {
884         ostringstream dir;
885         dir << *tree << "/" << setfill('0')
886             << EW << setw(3) << abs(baselon) << NS << setw(2) << abs(baselat) << "/"
887             << EW << setw(3) << abs(lon)     << NS << setw(2) << abs(lat);
888         WaitingTile w(dir.str(),refresh);
889         _svnThread->request( w );
890         refresh=false;
891     }
892 }
893
894
895 void SGTerraSync::syncAreas( int lat, int lon, int lat_dir, int lon_dir )
896 {
897     if ( lat_dir == 0 && lon_dir == 0 ) {
898         // do surrounding 8 1x1 degree areas.
899         for ( int i = lat - 1; i <= lat + 1; ++i ) {
900             for ( int j = lon - 1; j <= lon + 1; ++j ) {
901                 if ( i != lat || j != lon ) {
902                     syncArea( i, j );
903                 }
904             }
905         }
906     } else {
907         if ( lat_dir != 0 ) {
908             syncArea( lat + lat_dir, lon - 1 );
909             syncArea( lat + lat_dir, lon + 1 );
910             syncArea( lat + lat_dir, lon );
911         }
912         if ( lon_dir != 0 ) {
913             syncArea( lat - 1, lon + lon_dir );
914             syncArea( lat + 1, lon + lon_dir );
915             syncArea( lat, lon + lon_dir );
916         }
917     }
918
919     // do current 1x1 degree area first
920     syncArea( lat, lon );
921 }
922
923
924 bool SGTerraSync::schedulePosition(int lat, int lon)
925 {
926     bool Ok = false;
927
928     // Ignore messages where the location does not change
929     if ( lat != last_lat || lon != last_lon )
930     {
931         if (_svnThread->_running)
932         {
933             SG_LOG(SG_TERRAIN,SG_DEBUG, "Requesting scenery update for position " <<
934                                         lat << "," << lon);
935             int lat_dir=0;
936             int lon_dir=0;
937             if ( last_lat != NOWHERE && last_lon != NOWHERE )
938             {
939                 int dist = lat - last_lat;
940                 if ( dist != 0 )
941                 {
942                     lat_dir = dist / abs(dist);
943                 }
944                 else
945                 {
946                     lat_dir = 0;
947                 }
948                 dist = lon - last_lon;
949                 if ( dist != 0 )
950                 {
951                     lon_dir = dist / abs(dist);
952                 } else
953                 {
954                     lon_dir = 0;
955                 }
956             }
957
958             SG_LOG(SG_TERRAIN,SG_DEBUG, "Scenery update for " <<
959                    "lat = " << lat << ", lon = " << lon <<
960                    ", lat_dir = " << lat_dir << ",  " <<
961                    "lon_dir = " << lon_dir);
962
963             syncAreas( lat, lon, lat_dir, lon_dir );
964             Ok = true;
965         }
966         last_lat = lat;
967         last_lon = lon;
968     }
969
970     return Ok;
971 }