1 /****************************************************************************
2 **
3 ** Copyright (C) 2015 The Qt Company Ltd.
4 ** Contact: http://www.qt.io/licensing/
5 **
6 ** This file is part of the QtLocation module of the Qt Toolkit.
7 **
8 ** $QT_BEGIN_LICENSE:LGPL3$
9 ** Commercial License Usage
10 ** Licensees holding valid commercial Qt licenses may use this file in
11 ** accordance with the commercial license agreement provided with the
12 ** Software or, alternatively, in accordance with the terms contained in
13 ** a written agreement between you and The Qt Company. For licensing terms
14 ** and conditions see http://www.qt.io/terms-conditions. For further
15 ** information use the contact form at http://www.qt.io/contact-us.
16 **
17 ** GNU Lesser General Public License Usage
18 ** Alternatively, this file may be used under the terms of the GNU Lesser
19 ** General Public License version 3 as published by the Free Software
20 ** Foundation and appearing in the file LICENSE.LGPLv3 included in the
21 ** packaging of this file. Please review the following information to
22 ** ensure the GNU Lesser General Public License version 3 requirements
23 ** will be met: https://www.gnu.org/licenses/lgpl.html.
24 **
25 ** GNU General Public License Usage
26 ** Alternatively, this file may be used under the terms of the GNU
27 ** General Public License version 2.0 or later as published by the Free
28 ** Software Foundation and appearing in the file LICENSE.GPL included in
29 ** the packaging of this file. Please review the following information to
30 ** ensure the GNU General Public License version 2.0 requirements will be
31 ** met: http://www.gnu.org/licenses/gpl-2.0.html.
32 **
33 ** $QT_END_LICENSE$
34 **
35 ****************************************************************************/
36 #include "qgeofiletilecache_p.h"
37
38 #include "qgeotilespec_p.h"
39
40 #include "qgeomappingmanager_p.h"
41
42 #include <QDir>
43 #include <QStandardPaths>
44 #include <QMetaType>
45 #include <QPixmap>
46 #include <QDebug>
47
48 Q_DECLARE_METATYPE(QList<QGeoTileSpec>)
49 Q_DECLARE_METATYPE(QSet<QGeoTileSpec>)
50
51 QT_BEGIN_NAMESPACE
52
53 class QGeoCachedTileMemory
54 {
55 public:
~QGeoCachedTileMemory()56 ~QGeoCachedTileMemory()
57 {
58 if (cache)
59 cache->evictFromMemoryCache(this);
60 }
61
62 QGeoTileSpec spec;
63 QGeoFileTileCache *cache;
64 QByteArray bytes;
65 QString format;
66 };
67
aboutToBeRemoved(const QGeoTileSpec & key,QSharedPointer<QGeoCachedTileDisk> obj)68 void QCache3QTileEvictionPolicy::aboutToBeRemoved(const QGeoTileSpec &key, QSharedPointer<QGeoCachedTileDisk> obj)
69 {
70 Q_UNUSED(key);
71 // set the cache pointer to zero so we can't call evictFromDiskCache
72 obj->cache = 0;
73 }
74
aboutToBeEvicted(const QGeoTileSpec & key,QSharedPointer<QGeoCachedTileDisk> obj)75 void QCache3QTileEvictionPolicy::aboutToBeEvicted(const QGeoTileSpec &key, QSharedPointer<QGeoCachedTileDisk> obj)
76 {
77 Q_UNUSED(key);
78 Q_UNUSED(obj);
79 // leave the pointer set if it's a real eviction
80 }
81
~QGeoCachedTileDisk()82 QGeoCachedTileDisk::~QGeoCachedTileDisk()
83 {
84 if (cache)
85 cache->evictFromDiskCache(this);
86 }
87
QGeoFileTileCache(const QString & directory,QObject * parent)88 QGeoFileTileCache::QGeoFileTileCache(const QString &directory, QObject *parent)
89 : QAbstractGeoTileCache(parent), directory_(directory), minTextureUsage_(0), extraTextureUsage_(0)
90 ,costStrategyDisk_(ByteSize), costStrategyMemory_(ByteSize), costStrategyTexture_(ByteSize)
91 ,isDiskCostSet_(false), isMemoryCostSet_(false), isTextureCostSet_(false)
92 {
93
94 }
95
init()96 void QGeoFileTileCache::init()
97 {
98 const QString basePath = baseCacheDirectory() + QLatin1String("QtLocation/");
99
100 // delete old tiles from QtLocation 5.7 or prior
101 // Newer version use plugin-specific subdirectories, versioned with qt version so those are not affected.
102 // TODO Remove cache cleanup in Qt 6
103 QDir baseDir(basePath);
104 if (baseDir.exists()) {
105 const QStringList oldCacheFiles = baseDir.entryList(QDir::Files);
106 foreach (const QString& file, oldCacheFiles)
107 baseDir.remove(file);
108 const QStringList oldCacheDirs = { QStringLiteral("osm"), QStringLiteral("mapbox"), QStringLiteral("here") };
109 foreach (const QString& d, oldCacheDirs) {
110 QDir oldCacheDir(basePath + QLatin1Char('/') + d);
111 if (oldCacheDir.exists())
112 oldCacheDir.removeRecursively();
113 }
114 }
115
116 if (directory_.isEmpty()) {
117 directory_ = baseLocationCacheDirectory();
118 qWarning() << "Plugin uses uninitialized QGeoFileTileCache directory which was deleted during startup";
119 }
120
121 const bool directoryCreated = QDir::root().mkpath(directory_);
122 if (!directoryCreated)
123 qWarning() << "Failed to create cache directory " << directory_;
124
125 // default values
126 if (!isDiskCostSet_) { // If setMaxDiskUsage has not been called yet
127 if (costStrategyDisk_ == ByteSize)
128 setMaxDiskUsage(50 * 1024 * 1024);
129 else
130 setMaxDiskUsage(1000);
131 }
132
133 if (!isMemoryCostSet_) { // If setMaxMemoryUsage has not been called yet
134 if (costStrategyMemory_ == ByteSize)
135 setMaxMemoryUsage(3 * 1024 * 1024);
136 else
137 setMaxMemoryUsage(100);
138 }
139
140 if (!isTextureCostSet_) { // If setExtraTextureUsage has not been called yet
141 if (costStrategyTexture_ == ByteSize)
142 setExtraTextureUsage(6 * 1024 * 1024);
143 else
144 setExtraTextureUsage(30); // byte size of texture is >> compressed image, hence unitary cost should be lower
145 }
146
147 loadTiles();
148 }
149
loadTiles()150 void QGeoFileTileCache::loadTiles()
151 {
152 QStringList formats;
153 formats << QLatin1String("*.*");
154
155 QDir dir(directory_);
156 QStringList files = dir.entryList(formats, QDir::Files);
157 #if 0 // workaround for QTBUG-60581
158 // Method:
159 // 1. read each queue file then, if each file exists, deserialize the data into the appropriate
160 // cache queue.
161 for (int i = 1; i<=4; i++) {
162 QString filename = dir.filePath(QString::fromLatin1("queue") + QString::number(i));
163 QFile file(filename);
164 if (!file.open(QIODevice::ReadOnly))
165 continue;
166 QList<QSharedPointer<QGeoCachedTileDisk> > queue;
167 QList<QGeoTileSpec> specs;
168 QList<int> costs;
169 while (!file.atEnd()) {
170 QByteArray line = file.readLine().trimmed();
171 QString filename = QString::fromLatin1(line.constData(), line.length());
172 if (dir.exists(filename)){
173 files.removeOne(filename);
174 QGeoTileSpec spec = filenameToTileSpec(filename);
175 if (spec.zoom() == -1)
176 continue;
177 QSharedPointer<QGeoCachedTileDisk> tileDisk(new QGeoCachedTileDisk);
178 tileDisk->filename = dir.filePath(filename);
179 tileDisk->cache = this;
180 tileDisk->spec = spec;
181 QFileInfo fi(tileDisk->filename);
182 specs.append(spec);
183 queue.append(tileDisk);
184 if (costStrategyDisk_ == ByteSize)
185 costs.append(fi.size());
186 else
187 costs.append(1);
188
189 }
190 }
191
192 diskCache_.deserializeQueue(i, specs, queue, costs);
193 file.close();
194 }
195 #endif
196 // 2. remaining tiles that aren't registered in a queue get pushed into cache here
197 // this is a backup, in case the queue manifest files get deleted or out of sync due to
198 // the application not closing down properly
199 for (int i = 0; i < files.size(); ++i) {
200 QGeoTileSpec spec = filenameToTileSpec(files.at(i));
201 if (spec.zoom() == -1)
202 continue;
203 QString filename = dir.filePath(files.at(i));
204 addToDiskCache(spec, filename);
205 }
206 }
207
~QGeoFileTileCache()208 QGeoFileTileCache::~QGeoFileTileCache()
209 {
210 #if 0 // workaround for QTBUG-60581
211 // write disk cache queues to disk
212 QDir dir(directory_);
213 for (int i = 1; i<=4; i++) {
214 QString filename = dir.filePath(QString::fromLatin1("queue") + QString::number(i));
215 QFile file(filename);
216 if (!file.open(QIODevice::WriteOnly)){
217 qWarning() << "Unable to write tile cache file " << filename;
218 continue;
219 }
220 QList<QSharedPointer<QGeoCachedTileDisk> > queue;
221 diskCache_.serializeQueue(i, queue);
222 foreach (const QSharedPointer<QGeoCachedTileDisk> &tile, queue) {
223 if (tile.isNull())
224 continue;
225
226 // we just want the filename here, not the full path
227 int index = tile->filename.lastIndexOf(QLatin1Char('/'));
228 QByteArray filename = tile->filename.mid(index + 1).toLatin1() + '\n';
229 file.write(filename);
230 }
231 file.close();
232 }
233 #endif
234 }
235
printStats()236 void QGeoFileTileCache::printStats()
237 {
238 textureCache_.printStats();
239 memoryCache_.printStats();
240 diskCache_.printStats();
241 }
242
setMaxDiskUsage(int diskUsage)243 void QGeoFileTileCache::setMaxDiskUsage(int diskUsage)
244 {
245 diskCache_.setMaxCost(diskUsage);
246 isDiskCostSet_ = true;
247 }
248
maxDiskUsage() const249 int QGeoFileTileCache::maxDiskUsage() const
250 {
251 return diskCache_.maxCost();
252 }
253
diskUsage() const254 int QGeoFileTileCache::diskUsage() const
255 {
256 return diskCache_.totalCost();
257 }
258
setMaxMemoryUsage(int memoryUsage)259 void QGeoFileTileCache::setMaxMemoryUsage(int memoryUsage)
260 {
261 memoryCache_.setMaxCost(memoryUsage);
262 isMemoryCostSet_ = true;
263 }
264
maxMemoryUsage() const265 int QGeoFileTileCache::maxMemoryUsage() const
266 {
267 return memoryCache_.maxCost();
268 }
269
memoryUsage() const270 int QGeoFileTileCache::memoryUsage() const
271 {
272 return memoryCache_.totalCost();
273 }
274
setExtraTextureUsage(int textureUsage)275 void QGeoFileTileCache::setExtraTextureUsage(int textureUsage)
276 {
277 extraTextureUsage_ = textureUsage;
278 textureCache_.setMaxCost(minTextureUsage_ + extraTextureUsage_);
279 isTextureCostSet_ = true;
280 }
281
setMinTextureUsage(int textureUsage)282 void QGeoFileTileCache::setMinTextureUsage(int textureUsage)
283 {
284 minTextureUsage_ = textureUsage;
285 textureCache_.setMaxCost(minTextureUsage_ + extraTextureUsage_);
286 }
287
maxTextureUsage() const288 int QGeoFileTileCache::maxTextureUsage() const
289 {
290 return textureCache_.maxCost();
291 }
292
minTextureUsage() const293 int QGeoFileTileCache::minTextureUsage() const
294 {
295 return minTextureUsage_;
296 }
297
298
textureUsage() const299 int QGeoFileTileCache::textureUsage() const
300 {
301 return textureCache_.totalCost();
302 }
303
clearAll()304 void QGeoFileTileCache::clearAll()
305 {
306 textureCache_.clear();
307 memoryCache_.clear();
308 diskCache_.clear();
309 QDir dir(directory_);
310 dir.setNameFilters(QStringList() << QLatin1String("*-*-*-*.*"));
311 dir.setFilter(QDir::Files);
312 foreach (QString dirFile, dir.entryList()) {
313 dir.remove(dirFile);
314 }
315 }
316
clearMapId(const int mapId)317 void QGeoFileTileCache::clearMapId(const int mapId)
318 {
319 for (const QGeoTileSpec &k : diskCache_.keys())
320 if (k.mapId() == mapId)
321 diskCache_.remove(k, true);
322 for (const QGeoTileSpec &k : memoryCache_.keys())
323 if (k.mapId() == mapId)
324 memoryCache_.remove(k);
325 for (const QGeoTileSpec &k : textureCache_.keys())
326 if (k.mapId() == mapId)
327 textureCache_.remove(k);
328
329 // TODO: It seems the cache leaves residues, like some tiles do not get picked up.
330 // After the above calls, files that shouldnt be left behind are still on disk.
331 // Do an additional pass and make sure what has to be deleted gets deleted.
332 QDir dir(directory_);
333 QStringList formats;
334 formats << QLatin1String("*.*");
335 QStringList files = dir.entryList(formats, QDir::Files);
336 qWarning() << "Old tile data detected. Cache eviction left out "<< files.size() << "tiles";
337 for (const QString &tileFileName : files) {
338 QGeoTileSpec spec = filenameToTileSpec(tileFileName);
339 if (spec.mapId() != mapId)
340 continue;
341 QFile::remove(dir.filePath(tileFileName));
342 }
343 }
344
setCostStrategyDisk(QAbstractGeoTileCache::CostStrategy costStrategy)345 void QGeoFileTileCache::setCostStrategyDisk(QAbstractGeoTileCache::CostStrategy costStrategy)
346 {
347 costStrategyDisk_ = costStrategy;
348 }
349
costStrategyDisk() const350 QAbstractGeoTileCache::CostStrategy QGeoFileTileCache::costStrategyDisk() const
351 {
352 return costStrategyDisk_;
353 }
354
setCostStrategyMemory(QAbstractGeoTileCache::CostStrategy costStrategy)355 void QGeoFileTileCache::setCostStrategyMemory(QAbstractGeoTileCache::CostStrategy costStrategy)
356 {
357 costStrategyMemory_ = costStrategy;
358 }
359
costStrategyMemory() const360 QAbstractGeoTileCache::CostStrategy QGeoFileTileCache::costStrategyMemory() const
361 {
362 return costStrategyMemory_;
363 }
364
setCostStrategyTexture(QAbstractGeoTileCache::CostStrategy costStrategy)365 void QGeoFileTileCache::setCostStrategyTexture(QAbstractGeoTileCache::CostStrategy costStrategy)
366 {
367 costStrategyTexture_ = costStrategy;
368 }
369
costStrategyTexture() const370 QAbstractGeoTileCache::CostStrategy QGeoFileTileCache::costStrategyTexture() const
371 {
372 return costStrategyTexture_;
373 }
374
get(const QGeoTileSpec & spec)375 QSharedPointer<QGeoTileTexture> QGeoFileTileCache::get(const QGeoTileSpec &spec)
376 {
377 QSharedPointer<QGeoTileTexture> tt = getFromMemory(spec);
378 if (tt)
379 return tt;
380 return getFromDisk(spec);
381 }
382
insert(const QGeoTileSpec & spec,const QByteArray & bytes,const QString & format,QAbstractGeoTileCache::CacheAreas areas)383 void QGeoFileTileCache::insert(const QGeoTileSpec &spec,
384 const QByteArray &bytes,
385 const QString &format,
386 QAbstractGeoTileCache::CacheAreas areas)
387 {
388 if (bytes.isEmpty())
389 return;
390
391 if (areas & QAbstractGeoTileCache::DiskCache) {
392 QString filename = tileSpecToFilename(spec, format, directory_);
393 addToDiskCache(spec, filename, bytes);
394 }
395
396 if (areas & QAbstractGeoTileCache::MemoryCache) {
397 addToMemoryCache(spec, bytes, format);
398 }
399
400 /* inserts do not hit the texture cache -- this actually reduces overall
401 * cache hit rates because many tiles come too late to be useful
402 * and act as a poison */
403 }
404
tileSpecToFilenameDefault(const QGeoTileSpec & spec,const QString & format,const QString & directory)405 QString QGeoFileTileCache::tileSpecToFilenameDefault(const QGeoTileSpec &spec, const QString &format, const QString &directory)
406 {
407 QString filename = spec.plugin();
408 filename += QLatin1String("-");
409 filename += QString::number(spec.mapId());
410 filename += QLatin1String("-");
411 filename += QString::number(spec.zoom());
412 filename += QLatin1String("-");
413 filename += QString::number(spec.x());
414 filename += QLatin1String("-");
415 filename += QString::number(spec.y());
416
417 //Append version if real version number to ensure backwards compatibility and eviction of old tiles
418 if (spec.version() != -1) {
419 filename += QLatin1String("-");
420 filename += QString::number(spec.version());
421 }
422
423 filename += QLatin1String(".");
424 filename += format;
425
426 QDir dir = QDir(directory);
427
428 return dir.filePath(filename);
429 }
430
filenameToTileSpecDefault(const QString & filename)431 QGeoTileSpec QGeoFileTileCache::filenameToTileSpecDefault(const QString &filename)
432 {
433 QGeoTileSpec emptySpec;
434
435 QStringList parts = filename.split('.');
436
437 if (parts.length() != 2)
438 return emptySpec;
439
440 QString name = parts.at(0);
441 QStringList fields = name.split('-');
442
443 int length = fields.length();
444 if (length != 5 && length != 6)
445 return emptySpec;
446
447 QList<int> numbers;
448
449 bool ok = false;
450 for (int i = 1; i < length; ++i) {
451 ok = false;
452 int value = fields.at(i).toInt(&ok);
453 if (!ok)
454 return emptySpec;
455 numbers.append(value);
456 }
457
458 //File name without version, append default
459 if (numbers.length() < 5)
460 numbers.append(-1);
461
462 return QGeoTileSpec(fields.at(0),
463 numbers.at(0),
464 numbers.at(1),
465 numbers.at(2),
466 numbers.at(3),
467 numbers.at(4));
468 }
469
evictFromDiskCache(QGeoCachedTileDisk * td)470 void QGeoFileTileCache::evictFromDiskCache(QGeoCachedTileDisk *td)
471 {
472 QFile::remove(td->filename);
473 }
474
evictFromMemoryCache(QGeoCachedTileMemory *)475 void QGeoFileTileCache::evictFromMemoryCache(QGeoCachedTileMemory * /* tm */)
476 {
477 }
478
addToDiskCache(const QGeoTileSpec & spec,const QString & filename)479 QSharedPointer<QGeoCachedTileDisk> QGeoFileTileCache::addToDiskCache(const QGeoTileSpec &spec, const QString &filename)
480 {
481 QSharedPointer<QGeoCachedTileDisk> td(new QGeoCachedTileDisk);
482 td->spec = spec;
483 td->filename = filename;
484 td->cache = this;
485
486 int cost = 1;
487 if (costStrategyDisk_ == ByteSize) {
488 QFileInfo fi(filename);
489 cost = fi.size();
490 }
491 diskCache_.insert(spec, td, cost);
492 return td;
493 }
494
addToDiskCache(const QGeoTileSpec & spec,const QString & filename,const QByteArray & bytes)495 bool QGeoFileTileCache::addToDiskCache(const QGeoTileSpec &spec, const QString &filename, const QByteArray &bytes)
496 {
497 QSharedPointer<QGeoCachedTileDisk> td(new QGeoCachedTileDisk);
498 td->spec = spec;
499 td->filename = filename;
500 td->cache = this;
501
502 int cost = 1;
503 if (costStrategyDisk_ == ByteSize)
504 cost = bytes.size();
505
506 if (diskCache_.insert(spec, td, cost)) {
507 QFile file(filename);
508 file.open(QIODevice::WriteOnly);
509 file.write(bytes);
510 file.close();
511 return true;
512 }
513 return false;
514 }
515
addToMemoryCache(const QGeoTileSpec & spec,const QByteArray & bytes,const QString & format)516 void QGeoFileTileCache::addToMemoryCache(const QGeoTileSpec &spec, const QByteArray &bytes, const QString &format)
517 {
518 if (isTileBogus(bytes))
519 return;
520
521 QSharedPointer<QGeoCachedTileMemory> tm(new QGeoCachedTileMemory);
522 tm->spec = spec;
523 tm->cache = this;
524 tm->bytes = bytes;
525 tm->format = format;
526
527 int cost = 1;
528 if (costStrategyMemory_ == ByteSize)
529 cost = bytes.size();
530 memoryCache_.insert(spec, tm, cost);
531 }
532
addToTextureCache(const QGeoTileSpec & spec,const QImage & image)533 QSharedPointer<QGeoTileTexture> QGeoFileTileCache::addToTextureCache(const QGeoTileSpec &spec, const QImage &image)
534 {
535 QSharedPointer<QGeoTileTexture> tt(new QGeoTileTexture);
536 tt->spec = spec;
537 tt->image = image;
538
539 int cost = 1;
540 if (costStrategyTexture_ == ByteSize)
541 cost = image.width() * image.height() * image.depth() / 8;
542 textureCache_.insert(spec, tt, cost);
543
544 return tt;
545 }
546
getFromMemory(const QGeoTileSpec & spec)547 QSharedPointer<QGeoTileTexture> QGeoFileTileCache::getFromMemory(const QGeoTileSpec &spec)
548 {
549 QSharedPointer<QGeoTileTexture> tt = textureCache_.object(spec);
550 if (tt)
551 return tt;
552
553 QSharedPointer<QGeoCachedTileMemory> tm = memoryCache_.object(spec);
554 if (tm) {
555 QImage image;
556 if (!image.loadFromData(tm->bytes)) {
557 handleError(spec, QLatin1String("Problem with tile image"));
558 return QSharedPointer<QGeoTileTexture>(0);
559 }
560 QSharedPointer<QGeoTileTexture> tt = addToTextureCache(spec, image);
561 if (tt)
562 return tt;
563 }
564 return QSharedPointer<QGeoTileTexture>();
565 }
566
getFromDisk(const QGeoTileSpec & spec)567 QSharedPointer<QGeoTileTexture> QGeoFileTileCache::getFromDisk(const QGeoTileSpec &spec)
568 {
569 QSharedPointer<QGeoCachedTileDisk> td = diskCache_.object(spec);
570 if (td) {
571 const QString format = QFileInfo(td->filename).suffix();
572 QFile file(td->filename);
573 file.open(QIODevice::ReadOnly);
574 QByteArray bytes = file.readAll();
575 file.close();
576
577 QImage image;
578 // Some tiles from the servers could be valid images but the tile fetcher
579 // might be able to recognize them as tiles that should not be shown.
580 // If that's the case, the tile fetcher should write "NoRetry" inside the file.
581 if (isTileBogus(bytes)) {
582 QSharedPointer<QGeoTileTexture> tt(new QGeoTileTexture);
583 tt->spec = spec;
584 tt->image = image;
585 return tt;
586 }
587
588 // This is a truly invalid image. The fetcher should try again.
589 if (!image.loadFromData(bytes)) {
590 handleError(spec, QLatin1String("Problem with tile image"));
591 return QSharedPointer<QGeoTileTexture>(0);
592 }
593
594 // Converting it here, instead of in each QSGTexture::bind()
595 if (image.format() != QImage::Format_RGB32 && image.format() != QImage::Format_ARGB32_Premultiplied)
596 image = image.convertToFormat(QImage::Format_ARGB32_Premultiplied);
597
598 addToMemoryCache(spec, bytes, format);
599 QSharedPointer<QGeoTileTexture> tt = addToTextureCache(td->spec, image);
600 if (tt)
601 return tt;
602 }
603
604 return QSharedPointer<QGeoTileTexture>();
605 }
606
isTileBogus(const QByteArray & bytes) const607 bool QGeoFileTileCache::isTileBogus(const QByteArray &bytes) const
608 {
609 if (bytes.size() == 7 && bytes == QByteArrayLiteral("NoRetry"))
610 return true;
611 return false;
612 }
613
tileSpecToFilename(const QGeoTileSpec & spec,const QString & format,const QString & directory) const614 QString QGeoFileTileCache::tileSpecToFilename(const QGeoTileSpec &spec, const QString &format, const QString &directory) const
615 {
616 return tileSpecToFilenameDefault(spec, format, directory);
617 }
618
filenameToTileSpec(const QString & filename) const619 QGeoTileSpec QGeoFileTileCache::filenameToTileSpec(const QString &filename) const
620 {
621 return filenameToTileSpecDefault(filename);
622 }
623
directory() const624 QString QGeoFileTileCache::directory() const
625 {
626 return directory_;
627 }
628
629 QT_END_NAMESPACE
630