startForeground method
- @useResult
- required DownloadableRegion<
BaseRegion> region, - int parallelThreads = 5,
- int maxBufferLength = 200,
- bool skipExistingTiles = false,
- bool skipSeaTiles = true,
- int? rateLimit,
- Duration? maxReportInterval = const Duration(seconds: 1),
- bool disableRecovery = false,
- List<
String> ? obscuredQueryParams, - Object instanceId = 0,
Download a specified DownloadableRegion in the foreground, with a recovery session by default
Tip
To check the number of tiles in a region before starting a download, use check.
Streams a DownloadProgress
object containing statistics and information
about the download's progression status, once per tile and at intervals
of no longer than maxReportInterval
(after the first tile).
There are multiple options available to improve the speed of the download. These are ordered from most impact to least impact.
parallelThreads
(defaults to 5 | 1 to disable): number of simultaneous download threads to runmaxBufferLength
(defaults to 200 | 0 to disable): number of tiles to temporarily buffer before writing to the store (split evenly betweenparallelThreads
)skipExistingTiles
(defaults tofalse
): whether to skip downloading tiles that are already cachedskipSeaTiles
(defaults totrue
): whether to skip caching tiles that are entirely sea (based on a comparison to the tile at x0,y0,z17)
Warning
Using too many parallel threads may place significant strain on the tile server, so check your tile server's ToS for more information.
Warning
Using buffering will mean that an unexpected forceful quit (such as an app closure, cancel is safe) will result in losing the tiles that are currently in the buffer. It will also increase the memory (RAM) required.
Warning
Skipping sea tiles will not reduce the number of downloads - tiles must be downloaded to be compared against the sample sea tile. It is only designed to reduce the storage capacity consumed.
Although disabled null
by default, rateLimit
can be used to impose a
limit on the maximum number of tiles that can be attempted per second. This
is useful to avoid placing too much strain on tile servers and avoid
external rate limiting. Note that the rateLimit
is only approximate. Also
note that all tile attempts are rate limited, even ones that do not need a
server request.
To check whether the current DownloadProgress.tilesPerSecond statistic is
currently limited by rateLimit
, check
DownloadProgress.isTPSArtificiallyCapped.
A fresh DownloadProgress
event will always be emitted every
maxReportInterval
(if specified), which defaults to every 1 second,
regardless of whether any more tiles have been attempted/downloaded/failed.
This is to enable the DownloadProgress.elapsedDuration to be accurately
presented to the end user.
Tip
When tracking TileEvents across multiple DownloadProgress
events,
extra considerations are necessary. See
the documentation
for more information.
When this download is started, assuming disableRecovery
is false
(as
default), the recovery system will register this download, to allow it to
be recovered if it unexpectedly fails.
For more info, see RootRecovery.
For information about obscuredQueryParams
, see the
online documentation.
Will default to the value in the default FMTCTileProviderSettings.
To set additional headers, set it via TileProvider.headers
when
constructing the DownloadableRegion.
By default, only one download is allowed at any one time.
However, if necessary, multiple can be started by setting methods'
instanceId
argument to a unique value on methods. Whatever object
instanceId
is, it must have a valid and useful equality and hashCode
implementation, as it is used as the key in a Map
. Note that this unique
value must be known and remembered to control the state of the download.
Warning
Starting multiple simultaneous downloads may lead to a noticeable performance loss. Ensure you thoroughly test and profile your application.
Implementation
@useResult
Stream<DownloadProgress> startForeground({
required DownloadableRegion region,
int parallelThreads = 5,
int maxBufferLength = 200,
bool skipExistingTiles = false,
bool skipSeaTiles = true,
int? rateLimit,
Duration? maxReportInterval = const Duration(seconds: 1),
bool disableRecovery = false,
List<String>? obscuredQueryParams,
Object instanceId = 0,
}) async* {
FMTCBackendAccess.internal; // Verify intialisation
// Check input arguments for suitability
if (!(region.options.wmsOptions != null ||
region.options.urlTemplate != null)) {
throw ArgumentError(
"`.toDownloadable`'s `TileLayer` argument must specify an appropriate `urlTemplate` or `wmsOptions`",
'region.options.urlTemplate',
);
}
if (parallelThreads < 1) {
throw ArgumentError.value(
parallelThreads,
'parallelThreads',
'must be 1 or greater',
);
}
if (maxBufferLength < 0) {
throw ArgumentError.value(
maxBufferLength,
'maxBufferLength',
'must be 0 or greater',
);
}
if ((rateLimit ?? 2) < 1) {
throw ArgumentError.value(
rateLimit,
'rateLimit',
'must be 1 or greater, or null',
);
}
// Create download instance
final instance = DownloadInstance.registerIfAvailable(instanceId);
if (instance == null) {
throw StateError(
'A download instance with ID $instanceId already exists\nTo start '
'another download simultaneously, use a unique `instanceId`. Read the '
'documentation for additional considerations that should be taken.',
);
}
// Generate recovery ID (unless disabled)
final recoveryId = disableRecovery
? null
: Object.hash(instanceId, DateTime.timestamp().millisecondsSinceEpoch);
// Start download thread
final receivePort = ReceivePort();
await Isolate.spawn(
_downloadManager,
(
sendPort: receivePort.sendPort,
region: region,
storeName: _storeName,
parallelThreads: parallelThreads,
maxBufferLength: maxBufferLength,
skipExistingTiles: skipExistingTiles,
skipSeaTiles: skipSeaTiles,
maxReportInterval: maxReportInterval,
rateLimit: rateLimit,
obscuredQueryParams:
obscuredQueryParams?.map((e) => RegExp('$e=[^&]*')).toList() ??
FMTCTileProviderSettings.instance.obscuredQueryParams.toList(),
recoveryId: recoveryId,
backend: FMTCBackendAccessThreadSafe.internal,
),
onExit: receivePort.sendPort,
debugName: '[FMTC] Master Bulk Download Thread',
);
// Setup control mechanisms (completers)
final cancelCompleter = Completer<void>();
Completer<void>? pauseCompleter;
await for (final evt in receivePort) {
// Handle new progress message
if (evt is DownloadProgress) {
yield evt;
continue;
}
// Handle pause comms
if (evt == 1) {
pauseCompleter?.complete();
continue;
}
// Handle shutdown (both normal and cancellation)
if (evt == null) break;
// Handle recovery system startup (unless disabled)
if (evt == 2) {
FMTCRoot.recovery._downloadsOngoing.add(recoveryId!);
continue;
}
// Setup control mechanisms (senders)
if (evt is SendPort) {
instance
..requestCancel = () {
evt.send(null);
return cancelCompleter.future;
}
..requestPause = () {
evt.send(1);
return (pauseCompleter = Completer()).future
..then((_) => instance.isPaused = true);
}
..requestResume = () {
evt.send(2);
instance.isPaused = false;
};
continue;
}
throw UnimplementedError('Unrecognised message');
}
// Handle shutdown (both normal and cancellation)
receivePort.close();
if (recoveryId != null) await FMTCRoot.recovery.cancel(recoveryId);
DownloadInstance.unregister(instanceId);
cancelCompleter.complete();
}