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