Files
MapLibreParticleLab/platform/darwin/src/MLNOfflinePack.mm
Bart Louwers 2a8ebc4906 core: rename mbgl directories to mln (#4511)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-08-22 20:50:14 +02:00

364 lines
13 KiB
Plaintext

#import "MLNOfflinePack_Private.h"
#import "MLNLoggingConfiguration_Private.h"
#import "MLNOfflineRegion_Private.h"
#import "MLNOfflineStorage_Private.h"
#import "MLNShapeOfflineRegion.h"
#import "MLNShapeOfflineRegion_Private.h"
#import "MLNTilePyramidOfflineRegion.h"
#import "MLNTilePyramidOfflineRegion_Private.h"
#import "NSValue+MLNAdditions.h"
#include <mln/map/map_options.hpp>
#include <mln/storage/database_file_source.hpp>
#include <mln/util/variant.hpp>
const MLNExceptionName MLNInvalidOfflinePackException = @"MLNInvalidOfflinePackException";
/**
Assert that the current offline pack is valid.
This macro should be used at the beginning of any public-facing instance method
of ``MLNOfflinePack``. For private methods, an assertion is more appropriate.
*/
#define MLNAssertOfflinePackIsValid() \
do { \
if (_state == MLNOfflinePackStateInvalid) { \
[NSException \
raise:MLNInvalidOfflinePackException \
format:@"-[MLNOfflineStorage removePack:withCompletionHandler:] has been called " \
@"on this instance of MLNOfflinePack, rendering it invalid. It is an " \
@"error to send any message to this pack."]; \
} \
} while (NO);
@interface MLNTilePyramidOfflineRegion () <MLNOfflineRegion_Private,
MLNTilePyramidOfflineRegion_Private>
@end
@interface MLNShapeOfflineRegion () <MLNOfflineRegion_Private, MLNShapeOfflineRegion_Private>
@end
class MBGLOfflineRegionObserver : public mln::OfflineRegionObserver {
public:
MBGLOfflineRegionObserver(MLNOfflinePack *pack_) : pack(pack_) {}
void statusChanged(mln::OfflineRegionStatus status) override;
void responseError(mln::Response::Error error) override;
void mapboxTileCountLimitExceeded(uint64_t limit) override;
private:
__weak MLNOfflinePack *pack = nullptr;
};
@interface MLNOfflinePack ()
@property (nonatomic, nullable, readwrite) mln::OfflineRegion *mbglOfflineRegion;
@property (nonatomic, readwrite) MLNOfflinePackProgress progress;
@end
@implementation MLNOfflinePack {
BOOL _isSuspending;
std::shared_ptr<mln::DatabaseFileSource> _mbglDatabaseFileSource;
}
- (instancetype)init {
MLNLogInfo(@"Calling this initializer is not allowed.");
if (self = [super init]) {
_state = MLNOfflinePackStateInvalid;
NSLog(@"%s called; did you mean to call +[MLNOfflineStorage "
@"addPackForRegion:withContext:completionHandler:] instead?",
__PRETTY_FUNCTION__);
}
return self;
}
- (instancetype)initWithMBGLRegion:(mln::OfflineRegion *)region {
if (self = [super init]) {
_mbglOfflineRegion = region;
_state = MLNOfflinePackStateUnknown;
_mbglDatabaseFileSource = [[MLNOfflineStorage sharedOfflineStorage] mbglDatabaseFileSource];
_mbglDatabaseFileSource->setOfflineRegionObserver(
*_mbglOfflineRegion, std::make_unique<MBGLOfflineRegionObserver>(self));
}
return self;
}
- (void)dealloc {
MLNAssert(_state == MLNOfflinePackStateInvalid,
@"MLNOfflinePack was not invalided prior to deallocation.");
}
- (nullable NSNumber *)regionId {
if (_state == MLNOfflinePackStateInvalid || !_mbglOfflineRegion) {
return nil;
}
return @(_mbglOfflineRegion->getID());
}
- (id<MLNOfflineRegion>)region {
MLNAssertOfflinePackIsValid();
const mln::OfflineRegionDefinition &regionDefinition = _mbglOfflineRegion->getDefinition();
MLNAssert([MLNTilePyramidOfflineRegion conformsToProtocol:@protocol(MLNOfflineRegion_Private)],
@"MLNTilePyramidOfflineRegion should conform to MLNOfflineRegion_Private.");
MLNAssert([MLNShapeOfflineRegion conformsToProtocol:@protocol(MLNOfflineRegion_Private)],
@"MLNShapeOfflineRegion should conform to MLNOfflineRegion_Private.");
return std::visit(
mln::overloaded{[&](const mln::OfflineTilePyramidRegionDefinition def) {
return (id<MLNOfflineRegion>)[[MLNTilePyramidOfflineRegion alloc]
initWithOfflineRegionDefinition:def];
},
[&](const mln::OfflineGeometryRegionDefinition &def) {
return (id<MLNOfflineRegion>)[[MLNShapeOfflineRegion alloc]
initWithOfflineRegionDefinition:def];
}},
regionDefinition);
}
- (NSData *)context {
MLNAssertOfflinePackIsValid();
const mln::OfflineRegionMetadata &metadata = _mbglOfflineRegion->getMetadata();
return [NSData dataWithBytes:&metadata[0] length:metadata.size()];
}
- (void)setContext:(NSData *)context
completionHandler:(void (^_Nullable)(NSError *_Nullable error))completion {
MLNAssertOfflinePackIsValid();
mln::OfflineRegionMetadata metadata(context.length);
[context getBytes:&metadata[0] length:metadata.size()];
[self willChangeValueForKey:@"context"];
__weak MLNOfflinePack *weakSelf = self;
_mbglDatabaseFileSource->updateOfflineMetadata(
_mbglOfflineRegion->getID(), metadata,
[&, completion, weakSelf](
mln::expected<mln::OfflineRegionMetadata, std::exception_ptr> mbglOfflineRegionMetadata) {
NSError *error;
if (!mbglOfflineRegionMetadata) {
NSString *errorDescription =
@(mln::util::toString(mbglOfflineRegionMetadata.error()).c_str());
error = [NSError errorWithDomain:MLNErrorDomain
code:MLNErrorCodeModifyingOfflineStorageFailed
userInfo:errorDescription ? @{
NSLocalizedDescriptionKey : errorDescription,
}
: nil];
}
dispatch_async(dispatch_get_main_queue(), [&, completion, weakSelf, error](void) {
[weakSelf reloadWithCompletionHandler:^(NSError *_Nullable reloadingError) {
MLNOfflinePack *strongSelf = weakSelf;
[strongSelf didChangeValueForKey:@"context"];
if (completion) {
completion(error ?: reloadingError);
}
}];
});
});
}
- (void)reloadWithCompletionHandler:(void (^)(NSError *_Nullable error))completion {
auto regionID = _mbglOfflineRegion->getID();
MLNOfflineStorage *sharedOfflineStorage = [MLNOfflineStorage sharedOfflineStorage];
__weak MLNOfflinePack *weakSelf = self;
[sharedOfflineStorage getPacksWithCompletionHandler:^(NSArray<MLNOfflinePack *> *packs,
__unused NSError *_Nullable error) {
MLNOfflinePack *strongSelf = weakSelf;
for (MLNOfflinePack *pack in packs) {
if (pack.mbglOfflineRegion->getID() == regionID) {
strongSelf.mbglOfflineRegion = pack.mbglOfflineRegion;
}
[pack invalidate];
}
completion(error);
}];
}
- (void)resume {
MLNLogInfo(@"Resuming pack download.");
MLNAssertOfflinePackIsValid();
self.state = MLNOfflinePackStateActive;
_mbglDatabaseFileSource->setOfflineRegionDownloadState(*_mbglOfflineRegion,
mln::OfflineRegionDownloadState::Active);
}
- (void)suspend {
MLNLogInfo(@"Suspending pack download.");
MLNAssertOfflinePackIsValid();
if (self.state == MLNOfflinePackStateActive) {
self.state = MLNOfflinePackStateInactive;
_isSuspending = YES;
}
_mbglDatabaseFileSource->setOfflineRegionDownloadState(*_mbglOfflineRegion,
mln::OfflineRegionDownloadState::Inactive);
}
- (void)invalidate {
MLNLogInfo(@"Invalidating pack.");
MLNAssert(_state != MLNOfflinePackStateInvalid,
@"Cannot invalidate an already invalid offline pack.");
MLNAssert(self.mbglOfflineRegion, @"Should have a valid region");
@synchronized(self) {
self.state = MLNOfflinePackStateInvalid;
if (self.mbglOfflineRegion) {
_mbglDatabaseFileSource->setOfflineRegionObserver(*self.mbglOfflineRegion, nullptr);
}
self.mbglOfflineRegion = nil;
}
}
- (void)setState:(MLNOfflinePackState)state {
MLNLogDebug(@"Setting state: %ld", (long)state);
if (!self.mbglOfflineRegion) {
// A progress update has arrived after the call to
// -[MLNOfflineStorage removePack:withCompletionHandler:] but before the
// removal is complete and the completion handler is called.
MLNAssert(_state == MLNOfflinePackStateInvalid,
@"A valid MLNOfflinePack has no mln::OfflineRegion.");
return;
}
MLNAssert(_state != MLNOfflinePackStateInvalid,
@"Cannot change the state of an invalid offline pack.");
if (!_isSuspending || state != MLNOfflinePackStateActive) {
_isSuspending = NO;
_state = state;
}
}
- (void)requestProgress {
MLNLogInfo(@"Requesting pack progress.");
MLNAssertOfflinePackIsValid();
__weak MLNOfflinePack *weakSelf = self;
_mbglDatabaseFileSource->getOfflineRegionStatus(
*_mbglOfflineRegion,
[&, weakSelf](mln::expected<mln::OfflineRegionStatus, std::exception_ptr> status) {
if (status) {
mln::OfflineRegionStatus checkedStatus = *status;
dispatch_async(dispatch_get_main_queue(), ^{
MLNOfflinePack *strongSelf = weakSelf;
[strongSelf offlineRegionStatusDidChange:checkedStatus];
});
}
});
}
- (void)offlineRegionStatusDidChange:(mln::OfflineRegionStatus)status {
MLNAssert(_state != MLNOfflinePackStateInvalid,
@"Cannot change update progress of an invalid offline pack.");
switch (status.downloadState) {
case mln::OfflineRegionDownloadState::Inactive:
self.state = status.complete() ? MLNOfflinePackStateComplete : MLNOfflinePackStateInactive;
break;
case mln::OfflineRegionDownloadState::Active:
self.state = MLNOfflinePackStateActive;
break;
}
if (_isSuspending) {
return;
}
MLNOfflinePackProgress progress;
progress.countOfResourcesCompleted = status.completedResourceCount;
progress.countOfBytesCompleted = status.completedResourceSize;
progress.countOfTilesCompleted = status.completedTileCount;
progress.countOfTileBytesCompleted = status.completedTileSize;
progress.countOfResourcesExpected = status.requiredResourceCount;
progress.maximumResourcesExpected =
status.requiredResourceCountIsPrecise ? status.requiredResourceCount : UINT64_MAX;
self.progress = progress;
NSDictionary *userInfo = @{
MLNOfflinePackUserInfoKeyState : @(self.state),
MLNOfflinePackUserInfoKeyProgress : [NSValue valueWithMLNOfflinePackProgress:progress]
};
NSNotificationCenter *noteCenter = [NSNotificationCenter defaultCenter];
[noteCenter postNotificationName:MLNOfflinePackProgressChangedNotification
object:self
userInfo:userInfo];
}
- (void)didReceiveError:(NSError *)error {
MLNLogError(@"Error: %@", error.localizedDescription);
MLNLogInfo(@"Notifying about pack error.");
NSDictionary *userInfo = @{MLNOfflinePackUserInfoKeyError : error};
NSNotificationCenter *noteCenter = [NSNotificationCenter defaultCenter];
[noteCenter postNotificationName:MLNOfflinePackErrorNotification object:self userInfo:userInfo];
}
- (void)didReceiveMaximumAllowedMapboxTiles:(uint64_t)limit {
MLNLogInfo(@"Notifying reached maximum allowed Mapbox tiles: %lu", (unsigned long)limit);
NSDictionary *userInfo = @{MLNOfflinePackUserInfoKeyMaximumCount : @(limit)};
NSNotificationCenter *noteCenter = [NSNotificationCenter defaultCenter];
[noteCenter postNotificationName:MLNOfflinePackMaximumMapboxTilesReachedNotification
object:self
userInfo:userInfo];
}
NSError *MLNErrorFromResponseError(mln::Response::Error error) {
NSInteger errorCode = MLNErrorCodeUnknown;
switch (error.reason) {
case mln::Response::Error::Reason::NotFound:
errorCode = MLNErrorCodeNotFound;
break;
case mln::Response::Error::Reason::Server:
errorCode = MLNErrorCodeBadServerResponse;
break;
case mln::Response::Error::Reason::Connection:
errorCode = MLNErrorCodeConnectionFailed;
break;
default:
break;
}
return [NSError errorWithDomain:MLNErrorDomain
code:errorCode
userInfo:@{
NSLocalizedFailureReasonErrorKey : @(error.message.c_str())
}];
}
@end
void MBGLOfflineRegionObserver::statusChanged(mln::OfflineRegionStatus status) {
__weak MLNOfflinePack *weakPack = pack;
dispatch_async(dispatch_get_main_queue(), ^{
[weakPack offlineRegionStatusDidChange:status];
});
}
void MBGLOfflineRegionObserver::responseError(mln::Response::Error error) {
__weak MLNOfflinePack *weakPack = pack;
dispatch_async(dispatch_get_main_queue(), ^{
[weakPack didReceiveError:MLNErrorFromResponseError(error)];
});
}
void MBGLOfflineRegionObserver::mapboxTileCountLimitExceeded(uint64_t limit) {
__weak MLNOfflinePack *weakPack = pack;
dispatch_async(dispatch_get_main_queue(), ^{
[weakPack didReceiveMaximumAllowedMapboxTiles:limit];
});
}