]> git.mxchange.org Git - simgear.git/blob - simgear/scene/tsync/terrasync.cxx
Reposition hook for terrasync.
[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 #   include <time.h>
41 #   include <process.h>
42 #endif
43
44 #include <stdlib.h>             // atoi() atof() abs() system()
45 #include <signal.h>             // signal()
46 #include <string.h>
47
48 #include <iostream>
49 #include <fstream>
50 #include <string>
51 #include <map>
52
53 #include <simgear/compiler.h>
54
55 #include "terrasync.hxx"
56
57 #include <simgear/bucket/newbucket.hxx>
58 #include <simgear/misc/sg_path.hxx>
59 #include <simgear/misc/strutils.hxx>
60 #include <simgear/threads/SGQueue.hxx>
61 #include <simgear/misc/sg_dir.hxx>
62 #include <simgear/debug/BufferedLogCallback.hxx>
63 #include <simgear/props/props_io.hxx>
64 #include <simgear/io/HTTPClient.hxx>
65 #include <simgear/io/SVNRepository.hxx>
66 #include <simgear/structure/exception.hxx>
67
68 static const bool svn_built_in_available = true;
69
70 using namespace simgear;
71 using namespace std;
72
73 const char* rsync_cmd = 
74         "rsync --verbose --archive --delete --perms --owner --group";
75
76 const char* svn_options =
77         "checkout -q";
78
79 namespace UpdateInterval
80 {
81     // interval in seconds to allow an update to repeat after a successful update (=daily)
82     static const double SuccessfulAttempt = 24*60*60;
83     // interval in seconds to allow another update after a failed attempt (10 minutes)
84     static const double FailedAttempt     = 10*60;
85 }
86
87 typedef map<string,time_t> CompletedTiles;
88
89 ///////////////////////////////////////////////////////////////////////////////
90 // helper functions ///////////////////////////////////////////////////////////
91 ///////////////////////////////////////////////////////////////////////////////
92 string stripPath(string path)
93 {
94     // svn doesn't like trailing white-spaces or path separators - strip them!
95     path = simgear::strutils::strip(path);
96     size_t slen = path.length();
97     while ((slen>0)&&
98             ((path[slen-1]=='/')||(path[slen-1]=='\\')))
99     {
100         slen--;
101     }
102     return path.substr(0,slen);
103 }
104
105 bool hasWhitespace(string path)
106 {
107     return path.find(' ')!=string::npos;
108 }
109
110 ///////////////////////////////////////////////////////////////////////////////
111 // WaitingSyncItem ////////////////////////////////////////////////////////////
112 ///////////////////////////////////////////////////////////////////////////////
113 class WaitingSyncItem
114 {
115 public:
116     enum Type
117     {
118         Stop = 0,   ///< special item indicating to stop the SVNThread
119         Tile,
120         AirportData,
121         SharedModels
122     };
123     
124     WaitingSyncItem() :
125         _dir(),
126         _type(Stop),
127         _refreshScenery(false)
128     {
129     }
130     
131     WaitingSyncItem(string dir, Type ty) :
132         _dir(dir),
133         _type(ty),
134         _refreshScenery(false)
135     {}
136         
137     void setRefresh()
138     { _refreshScenery = true; }
139         
140     string _dir;
141     Type _type;
142     bool _refreshScenery;
143 };
144
145 ///////////////////////////////////////////////////////////////////////////////
146
147 /**
148  * @brief SyncSlot encapsulates a queue of sync items we will fetch
149  * serially. Multiple slots exist to sync different types of item in
150  * parallel.
151  */
152 class SyncSlot
153 {
154 public:
155     SyncSlot() :
156         isNewDirectory(false),
157         busy(false)
158     {}
159     
160     WaitingSyncItem currentItem;
161     bool isNewDirectory;
162     std::queue<WaitingSyncItem> queue;
163     std::auto_ptr<SVNRepository> repository;
164     SGTimeStamp stamp;
165     bool busy; ///< is the slot working or idle
166     
167     unsigned int nextWarnTimeout;
168 };
169
170 static const int SYNC_SLOT_TILES = 0; ///< Terrain and Objects sync
171 static const int SYNC_SLOT_SHARED_DATA = 1; /// shared Models and Airport data
172 static const int NUM_SYNC_SLOTS = 2;
173
174 /**
175  * @brief translate a sync item type into one of the available slots.
176  * This provides the scheduling / balancing / prioritising between slots.
177  */
178 static unsigned int syncSlotForType(WaitingSyncItem::Type ty)
179 {
180     switch (ty) {
181     case WaitingSyncItem::Tile: return SYNC_SLOT_TILES;
182     case WaitingSyncItem::SharedModels:
183     case WaitingSyncItem::AirportData:
184         return SYNC_SLOT_SHARED_DATA;
185     default:
186         return SYNC_SLOT_SHARED_DATA;
187     }
188 }
189
190
191 ///////////////////////////////////////////////////////////////////////////////
192 // SGTerraSync::SvnThread /////////////////////////////////////////////////////
193 ///////////////////////////////////////////////////////////////////////////////
194 class SGTerraSync::SvnThread : public SGThread
195 {
196 public:
197    SvnThread();
198    virtual ~SvnThread( ) { stop(); }
199
200    void stop();
201    bool start();
202
203    bool isIdle() {return waitingTiles.empty();}
204    void request(const WaitingSyncItem& dir) {waitingTiles.push_front(dir);}
205    bool isDirty() { bool r = _is_dirty;_is_dirty = false;return r;}
206    bool hasNewTiles() { return !_freshTiles.empty();}
207    WaitingSyncItem getNewTile() { return _freshTiles.pop_front();}
208
209    void   setSvnServer(string server)       { _svn_server   = stripPath(server);}
210    void   setExtSvnUtility(string svn_util) { _svn_command  = simgear::strutils::strip(svn_util);}
211    void   setRsyncServer(string server)     { _rsync_server = simgear::strutils::strip(server);}
212    void   setLocalDir(string dir)           { _local_dir    = stripPath(dir);}
213    string getLocalDir()                     { return _local_dir;}
214    void   setUseSvn(bool use_svn)           { _use_svn = use_svn;}
215    void   setAllowedErrorCount(int errors)  {_allowed_errors = errors;}
216
217    void   setCachePath(const SGPath& p)     {_persistentCachePath = p;}
218    void   setCacheHits(unsigned int hits)   {_cache_hits = hits;}
219    void   setUseBuiltin(bool built_in) { _use_built_in = built_in;}
220
221    volatile bool _active;
222    volatile bool _running;
223    volatile bool _busy;
224    volatile bool _stalled;
225    volatile int  _fail_count;
226    volatile int  _updated_tile_count;
227    volatile int  _success_count;
228    volatile int  _consecutive_errors;
229    volatile int  _allowed_errors;
230    volatile int  _cache_hits;
231    volatile int _transfer_rate;
232 private:
233    virtual void run();
234     
235     // external model run and helpers
236     void runExternal();
237     void syncPathExternal(const WaitingSyncItem& next);
238     bool runExternalSyncCommand(const char* dir);
239
240     // internal mode run and helpers
241     void runInternal();
242     void updateSyncSlot(SyncSlot& slot);
243
244     // commond helpers between both internal and external models
245     
246     bool isPathCached(const WaitingSyncItem& next) const;
247     void initCompletedTilesPersistentCache();
248     void writeCompletedTilesPersistentCache() const;
249     void updated(const WaitingSyncItem& item, bool isNewDirectory);
250     void fail(const WaitingSyncItem& failedItem);
251     
252     bool _use_built_in;
253     HTTP::Client _http;
254     SyncSlot _syncSlots[NUM_SYNC_SLOTS];
255
256    volatile bool _is_dirty;
257    volatile bool _stop;
258    SGBlockingDeque <WaitingSyncItem> waitingTiles;
259    CompletedTiles _completedTiles;
260    SGBlockingDeque <WaitingSyncItem> _freshTiles;
261    bool _use_svn;
262    string _svn_server;
263    string _svn_command;
264    string _rsync_server;
265    string _local_dir;
266    SGPath _persistentCachePath;
267 };
268
269 SGTerraSync::SvnThread::SvnThread() :
270     _active(false),
271     _running(false),
272     _busy(false),
273     _stalled(false),
274     _fail_count(0),
275     _updated_tile_count(0),
276     _success_count(0),
277     _consecutive_errors(0),
278     _allowed_errors(6),
279     _cache_hits(0),
280     _transfer_rate(0),
281     _use_built_in(true),
282     _is_dirty(false),
283     _stop(false),
284     _use_svn(true)
285 {
286 }
287
288 void SGTerraSync::SvnThread::stop()
289 {
290     // drop any pending requests
291     waitingTiles.clear();
292
293     if (!_running)
294         return;
295
296     // set stop flag and wake up the thread with an empty request
297     _stop = true;
298     WaitingSyncItem w(string(), WaitingSyncItem::Stop);
299     request(w);
300     join();
301     _running = false;
302 }
303
304 bool SGTerraSync::SvnThread::start()
305 {
306     if (_running)
307         return false;
308
309     if (_local_dir=="")
310     {
311         SG_LOG(SG_TERRAIN,SG_ALERT,
312                "Cannot start scenery download. Local cache directory is undefined.");
313         _fail_count++;
314         _stalled = true;
315         return false;
316     }
317
318     SGPath path(_local_dir);
319     if (!path.exists())
320     {
321         SG_LOG(SG_TERRAIN,SG_ALERT,
322                "Cannot start scenery download. Directory '" << _local_dir <<
323                "' does not exist. Set correct directory path or create directory folder.");
324         _fail_count++;
325         _stalled = true;
326         return false;
327     }
328
329     path.append("version");
330     if (path.exists())
331     {
332         SG_LOG(SG_TERRAIN,SG_ALERT,
333                "Cannot start scenery download. Directory '" << _local_dir <<
334                "' contains the base package. Use a separate directory.");
335         _fail_count++;
336         _stalled = true;
337         return false;
338     }
339
340     _use_svn |= _use_built_in;
341
342     if ((_use_svn)&&(_svn_server==""))
343     {
344         SG_LOG(SG_TERRAIN,SG_ALERT,
345                "Cannot start scenery download. Subversion scenery server is undefined.");
346         _fail_count++;
347         _stalled = true;
348         return false;
349     }
350     if ((!_use_svn)&&(_rsync_server==""))
351     {
352         SG_LOG(SG_TERRAIN,SG_ALERT,
353                "Cannot start scenery download. Rsync scenery server is undefined.");
354         _fail_count++;
355         _stalled = true;
356         return false;
357     }
358
359     _fail_count = 0;
360     _updated_tile_count = 0;
361     _success_count = 0;
362     _consecutive_errors = 0;
363     _stop = false;
364     _stalled = false;
365     _running = true;
366
367     string status;
368
369     if (_use_svn && _use_built_in)
370         status = "Using built-in SVN support. ";
371     else if (_use_svn)
372     {
373         status = "Using external SVN utility '";
374         status += _svn_command;
375         status += "'. ";
376     }
377     else
378     {
379         status = "Using RSYNC. ";
380     }
381
382     // not really an alert - but we want to (always) see this message, so user is
383     // aware we're downloading scenery (and using bandwidth).
384     SG_LOG(SG_TERRAIN,SG_ALERT,
385            "Starting automatic scenery download/synchronization. "
386            << status
387            << "Directory: '" << _local_dir << "'.");
388
389     SGThread::start();
390     return true;
391 }
392
393 bool SGTerraSync::SvnThread::runExternalSyncCommand(const char* dir)
394 {
395     ostringstream buf;
396     SGPath localPath( _local_dir );
397     localPath.append( dir );
398
399     if (_use_svn)
400     {
401         buf << "\"" << _svn_command << "\" "
402             << svn_options << " "
403             << "\"" << _svn_server << "/" << dir << "\" "
404             << "\"" << localPath.str_native() << "\"";
405     } else {
406         buf << rsync_cmd << " "
407             << "\"" << _rsync_server << "/" << dir << "/\" "
408             << "\"" << localPath.str_native() << "/\"";
409     }
410
411     string command;
412 #ifdef SG_WINDOWS
413         // windows command line parsing is just lovely...
414         // to allow white spaces, the system call needs this:
415         // ""C:\Program Files\something.exe" somearg "some other arg""
416         // Note: whitespace strings quoted by a pair of "" _and_ the 
417         //       entire string needs to be wrapped by "" too.
418         // The svn url needs forward slashes (/) as a path separator while
419         // the local path needs windows-native backslash as a path separator.
420     command = "\"" + buf.str() + "\"";
421 #else
422     command = buf.str();
423 #endif
424     SG_LOG(SG_TERRAIN,SG_DEBUG, "sync command '" << command << "'");
425
426 #ifdef SG_WINDOWS
427     // tbd: does Windows support "popen"?
428     int rc = system( command.c_str() );
429 #else
430     FILE* pipe = popen( command.c_str(), "r");
431     int rc=-1;
432     // wait for external process to finish
433     if (pipe)
434         rc = pclose(pipe);
435 #endif
436
437     if (rc)
438     {
439         SG_LOG(SG_TERRAIN,SG_ALERT,
440                "Failed to synchronize directory '" << dir << "', " <<
441                "error code= " << rc);
442         return false;
443     }
444     return true;
445 }
446
447 void SGTerraSync::SvnThread::run()
448 {
449     _active = true;
450     initCompletedTilesPersistentCache();
451
452     
453     if (_use_built_in) {
454         runInternal();
455     } else {
456         runExternal();
457     }
458     
459     _active = false;
460     _running = false;
461     _is_dirty = true;
462 }
463
464 void SGTerraSync::SvnThread::runExternal()
465 {
466     while (!_stop)
467     {
468         WaitingSyncItem next = waitingTiles.pop_front();
469         if (_stop)
470            break;
471
472         if (isPathCached(next)) {
473             _cache_hits++;
474             SG_LOG(SG_TERRAIN, SG_DEBUG,
475                    "Cache hit for: '" << next._dir << "'");
476             continue;
477         }
478         
479         syncPathExternal(next);
480
481         if ((_allowed_errors >= 0)&&
482             (_consecutive_errors >= _allowed_errors))
483         {
484             _stalled = true;
485             _stop = true;
486         }
487     } // of thread running loop
488 }
489
490 void SGTerraSync::SvnThread::syncPathExternal(const WaitingSyncItem& next)
491 {
492     _busy = true;
493     SGPath path( _local_dir );
494     path.append( next._dir );
495     bool isNewDirectory = !path.exists();
496     
497     try {
498         if (isNewDirectory) {
499             int rc = path.create_dir( 0755 );
500             if (rc) {
501                 SG_LOG(SG_TERRAIN,SG_ALERT,
502                        "Cannot create directory '" << path << "', return code = " << rc );
503                 throw sg_exception("Cannot create directory for terrasync", path.str());
504             }
505         }
506         
507         if (!runExternalSyncCommand(next._dir.c_str())) {
508             throw sg_exception("Running external sync command failed");
509         }
510     } catch (sg_exception& e) {
511         fail(next);
512         _busy = false;
513         return;
514     }
515     
516     updated(next, isNewDirectory);
517     _busy = false;
518 }
519
520 void SGTerraSync::SvnThread::updateSyncSlot(SyncSlot &slot)
521 {
522     if (slot.repository.get()) {
523         if (slot.repository->isDoingSync()) {
524 #if 0
525             if (slot.stamp.elapsedMSec() > slot.nextWarnTimeout) {
526                 SG_LOG(SG_IO, SG_INFO, "sync taking a long time:" << slot.currentItem._dir << " taken " << slot.stamp.elapsedMSec());
527                 SG_LOG(SG_IO, SG_INFO, "HTTP status:" << _http.hasActiveRequests());
528                 slot.nextWarnTimeout += 10000;
529             }
530 #endif
531             return; // easy, still working
532         }
533         
534         // check result
535         SVNRepository::ResultCode res = slot.repository->failure();
536         if (res == SVNRepository::SVN_ERROR_NOT_FOUND) {
537             // this is fine, but maybe we should use a different return code
538             // in the future to higher layers can distinguish this case
539         } else if (res != SVNRepository::SVN_NO_ERROR) {
540             fail(slot.currentItem);
541         } else {
542             updated(slot.currentItem, slot.isNewDirectory);
543             SG_LOG(SG_IO, SG_DEBUG, "sync of " << slot.repository->baseUrl() << " finished ("
544                    << slot.stamp.elapsedMSec() << " msec");
545         }
546
547         // whatever happened, we're done with this repository instance
548         slot.busy = false;
549         slot.repository.reset();
550     }
551     
552     // init and start sync of the next repository
553     if (!slot.queue.empty()) {
554         slot.currentItem = slot.queue.front();
555         slot.queue.pop();
556         
557         SGPath path(_local_dir);
558         path.append(slot.currentItem._dir);
559         slot.isNewDirectory = !path.exists();
560         if (slot.isNewDirectory) {
561             int rc = path.create_dir( 0755 );
562             if (rc) {
563                 SG_LOG(SG_TERRAIN,SG_ALERT,
564                        "Cannot create directory '" << path << "', return code = " << rc );
565                 fail(slot.currentItem);
566                 return;
567             }
568         } // of creating directory step
569         
570         slot.repository.reset(new SVNRepository(path, &_http));
571         slot.repository->setBaseUrl(_svn_server + "/" + slot.currentItem._dir);
572         slot.repository->update();
573         
574         slot.nextWarnTimeout = 20000;
575         slot.stamp.stamp();
576         slot.busy = true;
577         SG_LOG(SG_IO, SG_DEBUG, "sync of " << slot.repository->baseUrl() << " started, queue size is " << slot.queue.size());
578     }
579 }
580
581 void SGTerraSync::SvnThread::runInternal()
582 {
583     while (!_stop) {
584         _http.update(100);
585         _transfer_rate = _http.transferRateBytesPerSec();
586         if (_stop)
587             break;
588  
589         // drain the waiting tiles queue into the sync slot queues.
590         while (!waitingTiles.empty()) {
591             WaitingSyncItem next = waitingTiles.pop_front();
592             if (isPathCached(next)) {
593                 _cache_hits++;
594                 SG_LOG(SG_TERRAIN, SG_DEBUG,
595                        "Cache hit for: '" << next._dir << "'");
596                 continue;
597             }
598
599             unsigned int slot = syncSlotForType(next._type);
600             _syncSlots[slot].queue.push(next);
601         }
602        
603         bool anySlotBusy = false;
604         // update each sync slot in turn
605         for (unsigned int slot=0; slot < NUM_SYNC_SLOTS; ++slot) {
606             updateSyncSlot(_syncSlots[slot]);
607             anySlotBusy |= _syncSlots[slot].busy;
608         }
609
610         _busy = anySlotBusy;
611     } // of thread running loop
612 }
613
614 bool SGTerraSync::SvnThread::isPathCached(const WaitingSyncItem& next) const
615 {
616     CompletedTiles::const_iterator ii = _completedTiles.find( next._dir );
617     if (ii == _completedTiles.end()) {
618         return false;
619     }
620     
621     // check if the path still physically exists
622     SGPath p(_local_dir);
623     p.append(next._dir);
624     if (!p.exists()) {
625         return false;
626     }
627     
628     time_t now = time(0);
629     return (ii->second > now);
630 }
631
632 void SGTerraSync::SvnThread::fail(const WaitingSyncItem& failedItem)
633 {
634     time_t now = time(0);
635     _consecutive_errors++;
636     _fail_count++;
637     _completedTiles[ failedItem._dir ] = now + UpdateInterval::FailedAttempt;
638 }
639
640 void SGTerraSync::SvnThread::updated(const WaitingSyncItem& item, bool isNewDirectory)
641 {
642     time_t now = time(0);
643     _consecutive_errors = 0;
644     _success_count++;
645     SG_LOG(SG_TERRAIN,SG_INFO,
646            "Successfully synchronized directory '" << item._dir << "'");
647     if (item._refreshScenery) {
648         // updated a tile
649         _updated_tile_count++;
650         if (isNewDirectory)
651         {
652             // for now only report new directories to refresh display
653             // (i.e. only when ocean needs to be replaced with actual data)
654             _freshTiles.push_back(item);
655             _is_dirty = true;
656         }
657     }
658     
659     _completedTiles[ item._dir ] = now + UpdateInterval::SuccessfulAttempt;
660     writeCompletedTilesPersistentCache();
661 }
662
663 void SGTerraSync::SvnThread::initCompletedTilesPersistentCache()
664 {
665     if (!_persistentCachePath.exists()) {
666         return;
667     }
668     
669     SGPropertyNode_ptr cacheRoot(new SGPropertyNode);
670     time_t now = time(0);
671     
672     readProperties(_persistentCachePath.str(), cacheRoot);
673     for (int i=0; i<cacheRoot->nChildren(); ++i) {
674         SGPropertyNode* entry = cacheRoot->getChild(i);
675         string tileName = entry->getStringValue("path");
676         time_t stamp = entry->getIntValue("stamp");
677         if (stamp < now) {
678             continue;
679         }
680         
681         _completedTiles[tileName] = stamp;
682     }
683 }
684
685 void SGTerraSync::SvnThread::writeCompletedTilesPersistentCache() const
686 {
687     // cache is disabled
688     if (_persistentCachePath.isNull()) {
689         return;
690     }
691     
692     std::ofstream f(_persistentCachePath.c_str(), std::ios::trunc);
693     if (!f.is_open()) {
694         return;
695     }
696     
697     SGPropertyNode_ptr cacheRoot(new SGPropertyNode);
698     CompletedTiles::const_iterator it = _completedTiles.begin();
699     for (; it != _completedTiles.end(); ++it) {
700         SGPropertyNode* entry = cacheRoot->addChild("entry");
701         entry->setStringValue("path", it->first);
702         entry->setIntValue("stamp", it->second);
703     }
704     
705     writeProperties(f, cacheRoot, true /* write_all */);
706     f.close();
707 }
708
709 ///////////////////////////////////////////////////////////////////////////////
710 // SGTerraSync ////////////////////////////////////////////////////////////////
711 ///////////////////////////////////////////////////////////////////////////////
712 SGTerraSync::SGTerraSync(SGPropertyNode_ptr root) :
713     _svnThread(NULL),
714     last_lat(NOWHERE),
715     last_lon(NOWHERE),
716     _terraRoot(root->getNode("/sim/terrasync",true)),
717     _bound(false),
718     _inited(false),
719     _refreshCb(NULL),
720     _userCbData(NULL)
721 {
722     _svnThread = new SvnThread();
723     _log = new BufferedLogCallback(SG_TERRAIN, SG_INFO);
724     _log->truncateAt(255);
725     
726     sglog().addCallback(_log);
727 }
728
729 SGTerraSync::~SGTerraSync()
730 {
731     _tiedProperties.Untie();
732     delete _svnThread;
733     _svnThread = NULL;
734     sglog().removeCallback(_log);
735     delete _log;
736 }
737
738 void SGTerraSync::init()
739 {
740     if (_inited) {
741         return;
742     }
743     
744     _inited = true;
745     _refreshDisplay = _terraRoot->getNode("refresh-display",true);
746     _terraRoot->setBoolValue("built-in-svn-available",svn_built_in_available);
747         
748     reinit();
749 }
750
751 void SGTerraSync::reinit()
752 {
753     // do not reinit when enabled and we're already up and running
754     if ((_terraRoot->getBoolValue("enabled",false))&&
755          (_svnThread->_active && _svnThread->_running))
756     {
757         return;
758     }
759     
760     _svnThread->stop();
761
762     if (_terraRoot->getBoolValue("enabled",false))
763     {
764         _svnThread->setSvnServer(_terraRoot->getStringValue("svn-server",""));
765         _svnThread->setRsyncServer(_terraRoot->getStringValue("rsync-server",""));
766         _svnThread->setLocalDir(_terraRoot->getStringValue("scenery-dir",""));
767         _svnThread->setAllowedErrorCount(_terraRoot->getIntValue("max-errors",5));
768         _svnThread->setUseBuiltin(_terraRoot->getBoolValue("use-built-in-svn",true));
769         _svnThread->setCachePath(SGPath(_terraRoot->getStringValue("cache-path","")));
770         _svnThread->setCacheHits(_terraRoot->getIntValue("cache-hit", 0));
771         _svnThread->setUseSvn(_terraRoot->getBoolValue("use-svn",true));
772         _svnThread->setExtSvnUtility(_terraRoot->getStringValue("ext-svn-utility","svn"));
773
774         if (_svnThread->start())
775         {
776             syncAirportsModels();
777             if (last_lat != NOWHERE && last_lon != NOWHERE)
778             {
779                 // reschedule most recent position
780                 int lat = last_lat;
781                 int lon = last_lon;
782                 last_lat = NOWHERE;
783                 last_lon = NOWHERE;
784                 schedulePosition(lat, lon);
785             }
786         }
787     }
788
789     _stalledNode->setBoolValue(_svnThread->_stalled);
790 }
791
792 void SGTerraSync::bind()
793 {
794     if (_bound) {
795         return;
796     }
797     
798     _bound = true;
799     _tiedProperties.Tie( _terraRoot->getNode("busy", true), (bool*) &_svnThread->_busy );
800     _tiedProperties.Tie( _terraRoot->getNode("active", true), (bool*) &_svnThread->_active );
801     _tiedProperties.Tie( _terraRoot->getNode("update-count", true), (int*) &_svnThread->_success_count );
802     _tiedProperties.Tie( _terraRoot->getNode("error-count", true), (int*) &_svnThread->_fail_count );
803     _tiedProperties.Tie( _terraRoot->getNode("tile-count", true), (int*) &_svnThread->_updated_tile_count );
804     _tiedProperties.Tie( _terraRoot->getNode("cache-hits", true), (int*) &_svnThread->_cache_hits );
805     _tiedProperties.Tie( _terraRoot->getNode("transfer-rate-bytes-sec", true), (int*) &_svnThread->_transfer_rate );
806     
807     _terraRoot->getNode("busy", true)->setAttribute(SGPropertyNode::WRITE,false);
808     _terraRoot->getNode("active", true)->setAttribute(SGPropertyNode::WRITE,false);
809     _terraRoot->getNode("update-count", true)->setAttribute(SGPropertyNode::WRITE,false);
810     _terraRoot->getNode("error-count", true)->setAttribute(SGPropertyNode::WRITE,false);
811     _terraRoot->getNode("tile-count", true)->setAttribute(SGPropertyNode::WRITE,false);
812     _terraRoot->getNode("use-built-in-svn", true)->setAttribute(SGPropertyNode::USERARCHIVE,false);
813     _terraRoot->getNode("use-svn", true)->setAttribute(SGPropertyNode::USERARCHIVE,false);
814     // stalled is used as a signal handler (to connect listeners triggering GUI pop-ups)
815     _stalledNode = _terraRoot->getNode("stalled", true);
816     _stalledNode->setBoolValue(_svnThread->_stalled);
817     _stalledNode->setAttribute(SGPropertyNode::PRESERVE,true);
818 }
819
820 void SGTerraSync::unbind()
821 {
822     _svnThread->stop();
823     _tiedProperties.Untie();
824     _bound = false;
825     _inited = false;
826 }
827
828 void SGTerraSync::update(double)
829 {
830     static SGBucket bucket;
831     if (_svnThread->isDirty())
832     {
833         if (!_svnThread->_active)
834         {
835             if (_svnThread->_stalled)
836             {
837                 SG_LOG(SG_TERRAIN,SG_ALERT,
838                        "Automatic scenery download/synchronization stalled. Too many errors.");
839             }
840             else
841             {
842                 // not really an alert - just always show this message
843                 SG_LOG(SG_TERRAIN,SG_ALERT,
844                         "Automatic scenery download/synchronization has stopped.");
845             }
846             _stalledNode->setBoolValue(_svnThread->_stalled);
847         }
848
849         if (!_refreshDisplay->getBoolValue())
850             return;
851
852         while (_svnThread->hasNewTiles())
853         {
854             WaitingSyncItem next = _svnThread->getNewTile();
855             if (next._refreshScenery)
856             {
857                 refreshScenery(_svnThread->getLocalDir(),next._dir);
858             }
859         }
860     }
861 }
862
863 void SGTerraSync::refreshScenery(SGPath path,const string& relativeDir)
864 {
865     // find tiles to be refreshed
866     if (_refreshCb)
867     {
868         path.append(relativeDir);
869         if (path.exists())
870         {
871             simgear::Dir dir(path);
872             //TODO need to be smarter here. only update tiles which actually
873             // changed recently. May also be possible to use information from the
874             // built-in SVN client directly (instead of checking directory contents).
875             PathList tileList = dir.children(simgear::Dir::TYPE_FILE, ".stg");
876             for (unsigned int i=0; i<tileList.size(); ++i)
877             {
878                 // reload scenery tile
879                 long index = atoi(tileList[i].file().c_str());
880                 _refreshCb(_userCbData, index);
881             }
882         }
883     }
884 }
885
886 bool SGTerraSync::isIdle() {return _svnThread->isIdle();}
887
888 void SGTerraSync::setTileRefreshCb(SGTerraSyncCallback refreshCb, void* userCbData)
889 {
890     _refreshCb = refreshCb;
891     _userCbData = userCbData;
892 }
893
894 void SGTerraSync::syncAirportsModels()
895 {
896     static const char* bounds = "MZAJKL"; // airport sync order: K-L, A-J, M-Z
897     // note "request" method uses LIFO order, i.e. processes most recent request first
898     for( unsigned i = 0; i < strlen(bounds)/2; i++ )
899     {
900         for ( char synced_other = bounds[2*i]; synced_other <= bounds[2*i+1]; synced_other++ )
901         {
902             ostringstream dir;
903             dir << "Airports/" << synced_other;
904             WaitingSyncItem w(dir.str(), WaitingSyncItem::AirportData);
905             _svnThread->request( w );
906         }
907     }
908     
909     WaitingSyncItem w("Models", WaitingSyncItem::SharedModels);
910     _svnThread->request( w );
911 }
912
913
914 void SGTerraSync::syncArea( int lat, int lon )
915 {
916     if ( lat < -90 || lat > 90 || lon < -180 || lon > 180 )
917         return;
918     char NS, EW;
919     int baselat, baselon;
920
921     if ( lat < 0 ) {
922         int base = (int)(lat / 10);
923         if ( lat == base * 10 ) {
924             baselat = base * 10;
925         } else {
926             baselat = (base - 1) * 10;
927         }
928         NS = 's';
929     } else {
930         baselat = (int)(lat / 10) * 10;
931         NS = 'n';
932     }
933     if ( lon < 0 ) {
934         int base = (int)(lon / 10);
935         if ( lon == base * 10 ) {
936             baselon = base * 10;
937         } else {
938             baselon = (base - 1) * 10;
939         }
940         EW = 'w';
941     } else {
942         baselon = (int)(lon / 10) * 10;
943         EW = 'e';
944     }
945
946     const char* terrainobjects[3] = { "Terrain", "Objects",  0 };
947     bool refresh=true;
948     for (const char** tree = &terrainobjects[0]; *tree; tree++)
949     {
950         ostringstream dir;
951         dir << *tree << "/" << setfill('0')
952             << EW << setw(3) << abs(baselon) << NS << setw(2) << abs(baselat) << "/"
953             << EW << setw(3) << abs(lon)     << NS << setw(2) << abs(lat);
954         
955         WaitingSyncItem w(dir.str(), WaitingSyncItem::Tile);
956         if (refresh) {
957             w.setRefresh();
958         }
959         
960         _svnThread->request( w );
961         refresh=false;
962     }
963 }
964
965
966 void SGTerraSync::syncAreas( int lat, int lon, int lat_dir, int lon_dir )
967 {
968     if ( lat_dir == 0 && lon_dir == 0 ) {
969         // do surrounding 8 1x1 degree areas.
970         for ( int i = lat - 1; i <= lat + 1; ++i ) {
971             for ( int j = lon - 1; j <= lon + 1; ++j ) {
972                 if ( i != lat || j != lon ) {
973                     syncArea( i, j );
974                 }
975             }
976         }
977     } else {
978         if ( lat_dir != 0 ) {
979             syncArea( lat + lat_dir, lon - 1 );
980             syncArea( lat + lat_dir, lon + 1 );
981             syncArea( lat + lat_dir, lon );
982         }
983         if ( lon_dir != 0 ) {
984             syncArea( lat - 1, lon + lon_dir );
985             syncArea( lat + 1, lon + lon_dir );
986             syncArea( lat, lon + lon_dir );
987         }
988     }
989
990     // do current 1x1 degree area first
991     syncArea( lat, lon );
992 }
993
994
995 bool SGTerraSync::schedulePosition(int lat, int lon)
996 {
997     bool Ok = false;
998
999     // Ignore messages where the location does not change
1000     if ( lat != last_lat || lon != last_lon )
1001     {
1002         if (_svnThread->_running)
1003         {
1004             SG_LOG(SG_TERRAIN,SG_DEBUG, "Requesting scenery update for position " <<
1005                                         lat << "," << lon);
1006             int lat_dir=0;
1007             int lon_dir=0;
1008             if ( last_lat != NOWHERE && last_lon != NOWHERE )
1009             {
1010                 int dist = lat - last_lat;
1011                 if ( dist != 0 )
1012                 {
1013                     lat_dir = dist / abs(dist);
1014                 }
1015                 else
1016                 {
1017                     lat_dir = 0;
1018                 }
1019                 dist = lon - last_lon;
1020                 if ( dist != 0 )
1021                 {
1022                     lon_dir = dist / abs(dist);
1023                 } else
1024                 {
1025                     lon_dir = 0;
1026                 }
1027             }
1028
1029             SG_LOG(SG_TERRAIN,SG_DEBUG, "Scenery update for " <<
1030                    "lat = " << lat << ", lon = " << lon <<
1031                    ", lat_dir = " << lat_dir << ",  " <<
1032                    "lon_dir = " << lon_dir);
1033
1034             syncAreas( lat, lon, lat_dir, lon_dir );
1035             Ok = true;
1036         }
1037         last_lat = lat;
1038         last_lon = lon;
1039     }
1040
1041     return Ok;
1042 }
1043
1044 void SGTerraSync::reposition()
1045 {
1046     last_lat = last_lon = NOWHERE;
1047 }