]> git.openstreetmap.org Git - nominatim.git/blob - utils/update.php
better grouping
[nominatim.git] / utils / update.php
1 <?php
2
3 require_once(CONST_BasePath.'/lib/init-cmd.php');
4 require_once(CONST_BasePath.'/lib/setup_functions.php');
5 require_once(CONST_BasePath.'/lib/setup/SetupClass.php');
6 require_once(CONST_BasePath.'/lib/setup/AddressLevelParser.php');
7
8 ini_set('memory_limit', '800M');
9
10 use Nominatim\Setup\SetupFunctions as SetupFunctions;
11
12 // (long-opt, short-opt, min-occurs, max-occurs, num-arguments, num-arguments, type, help)
13 $aCMDOptions
14 = array(
15    'Import / update / index osm data',
16    array('help', 'h', 0, 1, 0, 0, false, 'Show Help'),
17    array('quiet', 'q', 0, 1, 0, 0, 'bool', 'Quiet output'),
18    array('verbose', 'v', 0, 1, 0, 0, 'bool', 'Verbose output'),
19
20    array('init-updates', '', 0, 1, 0, 0, 'bool', 'Set up database for updating'),
21    array('check-for-updates', '', 0, 1, 0, 0, 'bool', 'Check if new updates are available'),
22    array('no-update-functions', '', 0, 1, 0, 0, 'bool', 'Do not update trigger functions to support differential updates (assuming the diff update logic is already present)'),
23    array('import-osmosis', '', 0, 1, 0, 0, 'bool', 'Import updates once'),
24    array('import-osmosis-all', '', 0, 1, 0, 0, 'bool', 'Import updates forever'),
25    array('no-index', '', 0, 1, 0, 0, 'bool', 'Do not index the new data'),
26
27    array('calculate-postcodes', '', 0, 1, 0, 0, 'bool', 'Update postcode centroid table'),
28
29    array('import-file', '', 0, 1, 1, 1, 'realpath', 'Re-import data from an OSM file'),
30    array('import-diff', '', 0, 1, 1, 1, 'realpath', 'Import a diff (osc) file from local file system'),
31    array('osm2pgsql-cache', '', 0, 1, 1, 1, 'int', 'Cache size used by osm2pgsql'),
32
33    array('import-node', '', 0, 1, 1, 1, 'int', 'Re-import node'),
34    array('import-way', '', 0, 1, 1, 1, 'int', 'Re-import way'),
35    array('import-relation', '', 0, 1, 1, 1, 'int', 'Re-import relation'),
36    array('import-from-main-api', '', 0, 1, 0, 0, 'bool', 'Use OSM API instead of Overpass to download objects'),
37
38    array('index', '', 0, 1, 0, 0, 'bool', 'Index'),
39    array('index-rank', '', 0, 1, 1, 1, 'int', 'Rank to start indexing from'),
40    array('index-instances', '', 0, 1, 1, 1, 'int', 'Number of indexing instances (threads)'),
41
42    array('deduplicate', '', 0, 1, 0, 0, 'bool', 'Deduplicate tokens'),
43    array('recompute-word-counts', '', 0, 1, 0, 0, 'bool', 'Compute frequency of full-word search terms'),
44    array('update-address-levels', '', 0, 1, 0, 0, 'bool', 'Reimport address level configuration (EXPERT)'),
45    array('recompute-importance', '', 0, 1, 0, 0, 'bool', 'Recompute place importances'),
46    array('no-npi', '', 0, 1, 0, 0, 'bool', '(obsolete)'),
47   );
48
49 getCmdOpt($_SERVER['argv'], $aCMDOptions, $aResult, true, true);
50
51 if (!isset($aResult['index-instances'])) $aResult['index-instances'] = 1;
52 if (!isset($aResult['index-rank'])) $aResult['index-rank'] = 0;
53
54 date_default_timezone_set('Etc/UTC');
55
56 $oDB = new Nominatim\DB();
57 $oDB->connect();
58
59 $aDSNInfo = Nominatim\DB::parseDSN(CONST_Database_DSN);
60 if (!isset($aDSNInfo['port']) || !$aDSNInfo['port']) $aDSNInfo['port'] = 5432;
61
62 // cache memory to be used by osm2pgsql, should not be more than the available memory
63 $iCacheMemory = (isset($aResult['osm2pgsql-cache'])?$aResult['osm2pgsql-cache']:2000);
64 if ($iCacheMemory + 500 > getTotalMemoryMB()) {
65     $iCacheMemory = getCacheMemoryMB();
66     echo "WARNING: resetting cache memory to $iCacheMemory\n";
67 }
68 $sOsm2pgsqlCmd = CONST_Osm2pgsql_Binary.' -klas --number-processes 1 -C '.$iCacheMemory.' -O gazetteer -S '.CONST_Import_Style.' -d '.$aDSNInfo['database'].' -P '.$aDSNInfo['port'];
69 if (isset($aDSNInfo['username']) && $aDSNInfo['username']) {
70     $sOsm2pgsqlCmd .= ' -U ' . $aDSNInfo['username'];
71 }
72 if (isset($aDSNInfo['hostspec']) && $aDSNInfo['hostspec']) {
73     $sOsm2pgsqlCmd .= ' -H ' . $aDSNInfo['hostspec'];
74 }
75 $aProcEnv = null;
76 if (isset($aDSNInfo['password']) && $aDSNInfo['password']) {
77     $aProcEnv = array_merge(array('PGPASSWORD' => $aDSNInfo['password']), $_ENV);
78 }
79
80 if (!is_null(CONST_Osm2pgsql_Flatnode_File) && CONST_Osm2pgsql_Flatnode_File) {
81     $sOsm2pgsqlCmd .= ' --flat-nodes '.CONST_Osm2pgsql_Flatnode_File;
82 }
83
84 $sIndexCmd = CONST_BasePath.'/nominatim/nominatim.py';
85 if (!$aResult['quiet']) {
86     $sIndexCmd .= ' -v';
87 }
88 if ($aResult['verbose']) {
89     $sIndexCmd .= ' -v';
90 }
91
92 if ($aResult['init-updates']) {
93     // sanity check that the replication URL is correct
94     $sBaseState = file_get_contents(CONST_Replication_Url.'/state.txt');
95     if ($sBaseState === false) {
96         echo "\nCannot find state.txt file at the configured replication URL.\n";
97         echo "Does the URL point to a directory containing OSM update data?\n\n";
98         fail('replication URL not reachable.');
99     }
100     // sanity check for pyosmium-get-changes
101     if (!CONST_Pyosmium_Binary) {
102         echo "\nCONST_Pyosmium_Binary not configured.\n";
103         echo "You need to install pyosmium and set up the path to pyosmium-get-changes\n";
104         echo "in your local settings file.\n\n";
105         fail('CONST_Pyosmium_Binary not configured');
106     }
107     $aOutput = 0;
108     $sCmd = CONST_Pyosmium_Binary.' --help';
109     exec($sCmd, $aOutput, $iRet);
110     if ($iRet != 0) {
111         echo "Cannot execute pyosmium-get-changes.\n";
112         echo "Make sure you have pyosmium installed correctly\n";
113         echo "and have set up CONST_Pyosmium_Binary to point to pyosmium-get-changes.\n";
114         fail('pyosmium-get-changes not found or not usable');
115     }
116
117     if (!$aResult['no-update-functions']) {
118         // instantiate setupClass to use the function therein
119         $cSetup = new SetupFunctions(array(
120                                       'enable-diff-updates' => true,
121                                       'verbose' => $aResult['verbose']
122                                      ));
123         $cSetup->connect();
124         $cSetup->createFunctions();
125     }
126
127     $sDatabaseDate = getDatabaseDate($oDB);
128     if (!$sDatabaseDate) {
129         fail('Cannot determine date of database.');
130     }
131     $sWindBack = strftime('%Y-%m-%dT%H:%M:%SZ', strtotime($sDatabaseDate) - (3*60*60));
132
133     // get the appropriate state id
134     $aOutput = 0;
135     $sCmd = CONST_Pyosmium_Binary.' -D '.$sWindBack.' --server '.CONST_Replication_Url;
136     exec($sCmd, $aOutput, $iRet);
137     if ($iRet != 0 || $aOutput[0] == 'None') {
138         fail('Error running pyosmium tools');
139     }
140
141     $oDB->exec('TRUNCATE import_status');
142     $sSQL = "INSERT INTO import_status (lastimportdate, sequence_id, indexed) VALUES('";
143     $sSQL .= $sDatabaseDate."',".$aOutput[0].', true)';
144
145     try {
146         $oDB->exec($sSQL);
147     } catch (\Nominatim\DatabaseError $e) {
148         fail('Could not enter sequence into database.');
149     }
150
151     echo "Done. Database updates will start at sequence $aOutput[0] ($sWindBack)\n";
152 }
153
154 if ($aResult['check-for-updates']) {
155     $aLastState = $oDB->getRow('SELECT sequence_id FROM import_status');
156
157     if (!$aLastState['sequence_id']) {
158         fail('Updates not set up. Please run ./utils/update.php --init-updates.');
159     }
160
161     system(CONST_BasePath.'/utils/check_server_for_updates.py '.CONST_Replication_Url.' '.$aLastState['sequence_id'], $iRet);
162     exit($iRet);
163 }
164
165 if (isset($aResult['import-diff']) || isset($aResult['import-file'])) {
166     // import diffs and files directly (e.g. from osmosis --rri)
167     $sNextFile = isset($aResult['import-diff']) ? $aResult['import-diff'] : $aResult['import-file'];
168
169     if (!file_exists($sNextFile)) {
170         fail("Cannot open $sNextFile\n");
171     }
172
173     // Import the file
174     $sCMD = $sOsm2pgsqlCmd.' '.$sNextFile;
175     echo $sCMD."\n";
176     $iErrorLevel = runWithEnv($sCMD, $aProcEnv);
177
178     if ($iErrorLevel) {
179         fail("Error from osm2pgsql, $iErrorLevel\n");
180     }
181
182     // Don't update the import status - we don't know what this file contains
183 }
184
185 if ($aResult['calculate-postcodes']) {
186     info('Update postcodes centroids');
187     $sTemplate = file_get_contents(CONST_BasePath.'/sql/update-postcodes.sql');
188     runSQLScript($sTemplate, true, true);
189 }
190
191 $sTemporaryFile = CONST_BasePath.'/data/osmosischange.osc';
192 $bHaveDiff = false;
193 $bUseOSMApi = isset($aResult['import-from-main-api']) && $aResult['import-from-main-api'];
194 $sContentURL = '';
195 if (isset($aResult['import-node']) && $aResult['import-node']) {
196     if ($bUseOSMApi) {
197         $sContentURL = 'https://www.openstreetmap.org/api/0.6/node/'.$aResult['import-node'];
198     } else {
199         $sContentURL = 'https://overpass-api.de/api/interpreter?data=node('.$aResult['import-node'].');out%20meta;';
200     }
201 }
202
203 if (isset($aResult['import-way']) && $aResult['import-way']) {
204     if ($bUseOSMApi) {
205         $sContentURL = 'https://www.openstreetmap.org/api/0.6/way/'.$aResult['import-way'].'/full';
206     } else {
207         $sContentURL = 'https://overpass-api.de/api/interpreter?data=(way('.$aResult['import-way'].');node(w););out%20meta;';
208     }
209 }
210
211 if (isset($aResult['import-relation']) && $aResult['import-relation']) {
212     if ($bUseOSMApi) {
213         $sContentURLsModifyXMLstr = 'https://www.openstreetmap.org/api/0.6/relation/'.$aResult['import-relation'].'/full';
214     } else {
215         $sContentURL = 'https://overpass-api.de/api/interpreter?data=((rel('.$aResult['import-relation'].');way(r);node(w));node(r));out%20meta;';
216     }
217 }
218
219 if ($sContentURL) {
220     file_put_contents($sTemporaryFile, file_get_contents($sContentURL));
221     $bHaveDiff = true;
222 }
223
224 if ($bHaveDiff) {
225     // import generated change file
226     $sCMD = $sOsm2pgsqlCmd.' '.$sTemporaryFile;
227     echo $sCMD."\n";
228     $iErrorLevel = runWithEnv($sCMD, $aProcEnv);
229     if ($iErrorLevel) {
230         fail("osm2pgsql exited with error level $iErrorLevel\n");
231     }
232 }
233
234 if ($aResult['deduplicate']) {
235     $oDB = new Nominatim\DB();
236     $oDB->connect();
237
238     if ($oDB->getPostgresVersion() < 9.3) {
239         fail('ERROR: deduplicate is only currently supported in postgresql 9.3');
240     }
241
242     $sSQL = 'select partition from country_name order by country_code';
243     $aPartitions = $oDB->getCol($sSQL);
244     $aPartitions[] = 0;
245
246     // we don't care about empty search_name_* partitions, they can't contain mentions of duplicates
247     foreach ($aPartitions as $i => $sPartition) {
248         $sSQL = 'select count(*) from search_name_'.$sPartition;
249         $nEntries = $oDB->getOne($sSQL);
250         if ($nEntries == 0) {
251             unset($aPartitions[$i]);
252         }
253     }
254
255     $sSQL = "select word_token,count(*) from word where substr(word_token, 1, 1) = ' '";
256     $sSQL .= ' and class is null and type is null and country_code is null';
257     $sSQL .= ' group by word_token having count(*) > 1 order by word_token';
258     $aDuplicateTokens = $oDB->getAll($sSQL);
259     foreach ($aDuplicateTokens as $aToken) {
260         if (trim($aToken['word_token']) == '' || trim($aToken['word_token']) == '-') continue;
261         echo 'Deduping '.$aToken['word_token']."\n";
262         $sSQL = 'select word_id,';
263         $sSQL .= ' (select count(*) from search_name where nameaddress_vector @> ARRAY[word_id]) as num';
264         $sSQL .= " from word where word_token = '".$aToken['word_token'];
265         $sSQL .= "' and class is null and type is null and country_code is null order by num desc";
266         $aTokenSet = $oDB->getAll($sSQL);
267
268         $aKeep = array_shift($aTokenSet);
269         $iKeepID = $aKeep['word_id'];
270
271         foreach ($aTokenSet as $aRemove) {
272             $sSQL = 'update search_name set';
273             $sSQL .= ' name_vector = array_replace(name_vector,'.$aRemove['word_id'].','.$iKeepID.'),';
274             $sSQL .= ' nameaddress_vector = array_replace(nameaddress_vector,'.$aRemove['word_id'].','.$iKeepID.')';
275             $sSQL .= ' where name_vector @> ARRAY['.$aRemove['word_id'].']';
276             $oDB->exec($sSQL);
277
278             $sSQL = 'update search_name set';
279             $sSQL .= ' nameaddress_vector = array_replace(nameaddress_vector,'.$aRemove['word_id'].','.$iKeepID.')';
280             $sSQL .= ' where nameaddress_vector @> ARRAY['.$aRemove['word_id'].']';
281             $oDB->exec($sSQL);
282
283             $sSQL = 'update location_area_country set';
284             $sSQL .= ' keywords = array_replace(keywords,'.$aRemove['word_id'].','.$iKeepID.')';
285             $sSQL .= ' where keywords @> ARRAY['.$aRemove['word_id'].']';
286             $oDB->exec($sSQL);
287
288             foreach ($aPartitions as $sPartition) {
289                 $sSQL = 'update search_name_'.$sPartition.' set';
290                 $sSQL .= ' name_vector = array_replace(name_vector,'.$aRemove['word_id'].','.$iKeepID.')';
291                 $sSQL .= ' where name_vector @> ARRAY['.$aRemove['word_id'].']';
292                 $oDB->exec($sSQL);
293
294                 $sSQL = 'update location_area_country set';
295                 $sSQL .= ' keywords = array_replace(keywords,'.$aRemove['word_id'].','.$iKeepID.')';
296                 $sSQL .= ' where keywords @> ARRAY['.$aRemove['word_id'].']';
297                 $oDB->exec($sSQL);
298             }
299
300             $sSQL = 'delete from word where word_id = '.$aRemove['word_id'];
301             $oDB->exec($sSQL);
302         }
303     }
304 }
305
306 if ($aResult['recompute-word-counts']) {
307     info('Recompute frequency of full-word search terms');
308     $sTemplate = file_get_contents(CONST_BasePath.'/sql/words_from_search_name.sql');
309     runSQLScript($sTemplate, true, true);
310 }
311
312 if ($aResult['index']) {
313     $sCmd = $sIndexCmd
314             .' -d '.$aDSNInfo['database']
315             .' -P '.$aDSNInfo['port']
316             .' -t '.$aResult['index-instances']
317             .' -r '.$aResult['index-rank'];
318     if (isset($aDSNInfo['hostspec']) && $aDSNInfo['hostspec']) {
319         $sCmd .= ' -H ' . $aDSNInfo['hostspec'];
320     }
321     if (isset($aDSNInfo['username']) && $aDSNInfo['username']) {
322         $sCmd .= ' -U ' . $aDSNInfo['username'];
323     }
324
325     runWithEnv($sCmd, $aProcEnv);
326
327     $oDB->exec('update import_status set indexed = true');
328 }
329
330 if ($aResult['update-address-levels']) {
331     echo 'Updating address levels from '.CONST_Address_Level_Config.".\n";
332     $oAlParser = new \Nominatim\Setup\AddressLevelParser(CONST_Address_Level_Config);
333     $oAlParser->createTable($oDB, 'address_levels');
334 }
335
336 if ($aResult['recompute-importance']) {
337     echo "Updating importance values for database.\n";
338     $oDB = new Nominatim\DB();
339     $oDB->connect();
340
341     $sSQL = 'ALTER TABLE placex DISABLE TRIGGER ALL;';
342     $sSQL .= 'UPDATE placex SET (wikipedia, importance) =';
343     $sSQL .= '   (SELECT wikipedia, importance';
344     $sSQL .= '    FROM compute_importance(extratags, country_code, osm_type, osm_id));';
345     $sSQL .= 'UPDATE placex s SET wikipedia = d.wikipedia, importance = d.importance';
346     $sSQL .= ' FROM placex d';
347     $sSQL .= ' WHERE s.place_id = d.linked_place_id and d.wikipedia is not null';
348     $sSQL .= '       and (s.wikipedia is null or s.importance < d.importance);';
349     $sSQL .= 'ALTER TABLE placex ENABLE TRIGGER ALL;';
350     $oDB->exec($sSQL);
351 }
352
353 if ($aResult['import-osmosis'] || $aResult['import-osmosis-all']) {
354     //
355     if (strpos(CONST_Replication_Url, 'download.geofabrik.de') !== false && CONST_Replication_Update_Interval < 86400) {
356         fail('Error: Update interval too low for download.geofabrik.de. ' .
357              "Please check install documentation (http://nominatim.org/release-docs/latest/Import-and-Update#setting-up-the-update-process)\n");
358     }
359
360     $sImportFile = CONST_InstallPath.'/osmosischange.osc';
361     $sCMDDownload = CONST_Pyosmium_Binary.' --server '.CONST_Replication_Url.' -o '.$sImportFile.' -s '.CONST_Replication_Max_Diff_size;
362     $sCMDImport = $sOsm2pgsqlCmd.' '.$sImportFile;
363     $sCMDIndex = $sIndexCmd
364                  .' -d '.$aDSNInfo['database']
365                  .' -P '.$aDSNInfo['port']
366                  .' -t '.$aResult['index-instances'];
367     if (isset($aDSNInfo['hostspec']) && $aDSNInfo['hostspec']) {
368         $sCMDIndex .= ' -H ' . $aDSNInfo['hostspec'];
369     }
370     if (isset($aDSNInfo['username']) && $aDSNInfo['username']) {
371         $sCMDIndex .= ' -U ' . $aDSNInfo['username'];
372     }
373
374     while (true) {
375         $fStartTime = time();
376         $aLastState = $oDB->getRow('SELECT *, EXTRACT (EPOCH FROM lastimportdate) as unix_ts FROM import_status');
377
378         if (!$aLastState['sequence_id']) {
379             echo "Updates not set up. Please run ./utils/update.php --init-updates.\n";
380             exit(1);
381         }
382
383         echo 'Currently at sequence '.$aLastState['sequence_id'].' ('.$aLastState['lastimportdate'].') - '.$aLastState['indexed']." indexed\n";
384
385         $sBatchEnd = $aLastState['lastimportdate'];
386         $iEndSequence = $aLastState['sequence_id'];
387
388         if ($aLastState['indexed']) {
389             // Sleep if the update interval has not yet been reached.
390             $fNextUpdate = $aLastState['unix_ts'] + CONST_Replication_Update_Interval;
391             if ($fNextUpdate > $fStartTime) {
392                 $iSleepTime = $fNextUpdate - $fStartTime;
393                 echo "Waiting for next update for $iSleepTime sec.";
394                 sleep($iSleepTime);
395             }
396
397             // Download the next batch of changes.
398             do {
399                 $fCMDStartTime = time();
400                 $iNextSeq = (int) $aLastState['sequence_id'];
401                 unset($aOutput);
402                 echo "$sCMDDownload -I $iNextSeq\n";
403                 if (file_exists($sImportFile)) {
404                     unlink($sImportFile);
405                 }
406                 exec($sCMDDownload.' -I '.$iNextSeq, $aOutput, $iResult);
407
408                 if ($iResult == 3) {
409                     echo 'No new updates. Sleeping for '.CONST_Replication_Recheck_Interval." sec.\n";
410                     sleep(CONST_Replication_Recheck_Interval);
411                 } elseif ($iResult != 0) {
412                     echo 'ERROR: updates failed.';
413                     exit($iResult);
414                 } else {
415                     $iEndSequence = (int)$aOutput[0];
416                 }
417             } while ($iResult);
418
419             // get the newest object from the diff file
420             $sBatchEnd = 0;
421             $iRet = 0;
422             exec(CONST_BasePath.'/utils/osm_file_date.py '.$sImportFile, $sBatchEnd, $iRet);
423             if ($iRet == 5) {
424                 echo "Diff file is empty. skipping import.\n";
425                 if (!$aResult['import-osmosis-all']) {
426                     exit(0);
427                 } else {
428                     continue;
429                 }
430             }
431             if ($iRet != 0) {
432                 fail('Error getting date from diff file.');
433             }
434             $sBatchEnd = $sBatchEnd[0];
435
436             // Import the file
437             $fCMDStartTime = time();
438             echo $sCMDImport."\n";
439             unset($sJunk);
440             $iErrorLevel = runWithEnv($sCMDImport, $aProcEnv);
441             if ($iErrorLevel) {
442                 echo "Error executing osm2pgsql: $iErrorLevel\n";
443                 exit($iErrorLevel);
444             }
445
446             // write the update logs
447             $iFileSize = filesize($sImportFile);
448             $sSQL = 'INSERT INTO import_osmosis_log';
449             $sSQL .= '(batchend, batchseq, batchsize, starttime, endtime, event)';
450             $sSQL .= " values ('$sBatchEnd',$iEndSequence,$iFileSize,'";
451             $sSQL .= date('Y-m-d H:i:s', $fCMDStartTime)."','";
452             $sSQL .= date('Y-m-d H:i:s')."','import')";
453             var_Dump($sSQL);
454             $oDB->exec($sSQL);
455
456             // update the status
457             $sSQL = "UPDATE import_status SET lastimportdate = '$sBatchEnd', indexed=false, sequence_id = $iEndSequence";
458             var_Dump($sSQL);
459             $oDB->exec($sSQL);
460             echo date('Y-m-d H:i:s')." Completed download step for $sBatchEnd in ".round((time()-$fCMDStartTime)/60, 2)." minutes\n";
461         }
462
463         // Index file
464         if (!$aResult['no-index']) {
465             $sThisIndexCmd = $sCMDIndex;
466             $fCMDStartTime = time();
467
468             echo "$sThisIndexCmd\n";
469             $iErrorLevel = runWithEnv($sThisIndexCmd, $aProcEnv);
470             if ($iErrorLevel) {
471                 echo "Error: $iErrorLevel\n";
472                 exit($iErrorLevel);
473             }
474
475             $sSQL = 'INSERT INTO import_osmosis_log';
476             $sSQL .= '(batchend, batchseq, batchsize, starttime, endtime, event)';
477             $sSQL .= " values ('$sBatchEnd',$iEndSequence,NULL,'";
478             $sSQL .= date('Y-m-d H:i:s', $fCMDStartTime)."','";
479             $sSQL .= date('Y-m-d H:i:s')."','index')";
480             var_Dump($sSQL);
481             $oDB->exec($sSQL);
482             echo date('Y-m-d H:i:s')." Completed index step for $sBatchEnd in ".round((time()-$fCMDStartTime)/60, 2)." minutes\n";
483
484             $sSQL = 'update import_status set indexed = true';
485             $oDB->exec($sSQL);
486         } else {
487             if ($aResult['import-osmosis-all']) {
488                 echo "Error: --no-index cannot be used with continuous imports (--import-osmosis-all).\n";
489                 exit(1);
490             }
491         }
492
493         $fDuration = time() - $fStartTime;
494         echo date('Y-m-d H:i:s')." Completed all for $sBatchEnd in ".round($fDuration/60, 2)." minutes\n";
495         if (!$aResult['import-osmosis-all']) exit(0);
496     }
497 }