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