startForeground method

  1. @useResult
Stream<DownloadProgress> startForeground({
  1. required DownloadableRegion<BaseRegion> region,
  2. int parallelThreads = 5,
  3. int maxBufferLength = 200,
  4. bool skipExistingTiles = false,
  5. bool skipSeaTiles = true,
  6. int? rateLimit,
  7. Duration? maxReportInterval = const Duration(seconds: 1),
  8. bool disableRecovery = false,
  9. List<String>? obscuredQueryParams,
  10. 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 run
  • maxBufferLength (defaults to 200 | 0 to disable): number of tiles to temporarily buffer before writing to the store (split evenly between parallelThreads)
  • skipExistingTiles (defaults to false): whether to skip downloading tiles that are already cached
  • skipSeaTiles (defaults to true): 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();
}