From e66fd54330370c7aea0c9b856d39b0797f89e0f1 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sat, 6 May 2023 09:57:14 +0100 Subject: [PATCH 001/168] Added `StoreManagement.pruneTilesOlderThan` method (#122) Fixed bug in example application Former-commit-id: 83341561675377810417b7bdec57f12b29dde04f [formerly 8c78247750b239c93115182b0f59974a705ae211] Former-commit-id: adcba4ea7a825a2f96decf4fd293b4c5079ce135 --- .../lib/screens/main/pages/map/map_view.dart | 8 ++--- lib/src/store/manage.dart | 30 +++++++++++++++++++ 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/example/lib/screens/main/pages/map/map_view.dart b/example/lib/screens/main/pages/map/map_view.dart index bb281017..8e2eed54 100644 --- a/example/lib/screens/main/pages/map/map_view.dart +++ b/example/lib/screens/main/pages/map/map_view.dart @@ -45,10 +45,10 @@ class _MapPageState extends State { zoom: 9.2, maxZoom: 22, maxBounds: LatLngBounds.fromPoints([ - LatLng(51.50440309992153, -0.7140577160848564), - LatLng(51.50359743451854, -0.5780629452415917), - LatLng(51.536279957688045, -0.5727737156917649), - LatLng(-51.53318462807709, -0.7227703206129361), + LatLng(-90, 180), + LatLng(90, 180), + LatLng(90, -180), + LatLng(-90, -180), ]), interactiveFlags: InteractiveFlag.all & ~InteractiveFlag.rotate, scrollWheelVelocity: 0.002, diff --git a/lib/src/store/manage.dart b/lib/src/store/manage.dart index e9966997..4ae61cc3 100644 --- a/lib/src/store/manage.dart +++ b/lib/src/store/manage.dart @@ -168,6 +168,14 @@ class StoreManagement { return newStore; } + /// Delete all tiles older that were last modified before [expiry] + /// + /// Ignores [FMTCTileProviderSettings.cachedValidDuration]. + Future pruneTilesOlderThan({required DateTime expiry}) => compute( + _pruneTilesOlderThanWorker, + [_name, _rootDirectory.absolute.path, expiry], + ); + /// Retrieves the most recently modified tile from the store, extracts it's /// bytes, and renders them to an [Image] /// @@ -297,3 +305,25 @@ class StoreManagement { ); } } + +Future _pruneTilesOlderThanWorker(List args) async { + final db = Isar.openSync( + [DbStoreDescriptorSchema, DbTileSchema, DbMetadataSchema], + name: DatabaseTools.hash(args[0]).toString(), + directory: args[1], + inspector: false, + ); + + db.writeTxnSync( + () => db.tiles.deleteAllSync( + db.tiles + .where() + .lastModifiedLessThan(args[2]) + .findAllSync() + .map((t) => t.id) + .toList(), + ), + ); + + await db.close(); +} From ed1f4883f5377c04b7a461eb06efb0bf4933fe15 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 6 May 2023 09:16:30 +0000 Subject: [PATCH 002/168] Built Example Applications Former-commit-id: 3d05d394db3ed02b35e3a10b6d09d38f890fd2f5 [formerly 2320595f3b28ab1a1aea03ff438ee93a12a7239d] Former-commit-id: f60e504decebdbcc85fca0ecd59308b70aa8da21 --- .../AndroidApplication.apk.REMOVED.git-id | 2 +- .../WindowsApplication.exe.REMOVED.git-id | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/prebuiltExampleApplications/AndroidApplication.apk.REMOVED.git-id b/prebuiltExampleApplications/AndroidApplication.apk.REMOVED.git-id index 75df540e..6d278680 100644 --- a/prebuiltExampleApplications/AndroidApplication.apk.REMOVED.git-id +++ b/prebuiltExampleApplications/AndroidApplication.apk.REMOVED.git-id @@ -1 +1 @@ -65b3c971c8dbaf56458d6cb1c7ac4a1304c968f9 \ No newline at end of file +a9723446c34253b0ebdb40ba45b37d41a3fe4bbc \ No newline at end of file diff --git a/prebuiltExampleApplications/WindowsApplication.exe.REMOVED.git-id b/prebuiltExampleApplications/WindowsApplication.exe.REMOVED.git-id index f2f46f04..72661659 100644 --- a/prebuiltExampleApplications/WindowsApplication.exe.REMOVED.git-id +++ b/prebuiltExampleApplications/WindowsApplication.exe.REMOVED.git-id @@ -1 +1 @@ -7ef87e80a133b049323a587b08c80813d2b98a97 \ No newline at end of file +b3664d65f4f2afecaced899eb762379e674a274e \ No newline at end of file From 15415f32a1f095b844eba8f8525b5ec95fd81f7f Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sun, 7 May 2023 15:30:24 +0100 Subject: [PATCH 003/168] Minor refactoring Updated CHANGELOG Bumped version to 8.1.0 Removed auto-updater from example app Bumped workflow dependency versions Removed unused files Former-commit-id: 1128b3e9c9e77a19e9b6eef34586e270295e9c95 [formerly 9ee0a99d7255fc340413794308d104e8c7731576] Former-commit-id: 9e8f7782723a7db240b6775dae05eaf066af629e --- .github/workflows/main.yml | 8 +- CHANGELOG.md | 9 +- dartdoc_options.yaml | 2 - example/android/app/build.gradle | 2 +- example/androidBuilder.bat | 16 --- example/currentAppVersion.txt | 1 - example/lib/screens/main/main.dart | 10 -- .../settingsAndAbout/settings_and_about.dart | 2 +- .../update/components/failed_to_check.dart | 26 ---- .../main/pages/update/components/header.dart | 20 --- .../pages/update/components/up_to_date.dart | 30 ----- .../update/components/update_available.dart | 48 ------- .../lib/screens/main/pages/update/update.dart | 122 ------------------ example/pubspec.yaml | 3 +- example/windowsBuilder.bat | 4 - lib/src/db/tools.dart | 1 + lib/src/providers/image_provider.dart | 113 ++++++++-------- pubspec.yaml | 2 +- windowsApplicationInstallerSetup.iss | 2 +- 19 files changed, 72 insertions(+), 349 deletions(-) delete mode 100644 dartdoc_options.yaml delete mode 100644 example/androidBuilder.bat delete mode 100644 example/currentAppVersion.txt delete mode 100644 example/lib/screens/main/pages/update/components/failed_to_check.dart delete mode 100644 example/lib/screens/main/pages/update/components/header.dart delete mode 100644 example/lib/screens/main/pages/update/components/up_to_date.dart delete mode 100644 example/lib/screens/main/pages/update/components/update_available.dart delete mode 100644 example/lib/screens/main/pages/update/update.dart delete mode 100644 example/windowsBuilder.bat diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 08c068e6..d2c0ac5f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,7 +10,7 @@ jobs: - name: Checkout Repository uses: actions/checkout@v3 - name: Run Dart Package Analyser - uses: axel-op/dart-package-analyzer@v3 + uses: axel-op/dart-package-analyzer@master id: analysis with: githubToken: ${{ secrets.GITHUB_TOKEN }} @@ -33,7 +33,7 @@ jobs: - name: Checkout Repository uses: actions/checkout@v3 - name: Setup Flutter Environment - uses: subosito/flutter-action@v2 + uses: subosito/flutter-action@main with: channel: "stable" - name: Get All Dependencies @@ -60,7 +60,7 @@ jobs: distribution: "temurin" java-version: "17" - name: Setup Flutter Environment - uses: subosito/flutter-action@v2 + uses: subosito/flutter-action@main with: channel: "stable" - name: Remove Existing Prebuilt Applications @@ -82,7 +82,7 @@ jobs: run: iscc "windowsApplicationInstallerSetup.iss" working-directory: . - name: Commit Output Directory - uses: EndBug/add-and-commit@v9.0.1 + uses: EndBug/add-and-commit@main with: message: "Built Example Applications" add: "prebuiltExampleApplications/" diff --git a/CHANGELOG.md b/CHANGELOG.md index 42db36b3..b32cea4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,13 +7,18 @@ Many thanks to my sponsors, no matter how much or how little they donated. Spons * @huulbaek * @andrewames * @ozzy1873 -* @mohammedX6 +* @eidolonFIRE * @weishuhn +* @mohammedX6 * and 3 anonymous or private donors # Changelog -## [8.0.0] - 2023/XX/XX +## [8.1.0] - 2023/05/XX + +* Added `StoreManagement.pruneTilesOlderThan` method + +## [8.0.0] - 2023/05/05 * Bulk downloading has been rewritten to use a new implementation that generates tile coordinates at the same time as downloading tiles * `check`ing the number of tiles in a region now uses a significantly faster and more efficient implementation diff --git a/dartdoc_options.yaml b/dartdoc_options.yaml deleted file mode 100644 index b795b9fe..00000000 --- a/dartdoc_options.yaml +++ /dev/null @@ -1,2 +0,0 @@ -dartdoc: - favicon: 'example/assets/icons/ProjectIcon.png' \ No newline at end of file diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index d7f28791..072775e5 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -18,7 +18,7 @@ if (flutterVersionCode == null) { def flutterVersionName = localProperties.getProperty('flutter.versionName') if (flutterVersionName == null) { - flutterVersionName = '8.0.0' + flutterVersionName = '8.1.0' } apply plugin: 'com.android.application' diff --git a/example/androidBuilder.bat b/example/androidBuilder.bat deleted file mode 100644 index eeaf794f..00000000 --- a/example/androidBuilder.bat +++ /dev/null @@ -1,16 +0,0 @@ -@ECHO OFF - -flutter clean | more -flutter build apk --split-per-abi --obfuscate --split-debug-info=/symbols | more -goto :choice - -:choice -set /P c=Install ('app-armeabi-v7a-release.apk') over active ADB connection [Y/N]? -if /I "%c%" EQU "Y" goto :install -if /I "%c%" EQU "N" EXIT /B -goto :choice - - -:install -adb install build\app\outputs\flutter-apk\app-armeabi-v7a-release.apk -EXIT /B \ No newline at end of file diff --git a/example/currentAppVersion.txt b/example/currentAppVersion.txt deleted file mode 100644 index fa5fce04..00000000 --- a/example/currentAppVersion.txt +++ /dev/null @@ -1 +0,0 @@ -8.0.0 \ No newline at end of file diff --git a/example/lib/screens/main/main.dart b/example/lib/screens/main/main.dart index b5e9601a..2a447f49 100644 --- a/example/lib/screens/main/main.dart +++ b/example/lib/screens/main/main.dart @@ -1,5 +1,3 @@ -import 'dart:io'; - import 'package:badges/badges.dart'; import 'package:flutter/material.dart' hide Badge; import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; @@ -13,7 +11,6 @@ import 'pages/map/map_view.dart'; import 'pages/recovery/recovery.dart'; import 'pages/settingsAndAbout/settings_and_about.dart'; import 'pages/stores/stores.dart'; -import 'pages/update/update.dart'; class MainScreen extends StatefulWidget { const MainScreen({ @@ -28,7 +25,6 @@ class MainScreen extends StatefulWidget { } class _MainScreenState extends State { - //static const Color backgroundColor = Color(0xFFeaf6f5); late final PageController _pageController; int _currentPageIndex = 0; bool extended = false; @@ -70,11 +66,6 @@ class _MainScreenState extends State { icon: Icon(Icons.settings), label: 'Settings', ), - if (Platform.isWindows || Platform.isAndroid) - const NavigationDestination( - icon: Icon(Icons.update), - label: 'Update', - ), ]; List get _pages => [ @@ -87,7 +78,6 @@ class _MainScreenState extends State { ), RecoveryPage(moveToDownloadPage: () => _onDestinationSelected(2)), const SettingsAndAboutPage(), - if (Platform.isWindows || Platform.isAndroid) const UpdatePage(), ]; void _onDestinationSelected(int index) { diff --git a/example/lib/screens/main/pages/settingsAndAbout/settings_and_about.dart b/example/lib/screens/main/pages/settingsAndAbout/settings_and_about.dart index 0b5693d0..0f2a872f 100644 --- a/example/lib/screens/main/pages/settingsAndAbout/settings_and_about.dart +++ b/example/lib/screens/main/pages/settingsAndAbout/settings_and_about.dart @@ -80,7 +80,7 @@ class _SettingsAndAboutPageState extends State { context: context, applicationName: 'FMTC Demo', applicationVersion: - 'for v8.0.0\n(on ${Platform().operatingSystemFormatted})', + 'for v8.1.0\n(on ${Platform().operatingSystemFormatted})', applicationIcon: Image.asset( 'assets/icons/ProjectIcon.png', height: 48, diff --git a/example/lib/screens/main/pages/update/components/failed_to_check.dart b/example/lib/screens/main/pages/update/components/failed_to_check.dart deleted file mode 100644 index 60b762ba..00000000 --- a/example/lib/screens/main/pages/update/components/failed_to_check.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:flutter/material.dart'; - -class FailedToCheck extends StatelessWidget { - const FailedToCheck({ - super.key, - }); - - @override - Widget build(BuildContext context) => Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: const [ - Icon( - Icons.error, - size: 38, - color: Colors.red, - ), - SizedBox(height: 10), - Text( - 'Failed To Check For Updates\nThe remote URL could not be reached', - textAlign: TextAlign.center, - ), - ], - ), - ); -} diff --git a/example/lib/screens/main/pages/update/components/header.dart b/example/lib/screens/main/pages/update/components/header.dart deleted file mode 100644 index b3efb980..00000000 --- a/example/lib/screens/main/pages/update/components/header.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:google_fonts/google_fonts.dart'; - -class Header extends StatelessWidget { - const Header({ - super.key, - required this.title, - }); - - final String title; - - @override - Widget build(BuildContext context) => Text( - title, - style: GoogleFonts.openSans( - fontWeight: FontWeight.bold, - fontSize: 24, - ), - ); -} diff --git a/example/lib/screens/main/pages/update/components/up_to_date.dart b/example/lib/screens/main/pages/update/components/up_to_date.dart deleted file mode 100644 index 8cbf5c91..00000000 --- a/example/lib/screens/main/pages/update/components/up_to_date.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:flutter/material.dart'; - -class UpToDate extends StatelessWidget { - const UpToDate({ - super.key, - }); - - @override - Widget build(BuildContext context) => Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: const [ - Icon( - Icons.done, - size: 38, - ), - SizedBox(height: 10), - Text( - 'Up To Date', - textAlign: TextAlign.center, - ), - Text( - "with the latest example app from the 'main' branch", - textAlign: TextAlign.center, - style: TextStyle(fontStyle: FontStyle.italic), - ), - ], - ), - ); -} diff --git a/example/lib/screens/main/pages/update/components/update_available.dart b/example/lib/screens/main/pages/update/components/update_available.dart deleted file mode 100644 index 6669e0f9..00000000 --- a/example/lib/screens/main/pages/update/components/update_available.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:google_fonts/google_fonts.dart'; - -Center buildUpdateAvailableWidget({ - required BuildContext context, - required String availableVersion, - required String currentVersion, - required void Function() updateApplication, -}) => - Center( - child: Flex( - mainAxisAlignment: MainAxisAlignment.center, - direction: MediaQuery.of(context).size.width < 400 - ? Axis.vertical - : Axis.horizontal, - children: [ - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - 'New Version Available!', - style: GoogleFonts.openSans( - fontWeight: FontWeight.bold, - fontSize: 24, - ), - textAlign: TextAlign.center, - ), - Text( - '$availableVersion > $currentVersion', - style: const TextStyle(fontSize: 20), - ), - ], - ), - const SizedBox( - width: 30, - height: 12, - ), - IconButton( - iconSize: 38, - icon: const Icon( - Icons.download, - color: Colors.green, - ), - onPressed: updateApplication, - ), - ], - ), - ); diff --git a/example/lib/screens/main/pages/update/update.dart b/example/lib/screens/main/pages/update/update.dart deleted file mode 100644 index c514b5e1..00000000 --- a/example/lib/screens/main/pages/update/update.dart +++ /dev/null @@ -1,122 +0,0 @@ -import 'dart:io'; - -import 'package:better_open_file/better_open_file.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart' show rootBundle; -import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; -import 'package:http/http.dart' as http; -import 'package:path/path.dart' as p; -import 'package:version/version.dart'; - -import '../../../../shared/components/loading_indicator.dart'; -import 'components/failed_to_check.dart'; -import 'components/header.dart'; -import 'components/up_to_date.dart'; -import 'components/update_available.dart'; - -class UpdatePage extends StatefulWidget { - const UpdatePage({super.key}); - - @override - State createState() => _UpdatePageState(); -} - -class _UpdatePageState extends State { - static const String versionURL = - 'https://raw.githubusercontent.com/JaffaKetchup/flutter_map_tile_caching/main/example/currentAppVersion.txt'; - static const String windowsURL = - 'https://github.com/JaffaKetchup/flutter_map_tile_caching/blob/main/prebuiltExampleApplications/WindowsApplication.exe?raw=true'; - static const String androidURL = - 'https://github.com/JaffaKetchup/flutter_map_tile_caching/blob/main/prebuiltExampleApplications/AndroidApplication.apk?raw=true'; - - bool updating = false; - - Future updateApplication() async { - setState(() => updating = true); - - final http.Response response = await http.get( - Uri.parse(Platform.isWindows ? windowsURL : androidURL), - ); - final File file = File( - p.join( - // ignore: invalid_use_of_internal_member, invalid_use_of_protected_member - FMTC.instance.rootDirectory.directory.absolute.path, - 'newAppVersion.${Platform.isWindows ? 'exe' : 'apk'}', - ), - ); - - await file.create(); - await file.writeAsBytes(response.bodyBytes); - - if (Platform.isWindows) { - await Process.start(file.absolute.path, []); - } else { - await OpenFile.open(file.absolute.path); - } - } - - @override - Widget build(BuildContext context) => Scaffold( - body: SafeArea( - child: Padding( - padding: const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Header(title: 'Update App'), - const SizedBox(height: 12), - Expanded( - child: updating - ? const LoadingIndicator( - message: - 'Downloading New Application Installer...\nThe app will automatically exit and run installer once downloaded', - ) - : FutureBuilder( - future: rootBundle.loadString( - 'currentAppVersion.txt', - cache: false, - ), - builder: (context, currentVersion) => currentVersion - .hasData - ? FutureBuilder( - future: http.read(Uri.parse(versionURL)), - builder: (context, availableVersion) => - availableVersion.hasError - ? const FailedToCheck() - : availableVersion.hasData - ? Version.parse( - availableVersion.data! - .trim(), - ) > - Version.parse( - currentVersion.data! - .trim(), - ) - ? buildUpdateAvailableWidget( - context: context, - availableVersion: - availableVersion.data! - .trim(), - currentVersion: - currentVersion.data! - .trim(), - updateApplication: - updateApplication, - ) - : const UpToDate() - : const LoadingIndicator( - message: - 'Checking For Updates...', - ), - ) - : const LoadingIndicator( - message: 'Loading App Version Information...', - ), - ), - ), - ], - ), - ), - ), - ); -} diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 59dac777..ab4b4be8 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -3,7 +3,7 @@ description: The example application for 'flutter_map_tile_caching', showcasing it's functionality and use-cases. publish_to: "none" -version: 8.0.0 +version: 8.1.0 environment: sdk: ">=2.18.0 <3.0.0" @@ -42,4 +42,3 @@ flutter: uses-material-design: true assets: - assets/icons/ - - currentAppVersion.txt diff --git a/example/windowsBuilder.bat b/example/windowsBuilder.bat deleted file mode 100644 index b096174c..00000000 --- a/example/windowsBuilder.bat +++ /dev/null @@ -1,4 +0,0 @@ -@ECHO OFF - -flutter clean | more -flutter build windows --obfuscate --split-debug-info=/symbols | more \ No newline at end of file diff --git a/lib/src/db/tools.dart b/lib/src/db/tools.dart index 695e6643..f378a041 100644 --- a/lib/src/db/tools.dart +++ b/lib/src/db/tools.dart @@ -28,6 +28,7 @@ class DatabaseTools { } } +@internal extension IsarExts on Isar { Future get descriptor async { final descriptor = await storeDescriptor.get(0); diff --git a/lib/src/providers/image_provider.dart b/lib/src/providers/image_provider.dart index ca56c691..aa226d04 100644 --- a/lib/src/providers/image_provider.dart +++ b/lib/src/providers/image_provider.dart @@ -141,74 +141,71 @@ class FMTCImageProvider extends ImageProvider { ); } - if (needsCreating || needsUpdating) { - final StreamedResponse response; + if (!needsCreating && !needsUpdating) { + return finish(bytes: bytes, cacheHit: true); + } - try { - response = await provider.httpClient.send( - Request('GET', Uri.parse(networkUrl)) - ..headers.addAll(provider.headers), - ); - } catch (_) { - return finish( - bytes: !needsCreating ? bytes : null, - throwError: needsCreating - ? 'Failed to load the tile from the cache or the network because it was missing from the cache and a connection to the server could not be established.' - : null, - throwErrorType: FMTCBrowsingErrorType.noConnectionDuringFetch, - cacheHit: false, - ); - } + final StreamedResponse response; - if (response.statusCode != 200) { - return finish( - bytes: !needsCreating ? bytes : null, - throwError: needsCreating - ? 'Failed to load the tile from the cache or the network because it was missing from the cache and the server responded with a HTTP code of ${response.statusCode}' - : null, - throwErrorType: FMTCBrowsingErrorType.negativeFetchResponse, - cacheHit: false, - ); - } + try { + response = await provider.httpClient.send( + Request('GET', Uri.parse(networkUrl))..headers.addAll(provider.headers), + ); + } catch (_) { + return finish( + bytes: !needsCreating ? bytes : null, + throwError: needsCreating + ? 'Failed to load the tile from the cache or the network because it was missing from the cache and a connection to the server could not be established.' + : null, + throwErrorType: FMTCBrowsingErrorType.noConnectionDuringFetch, + cacheHit: false, + ); + } - int bytesReceivedLength = 0; - bytes = []; - await for (final byte in response.stream) { - bytesReceivedLength += byte.length; - bytes.addAll(byte); - chunkEvents.add( - ImageChunkEvent( - cumulativeBytesLoaded: bytesReceivedLength, - expectedTotalBytes: response.contentLength, - ), - ); - } + if (response.statusCode != 200) { + return finish( + bytes: !needsCreating ? bytes : null, + throwError: needsCreating + ? 'Failed to load the tile from the cache or the network because it was missing from the cache and the server responded with a HTTP code of ${response.statusCode}' + : null, + throwErrorType: FMTCBrowsingErrorType.negativeFetchResponse, + cacheHit: false, + ); + } - unawaited( - db.writeTxn( - () => db.tiles.put(DbTile(url: matcherUrl, bytes: bytes!)), + int bytesReceivedLength = 0; + bytes = []; + await for (final byte in response.stream) { + bytesReceivedLength += byte.length; + bytes.addAll(byte); + chunkEvents.add( + ImageChunkEvent( + cumulativeBytesLoaded: bytesReceivedLength, + expectedTotalBytes: response.contentLength, ), ); + } - if (needsCreating && provider.settings.maxStoreLength != 0) { - unawaited( - _removeOldestQueue.add( - () => compute( - _removeOldestTile, - [ - provider.storeDirectory.storeName, - directory, - provider.settings.maxStoreLength, - ], - ), - ), - ); - } + unawaited( + db.writeTxn(() => db.tiles.put(DbTile(url: matcherUrl, bytes: bytes!))), + ); - return finish(bytes: bytes, cacheHit: false); + if (needsCreating && provider.settings.maxStoreLength != 0) { + unawaited( + _removeOldestQueue.add( + () => compute( + _removeOldestTile, + [ + provider.storeDirectory.storeName, + directory, + provider.settings.maxStoreLength, + ], + ), + ), + ); } - return finish(bytes: bytes, cacheHit: true); + return finish(bytes: bytes, cacheHit: false); } @override diff --git a/pubspec.yaml b/pubspec.yaml index ae25f3e3..1419c9f5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_map_tile_caching description: Plugin for 'flutter_map' providing advanced caching functionality, with ability to download map regions for offline use. -version: 8.0.0 +version: 8.1.0 repository: https://github.com/JaffaKetchup/flutter_map_tile_caching issue_tracker: https://github.com/JaffaKetchup/flutter_map_tile_caching/issues documentation: https://fmtc.jaffaketchup.dev diff --git a/windowsApplicationInstallerSetup.iss b/windowsApplicationInstallerSetup.iss index 9f1ce59a..d18c6cbd 100644 --- a/windowsApplicationInstallerSetup.iss +++ b/windowsApplicationInstallerSetup.iss @@ -1,7 +1,7 @@ ; Script generated by the Inno Setup Script Wizard #define MyAppName "FMTC Demo" -#define MyAppVersion "for 8.0.0" +#define MyAppVersion "for 8.1.0" #define MyAppPublisher "JaffaKetchup Development" #define MyAppURL "https://github.com/JaffaKetchup/flutter_map_tile_caching" #define MyAppSupportURL "https://github.com/JaffaKetchup/flutter_map_tile_caching/issues" From ba6380a6e7f87256736862f92adb85ca38ffd5d8 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 7 May 2023 14:49:17 +0000 Subject: [PATCH 004/168] Built Example Applications Former-commit-id: 73c8d75acec4f9c582243063b78b4497be21311b [formerly 026a74c543c445b1beadca586a3822a48ebcf5b2] Former-commit-id: e634e209d7a0886ebf2362e572897c01576892e4 --- .../AndroidApplication.apk.REMOVED.git-id | 2 +- .../WindowsApplication.exe.REMOVED.git-id | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/prebuiltExampleApplications/AndroidApplication.apk.REMOVED.git-id b/prebuiltExampleApplications/AndroidApplication.apk.REMOVED.git-id index 6d278680..ce74721c 100644 --- a/prebuiltExampleApplications/AndroidApplication.apk.REMOVED.git-id +++ b/prebuiltExampleApplications/AndroidApplication.apk.REMOVED.git-id @@ -1 +1 @@ -a9723446c34253b0ebdb40ba45b37d41a3fe4bbc \ No newline at end of file +e5367d63acb2b620131bac705081ff7f723266fd \ No newline at end of file diff --git a/prebuiltExampleApplications/WindowsApplication.exe.REMOVED.git-id b/prebuiltExampleApplications/WindowsApplication.exe.REMOVED.git-id index 72661659..7796a3ec 100644 --- a/prebuiltExampleApplications/WindowsApplication.exe.REMOVED.git-id +++ b/prebuiltExampleApplications/WindowsApplication.exe.REMOVED.git-id @@ -1 +1 @@ -b3664d65f4f2afecaced899eb762379e674a274e \ No newline at end of file +2d52446ae081d12a0bd9c232fb70a335f98aba9e \ No newline at end of file From 4f1dfc23b635379f8667881ca14bd515c4396a2c Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Tue, 9 May 2023 10:24:59 +0100 Subject: [PATCH 005/168] Updated build tool versions Former-commit-id: e1fb158923922800d7a51082aa31dfae5dc1a482 [formerly f0f438c9b9404e95ab1381dfdc8c162a632b0e92] Former-commit-id: 447bc1cb452dad6e7af9a58c1a8ba38c68e78d59 --- example/android/build.gradle | 6 ++++-- example/android/gradle/wrapper/gradle-wrapper.properties | 3 +-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/example/android/build.gradle b/example/android/build.gradle index 96f87494..4e43ac75 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -1,6 +1,8 @@ buildscript { - ext.kotlin_version = '1.7.0' ext { + kotlin_version = '1.8.21' + gradle_version = '7.4.0' + compileSdkVersion = 33 targetSdkVersion = 29 minSdkVersion = 23 @@ -13,7 +15,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.2.0' + classpath "com.android.tools.build:gradle:$gradle_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index cc5527d7..ceccc3a8 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Fri Jun 23 08:50:38 CEST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip From be072a234cf9d94c93f536eeb7c304b6c8b113a4 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 9 May 2023 09:44:47 +0000 Subject: [PATCH 006/168] Built Example Applications Former-commit-id: c612dae7267ff6044b1c03096abe14a65db133ef [formerly 984345180c521cee5a7716ba3134130453e904da] Former-commit-id: bb2da5c8d718de12b85a83d67df01469306a07b9 --- .../AndroidApplication.apk.REMOVED.git-id | 2 +- .../WindowsApplication.exe.REMOVED.git-id | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/prebuiltExampleApplications/AndroidApplication.apk.REMOVED.git-id b/prebuiltExampleApplications/AndroidApplication.apk.REMOVED.git-id index ce74721c..af3bc3b4 100644 --- a/prebuiltExampleApplications/AndroidApplication.apk.REMOVED.git-id +++ b/prebuiltExampleApplications/AndroidApplication.apk.REMOVED.git-id @@ -1 +1 @@ -e5367d63acb2b620131bac705081ff7f723266fd \ No newline at end of file +1533e0340fd2d6289c8042ff58907697caadc04c \ No newline at end of file diff --git a/prebuiltExampleApplications/WindowsApplication.exe.REMOVED.git-id b/prebuiltExampleApplications/WindowsApplication.exe.REMOVED.git-id index 7796a3ec..3d2a8a85 100644 --- a/prebuiltExampleApplications/WindowsApplication.exe.REMOVED.git-id +++ b/prebuiltExampleApplications/WindowsApplication.exe.REMOVED.git-id @@ -1 +1 @@ -2d52446ae081d12a0bd9c232fb70a335f98aba9e \ No newline at end of file +805101ceea0d17220214e4b585ac34a94e8b0b69 \ No newline at end of file From 1572819cb810ced2ad066a13fa86bc05677dd181 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 11 May 2023 09:28:21 +0100 Subject: [PATCH 007/168] Update to better support Flutter 3.10 Updated example app dependencies Former-commit-id: 6dab1dca5222a1f42eb225c74a8e61d74503ac6e [formerly 57eb7c03413c45071944d99a70770b9050fde942] Former-commit-id: 07a102fb0064983ba4014dfd5f917b6c534d493b --- .../components/section_separator.dart | 4 +-- .../components/usage_warning.dart | 4 +-- .../components/horizontal_layout.dart | 4 +-- .../downloading/components/tile_image.dart | 4 +-- .../components/vertical_layout.dart | 4 +-- .../recovery/components/empty_indicator.dart | 4 +-- .../settingsAndAbout/settings_and_about.dart | 4 +-- .../stores/components/empty_indicator.dart | 4 +-- .../pages/stores/components/store_tile.dart | 4 +-- example/pubspec.yaml | 4 +-- lib/src/providers/image_provider.dart | 28 ++++++++++--------- lib/src/store/manage.dart | 2 ++ 12 files changed, 37 insertions(+), 33 deletions(-) diff --git a/example/lib/screens/download_region/components/section_separator.dart b/example/lib/screens/download_region/components/section_separator.dart index c2ecb1d4..9a0969c8 100644 --- a/example/lib/screens/download_region/components/section_separator.dart +++ b/example/lib/screens/download_region/components/section_separator.dart @@ -4,8 +4,8 @@ class SectionSeparator extends StatelessWidget { const SectionSeparator({super.key}); @override - Widget build(BuildContext context) => Column( - children: const [ + Widget build(BuildContext context) => const Column( + children: [ SizedBox(height: 5), Divider(), SizedBox(height: 5), diff --git a/example/lib/screens/download_region/components/usage_warning.dart b/example/lib/screens/download_region/components/usage_warning.dart index 30d20a1c..c0ba4742 100644 --- a/example/lib/screens/download_region/components/usage_warning.dart +++ b/example/lib/screens/download_region/components/usage_warning.dart @@ -6,8 +6,8 @@ class UsageWarning extends StatelessWidget { }); @override - Widget build(BuildContext context) => Row( - children: const [ + Widget build(BuildContext context) => const Row( + children: [ Icon( Icons.warning_amber, color: Colors.red, diff --git a/example/lib/screens/main/pages/downloading/components/horizontal_layout.dart b/example/lib/screens/main/pages/downloading/components/horizontal_layout.dart index 98a8b23d..3b313826 100644 --- a/example/lib/screens/main/pages/downloading/components/horizontal_layout.dart +++ b/example/lib/screens/main/pages/downloading/components/horizontal_layout.dart @@ -114,9 +114,9 @@ class HorizontalLayout extends StatelessWidget { const SizedBox(height: 30), Expanded( child: data.failedTiles.isEmpty - ? Column( + ? const Column( mainAxisAlignment: MainAxisAlignment.center, - children: const [ + children: [ Icon(Icons.report_off, size: 36), SizedBox(height: 10), Text('No Failed Tiles'), diff --git a/example/lib/screens/main/pages/downloading/components/tile_image.dart b/example/lib/screens/main/pages/downloading/components/tile_image.dart index d60da7cc..13197b23 100644 --- a/example/lib/screens/main/pages/downloading/components/tile_image.dart +++ b/example/lib/screens/main/pages/downloading/components/tile_image.dart @@ -23,10 +23,10 @@ Widget tileImage({ ), ), if (data.percentageProgress == 100) - Column( + const Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, - children: const [ + children: [ Icon( Icons.done_all, size: 36, diff --git a/example/lib/screens/main/pages/downloading/components/vertical_layout.dart b/example/lib/screens/main/pages/downloading/components/vertical_layout.dart index 72170247..a7cbd559 100644 --- a/example/lib/screens/main/pages/downloading/components/vertical_layout.dart +++ b/example/lib/screens/main/pages/downloading/components/vertical_layout.dart @@ -84,9 +84,9 @@ class VerticalLayout extends StatelessWidget { const SizedBox(height: 15), Expanded( child: data.failedTiles.isEmpty - ? Column( + ? const Column( mainAxisAlignment: MainAxisAlignment.center, - children: const [ + children: [ Icon(Icons.report_off, size: 36), SizedBox(height: 10), Text('No Failed Tiles'), diff --git a/example/lib/screens/main/pages/recovery/components/empty_indicator.dart b/example/lib/screens/main/pages/recovery/components/empty_indicator.dart index a72071d7..245ec722 100644 --- a/example/lib/screens/main/pages/recovery/components/empty_indicator.dart +++ b/example/lib/screens/main/pages/recovery/components/empty_indicator.dart @@ -6,10 +6,10 @@ class EmptyIndicator extends StatelessWidget { }); @override - Widget build(BuildContext context) => Center( + Widget build(BuildContext context) => const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, - children: const [ + children: [ Icon(Icons.done, size: 38), SizedBox(height: 10), Text('No Recoverable Regions Found'), diff --git a/example/lib/screens/main/pages/settingsAndAbout/settings_and_about.dart b/example/lib/screens/main/pages/settingsAndAbout/settings_and_about.dart index 0f2a872f..8bf17519 100644 --- a/example/lib/screens/main/pages/settingsAndAbout/settings_and_about.dart +++ b/example/lib/screens/main/pages/settingsAndAbout/settings_and_about.dart @@ -96,9 +96,9 @@ class _SettingsAndAboutPageState extends State { Expanded( child: SingleChildScrollView( controller: creditsScrollController, - child: Column( + child: const Column( crossAxisAlignment: CrossAxisAlignment.start, - children: const [ + children: [ Text( "An example application for the 'flutter_map_tile_caching' project, built by Luka S (JaffaKetchup). Tap on the above button to show more detailed information.\n", ), diff --git a/example/lib/screens/main/pages/stores/components/empty_indicator.dart b/example/lib/screens/main/pages/stores/components/empty_indicator.dart index 89a51da4..82af5ebd 100644 --- a/example/lib/screens/main/pages/stores/components/empty_indicator.dart +++ b/example/lib/screens/main/pages/stores/components/empty_indicator.dart @@ -6,10 +6,10 @@ class EmptyIndicator extends StatelessWidget { }); @override - Widget build(BuildContext context) => Center( + Widget build(BuildContext context) => const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, - children: const [ + children: [ Icon(Icons.folder_off, size: 36), SizedBox(height: 10), Text('No Stores Found'), diff --git a/example/lib/screens/main/pages/stores/components/store_tile.dart b/example/lib/screens/main/pages/stores/components/store_tile.dart index 2cc5b9d3..6e9e8069 100644 --- a/example/lib/screens/main/pages/stores/components/store_tile.dart +++ b/example/lib/screens/main/pages/stores/components/store_tile.dart @@ -258,9 +258,9 @@ class _StoreTileState extends State { : Column( children: [ const SizedBox(height: 10), - Row( + const Row( mainAxisSize: MainAxisSize.min, - children: const [ + children: [ Icon(Icons.broken_image, size: 34), Icon(Icons.error, size: 34), ], diff --git a/example/pubspec.yaml b/example/pubspec.yaml index ab4b4be8..6ab2a0bf 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -14,13 +14,13 @@ dependencies: better_open_file: ^3.6.4 flutter: sdk: flutter - flutter_foreground_task: ^3.9.0 + flutter_foreground_task: ^4.1.0 flutter_map: ^4.0.0 flutter_map_tile_caching: flutter_speed_dial: ^6.0.0 fmtc_plus_background_downloading: ^7.0.0 fmtc_plus_sharing: ^8.0.0 - google_fonts: ^3.0.1 + google_fonts: ^4.0.4 http: ^0.13.4 intl: ^0.18.0 latlong2: ^0.8.1 diff --git a/lib/src/providers/image_provider.dart b/lib/src/providers/image_provider.dart index aa226d04..a814faff 100644 --- a/lib/src/providers/image_provider.dart +++ b/lib/src/providers/image_provider.dart @@ -48,31 +48,33 @@ class FMTCImageProvider extends ImageProvider { }) : db = FMTCRegistry.instance(provider.storeDirectory.storeName); @override - ImageStreamCompleter loadBuffer( + ImageStreamCompleter loadImage( FMTCImageProvider key, - DecoderBufferCallback decode, + ImageDecoderCallback decode, ) { - // ignore: close_sinks final StreamController chunkEvents = StreamController(); return MultiFrameImageStreamCompleter( - codec: _loadAsync(key: key, decode: decode, chunkEvents: chunkEvents), + codec: _loadAsync(key, chunkEvents, decode), chunkEvents: chunkEvents.stream, scale: 1, debugLabel: coords.toString(), - informationCollector: () => [DiagnosticsProperty('Coordinates', coords)], + informationCollector: () => [ + DiagnosticsProperty('Tile coordinates', coords), + DiagnosticsProperty('Root directory', directory), + DiagnosticsProperty('Store name', provider.storeDirectory.storeName), + DiagnosticsProperty('Current provider', key), + ], ); } - Future _loadAsync({ - required FMTCImageProvider key, - required DecoderBufferCallback decode, - required StreamController chunkEvents, - }) async { - Future cacheHitMiss({ - required bool hit, - }) => + Future _loadAsync( + FMTCImageProvider key, + StreamController chunkEvents, + ImageDecoderCallback decode, + ) async { + Future cacheHitMiss({required bool hit}) => (hit ? _cacheHitsQueue : _cacheMissesQueue).add(() async { if (db.isOpen) { await db.writeTxn(() async { diff --git a/lib/src/store/manage.dart b/lib/src/store/manage.dart index 4ae61cc3..d0ea84b4 100644 --- a/lib/src/store/manage.dart +++ b/lib/src/store/manage.dart @@ -191,6 +191,7 @@ class StoreManagement { double? size, Key? key, double scale = 1.0, + // ignore: avoid_positional_boolean_parameters Widget Function(BuildContext, Widget, int?, bool)? frameBuilder, Widget Function(BuildContext, Object, StackTrace?)? errorBuilder, String? semanticLabel, @@ -254,6 +255,7 @@ class StoreManagement { double? size, Key? key, double scale = 1.0, + // ignore: avoid_positional_boolean_parameters Widget Function(BuildContext, Widget, int?, bool)? frameBuilder, Widget Function(BuildContext, Object, StackTrace?)? errorBuilder, String? semanticLabel, From 99213027bcbf7e95d394f7f98d9619a397bf504a Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 11 May 2023 14:17:47 +0100 Subject: [PATCH 008/168] Revert commit 1572819cb810ced2ad066a13fa86bc05677dd181 [formerly 07a102fb0064983ba4014dfd5f917b6c534d493b] [formerly 6dab1dca5222a1f42eb225c74a8e61d74503ac6e [formerly 57eb7c03413c45071944d99a70770b9050fde942]] Former-commit-id: edb21836980971b427394fee221e1598be521fc8 [formerly a58fe13fe9ea5883e3eaa72aca8167403baa8bda] Former-commit-id: 8784e621871ebd74ad93f2858780d553189ef981 --- .../components/section_separator.dart | 4 +-- .../components/usage_warning.dart | 4 +-- .../components/horizontal_layout.dart | 4 +-- .../downloading/components/tile_image.dart | 4 +-- .../components/vertical_layout.dart | 4 +-- .../recovery/components/empty_indicator.dart | 4 +-- .../settingsAndAbout/settings_and_about.dart | 4 +-- .../stores/components/empty_indicator.dart | 4 +-- .../pages/stores/components/store_tile.dart | 4 +-- example/pubspec.yaml | 4 +-- lib/src/providers/image_provider.dart | 28 +++++++++---------- lib/src/store/manage.dart | 2 -- 12 files changed, 33 insertions(+), 37 deletions(-) diff --git a/example/lib/screens/download_region/components/section_separator.dart b/example/lib/screens/download_region/components/section_separator.dart index 9a0969c8..c2ecb1d4 100644 --- a/example/lib/screens/download_region/components/section_separator.dart +++ b/example/lib/screens/download_region/components/section_separator.dart @@ -4,8 +4,8 @@ class SectionSeparator extends StatelessWidget { const SectionSeparator({super.key}); @override - Widget build(BuildContext context) => const Column( - children: [ + Widget build(BuildContext context) => Column( + children: const [ SizedBox(height: 5), Divider(), SizedBox(height: 5), diff --git a/example/lib/screens/download_region/components/usage_warning.dart b/example/lib/screens/download_region/components/usage_warning.dart index c0ba4742..30d20a1c 100644 --- a/example/lib/screens/download_region/components/usage_warning.dart +++ b/example/lib/screens/download_region/components/usage_warning.dart @@ -6,8 +6,8 @@ class UsageWarning extends StatelessWidget { }); @override - Widget build(BuildContext context) => const Row( - children: [ + Widget build(BuildContext context) => Row( + children: const [ Icon( Icons.warning_amber, color: Colors.red, diff --git a/example/lib/screens/main/pages/downloading/components/horizontal_layout.dart b/example/lib/screens/main/pages/downloading/components/horizontal_layout.dart index 3b313826..98a8b23d 100644 --- a/example/lib/screens/main/pages/downloading/components/horizontal_layout.dart +++ b/example/lib/screens/main/pages/downloading/components/horizontal_layout.dart @@ -114,9 +114,9 @@ class HorizontalLayout extends StatelessWidget { const SizedBox(height: 30), Expanded( child: data.failedTiles.isEmpty - ? const Column( + ? Column( mainAxisAlignment: MainAxisAlignment.center, - children: [ + children: const [ Icon(Icons.report_off, size: 36), SizedBox(height: 10), Text('No Failed Tiles'), diff --git a/example/lib/screens/main/pages/downloading/components/tile_image.dart b/example/lib/screens/main/pages/downloading/components/tile_image.dart index 13197b23..d60da7cc 100644 --- a/example/lib/screens/main/pages/downloading/components/tile_image.dart +++ b/example/lib/screens/main/pages/downloading/components/tile_image.dart @@ -23,10 +23,10 @@ Widget tileImage({ ), ), if (data.percentageProgress == 100) - const Column( + Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, - children: [ + children: const [ Icon( Icons.done_all, size: 36, diff --git a/example/lib/screens/main/pages/downloading/components/vertical_layout.dart b/example/lib/screens/main/pages/downloading/components/vertical_layout.dart index a7cbd559..72170247 100644 --- a/example/lib/screens/main/pages/downloading/components/vertical_layout.dart +++ b/example/lib/screens/main/pages/downloading/components/vertical_layout.dart @@ -84,9 +84,9 @@ class VerticalLayout extends StatelessWidget { const SizedBox(height: 15), Expanded( child: data.failedTiles.isEmpty - ? const Column( + ? Column( mainAxisAlignment: MainAxisAlignment.center, - children: [ + children: const [ Icon(Icons.report_off, size: 36), SizedBox(height: 10), Text('No Failed Tiles'), diff --git a/example/lib/screens/main/pages/recovery/components/empty_indicator.dart b/example/lib/screens/main/pages/recovery/components/empty_indicator.dart index 245ec722..a72071d7 100644 --- a/example/lib/screens/main/pages/recovery/components/empty_indicator.dart +++ b/example/lib/screens/main/pages/recovery/components/empty_indicator.dart @@ -6,10 +6,10 @@ class EmptyIndicator extends StatelessWidget { }); @override - Widget build(BuildContext context) => const Center( + Widget build(BuildContext context) => Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, - children: [ + children: const [ Icon(Icons.done, size: 38), SizedBox(height: 10), Text('No Recoverable Regions Found'), diff --git a/example/lib/screens/main/pages/settingsAndAbout/settings_and_about.dart b/example/lib/screens/main/pages/settingsAndAbout/settings_and_about.dart index 8bf17519..0f2a872f 100644 --- a/example/lib/screens/main/pages/settingsAndAbout/settings_and_about.dart +++ b/example/lib/screens/main/pages/settingsAndAbout/settings_and_about.dart @@ -96,9 +96,9 @@ class _SettingsAndAboutPageState extends State { Expanded( child: SingleChildScrollView( controller: creditsScrollController, - child: const Column( + child: Column( crossAxisAlignment: CrossAxisAlignment.start, - children: [ + children: const [ Text( "An example application for the 'flutter_map_tile_caching' project, built by Luka S (JaffaKetchup). Tap on the above button to show more detailed information.\n", ), diff --git a/example/lib/screens/main/pages/stores/components/empty_indicator.dart b/example/lib/screens/main/pages/stores/components/empty_indicator.dart index 82af5ebd..89a51da4 100644 --- a/example/lib/screens/main/pages/stores/components/empty_indicator.dart +++ b/example/lib/screens/main/pages/stores/components/empty_indicator.dart @@ -6,10 +6,10 @@ class EmptyIndicator extends StatelessWidget { }); @override - Widget build(BuildContext context) => const Center( + Widget build(BuildContext context) => Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, - children: [ + children: const [ Icon(Icons.folder_off, size: 36), SizedBox(height: 10), Text('No Stores Found'), diff --git a/example/lib/screens/main/pages/stores/components/store_tile.dart b/example/lib/screens/main/pages/stores/components/store_tile.dart index 6e9e8069..2cc5b9d3 100644 --- a/example/lib/screens/main/pages/stores/components/store_tile.dart +++ b/example/lib/screens/main/pages/stores/components/store_tile.dart @@ -258,9 +258,9 @@ class _StoreTileState extends State { : Column( children: [ const SizedBox(height: 10), - const Row( + Row( mainAxisSize: MainAxisSize.min, - children: [ + children: const [ Icon(Icons.broken_image, size: 34), Icon(Icons.error, size: 34), ], diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 6ab2a0bf..ab4b4be8 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -14,13 +14,13 @@ dependencies: better_open_file: ^3.6.4 flutter: sdk: flutter - flutter_foreground_task: ^4.1.0 + flutter_foreground_task: ^3.9.0 flutter_map: ^4.0.0 flutter_map_tile_caching: flutter_speed_dial: ^6.0.0 fmtc_plus_background_downloading: ^7.0.0 fmtc_plus_sharing: ^8.0.0 - google_fonts: ^4.0.4 + google_fonts: ^3.0.1 http: ^0.13.4 intl: ^0.18.0 latlong2: ^0.8.1 diff --git a/lib/src/providers/image_provider.dart b/lib/src/providers/image_provider.dart index a814faff..aa226d04 100644 --- a/lib/src/providers/image_provider.dart +++ b/lib/src/providers/image_provider.dart @@ -48,33 +48,31 @@ class FMTCImageProvider extends ImageProvider { }) : db = FMTCRegistry.instance(provider.storeDirectory.storeName); @override - ImageStreamCompleter loadImage( + ImageStreamCompleter loadBuffer( FMTCImageProvider key, - ImageDecoderCallback decode, + DecoderBufferCallback decode, ) { + // ignore: close_sinks final StreamController chunkEvents = StreamController(); return MultiFrameImageStreamCompleter( - codec: _loadAsync(key, chunkEvents, decode), + codec: _loadAsync(key: key, decode: decode, chunkEvents: chunkEvents), chunkEvents: chunkEvents.stream, scale: 1, debugLabel: coords.toString(), - informationCollector: () => [ - DiagnosticsProperty('Tile coordinates', coords), - DiagnosticsProperty('Root directory', directory), - DiagnosticsProperty('Store name', provider.storeDirectory.storeName), - DiagnosticsProperty('Current provider', key), - ], + informationCollector: () => [DiagnosticsProperty('Coordinates', coords)], ); } - Future _loadAsync( - FMTCImageProvider key, - StreamController chunkEvents, - ImageDecoderCallback decode, - ) async { - Future cacheHitMiss({required bool hit}) => + Future _loadAsync({ + required FMTCImageProvider key, + required DecoderBufferCallback decode, + required StreamController chunkEvents, + }) async { + Future cacheHitMiss({ + required bool hit, + }) => (hit ? _cacheHitsQueue : _cacheMissesQueue).add(() async { if (db.isOpen) { await db.writeTxn(() async { diff --git a/lib/src/store/manage.dart b/lib/src/store/manage.dart index d0ea84b4..4ae61cc3 100644 --- a/lib/src/store/manage.dart +++ b/lib/src/store/manage.dart @@ -191,7 +191,6 @@ class StoreManagement { double? size, Key? key, double scale = 1.0, - // ignore: avoid_positional_boolean_parameters Widget Function(BuildContext, Widget, int?, bool)? frameBuilder, Widget Function(BuildContext, Object, StackTrace?)? errorBuilder, String? semanticLabel, @@ -255,7 +254,6 @@ class StoreManagement { double? size, Key? key, double scale = 1.0, - // ignore: avoid_positional_boolean_parameters Widget Function(BuildContext, Widget, int?, bool)? frameBuilder, Widget Function(BuildContext, Object, StackTrace?)? errorBuilder, String? semanticLabel, From da33621ad477e0cb97967663eee8eb0f74140d8c Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 11 May 2023 14:25:54 +0100 Subject: [PATCH 009/168] Added support for Flutter 3.10 to example app Former-commit-id: 5d90a72e39ef3f48b0b20b903414c7a63f886c8c [formerly d80244dbbeebea18cd3ac5b8e08f2eeaec893af3] Former-commit-id: f59c2c770a8ae37d274840e9412ea564a4e2dfdb --- .../download_region/components/section_separator.dart | 4 ++-- .../download_region/components/usage_warning.dart | 4 ++-- .../downloading/components/horizontal_layout.dart | 4 ++-- .../main/pages/downloading/components/tile_image.dart | 4 ++-- .../pages/downloading/components/vertical_layout.dart | 4 ++-- .../pages/recovery/components/empty_indicator.dart | 4 ++-- .../pages/settingsAndAbout/settings_and_about.dart | 4 ++-- .../main/pages/stores/components/empty_indicator.dart | 4 ++-- .../main/pages/stores/components/store_tile.dart | 4 ++-- example/pubspec.yaml | 4 ++-- lib/src/providers/image_provider.dart | 10 ++++++++++ lib/src/store/manage.dart | 2 ++ 12 files changed, 32 insertions(+), 20 deletions(-) diff --git a/example/lib/screens/download_region/components/section_separator.dart b/example/lib/screens/download_region/components/section_separator.dart index c2ecb1d4..9a0969c8 100644 --- a/example/lib/screens/download_region/components/section_separator.dart +++ b/example/lib/screens/download_region/components/section_separator.dart @@ -4,8 +4,8 @@ class SectionSeparator extends StatelessWidget { const SectionSeparator({super.key}); @override - Widget build(BuildContext context) => Column( - children: const [ + Widget build(BuildContext context) => const Column( + children: [ SizedBox(height: 5), Divider(), SizedBox(height: 5), diff --git a/example/lib/screens/download_region/components/usage_warning.dart b/example/lib/screens/download_region/components/usage_warning.dart index 30d20a1c..c0ba4742 100644 --- a/example/lib/screens/download_region/components/usage_warning.dart +++ b/example/lib/screens/download_region/components/usage_warning.dart @@ -6,8 +6,8 @@ class UsageWarning extends StatelessWidget { }); @override - Widget build(BuildContext context) => Row( - children: const [ + Widget build(BuildContext context) => const Row( + children: [ Icon( Icons.warning_amber, color: Colors.red, diff --git a/example/lib/screens/main/pages/downloading/components/horizontal_layout.dart b/example/lib/screens/main/pages/downloading/components/horizontal_layout.dart index 98a8b23d..3b313826 100644 --- a/example/lib/screens/main/pages/downloading/components/horizontal_layout.dart +++ b/example/lib/screens/main/pages/downloading/components/horizontal_layout.dart @@ -114,9 +114,9 @@ class HorizontalLayout extends StatelessWidget { const SizedBox(height: 30), Expanded( child: data.failedTiles.isEmpty - ? Column( + ? const Column( mainAxisAlignment: MainAxisAlignment.center, - children: const [ + children: [ Icon(Icons.report_off, size: 36), SizedBox(height: 10), Text('No Failed Tiles'), diff --git a/example/lib/screens/main/pages/downloading/components/tile_image.dart b/example/lib/screens/main/pages/downloading/components/tile_image.dart index d60da7cc..13197b23 100644 --- a/example/lib/screens/main/pages/downloading/components/tile_image.dart +++ b/example/lib/screens/main/pages/downloading/components/tile_image.dart @@ -23,10 +23,10 @@ Widget tileImage({ ), ), if (data.percentageProgress == 100) - Column( + const Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, - children: const [ + children: [ Icon( Icons.done_all, size: 36, diff --git a/example/lib/screens/main/pages/downloading/components/vertical_layout.dart b/example/lib/screens/main/pages/downloading/components/vertical_layout.dart index 72170247..a7cbd559 100644 --- a/example/lib/screens/main/pages/downloading/components/vertical_layout.dart +++ b/example/lib/screens/main/pages/downloading/components/vertical_layout.dart @@ -84,9 +84,9 @@ class VerticalLayout extends StatelessWidget { const SizedBox(height: 15), Expanded( child: data.failedTiles.isEmpty - ? Column( + ? const Column( mainAxisAlignment: MainAxisAlignment.center, - children: const [ + children: [ Icon(Icons.report_off, size: 36), SizedBox(height: 10), Text('No Failed Tiles'), diff --git a/example/lib/screens/main/pages/recovery/components/empty_indicator.dart b/example/lib/screens/main/pages/recovery/components/empty_indicator.dart index a72071d7..245ec722 100644 --- a/example/lib/screens/main/pages/recovery/components/empty_indicator.dart +++ b/example/lib/screens/main/pages/recovery/components/empty_indicator.dart @@ -6,10 +6,10 @@ class EmptyIndicator extends StatelessWidget { }); @override - Widget build(BuildContext context) => Center( + Widget build(BuildContext context) => const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, - children: const [ + children: [ Icon(Icons.done, size: 38), SizedBox(height: 10), Text('No Recoverable Regions Found'), diff --git a/example/lib/screens/main/pages/settingsAndAbout/settings_and_about.dart b/example/lib/screens/main/pages/settingsAndAbout/settings_and_about.dart index 0f2a872f..8bf17519 100644 --- a/example/lib/screens/main/pages/settingsAndAbout/settings_and_about.dart +++ b/example/lib/screens/main/pages/settingsAndAbout/settings_and_about.dart @@ -96,9 +96,9 @@ class _SettingsAndAboutPageState extends State { Expanded( child: SingleChildScrollView( controller: creditsScrollController, - child: Column( + child: const Column( crossAxisAlignment: CrossAxisAlignment.start, - children: const [ + children: [ Text( "An example application for the 'flutter_map_tile_caching' project, built by Luka S (JaffaKetchup). Tap on the above button to show more detailed information.\n", ), diff --git a/example/lib/screens/main/pages/stores/components/empty_indicator.dart b/example/lib/screens/main/pages/stores/components/empty_indicator.dart index 89a51da4..82af5ebd 100644 --- a/example/lib/screens/main/pages/stores/components/empty_indicator.dart +++ b/example/lib/screens/main/pages/stores/components/empty_indicator.dart @@ -6,10 +6,10 @@ class EmptyIndicator extends StatelessWidget { }); @override - Widget build(BuildContext context) => Center( + Widget build(BuildContext context) => const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, - children: const [ + children: [ Icon(Icons.folder_off, size: 36), SizedBox(height: 10), Text('No Stores Found'), diff --git a/example/lib/screens/main/pages/stores/components/store_tile.dart b/example/lib/screens/main/pages/stores/components/store_tile.dart index 2cc5b9d3..6e9e8069 100644 --- a/example/lib/screens/main/pages/stores/components/store_tile.dart +++ b/example/lib/screens/main/pages/stores/components/store_tile.dart @@ -258,9 +258,9 @@ class _StoreTileState extends State { : Column( children: [ const SizedBox(height: 10), - Row( + const Row( mainAxisSize: MainAxisSize.min, - children: const [ + children: [ Icon(Icons.broken_image, size: 34), Icon(Icons.error, size: 34), ], diff --git a/example/pubspec.yaml b/example/pubspec.yaml index ab4b4be8..b94de352 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -6,8 +6,8 @@ publish_to: "none" version: 8.1.0 environment: - sdk: ">=2.18.0 <3.0.0" - flutter: ">=3.7.0" + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.10.0" dependencies: badges: ^3.0.2 diff --git a/lib/src/providers/image_provider.dart b/lib/src/providers/image_provider.dart index aa226d04..b1fba86d 100644 --- a/lib/src/providers/image_provider.dart +++ b/lib/src/providers/image_provider.dart @@ -19,6 +19,10 @@ import '../db/registry.dart'; import '../db/tools.dart'; /// A specialised [ImageProvider] dedicated to 'flutter_map_tile_caching' +/// +/// TODO: When v9 is released with Isar v4, bump to minimum Dart 3 and +/// Flutter 3.10, then replace deprecated methods with [loadImage], as in +/// https://github.com/JaffaKetchup/flutter_map_tile_caching/blob/57eb7c03413c45071944d99a70770b9050fde942/lib/src/providers/image_provider.dart class FMTCImageProvider extends ImageProvider { /// An instance of the [FMTCTileProvider] in use final FMTCTileProvider provider; @@ -47,9 +51,14 @@ class FMTCImageProvider extends ImageProvider { required this.directory, }) : db = FMTCRegistry.instance(provider.storeDirectory.storeName); + // TODO: When v9 is released with Isar v4, bump to minimum Dart 3 and + // Flutter 3.10, then replace deprecated methods with [loadImage], as in + // https://github.com/JaffaKetchup/flutter_map_tile_caching/blob/57eb7c03413c45071944d99a70770b9050fde942/lib/src/providers/image_provider.dart + @override ImageStreamCompleter loadBuffer( FMTCImageProvider key, + // ignore: deprecated_member_use DecoderBufferCallback decode, ) { // ignore: close_sinks @@ -67,6 +76,7 @@ class FMTCImageProvider extends ImageProvider { Future _loadAsync({ required FMTCImageProvider key, + // ignore: deprecated_member_use required DecoderBufferCallback decode, required StreamController chunkEvents, }) async { diff --git a/lib/src/store/manage.dart b/lib/src/store/manage.dart index 4ae61cc3..d0ea84b4 100644 --- a/lib/src/store/manage.dart +++ b/lib/src/store/manage.dart @@ -191,6 +191,7 @@ class StoreManagement { double? size, Key? key, double scale = 1.0, + // ignore: avoid_positional_boolean_parameters Widget Function(BuildContext, Widget, int?, bool)? frameBuilder, Widget Function(BuildContext, Object, StackTrace?)? errorBuilder, String? semanticLabel, @@ -254,6 +255,7 @@ class StoreManagement { double? size, Key? key, double scale = 1.0, + // ignore: avoid_positional_boolean_parameters Widget Function(BuildContext, Widget, int?, bool)? frameBuilder, Widget Function(BuildContext, Object, StackTrace?)? errorBuilder, String? semanticLabel, From 10b6b2b2d5995318618ed2fc36c23c60ee890930 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 11 May 2023 14:34:44 +0100 Subject: [PATCH 010/168] Bumped version Updated CHANGELOG Former-commit-id: 42d1051cf964ee30803ec00706ffe6f98a62ca1f [formerly f07e0a959445cd6703b2097d69f0dbcabcc85f31] Former-commit-id: db8a26b1bd38c52ad9729c6b3e2ea8c8dbc580d7 --- CHANGELOG.md | 110 +++++++++++++++++++++++-------------------- example/pubspec.yaml | 2 +- pubspec.yaml | 2 +- 3 files changed, 60 insertions(+), 54 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b32cea4f..00fed48e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,9 +14,12 @@ Many thanks to my sponsors, no matter how much or how little they donated. Spons # Changelog -## [8.1.0] - 2023/05/XX +## [8.1.0] - 2023/05/11 * Added `StoreManagement.pruneTilesOlderThan` method +* Example application updates + * Added support for Flutter 3.10 + * Removed auto-updater ## [8.0.0] - 2023/05/05 @@ -36,57 +39,60 @@ Many thanks to my sponsors, no matter how much or how little they donated. Spons * Added support for custom `HttpClient`s/`BaseClient`s * Added support for Isar v3.1 (bug fixes & stability improvements) -## [7.2.0] - 2023/03/03 - -* Stability improvements - * Starting multiple downloads no longer causes `LateInitializationErrors` - * Migrator storage and memory usage no longer spikes as significantly as previously, thanks to transaction batching - * Opening and processing of stores on initialisation is more robust and less error-prone to filename variations - * Root statistic watching now works on all platforms -* Multiple minor bug fixes and documentation improvements -* Added `maxStoreLength` config to example app - -## [7.1.2] - 2023/02/18 - -* Minor bug fixes - -## [7.1.1] - 2023/02/16 - -* Major bug fixes -* Added debug mode - -## [7.1.0] - 2023/02/14 - -* Added URL query params obscurer feature -* Added `headers` and `httpClient` parameters to `getTileProvider` -* Minor documentation improvements -* Minor bug fixes - -## [7.0.2] - 2023/02/12 - -* Minor changes to example application - -## [7.0.1] - 2023/02/11 - -* Minor bug fixes -* Minor improvements - -## [7.0.0] - 2023/02/04 - -* Migrated to Isar database -* Major performance improvements, thanks to Isar -* Added buffering to bulk tile downloading -* Added method to catch tile retrieval errors -* Removed v4 -> v5 migrator & added v6 -> v7 migrator -* Removed some synchronous methods from structure management -* Removed 'fmtc_advanced' import file - -Plus the usual: - -* Minor performance improvements -* Bug fixes -* Dependency updates -* Documentation improvements +> **Version 7 was made unstable due to a non-semantic versioning compliant update of a dependency.** +> **This means the pub version resolver can never resolve FMTC v7 without introducing compilation errors.** +> +> ## [7.2.0] - 2023/03/03 +> +> * Stability improvements +> * Starting multiple downloads no longer causes `LateInitializationErrors` +> * Migrator storage and memory usage no longer spikes as significantly as previously, thanks to transaction batching +> * Opening and processing of stores on initialisation is more robust and less error-prone to filename variations +> * Root statistic watching now works on all platforms +> * Multiple minor bug fixes and documentation improvements +> * Added `maxStoreLength` config to example app +> +> ## [7.1.2] - 2023/02/18 +> +> * Minor bug fixes +> +> ## [7.1.1] - 2023/02/16 +> +> * Major bug fixes +> * Added debug mode +> +> ## [7.1.0] - 2023/02/14 +> +> * Added URL query params obscurer feature +> * Added `headers` and `httpClient` parameters to `getTileProvider` +> * Minor documentation improvements +> * Minor bug fixes +> +> ## [7.0.2] - 2023/02/12 +> +> * Minor changes to example application +> +> ## [7.0.1] - 2023/02/11 +> +> * Minor bug fixes +> * Minor improvements +> +> ## [7.0.0] - 2023/02/04 +> +> * Migrated to Isar database +> * Major performance improvements, thanks to Isar +> * Added buffering to bulk tile downloading +> * Added method to catch tile retrieval errors +> * Removed v4 -> v5 migrator & added v6 -> v7 migrator +> * Removed some synchronous methods from structure management +> * Removed 'fmtc_advanced' import file +> +> Plus the usual: +> +> * Minor performance improvements +> * Bug fixes +> * Dependency updates +> * Documentation improvements ## [6.2.0] - 2022/10/25 diff --git a/example/pubspec.yaml b/example/pubspec.yaml index b94de352..14489ea5 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -18,7 +18,7 @@ dependencies: flutter_map: ^4.0.0 flutter_map_tile_caching: flutter_speed_dial: ^6.0.0 - fmtc_plus_background_downloading: ^7.0.0 + fmtc_plus_background_downloading: ^8.0.0 fmtc_plus_sharing: ^8.0.0 google_fonts: ^3.0.1 http: ^0.13.4 diff --git a/pubspec.yaml b/pubspec.yaml index 1419c9f5..79847382 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,7 +17,7 @@ platforms: windows: environment: - sdk: ">=2.17.0 <3.0.0" + sdk: ">=2.17.0 <4.0.0" flutter: ">=3.7.0" dependencies: From 6deec5247d3142e92c15c0c9abcaf735ea7b4df2 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Mon, 15 May 2023 20:54:43 +0100 Subject: [PATCH 011/168] Bumped version to v9 Multiple improvements: see CHANGELOG Former-commit-id: 4ee4cb56ad5da454c85a7e0011090666c74f9f5a [formerly 96d55e3b9e2b67be5d88d33e8ca64ac1df1e6fab] Former-commit-id: 0dcb92abf35b49fba1a2b2f91f406e4d76858b68 --- CHANGELOG.md | 10 +- example/android/app/build.gradle | 4 +- .../lib/screens/main/pages/map/map_view.dart | 2 +- .../recovery/components/recovery_list.dart | 17 +- .../components/recovery_start_button.dart | 8 +- .../settingsAndAbout/settings_and_about.dart | 2 +- example/pubspec.yaml | 13 +- lib/src/bulk_download/downloader.dart | 22 +- lib/src/bulk_download/tile_loops/count.dart | 203 ++++++++-------- .../bulk_download/tile_loops/generate.dart | 227 +++++++++--------- lib/src/bulk_download/tile_loops/shared.dart | 26 +- lib/src/db/defs/recovery.dart | 5 +- lib/src/providers/image_provider.dart | 42 ++-- lib/src/regions/base_region.dart | 25 +- lib/src/regions/circle.dart | 15 +- lib/src/regions/downloadable_region.dart | 127 +++++----- lib/src/regions/line.dart | 16 +- lib/src/regions/recovered_region.dart | 79 +++--- lib/src/regions/rectangle.dart | 33 +-- lib/src/root/recovery.dart | 26 +- lib/src/store/download.dart | 12 +- pubspec.yaml | 13 +- 22 files changed, 483 insertions(+), 444 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00fed48e..9d5ab134 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,12 +14,14 @@ Many thanks to my sponsors, no matter how much or how little they donated. Spons # Changelog -## [8.1.0] - 2023/05/11 +## [9.0.0] - 2023/XX/XX +* Added support for Flutter 3.10 and Dart 3 +* Added support for flutter_map v5 * Added `StoreManagement.pruneTilesOlderThan` method -* Example application updates - * Added support for Flutter 3.10 - * Removed auto-updater +* Replaced public facing `RegionType`/`type` with Dart 3 exhaustive switch statements through `BaseRegion/DownloadableRegion.when` & `RecoverableRegion.toRegion` +* Improved performance and fixed bugs +* Example application improvements ## [8.0.0] - 2023/05/05 diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 072775e5..f43b9091 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -13,12 +13,12 @@ if (flutterRoot == null) { def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { - flutterVersionCode = '8' + flutterVersionCode = '9' } def flutterVersionName = localProperties.getProperty('flutter.versionName') if (flutterVersionName == null) { - flutterVersionName = '8.1.0' + flutterVersionName = '9.0.0' } apply plugin: 'com.android.application' diff --git a/example/lib/screens/main/pages/map/map_view.dart b/example/lib/screens/main/pages/map/map_view.dart index 8e2eed54..76b053b7 100644 --- a/example/lib/screens/main/pages/map/map_view.dart +++ b/example/lib/screens/main/pages/map/map_view.dart @@ -78,7 +78,7 @@ class _MapPageState extends State { ), ), ) - : NetworkNoRetryTileProvider(), + : NetworkTileProvider(), maxZoom: 22, userAgentPackageName: 'dev.org.fmtc.example.app', panBuffer: 3, diff --git a/example/lib/screens/main/pages/recovery/components/recovery_list.dart b/example/lib/screens/main/pages/recovery/components/recovery_list.dart index e8950982..9dbbff67 100644 --- a/example/lib/screens/main/pages/recovery/components/recovery_list.dart +++ b/example/lib/screens/main/pages/recovery/components/recovery_list.dart @@ -24,6 +24,7 @@ class _RecoveryListState extends State { itemCount: widget.all.length, itemBuilder: (context, index) { final region = widget.all[index]; + return ListTile( leading: FutureBuilder( future: FMTC.instance.rootDirectory.recovery @@ -31,16 +32,20 @@ class _RecoveryListState extends State { builder: (context, isFailed) => Icon( isFailed.data != null ? Icons.warning - : region.type == RegionType.circle - ? Icons.circle_outlined - : region.type == RegionType.line - ? Icons.timeline - : Icons.rectangle_outlined, + : region.toRegion( + rectangle: (_) => Icons.rectangle_outlined, + circle: (_) => Icons.circle_outlined, + line: (_) => Icons.timeline, + ), color: isFailed.data != null ? Colors.red : null, ), ), title: Text( - '${region.storeName} - ${region.type.name[0].toUpperCase() + region.type.name.substring(1)} Type', + '${region.storeName} - ${region.toRegion( + rectangle: (_) => 'Rectangle', + circle: (_) => 'Circle', + line: (_) => 'Line', + )} Type', ), subtitle: FutureBuilder( future: Nominatim.reverseSearch( diff --git a/example/lib/screens/main/pages/recovery/components/recovery_start_button.dart b/example/lib/screens/main/pages/recovery/components/recovery_start_button.dart index 8eaa58fd..498473da 100644 --- a/example/lib/screens/main/pages/recovery/components/recovery_start_button.dart +++ b/example/lib/screens/main/pages/recovery/components/recovery_start_button.dart @@ -38,9 +38,11 @@ class RecoveryStartButton extends StatelessWidget { context, listen: false, ) - ..region = region - .toDownloadable(TileLayer()) - .originalRegion + ..region = region.toRegion( + rectangle: (r) => r, + circle: (c) => c, + line: (l) => l, + ) ..minZoom = region.minZoom ..maxZoom = region.maxZoom ..preventRedownload = region.preventRedownload diff --git a/example/lib/screens/main/pages/settingsAndAbout/settings_and_about.dart b/example/lib/screens/main/pages/settingsAndAbout/settings_and_about.dart index 8bf17519..4387eb8e 100644 --- a/example/lib/screens/main/pages/settingsAndAbout/settings_and_about.dart +++ b/example/lib/screens/main/pages/settingsAndAbout/settings_and_about.dart @@ -80,7 +80,7 @@ class _SettingsAndAboutPageState extends State { context: context, applicationName: 'FMTC Demo', applicationVersion: - 'for v8.1.0\n(on ${Platform().operatingSystemFormatted})', + 'for v9.0.0\n(on ${Platform().operatingSystemFormatted})', applicationIcon: Image.asset( 'assets/icons/ProjectIcon.png', height: 48, diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 14489ea5..e99bfcfd 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -3,10 +3,10 @@ description: The example application for 'flutter_map_tile_caching', showcasing it's functionality and use-cases. publish_to: "none" -version: 8.1.0 +version: 9.0.0 environment: - sdk: ">=2.18.0 <4.0.0" + sdk: ">=3.0.0 <4.0.0" flutter: ">=3.10.0" dependencies: @@ -14,13 +14,13 @@ dependencies: better_open_file: ^3.6.4 flutter: sdk: flutter - flutter_foreground_task: ^3.9.0 + flutter_foreground_task: ^4.1.0 flutter_map: ^4.0.0 flutter_map_tile_caching: flutter_speed_dial: ^6.0.0 fmtc_plus_background_downloading: ^8.0.0 fmtc_plus_sharing: ^8.0.0 - google_fonts: ^3.0.1 + google_fonts: ^4.0.4 http: ^0.13.4 intl: ^0.18.0 latlong2: ^0.8.1 @@ -32,9 +32,10 @@ dependencies: validators: ^3.0.0 version: ^3.0.2 -dev_dependencies: null - dependency_overrides: + flutter_map: + git: + url: https://github.com/fleaflet/flutter_map.git flutter_map_tile_caching: path: ../ diff --git a/lib/src/bulk_download/downloader.dart b/lib/src/bulk_download/downloader.dart index f8508977..a8bb187c 100644 --- a/lib/src/bulk_download/downloader.dart +++ b/lib/src/bulk_download/downloader.dart @@ -36,12 +36,12 @@ Future> bulkDownloader({ final recievePort = ReceivePort(); final tileIsolate = await Isolate.spawn( - region.type == RegionType.rectangle - ? TilesGenerator.rectangleTiles - : region.type == RegionType.circle - ? TilesGenerator.circleTiles - : TilesGenerator.lineTiles, - {'sendPort': recievePort.sendPort, ...generateTileLoopsInput(region)}, + region.when( + rectangle: (_) => TilesGenerator.rectangleTiles, + circle: (_) => TilesGenerator.circleTiles, + line: (_) => TilesGenerator.lineTiles, + ), + (sendPort: recievePort.sendPort, region: region), onExit: recievePort.sendPort, ); final tileQueue = StreamQueue( @@ -54,7 +54,7 @@ Future> bulkDownloader({ final requestTilePort = (await tileQueue.next) as SendPort; final threadCompleters = List.generate( - region.parallelThreads + 1, + region.parallelThreads, (_) => Completer(), growable: false, ); @@ -64,9 +64,9 @@ Future> bulkDownloader({ while (true) { requestTilePort.send(null); - final List? value; + final (int, int, int)? coords; try { - value = await tileQueue.next; + coords = await tileQueue.next; // ignore: avoid_catching_errors } on StateError { threadCompleter.complete(); @@ -86,7 +86,7 @@ Future> bulkDownloader({ break; } - if (value == null) { + if (coords == null) { await tileQueue.cancel(); threadCompleter.complete(); @@ -100,7 +100,7 @@ Future> bulkDownloader({ break; } - final coord = TileCoordinates(value[0], value[1], value[2]); + final coord = TileCoordinates(coords.$1, coords.$2, coords.$3); final url = provider.getTileUrl(coord, region.options); final existingTile = await tiles.tiles.get(DatabaseTools.hash(url)); diff --git a/lib/src/bulk_download/tile_loops/count.dart b/lib/src/bulk_download/tile_loops/count.dart index b3bea82f..ebc81689 100644 --- a/lib/src/bulk_download/tile_loops/count.dart +++ b/lib/src/bulk_download/tile_loops/count.dart @@ -4,32 +4,36 @@ part of 'shared.dart'; class TilesCounter { - static int rectangleTiles(Map input) { - final LatLngBounds bounds = input['rectOutline']; - final int minZoom = input['minZoom']; - final int maxZoom = input['maxZoom']; - final Crs crs = input['crs']; - final CustomPoint tileSize = input['tileSize']; - - int numberOfTiles = 0; - for (int zoomLvl = minZoom; zoomLvl <= maxZoom; zoomLvl++) { - final CustomPoint nwCustomPoint = crs - .latLngToPoint(bounds.northWest, zoomLvl.toDouble()) + static int rectangleTiles(DownloadableRegion region) { + final tileSize = _getTileSize(region); + final northWest = + (region.originalRegion as RectangleRegion).bounds.northWest; + final southEast = + (region.originalRegion as RectangleRegion).bounds.southEast; + + var numberOfTiles = 0; + + for (double zoomLvl = region.minZoom.toDouble(); + zoomLvl <= region.maxZoom; + zoomLvl++) { + final nwCustomPoint = region.crs + .latLngToPoint(northWest, zoomLvl) .unscaleBy(tileSize) .floor(); - final CustomPoint seCustomPoint = crs - .latLngToPoint(bounds.southEast, zoomLvl.toDouble()) + final seCustomPoint = region.crs + .latLngToPoint(southEast, zoomLvl) .unscaleBy(tileSize) .ceil() - const CustomPoint(1, 1); - numberOfTiles += (seCustomPoint.x - nwCustomPoint.x + 1).toInt() * - (seCustomPoint.y - nwCustomPoint.y + 1).toInt(); + numberOfTiles += (seCustomPoint.x - nwCustomPoint.x + 1) * + (seCustomPoint.y - nwCustomPoint.y + 1); } + return numberOfTiles; } - static int circleTiles(Map input) { + static int circleTiles(DownloadableRegion region) { // This took some time and is fairly complicated, so this is the overall explanation: // 1. Given a `LatLng` for every x degrees on a circle's circumference, convert it into a tile number // 2. Using a `Map` per zoom level, record all the X values in it without duplicates @@ -37,42 +41,35 @@ class TilesCounter { // 4. Loop over these XY values and add them to the list // Theoretically, this could have been done using the same method as `lineTiles`, but `lineTiles` was built after this algorithm and this makes more sense for a circle - final List circleOutline = input['circleOutline']; - final int minZoom = input['minZoom']; - final int maxZoom = input['maxZoom']; - final Crs crs = input['crs']; - final CustomPoint tileSize = input['tileSize']; + final tileSize = _getTileSize(region); + final circleOutline = (region.originalRegion as CircleRegion).toOutline(); // Format: Map>> - final Map>> outlineTileNums = {}; + final outlineTileNums = >>{}; int numberOfTiles = 0; - for (int zoomLvl = minZoom; zoomLvl <= maxZoom; zoomLvl++) { - outlineTileNums[zoomLvl] = >{}; + for (int zoomLvl = region.minZoom; zoomLvl <= region.maxZoom; zoomLvl++) { + outlineTileNums[zoomLvl] = {}; - for (final LatLng node in circleOutline) { - final CustomPoint tile = crs + for (final node in circleOutline) { + final tile = region.crs .latLngToPoint(node, zoomLvl.toDouble()) .unscaleBy(tileSize) .floor(); - outlineTileNums[zoomLvl]![tile.x.toInt()] ??= [ - 9223372036854775807, - -9223372036854775808, - ]; - - outlineTileNums[zoomLvl]![tile.x.toInt()] = [ - tile.y < outlineTileNums[zoomLvl]![tile.x.toInt()]![0] - ? tile.y.toInt() - : outlineTileNums[zoomLvl]![tile.x.toInt()]![0], - tile.y > outlineTileNums[zoomLvl]![tile.x.toInt()]![1] - ? tile.y.toInt() - : outlineTileNums[zoomLvl]![tile.x.toInt()]![1], + outlineTileNums[zoomLvl]![tile.x] ??= [_largestInt, _smallestInt]; + outlineTileNums[zoomLvl]![tile.x] = [ + tile.y < outlineTileNums[zoomLvl]![tile.x]![0] + ? tile.y + : outlineTileNums[zoomLvl]![tile.x]![0], + tile.y > outlineTileNums[zoomLvl]![tile.x]![1] + ? tile.y + : outlineTileNums[zoomLvl]![tile.x]![1], ]; } - for (final int x in outlineTileNums[zoomLvl]!.keys) { + for (final x in outlineTileNums[zoomLvl]!.keys) { numberOfTiles += outlineTileNums[zoomLvl]![x]![1] - outlineTileNums[zoomLvl]![x]![0] + 1; @@ -82,7 +79,7 @@ class TilesCounter { return numberOfTiles; } - static int lineTiles(Map input) { + static int lineTiles(DownloadableRegion region) { // This took some time and is fairly complicated, so this is the overall explanation: // 1. Given 4 `LatLng` points, create a 'straight' rectangle around the 'rotated' rectangle, that can be defined with just 2 `LatLng` points // 2. Convert the straight rectangle into tile numbers, and loop through the same as `rectangleTiles` @@ -96,31 +93,26 @@ class TilesCounter { final _Polygon polygon = x == 0 ? a : b; for (int i1 = 0; i1 < polygon.points.length; i1++) { - final int i2 = (i1 + 1) % polygon.points.length; - final CustomPoint p1 = polygon.points[i1]; - final CustomPoint p2 = polygon.points[i2]; - - final CustomPoint normal = - CustomPoint(p2.y - p1.y, p1.x - p2.x); - - double minA = double.infinity; - double maxA = double.negativeInfinity; - - for (final CustomPoint p in a.points) { - final num projected = normal.x * p.x + normal.y * p.y; - - if (projected < minA) minA = projected.toDouble(); - if (projected > maxA) maxA = projected.toDouble(); + final i2 = (i1 + 1) % polygon.points.length; + final p1 = polygon.points[i1]; + final p2 = polygon.points[i2]; + + final normal = CustomPoint(p2.y - p1.y, p1.x - p2.x); + + var minA = _largestInt; + var maxA = _smallestInt; + for (final p in a.points) { + final projected = normal.x * p.x + normal.y * p.y; + if (projected < minA) minA = projected; + if (projected > maxA) maxA = projected; } - double minB = double.infinity; - double maxB = double.negativeInfinity; - - for (final CustomPoint p in b.points) { - final num projected = normal.x * p.x + normal.y * p.y; - - if (projected < minB) minB = projected.toDouble(); - if (projected > maxB) maxB = projected.toDouble(); + var minB = _largestInt; + var maxB = _smallestInt; + for (final p in b.points) { + final projected = normal.x * p.x + normal.y * p.y; + if (projected < minB) minB = projected; + if (projected > maxB) maxB = projected; } if (maxA < minB || maxB < minA) return false; @@ -130,79 +122,80 @@ class TilesCounter { return true; } - final List> rects = input['lineOutline']; - final int minZoom = input['minZoom']; - final int maxZoom = input['maxZoom']; - final Crs crs = input['crs']; - final CustomPoint tileSize = input['tileSize']; + final tileSize = _getTileSize(region); + final lineOutline = (region.originalRegion as LineRegion).toOutlines(1); int numberOfTiles = 0; - for (int zoomLvl = minZoom; zoomLvl <= maxZoom; zoomLvl++) { - for (final List rect in rects) { - final LatLng rrBottomLeft = rect[0]; - final LatLng rrBottomRight = rect[1]; - final LatLng rrTopRight = rect[2]; - final LatLng rrTopLeft = rect[3]; - - final List rrAllLat = [ - rrTopLeft.latitude, - rrTopRight.latitude, - rrBottomLeft.latitude, - rrBottomRight.latitude, + for (double zoomLvl = region.minZoom.toDouble(); + zoomLvl <= region.maxZoom; + zoomLvl++) { + for (final rect in lineOutline) { + final rotatedRectangle = ( + bottomLeft: rect[0], + bottomRight: rect[1], + topRight: rect[2], + topLeft: rect[3], + ); + + final rotatedRectangleLats = [ + rotatedRectangle.topLeft.latitude, + rotatedRectangle.topRight.latitude, + rotatedRectangle.bottomLeft.latitude, + rotatedRectangle.bottomRight.latitude, ]; - final List rrAllLon = [ - rrTopLeft.longitude, - rrTopRight.longitude, - rrBottomLeft.longitude, - rrBottomRight.longitude, + final rotatedRectangleLngs = [ + rotatedRectangle.topLeft.longitude, + rotatedRectangle.topRight.longitude, + rotatedRectangle.bottomLeft.longitude, + rotatedRectangle.bottomRight.longitude, ]; - final CustomPoint rrNorthWest = crs - .latLngToPoint(rrTopLeft, zoomLvl.toDouble()) + final rotatedRectangleNW = region.crs + .latLngToPoint(rotatedRectangle.topLeft, zoomLvl) .unscaleBy(tileSize) .floor(); - final CustomPoint rrNorthEast = crs - .latLngToPoint(rrTopRight, zoomLvl.toDouble()) + final rotatedRectangleNE = region.crs + .latLngToPoint(rotatedRectangle.topRight, zoomLvl) .unscaleBy(tileSize) .ceil() - const CustomPoint(1, 0); - final CustomPoint rrSouthWest = crs - .latLngToPoint(rrBottomLeft, zoomLvl.toDouble()) + final rotatedRectangleSW = region.crs + .latLngToPoint(rotatedRectangle.bottomLeft, zoomLvl) .unscaleBy(tileSize) .ceil() - const CustomPoint(0, 1); - final CustomPoint rrSouthEast = crs - .latLngToPoint(rrBottomRight, zoomLvl.toDouble()) + final rotatedRectangleSE = region.crs + .latLngToPoint(rotatedRectangle.bottomRight, zoomLvl) .unscaleBy(tileSize) .ceil() - const CustomPoint(1, 1); - final CustomPoint srNorthWest = crs + final straightRectangleNW = region.crs .latLngToPoint( - LatLng(rrAllLat.max, rrAllLon.min), - zoomLvl.toDouble(), + LatLng(rotatedRectangleLats.max, rotatedRectangleLngs.min), + zoomLvl, ) .unscaleBy(tileSize) .floor(); - final CustomPoint srSouthEast = crs + final straightRectangleSE = region.crs .latLngToPoint( - LatLng(rrAllLat.min, rrAllLon.max), - zoomLvl.toDouble(), + LatLng(rotatedRectangleLats.min, rotatedRectangleLngs.max), + zoomLvl, ) .unscaleBy(tileSize) .ceil() - const CustomPoint(1, 1); - for (num x = srNorthWest.x; x <= srSouthEast.x; x++) { + for (int x = straightRectangleNW.x; x <= straightRectangleSE.x; x++) { bool foundOverlappingTile = false; - for (num y = srNorthWest.y; y <= srSouthEast.y; y++) { + for (int y = straightRectangleNW.y; y <= straightRectangleSE.y; y++) { if (overlap( _Polygon( - rrNorthWest, - rrNorthEast, - rrSouthEast, - rrSouthWest, + rotatedRectangleNW, + rotatedRectangleNE, + rotatedRectangleSE, + rotatedRectangleSW, ), _Polygon( CustomPoint(x, y), diff --git a/lib/src/bulk_download/tile_loops/generate.dart b/lib/src/bulk_download/tile_loops/generate.dart index 01f3cb08..d5b74831 100644 --- a/lib/src/bulk_download/tile_loops/generate.dart +++ b/lib/src/bulk_download/tile_loops/generate.dart @@ -4,33 +4,37 @@ part of 'shared.dart'; class TilesGenerator { - static Future rectangleTiles(Map input) async { - final SendPort sendPort = input['sendPort']; - final LatLngBounds bounds = input['rectOutline']; - final int minZoom = input['minZoom']; - final int maxZoom = input['maxZoom']; - final Crs crs = input['crs']; - final CustomPoint tileSize = input['tileSize']; + static Future rectangleTiles( + ({SendPort sendPort, DownloadableRegion region}) input, + ) async { + final region = input.region; + final tileSize = _getTileSize(region); + final northWest = + (region.originalRegion as RectangleRegion).bounds.northWest; + final southEast = + (region.originalRegion as RectangleRegion).bounds.southEast; final recievePort = ReceivePort(); - sendPort.send(recievePort.sendPort); + input.sendPort.send(recievePort.sendPort); final requestQueue = StreamQueue(recievePort); - for (int zoomLvl = minZoom; zoomLvl <= maxZoom; zoomLvl++) { - final CustomPoint nwCustomPoint = crs - .latLngToPoint(bounds.northWest, zoomLvl.toDouble()) + for (double zoomLvl = region.minZoom.toDouble(); + zoomLvl <= region.maxZoom; + zoomLvl++) { + final nwCustomPoint = region.crs + .latLngToPoint(northWest, zoomLvl) .unscaleBy(tileSize) .floor(); - final CustomPoint seCustomPoint = crs - .latLngToPoint(bounds.southEast, zoomLvl.toDouble()) + final seCustomPoint = region.crs + .latLngToPoint(southEast, zoomLvl) .unscaleBy(tileSize) .ceil() - const CustomPoint(1, 1); - for (num x = nwCustomPoint.x; x <= seCustomPoint.x; x++) { - for (num y = nwCustomPoint.y; y <= seCustomPoint.y; y++) { + for (int x = nwCustomPoint.x; x <= seCustomPoint.x; x++) { + for (int y = nwCustomPoint.y; y <= seCustomPoint.y; y++) { await requestQueue.next; - sendPort.send([x.toInt(), y.toInt(), zoomLvl]); + input.sendPort.send((x, y, zoomLvl.toInt())); } } } @@ -38,7 +42,9 @@ class TilesGenerator { Isolate.exit(); } - static Future circleTiles(Map input) async { + static Future circleTiles( + ({SendPort sendPort, DownloadableRegion region}) input, + ) async { // This took some time and is fairly complicated, so this is the overall explanation: // 1. Given a `LatLng` for every x degrees on a circle's circumference, convert it into a tile number // 2. Using a `Map` per zoom level, record all the X values in it without duplicates @@ -46,50 +52,43 @@ class TilesGenerator { // 4. Loop over these XY values and add them to the list // Theoretically, this could have been done using the same method as `lineTiles`, but `lineTiles` was built after this algorithm and this makes more sense for a circle - final SendPort sendPort = input['sendPort']; - final List circleOutline = input['circleOutline']; - final int minZoom = input['minZoom']; - final int maxZoom = input['maxZoom']; - final Crs crs = input['crs']; - final CustomPoint tileSize = input['tileSize']; + final region = input.region; + final tileSize = _getTileSize(region); + final circleOutline = (region.originalRegion as CircleRegion).toOutline(); final recievePort = ReceivePort(); - sendPort.send(recievePort.sendPort); + input.sendPort.send(recievePort.sendPort); final requestQueue = StreamQueue(recievePort); // Format: Map>> final Map>> outlineTileNums = {}; - for (int zoomLvl = minZoom; zoomLvl <= maxZoom; zoomLvl++) { - outlineTileNums[zoomLvl] = >{}; + for (int zoomLvl = region.minZoom; zoomLvl <= region.maxZoom; zoomLvl++) { + outlineTileNums[zoomLvl] = {}; - for (final LatLng node in circleOutline) { - final CustomPoint tile = crs + for (final node in circleOutline) { + final tile = region.crs .latLngToPoint(node, zoomLvl.toDouble()) .unscaleBy(tileSize) .floor(); - outlineTileNums[zoomLvl]![tile.x.toInt()] ??= [ - 9223372036854775807, - -9223372036854775808, - ]; - - outlineTileNums[zoomLvl]![tile.x.toInt()] = [ - tile.y < outlineTileNums[zoomLvl]![tile.x.toInt()]![0] - ? tile.y.toInt() - : outlineTileNums[zoomLvl]![tile.x.toInt()]![0], - tile.y > outlineTileNums[zoomLvl]![tile.x.toInt()]![1] - ? tile.y.toInt() - : outlineTileNums[zoomLvl]![tile.x.toInt()]![1], + outlineTileNums[zoomLvl]![tile.x] ??= [_largestInt, _smallestInt]; + outlineTileNums[zoomLvl]![tile.x] = [ + tile.y < outlineTileNums[zoomLvl]![tile.x]![0] + ? tile.y + : outlineTileNums[zoomLvl]![tile.x]![0], + tile.y > outlineTileNums[zoomLvl]![tile.x]![1] + ? tile.y + : outlineTileNums[zoomLvl]![tile.x]![1], ]; } - for (final int x in outlineTileNums[zoomLvl]!.keys) { + for (final x in outlineTileNums[zoomLvl]!.keys) { for (int y = outlineTileNums[zoomLvl]![x]![0]; y <= outlineTileNums[zoomLvl]![x]![1]; y++) { await requestQueue.next; - sendPort.send([x, y, zoomLvl]); + input.sendPort.send((x, y, zoomLvl)); } } } @@ -97,7 +96,9 @@ class TilesGenerator { Isolate.exit(); } - static Future lineTiles(Map input) async { + static Future lineTiles( + ({SendPort sendPort, DownloadableRegion region}) input, + ) async { // This took some time and is fairly complicated, so this is the overall explanation: // 1. Given 4 `LatLng` points, create a 'straight' rectangle around the 'rotated' rectangle, that can be defined with just 2 `LatLng` points // 2. Convert the straight rectangle into tile numbers, and loop through the same as `rectangleTiles` @@ -111,31 +112,26 @@ class TilesGenerator { final _Polygon polygon = x == 0 ? a : b; for (int i1 = 0; i1 < polygon.points.length; i1++) { - final int i2 = (i1 + 1) % polygon.points.length; - final CustomPoint p1 = polygon.points[i1]; - final CustomPoint p2 = polygon.points[i2]; - - final CustomPoint normal = - CustomPoint(p2.y - p1.y, p1.x - p2.x); - - double minA = double.infinity; - double maxA = double.negativeInfinity; - - for (final CustomPoint p in a.points) { - final num projected = normal.x * p.x + normal.y * p.y; - - if (projected < minA) minA = projected.toDouble(); - if (projected > maxA) maxA = projected.toDouble(); + final i2 = (i1 + 1) % polygon.points.length; + final p1 = polygon.points[i1]; + final p2 = polygon.points[i2]; + + final normal = CustomPoint(p2.y - p1.y, p1.x - p2.x); + + var minA = _largestInt; + var maxA = _smallestInt; + for (final p in a.points) { + final projected = normal.x * p.x + normal.y * p.y; + if (projected < minA) minA = projected; + if (projected > maxA) maxA = projected; } - double minB = double.infinity; - double maxB = double.negativeInfinity; - - for (final CustomPoint p in b.points) { - final num projected = normal.x * p.x + normal.y * p.y; - - if (projected < minB) minB = projected.toDouble(); - if (projected > maxB) maxB = projected.toDouble(); + var minB = _largestInt; + var maxB = _smallestInt; + for (final p in b.points) { + final projected = normal.x * p.x + normal.y * p.y; + if (projected < minB) minB = projected; + if (projected > maxB) maxB = projected; } if (maxA < minB || maxB < minA) return false; @@ -145,72 +141,83 @@ class TilesGenerator { return true; } - final SendPort sendPort = input['sendPort']; - final List> rects = input['lineOutline']; - final int minZoom = input['minZoom']; - final int maxZoom = input['maxZoom']; - final Crs crs = input['crs']; - final CustomPoint tileSize = input['tileSize']; + final region = input.region; + final tileSize = _getTileSize(region); + final lineOutline = (region.originalRegion as LineRegion).toOutlines(1); final recievePort = ReceivePort(); - sendPort.send(recievePort.sendPort); + input.sendPort.send(recievePort.sendPort); final requestQueue = StreamQueue(recievePort); - for (double zoomLvl = minZoom.toDouble(); zoomLvl <= maxZoom; zoomLvl++) { - for (final List rect in rects) { - final LatLng rrBottomLeft = rect[0]; - final LatLng rrBottomRight = rect[1]; - final LatLng rrTopRight = rect[2]; - final LatLng rrTopLeft = rect[3]; - - final List rrAllLat = [ - rrTopLeft.latitude, - rrTopRight.latitude, - rrBottomLeft.latitude, - rrBottomRight.latitude, + for (double zoomLvl = region.minZoom.toDouble(); + zoomLvl <= region.maxZoom; + zoomLvl++) { + for (final rect in lineOutline) { + final rotatedRectangle = ( + bottomLeft: rect[0], + bottomRight: rect[1], + topRight: rect[2], + topLeft: rect[3], + ); + + final rotatedRectangleLats = [ + rotatedRectangle.topLeft.latitude, + rotatedRectangle.topRight.latitude, + rotatedRectangle.bottomLeft.latitude, + rotatedRectangle.bottomRight.latitude, ]; - final List rrAllLon = [ - rrTopLeft.longitude, - rrTopRight.longitude, - rrBottomLeft.longitude, - rrBottomRight.longitude, + final rotatedRectangleLngs = [ + rotatedRectangle.topLeft.longitude, + rotatedRectangle.topRight.longitude, + rotatedRectangle.bottomLeft.longitude, + rotatedRectangle.bottomRight.longitude, ]; - final CustomPoint rrNorthWest = - crs.latLngToPoint(rrTopLeft, zoomLvl).unscaleBy(tileSize).floor(); - final CustomPoint rrNorthEast = - crs.latLngToPoint(rrTopRight, zoomLvl).unscaleBy(tileSize).ceil() - - const CustomPoint(1, 0); - final CustomPoint rrSouthWest = crs - .latLngToPoint(rrBottomLeft, zoomLvl) + final rotatedRectangleNW = region.crs + .latLngToPoint(rotatedRectangle.topLeft, zoomLvl) + .unscaleBy(tileSize) + .floor(); + final rotatedRectangleNE = region.crs + .latLngToPoint(rotatedRectangle.topRight, zoomLvl) + .unscaleBy(tileSize) + .ceil() - + const CustomPoint(1, 0); + final rotatedRectangleSW = region.crs + .latLngToPoint(rotatedRectangle.bottomLeft, zoomLvl) .unscaleBy(tileSize) .ceil() - const CustomPoint(0, 1); - final CustomPoint rrSouthEast = crs - .latLngToPoint(rrBottomRight, zoomLvl) + final rotatedRectangleSE = region.crs + .latLngToPoint(rotatedRectangle.bottomRight, zoomLvl) .unscaleBy(tileSize) .ceil() - const CustomPoint(1, 1); - final CustomPoint srNorthWest = crs - .latLngToPoint(LatLng(rrAllLat.max, rrAllLon.min), zoomLvl) + final straightRectangleNW = region.crs + .latLngToPoint( + LatLng(rotatedRectangleLats.max, rotatedRectangleLngs.min), + zoomLvl, + ) .unscaleBy(tileSize) .floor(); - final CustomPoint srSouthEast = crs - .latLngToPoint(LatLng(rrAllLat.min, rrAllLon.max), zoomLvl) + final straightRectangleSE = region.crs + .latLngToPoint( + LatLng(rotatedRectangleLats.min, rotatedRectangleLngs.max), + zoomLvl, + ) .unscaleBy(tileSize) .ceil() - const CustomPoint(1, 1); - for (num x = srNorthWest.x; x <= srSouthEast.x; x++) { + for (int x = straightRectangleNW.x; x <= straightRectangleSE.x; x++) { bool foundOverlappingTile = false; - for (num y = srNorthWest.y; y <= srSouthEast.y; y++) { + for (int y = straightRectangleNW.y; y <= straightRectangleSE.y; y++) { if (overlap( _Polygon( - rrNorthWest, - rrNorthEast, - rrSouthEast, - rrSouthWest, + rotatedRectangleNW, + rotatedRectangleNE, + rotatedRectangleSE, + rotatedRectangleSW, ), _Polygon( CustomPoint(x, y), @@ -220,7 +227,7 @@ class TilesGenerator { ), )) { await requestQueue.next; - sendPort.send([x.toInt(), y.toInt(), zoomLvl]); + input.sendPort.send((x, y, zoomLvl.toInt())); foundOverlappingTile = true; } else if (foundOverlappingTile) { break; diff --git a/lib/src/bulk_download/tile_loops/shared.dart b/lib/src/bulk_download/tile_loops/shared.dart index 80dccc2c..46dc49a9 100644 --- a/lib/src/bulk_download/tile_loops/shared.dart +++ b/lib/src/bulk_download/tile_loops/shared.dart @@ -8,7 +8,6 @@ import 'package:collection/collection.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/flutter_map.dart' hide Polygon; import 'package:latlong2/latlong.dart'; -import 'package:meta/meta.dart'; import '../../../flutter_map_tile_caching.dart'; @@ -16,23 +15,18 @@ part 'count.dart'; part 'generate.dart'; class _Polygon { - final CustomPoint nw; - final CustomPoint ne; - final CustomPoint se; - final CustomPoint sw; + final CustomPoint nw; + final CustomPoint ne; + final CustomPoint se; + final CustomPoint sw; _Polygon(this.nw, this.ne, this.se, this.sw); - List> get points => [nw, ne, se, sw]; + List> get points => [nw, ne, se, sw]; } -@internal -Map generateTileLoopsInput(DownloadableRegion region) => { - 'rectOutline': LatLngBounds.fromPoints(region.points.cast()), - 'circleOutline': region.points, - 'lineOutline': region.points.slices(4).toList(), - 'minZoom': region.minZoom, - 'maxZoom': region.maxZoom, - 'crs': region.crs, - 'tileSize': CustomPoint(region.options.tileSize, region.options.tileSize), - }; +const _largestInt = 9223372036854775807; +const _smallestInt = -9223372036854775808; + +CustomPoint _getTileSize(DownloadableRegion region) => + CustomPoint(region.options.tileSize, region.options.tileSize); diff --git a/lib/src/db/defs/recovery.dart b/lib/src/db/defs/recovery.dart index e58c169d..14079ab4 100644 --- a/lib/src/db/defs/recovery.dart +++ b/lib/src/db/defs/recovery.dart @@ -4,10 +4,11 @@ import 'package:isar/isar.dart'; import 'package:meta/meta.dart'; -import '../../../flutter_map_tile_caching.dart'; - part 'recovery.g.dart'; +@internal +enum RegionType { rectangle, circle, line } + @internal @Collection(accessor: 'recovery') class DbRecoverableRegion { diff --git a/lib/src/providers/image_provider.dart b/lib/src/providers/image_provider.dart index b1fba86d..dd51e697 100644 --- a/lib/src/providers/image_provider.dart +++ b/lib/src/providers/image_provider.dart @@ -19,10 +19,6 @@ import '../db/registry.dart'; import '../db/tools.dart'; /// A specialised [ImageProvider] dedicated to 'flutter_map_tile_caching' -/// -/// TODO: When v9 is released with Isar v4, bump to minimum Dart 3 and -/// Flutter 3.10, then replace deprecated methods with [loadImage], as in -/// https://github.com/JaffaKetchup/flutter_map_tile_caching/blob/57eb7c03413c45071944d99a70770b9050fde942/lib/src/providers/image_provider.dart class FMTCImageProvider extends ImageProvider { /// An instance of the [FMTCTileProvider] in use final FMTCTileProvider provider; @@ -51,38 +47,32 @@ class FMTCImageProvider extends ImageProvider { required this.directory, }) : db = FMTCRegistry.instance(provider.storeDirectory.storeName); - // TODO: When v9 is released with Isar v4, bump to minimum Dart 3 and - // Flutter 3.10, then replace deprecated methods with [loadImage], as in - // https://github.com/JaffaKetchup/flutter_map_tile_caching/blob/57eb7c03413c45071944d99a70770b9050fde942/lib/src/providers/image_provider.dart - @override - ImageStreamCompleter loadBuffer( + ImageStreamCompleter loadImage( FMTCImageProvider key, - // ignore: deprecated_member_use - DecoderBufferCallback decode, + ImageDecoderCallback decode, ) { - // ignore: close_sinks - final StreamController chunkEvents = - StreamController(); - + final chunkEvents = StreamController(); return MultiFrameImageStreamCompleter( - codec: _loadAsync(key: key, decode: decode, chunkEvents: chunkEvents), + codec: _loadAsync(key, chunkEvents, decode), chunkEvents: chunkEvents.stream, scale: 1, debugLabel: coords.toString(), - informationCollector: () => [DiagnosticsProperty('Coordinates', coords)], + informationCollector: () => [ + DiagnosticsProperty('Tile coordinates', coords), + DiagnosticsProperty('Root directory', directory), + DiagnosticsProperty('Store name', provider.storeDirectory.storeName), + DiagnosticsProperty('Current provider', key), + ], ); } - Future _loadAsync({ - required FMTCImageProvider key, - // ignore: deprecated_member_use - required DecoderBufferCallback decode, - required StreamController chunkEvents, - }) async { - Future cacheHitMiss({ - required bool hit, - }) => + Future _loadAsync( + FMTCImageProvider key, + StreamController chunkEvents, + ImageDecoderCallback decode, + ) async { + Future cacheHitMiss({required bool hit}) => (hit ? _cacheHitsQueue : _cacheMissesQueue).add(() async { if (db.isOpen) { await db.writeTxn(() async { diff --git a/lib/src/regions/base_region.dart b/lib/src/regions/base_region.dart index fce7a24a..f5f18a7a 100644 --- a/lib/src/regions/base_region.dart +++ b/lib/src/regions/base_region.dart @@ -14,7 +14,7 @@ part of flutter_map_tile_caching; /// - [RectangleRegion] /// - [CircleRegion] /// - [LineRegion] -abstract class BaseRegion { +sealed class BaseRegion { /// Create a geographical region that forms a particular shape /// /// It can be converted to a: @@ -43,6 +43,18 @@ abstract class BaseRegion { /// versions._ final String? name; + /// Output a value of type [T] dependent on `this` and its type + T when({ + required T Function(RectangleRegion rectangle) rectangle, + required T Function(CircleRegion circle) circle, + required T Function(LineRegion line) line, + }) => + switch (this) { + RectangleRegion() => rectangle(this as RectangleRegion), + CircleRegion() => circle(this as CircleRegion), + LineRegion() => line(this as LineRegion), + }; + /// Generate the [DownloadableRegion] ready for bulk downloading /// /// For more information see [DownloadableRegion]'s documentation. @@ -71,4 +83,15 @@ abstract class BaseRegion { /// /// Returns a `List` which can be used anywhere. List toOutline(); + + @override + @mustCallSuper + @mustBeOverridden + bool operator ==(Object other) => + identical(this, other) || (other is BaseRegion && other.name == name); + + @override + @mustBeOverridden + @mustCallSuper + int get hashCode => name.hashCode; } diff --git a/lib/src/regions/circle.dart b/lib/src/regions/circle.dart index f3cd487c..1ab365ae 100644 --- a/lib/src/regions/circle.dart +++ b/lib/src/regions/circle.dart @@ -42,12 +42,10 @@ class CircleRegion extends BaseRegion { void Function(Object?)? errorHandler, }) => DownloadableRegion._( - points: toOutline(), + this, minZoom: minZoom, maxZoom: maxZoom, options: options, - type: RegionType.circle, - originalRegion: this, parallelThreads: parallelThreads, preventRedownload: preventRedownload, seaTileRemoval: seaTileRemoval, @@ -113,4 +111,15 @@ class CircleRegion extends BaseRegion { return output; } + + @override + bool operator ==(Object other) => + identical(this, other) || + (other is CircleRegion && + other.center == center && + other.radius == radius && + super == other); + + @override + int get hashCode => Object.hashAllUnordered([center, radius, super.hashCode]); } diff --git a/lib/src/regions/downloadable_region.dart b/lib/src/regions/downloadable_region.dart index 2822e017..aa376aa0 100644 --- a/lib/src/regions/downloadable_region.dart +++ b/lib/src/regions/downloadable_region.dart @@ -3,32 +3,12 @@ part of flutter_map_tile_caching; -/// Describes what shape, and therefore rules, a [DownloadableRegion] conforms to -enum RegionType { - /// A region containing 2 points representing the top-left and bottom-right corners of a rectangle - rectangle, - - /// A region containing all the points along it's outline (one every degree) representing a circle - circle, - - /// A region with the border as the loci of a line at it's center representing multiple diagonal rectangles - line, -} - /// A downloadable region to be passed to bulk download functions /// -/// Should avoid manual construction. Use a supported region shape and the `.toDownloadable()` extension on it. -/// -/// Is returned from `.toDownloadable()`. -class DownloadableRegion

> { - /// The shape that this region conforms to - final RegionType type; - - /// The original [BaseRegion], used internally for recovery purposes - final BaseRegion originalRegion; - - /// All the vertices on the outline of a polygon - final P points; +/// Construct via [BaseRegion.toDownloadable]. +class DownloadableRegion { + /// A copy of the [BaseRegion] used to form this object + final R originalRegion; /// The minimum zoom level to fetch tiles for final int minZoom; @@ -41,7 +21,9 @@ class DownloadableRegion

> { /// The number of download threads allowed to run simultaneously /// - /// This will significantly increase speed, at the expense of faster battery drain. Note that some servers may forbid multithreading, in which case this should be set to 1, unless another limit is specified. + /// This will significantly increase speed, at the expense of faster battery + /// drain. Note that some servers may forbid multithreading, in which case this + /// should be set to 1, unless another limit is specified. /// /// Set to 1 to disable multithreading. Defaults to 10. final int parallelThreads; @@ -53,11 +35,18 @@ class DownloadableRegion

> { /// Whether to remove tiles that are entirely sea /// - /// The checks are conducted by comparing the bytes of the tile at x:0, y:0, and z:19 to the bytes of the currently downloading tile. If they match, the tile is deleted, otherwise the tile is kept. + /// The checks are conducted by comparing the bytes of the tile at x:0, y:0, + /// and z:19 to the bytes of the currently downloading tile. If they match, the + /// tile is deleted, otherwise the tile is kept. /// - /// This option is therefore not supported when using satellite tiles (because of the variations from tile to tile), on maps where the tile 0/0/19 is not entirely sea, or on servers where zoom level 19 is not supported. If not supported, set this to `false` to avoid wasting unnecessary time and to avoid errors. + /// This option is therefore not supported when using satellite tiles (because + /// of the variations from tile to tile), on maps where the tile 0/0/19 is not + /// entirely sea, or on servers where zoom level 19 is not supported. If not + /// supported, set this to `false` to avoid wasting unnecessary time and to + /// avoid errors. /// - /// This is a storage saving feature, not a time saving or data saving feature: tiles still have to be fully downloaded before they can be checked. + /// This is a storage saving feature, not a time saving or data saving feature: + /// tiles still have to be fully downloaded before they can be checked. /// /// Set to `false` to keep sea tiles, which is the default. final bool seaTileRemoval; @@ -72,19 +61,18 @@ class DownloadableRegion

> { /// Set to `null` to skip none, which is the default. final int? end; - /// The map projection to use to calculate tiles. Defaults to `Espg3857()`. + /// The map projection to use to calculate tiles. Defaults to [Epsg3857]. final Crs crs; - /// A function that takes any type of error as an argument to be called in the event a tile fetch fails + /// A function that takes any type of error as an argument to be called in the + /// event a tile fetch fails final void Function(Object?)? errorHandler; - DownloadableRegion._({ - required this.points, + DownloadableRegion._( + this.originalRegion, { required this.minZoom, required this.maxZoom, required this.options, - required this.type, - required this.originalRegion, required this.parallelThreads, required this.preventRedownload, required this.seaTileRemoval, @@ -105,43 +93,48 @@ class DownloadableRegion

> { } } + /// Output a value of type [T] dependent on [originalRegion] and its type + /// + /// Shortcut for [BaseRegion.when]. + T when({ + required T Function(RectangleRegion rectangle) rectangle, + required T Function(CircleRegion circle) circle, + required T Function(LineRegion line) line, + }) => + originalRegion.when(rectangle: rectangle, circle: circle, line: line); + @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - - return other is DownloadableRegion && - other.type == type && - other.originalRegion == originalRegion && - listEquals(other.points, points) && - other.minZoom == minZoom && - other.maxZoom == maxZoom && - other.options == options && - other.parallelThreads == parallelThreads && - other.preventRedownload == preventRedownload && - other.seaTileRemoval == seaTileRemoval && - other.start == start && - other.end == end && - other.crs == crs && - other.errorHandler == errorHandler; - } + bool operator ==(Object other) => + identical(this, other) || + (other is DownloadableRegion && + other.originalRegion == originalRegion && + other.minZoom == minZoom && + other.maxZoom == maxZoom && + other.options == options && + other.parallelThreads == parallelThreads && + other.preventRedownload == preventRedownload && + other.seaTileRemoval == seaTileRemoval && + other.start == start && + other.end == end && + other.crs == crs && + other.errorHandler == errorHandler); @override - int get hashCode => - type.hashCode ^ - originalRegion.hashCode ^ - points.hashCode ^ - minZoom.hashCode ^ - maxZoom.hashCode ^ - options.hashCode ^ - parallelThreads.hashCode ^ - preventRedownload.hashCode ^ - seaTileRemoval.hashCode ^ - start.hashCode ^ - end.hashCode ^ - crs.hashCode ^ - errorHandler.hashCode; + int get hashCode => Object.hashAllUnordered([ + originalRegion.hashCode, + minZoom.hashCode, + maxZoom.hashCode, + options.hashCode, + parallelThreads.hashCode, + preventRedownload.hashCode, + seaTileRemoval.hashCode, + start.hashCode, + end.hashCode, + crs.hashCode, + errorHandler.hashCode, + ]); @override String toString() => - 'DownloadableRegion(type: $type, originalRegion: $originalRegion, points: $points, minZoom: $minZoom, maxZoom: $maxZoom, options: $options, parallelThreads: $parallelThreads, preventRedownload: $preventRedownload, seaTileRemoval: $seaTileRemoval, start: $start, end: $end, crs: $crs, errorHandler: $errorHandler)'; + 'DownloadableRegion(originalRegion: $originalRegion, minZoom: $minZoom, maxZoom: $maxZoom, options: $options, parallelThreads: $parallelThreads, preventRedownload: $preventRedownload, seaTileRemoval: $seaTileRemoval, start: $start, end: $end, crs: $crs, errorHandler: $errorHandler)'; } diff --git a/lib/src/regions/line.dart b/lib/src/regions/line.dart index 87173b47..907b3d04 100644 --- a/lib/src/regions/line.dart +++ b/lib/src/regions/line.dart @@ -94,12 +94,10 @@ class LineRegion extends BaseRegion { void Function(Object?)? errorHandler, }) => DownloadableRegion._( - points: toOutline(), + this, minZoom: minZoom, maxZoom: maxZoom, options: options, - type: RegionType.line, - originalRegion: this, parallelThreads: parallelThreads, preventRedownload: preventRedownload, seaTileRemoval: seaTileRemoval, @@ -173,4 +171,16 @@ class LineRegion extends BaseRegion { @override List toOutline([int overlap = 1]) => toOutlines(overlap).expand((x) => x).toList(); + + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LineRegion && + other.line == line && + listEquals(other.line, line) && + other.radius == radius && + super == other); + + @override + int get hashCode => Object.hashAllUnordered([line, radius, super.hashCode]); } diff --git a/lib/src/regions/recovered_region.dart b/lib/src/regions/recovered_region.dart index d81f69af..19cc17e2 100644 --- a/lib/src/regions/recovered_region.dart +++ b/lib/src/regions/recovered_region.dart @@ -3,13 +3,21 @@ part of flutter_map_tile_caching; -/// A mixture between [BaseRegion] and [DownloadableRegion] containing all the salvaged data from a recovered download +/// A mixture between [BaseRegion] and [DownloadableRegion] containing all the +/// salvaged data from a recovered download /// -/// How does recovery work? At the start of a download, a file is created including information about the download. At the end of a download or when a download is correctly cancelled, this file is deleted. However, if there is no ongoing download (controlled by an internal variable) and the recovery file exists, the download has obviously been stopped incorrectly, meaning it can be recovered using the information within the recovery file. +/// How does recovery work? At the start of a download, a file is created +/// including information about the download. At the end of a download or when a +/// download is correctly cancelled, this file is deleted. However, if there is +/// no ongoing download (controlled by an internal variable) and the recovery +/// file exists, the download has obviously been stopped incorrectly, meaning it +/// can be recovered using the information within the recovery file. /// -/// The availability of [bounds], [line], [center] & [radius] depend on the [type] of the recovered region. +/// The availability of [bounds], [line], [center] & [radius] depend on the +/// [_type] of the recovered region. /// -/// Should avoid manual construction. Use [toDownloadable] to restore a valid [DownloadableRegion]. +/// Should avoid manual construction. Use [toDownloadable] to restore a valid +/// [DownloadableRegion]. class RecoveredRegion { /// A unique ID created for every bulk download operation /// @@ -26,8 +34,7 @@ class RecoveredRegion { /// Not actually used when converting to [DownloadableRegion]. final DateTime time; - /// The shape that this region conforms to - final RegionType type; + final RegionType _type; /// The bounds for a rectangular region final LatLngBounds? bounds; @@ -74,7 +81,7 @@ class RecoveredRegion { required this.id, required this.storeName, required this.time, - required this.type, + required RegionType type, required this.bounds, required this.center, required this.line, @@ -86,36 +93,38 @@ class RecoveredRegion { required this.parallelThreads, required this.preventRedownload, required this.seaTileRemoval, - }); - - /// Convert this region into a downloadable region + }) : _type = type; + + /// Convert this region into it's original [BaseRegion], calling the respective + /// callback with it + T toRegion({ + required T Function(RectangleRegion rectangle) rectangle, + required T Function(CircleRegion circle) circle, + required T Function(LineRegion line) line, + }) => + switch (_type) { + RegionType.rectangle => rectangle(RectangleRegion(bounds!)), + RegionType.circle => circle(CircleRegion(center!, radius!)), + RegionType.line => line(LineRegion(this.line!, radius!)), + }; + + /// Convert this region into a [DownloadableRegion] DownloadableRegion toDownloadable( TileLayer options, { Crs crs = const Epsg3857(), Function(Object?)? errorHandler, - }) { - final BaseRegion region = type == RegionType.rectangle - ? RectangleRegion(bounds!) - : type == RegionType.circle - ? CircleRegion(center!, radius!) - : LineRegion(line!, radius!); - - return DownloadableRegion._( - points: type == RegionType.line - ? (region as LineRegion).toOutlines() - : region.toOutline(), - minZoom: minZoom, - maxZoom: maxZoom, - options: options, - type: type, - originalRegion: region, - parallelThreads: parallelThreads, - preventRedownload: preventRedownload, - seaTileRemoval: seaTileRemoval, - start: start, - end: end, - crs: crs, - errorHandler: errorHandler, - ); - } + }) => + DownloadableRegion._( + toRegion(rectangle: (r) => r, circle: (c) => c, line: (l) => l), + minZoom: minZoom, + maxZoom: maxZoom, + options: options, + parallelThreads: parallelThreads, + preventRedownload: preventRedownload, + seaTileRemoval: seaTileRemoval, + start: start, + end: end, + crs: crs, + errorHandler: errorHandler, + ); } diff --git a/lib/src/regions/rectangle.dart b/lib/src/regions/rectangle.dart index 0a9145e2..ac0f0a7f 100644 --- a/lib/src/regions/rectangle.dart +++ b/lib/src/regions/rectangle.dart @@ -40,12 +40,10 @@ class RectangleRegion extends BaseRegion { void Function(Object?)? errorHandler, }) => DownloadableRegion._( - points: [bounds.northWest, bounds.southEast], + this, minZoom: minZoom, maxZoom: maxZoom, options: options, - type: RegionType.rectangle, - originalRegion: this, parallelThreads: parallelThreads, preventRedownload: preventRedownload, seaTileRemoval: seaTileRemoval, @@ -76,27 +74,20 @@ class RectangleRegion extends BaseRegion { label: label, labelStyle: labelStyle, labelPlacement: labelPlacement, - points: [ - LatLng( - bounds.southEast.latitude, - bounds.northWest.longitude, - ), - bounds.southEast, - LatLng( - bounds.northWest.latitude, - bounds.southEast.longitude, - ), - bounds.northWest, - ], + points: toOutline(), ) ], ); @override - List toOutline() => [ - LatLng(bounds.southEast.latitude, bounds.northWest.longitude), - bounds.southEast, - LatLng(bounds.northWest.latitude, bounds.southEast.longitude), - bounds.northWest, - ]; + List toOutline() => + [bounds.northEast, bounds.southEast, bounds.southWest, bounds.northWest]; + + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RectangleRegion && other.bounds == bounds && super == other); + + @override + int get hashCode => Object.hashAllUnordered([bounds, super.hashCode]); } diff --git a/lib/src/root/recovery.dart b/lib/src/root/recovery.dart index eff146cb..dc8c3464 100644 --- a/lib/src/root/recovery.dart +++ b/lib/src/root/recovery.dart @@ -99,7 +99,11 @@ class RootRecovery { id: id, storeName: storeName, time: DateTime.now(), - type: region.type, + type: region.when( + rectangle: (_) => RegionType.rectangle, + circle: (_) => RegionType.circle, + line: (_) => RegionType.line, + ), minZoom: region.minZoom, maxZoom: region.maxZoom, start: region.start, @@ -107,52 +111,52 @@ class RootRecovery { parallelThreads: region.parallelThreads, preventRedownload: region.preventRedownload, seaTileRemoval: region.seaTileRemoval, - nwLat: region.type == RegionType.rectangle + nwLat: region.originalRegion is RectangleRegion ? (region.originalRegion as RectangleRegion) .bounds .northWest .latitude : null, - nwLng: region.type == RegionType.rectangle + nwLng: region.originalRegion is RectangleRegion ? (region.originalRegion as RectangleRegion) .bounds .northWest .longitude : null, - seLat: region.type == RegionType.rectangle + seLat: region.originalRegion is RectangleRegion ? (region.originalRegion as RectangleRegion) .bounds .southEast .latitude : null, - seLng: region.type == RegionType.rectangle + seLng: region.originalRegion is RectangleRegion ? (region.originalRegion as RectangleRegion) .bounds .southEast .longitude : null, - centerLat: region.type == RegionType.circle + centerLat: region.originalRegion is CircleRegion ? (region.originalRegion as CircleRegion).center.latitude : null, - centerLng: region.type == RegionType.circle + centerLng: region.originalRegion is CircleRegion ? (region.originalRegion as CircleRegion).center.longitude : null, - linePointsLat: region.type == RegionType.line + linePointsLat: region.originalRegion is LineRegion ? (region.originalRegion as LineRegion) .line .map((c) => c.latitude) .toList() : null, - linePointsLng: region.type == RegionType.line + linePointsLng: region.originalRegion is LineRegion ? (region.originalRegion as LineRegion) .line .map((c) => c.longitude) .toList() : null, - circleRadius: region.type == RegionType.circle + circleRadius: region.originalRegion is CircleRegion ? (region.originalRegion as CircleRegion).radius : null, - lineRadius: region.type == RegionType.line + lineRadius: region.originalRegion is LineRegion ? (region.originalRegion as LineRegion).radius : null, ), diff --git a/lib/src/store/download.dart b/lib/src/store/download.dart index 31214697..423cf139 100644 --- a/lib/src/store/download.dart +++ b/lib/src/store/download.dart @@ -202,12 +202,12 @@ class DownloadManagement { /// /// Returns an `int` which is the number of tiles. Future check(DownloadableRegion region) => compute( - region.type == RegionType.rectangle - ? TilesCounter.rectangleTiles - : region.type == RegionType.circle - ? TilesCounter.circleTiles - : TilesCounter.lineTiles, - generateTileLoopsInput(region), + region.when( + rectangle: (_) => TilesCounter.rectangleTiles, + circle: (_) => TilesCounter.circleTiles, + line: (_) => TilesCounter.lineTiles, + ), + region, ); /// Cancels the ongoing foreground download and recovery session (within the diff --git a/pubspec.yaml b/pubspec.yaml index 79847382..733bdbe1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_map_tile_caching description: Plugin for 'flutter_map' providing advanced caching functionality, with ability to download map regions for offline use. -version: 8.1.0 +version: 9.0.0 repository: https://github.com/JaffaKetchup/flutter_map_tile_caching issue_tracker: https://github.com/JaffaKetchup/flutter_map_tile_caching/issues documentation: https://fmtc.jaffaketchup.dev @@ -17,8 +17,8 @@ platforms: windows: environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.7.0" + sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.10.0" dependencies: async: ^2.9.0 @@ -38,9 +38,14 @@ dependencies: stream_transform: ^2.0.0 watcher: ^1.0.2 +dependency_overrides: + flutter_map: + git: + url: https://github.com/fleaflet/flutter_map.git + dev_dependencies: build_runner: ^2.3.2 flutter_lints: ^2.0.1 isar_generator: ^3.1.0+1 -flutter: null \ No newline at end of file +flutter: null From e0074e45daa8306ef93de2a35a975240ad0ff5f1 Mon Sep 17 00:00:00 2001 From: Luka S Date: Mon, 15 May 2023 21:01:33 +0100 Subject: [PATCH 012/168] Update windowsApplicationInstallerSetup.iss Former-commit-id: 37dbaad895bc1a3776449f6d7e4cbca0b6762ccf [formerly 9f01c9bc38c0699bc3721e9259cd587d5b4af332] Former-commit-id: 8273e7d3dac36e6c26aca02fc2939728bd3f3dda --- windowsApplicationInstallerSetup.iss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windowsApplicationInstallerSetup.iss b/windowsApplicationInstallerSetup.iss index d18c6cbd..0f7ed39c 100644 --- a/windowsApplicationInstallerSetup.iss +++ b/windowsApplicationInstallerSetup.iss @@ -1,7 +1,7 @@ ; Script generated by the Inno Setup Script Wizard #define MyAppName "FMTC Demo" -#define MyAppVersion "for 8.1.0" +#define MyAppVersion "for 9.0.0" #define MyAppPublisher "JaffaKetchup Development" #define MyAppURL "https://github.com/JaffaKetchup/flutter_map_tile_caching" #define MyAppSupportURL "https://github.com/JaffaKetchup/flutter_map_tile_caching/issues" From 6380f2fde459b2b109bea28dd7061fa9f315564b Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 15 May 2023 20:24:51 +0000 Subject: [PATCH 013/168] Built Example Applications Former-commit-id: 918080759f5dd657707b5c3744aaa34cf05d3568 [formerly fc3d1e520c9aec7a4ed4c02e2a6d1ed1e4b98ce4] Former-commit-id: 49984d42f214ee7c562d23c00917c24e68f23e6d --- .../AndroidApplication.apk.REMOVED.git-id | 2 +- .../WindowsApplication.exe.REMOVED.git-id | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/prebuiltExampleApplications/AndroidApplication.apk.REMOVED.git-id b/prebuiltExampleApplications/AndroidApplication.apk.REMOVED.git-id index af3bc3b4..08c7ff94 100644 --- a/prebuiltExampleApplications/AndroidApplication.apk.REMOVED.git-id +++ b/prebuiltExampleApplications/AndroidApplication.apk.REMOVED.git-id @@ -1 +1 @@ -1533e0340fd2d6289c8042ff58907697caadc04c \ No newline at end of file +e46a012394ebe3b350a50d18e8a3dbd075c77885 \ No newline at end of file diff --git a/prebuiltExampleApplications/WindowsApplication.exe.REMOVED.git-id b/prebuiltExampleApplications/WindowsApplication.exe.REMOVED.git-id index 3d2a8a85..1d96279a 100644 --- a/prebuiltExampleApplications/WindowsApplication.exe.REMOVED.git-id +++ b/prebuiltExampleApplications/WindowsApplication.exe.REMOVED.git-id @@ -1 +1 @@ -805101ceea0d17220214e4b585ac34a94e8b0b69 \ No newline at end of file +5d1000af0313c75b82f82f8bf5a0295fb12f25ef \ No newline at end of file From 94fb014a62a968bac28726d5264f4c69a6b9b8c8 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Mon, 15 May 2023 21:30:49 +0100 Subject: [PATCH 014/168] Improve GitHub Workflow Former-commit-id: 329424efb51cdc84322efe2130e90a26e256830c [formerly 4cfe17c3221025d348783054356865edc9f3f8b3] Former-commit-id: b9ccc04165f0deb3b5b5a41cadf4e3949548f757 --- .github/workflows/delete_old_runs.yml | 16 -- .github/workflows/main.yml | 188 ++++++++++-------- .../AndroidApplication.apk.REMOVED.git-id | 1 - .../WindowsApplication.exe.REMOVED.git-id | 1 - windowsApplicationInstallerSetup.iss | 2 +- 5 files changed, 104 insertions(+), 104 deletions(-) delete mode 100644 .github/workflows/delete_old_runs.yml delete mode 100644 prebuiltExampleApplications/AndroidApplication.apk.REMOVED.git-id delete mode 100644 prebuiltExampleApplications/WindowsApplication.exe.REMOVED.git-id diff --git a/.github/workflows/delete_old_runs.yml b/.github/workflows/delete_old_runs.yml deleted file mode 100644 index dddde2d9..00000000 --- a/.github/workflows/delete_old_runs.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: Delete Old Workflow Runs -on: - schedule: - - cron: "0 0 * * 1" # Run every Monday - workflow_dispatch: - -jobs: - del_runs: - runs-on: ubuntu-latest - steps: - - uses: Mattraks/delete-workflow-runs@v2 - with: - token: ${{ github.token }} - repository: ${{ github.repository }} - retain_days: 7 - keep_minimum_runs: 0 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d2c0ac5f..4bf9623e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,89 +1,107 @@ -name: Analyse & Build -on: [push, workflow_dispatch] +name: CI/CD +on: + push: + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref || github.run_id }} + cancel-in-progress: true jobs: - package-analysis: - name: "Analyse Package" - runs-on: ubuntu-latest - if: github.event.head_commit.message != 'Built Example Applications' - steps: - - name: Checkout Repository - uses: actions/checkout@v3 - - name: Run Dart Package Analyser - uses: axel-op/dart-package-analyzer@master - id: analysis - with: - githubToken: ${{ secrets.GITHUB_TOKEN }} - - name: Check Package Scores - env: - TOTAL: ${{ steps.analysis.outputs.total }} - TOTAL_MAX: ${{ steps.analysis.outputs.total_max }} - run: | - if (( $TOTAL < $TOTAL_MAX )) - then - echo Total score below expected minimum score. Improve the score! - exit 1 - fi + score-package: + name: "Score Package" + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v3 + - name: Run Dart Package Analyser + uses: axel-op/dart-package-analyzer@master + id: analysis + with: + githubToken: ${{ secrets.GITHUB_TOKEN }} + - name: Check Package Scores + env: + TOTAL: ${{ steps.analysis.outputs.total }} + TOTAL_MAX: ${{ steps.analysis.outputs.total_max }} + run: | + if (( $TOTAL < $TOTAL_MAX )) + then + echo Package score less than available score. Improve the score! + exit 1 + fi + + analyse-code: + name: "Analyse Code" + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v3 + - name: Setup Flutter Environment + uses: subosito/flutter-action@v2 + with: + channel: "stable" + - name: Get All Dependencies + run: flutter pub get + - name: Check Formatting + run: dart format --output=none --set-exit-if-changed . + - name: Check Lints + run: dart analyze --fatal-infos --fatal-warnings - content-analysis: - name: "Analyse Contents" - runs-on: ubuntu-latest - if: github.event.head_commit.message != 'Built Example Applications' - steps: - - name: Checkout Repository - uses: actions/checkout@v3 - - name: Setup Flutter Environment - uses: subosito/flutter-action@main - with: - channel: "stable" - - name: Get All Dependencies - run: flutter pub get - - name: Check Formatting - run: dart format --output=none --set-exit-if-changed . - - name: Check Lints - run: dart analyze --fatal-infos --fatal-warnings + build-android: + name: "Build Android Example App" + runs-on: ubuntu-latest + needs: analyse-code + defaults: + run: + working-directory: ./example + steps: + - name: Checkout Repository + uses: actions/checkout@v3 + - name: Setup Java 17 Environment + uses: actions/setup-java@v3 + with: + distribution: "temurin" + java-version: "17" + - name: Setup Flutter Environment + uses: subosito/flutter-action@v2 + with: + channel: "stable" + - name: Build + run: flutter build apk --obfuscate --split-debug-info=/symbols + - name: Upload Artifact + uses: actions/upload-artifact@v3.1.2 + with: + name: apk-build + path: example/build/app/outputs/apk/release + if-no-files-found: error - build-example: - name: "Build Example Applications" - runs-on: windows-latest - needs: [content-analysis, package-analysis] - if: github.event.head_commit.message != 'Built Example Applications' - defaults: - run: - working-directory: ./example - steps: - - name: Checkout Repository - uses: actions/checkout@v3 - - name: Setup Java 17 Environment - uses: actions/setup-java@v3 - with: - distribution: "temurin" - java-version: "17" - - name: Setup Flutter Environment - uses: subosito/flutter-action@main - with: - channel: "stable" - - name: Remove Existing Prebuilt Applications - run: Remove-Item "prebuiltExampleApplications" -Recurse -ErrorAction Ignore - working-directory: . - - name: Create Prebuilt Applications (Output) Directory - run: md prebuiltExampleApplications - working-directory: . - - name: Get All Dependencies - run: flutter pub get - - name: Build Android Application - run: flutter build apk --obfuscate --split-debug-info=/symbols - - name: Move Android Application To Output Directory - run: move "example\build\app\outputs\flutter-apk\app-release.apk" "prebuiltExampleApplications\AndroidApplication.apk" - working-directory: . - - name: Build Windows Application - run: flutter build windows --obfuscate --split-debug-info=/symbols - - name: Create Windows Application Installer - run: iscc "windowsApplicationInstallerSetup.iss" - working-directory: . - - name: Commit Output Directory - uses: EndBug/add-and-commit@main - with: - message: "Built Example Applications" - add: "prebuiltExampleApplications/" - default_author: github_actions + build-windows: + name: "Build Windows Example App" + runs-on: windows-latest + needs: analyse-code + defaults: + run: + working-directory: ./example + steps: + - name: Checkout Repository + uses: actions/checkout@v3 + - name: Setup Java 17 Environment + uses: actions/setup-java@v3 + with: + distribution: "temurin" + java-version: "17" + - name: Setup Flutter Environment + uses: subosito/flutter-action@v2 + with: + channel: "stable" + - name: Build + run: flutter build windows --obfuscate --split-debug-info=/symbols + - name: Create Installer + run: iscc "windowsApplicationInstallerSetup.iss" + working-directory: . + - name: Upload Artifact + uses: actions/upload-artifact@v3.1.2 + with: + name: exe-build + path: windowsTemp/WindowsApplication.exe + if-no-files-found: error \ No newline at end of file diff --git a/prebuiltExampleApplications/AndroidApplication.apk.REMOVED.git-id b/prebuiltExampleApplications/AndroidApplication.apk.REMOVED.git-id deleted file mode 100644 index 08c7ff94..00000000 --- a/prebuiltExampleApplications/AndroidApplication.apk.REMOVED.git-id +++ /dev/null @@ -1 +0,0 @@ -e46a012394ebe3b350a50d18e8a3dbd075c77885 \ No newline at end of file diff --git a/prebuiltExampleApplications/WindowsApplication.exe.REMOVED.git-id b/prebuiltExampleApplications/WindowsApplication.exe.REMOVED.git-id deleted file mode 100644 index 1d96279a..00000000 --- a/prebuiltExampleApplications/WindowsApplication.exe.REMOVED.git-id +++ /dev/null @@ -1 +0,0 @@ -5d1000af0313c75b82f82f8bf5a0295fb12f25ef \ No newline at end of file diff --git a/windowsApplicationInstallerSetup.iss b/windowsApplicationInstallerSetup.iss index 0f7ed39c..9cbb2bdf 100644 --- a/windowsApplicationInstallerSetup.iss +++ b/windowsApplicationInstallerSetup.iss @@ -26,7 +26,7 @@ DisableWelcomePage=no LicenseFile=LICENSE PrivilegesRequired=lowest PrivilegesRequiredOverridesAllowed=dialog -OutputDir=prebuiltExampleApplications +OutputDir=windowsTemp OutputBaseFilename=WindowsApplication SetupIconFile=example\assets\icons\ProjectIcon.ico Compression=lzma From 5926a61e3070cd40060b04575656f291a03da7b0 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sat, 20 May 2023 21:16:00 +0100 Subject: [PATCH 015/168] Added support for line region to example application Other minor improvements Former-commit-id: dfdf2b57ffc98bfbe42b9b2e82b19ce07efc8827 [formerly e3614a835505d1ee4d1fa714569f9b20eaaec596] Former-commit-id: 3301e97b6e5d839342ddf69351900c439d5ae4cf --- CHANGELOG.md | 2 +- example/android/build.gradle | 2 +- .../pages/downloader/components/map_view.dart | 116 +++++++++++++++--- .../components/shape_controller_popup.dart | 80 ++++++------ .../lib/shared/state/download_provider.dart | 15 +++ example/lib/shared/vars/region_mode.dart | 1 + example/pubspec.yaml | 5 +- lib/src/regions/base_region.dart | 2 +- lib/src/regions/circle.dart | 6 +- lib/src/regions/downloadable_region.dart | 5 +- lib/src/regions/line.dart | 61 ++++----- lib/src/regions/rectangle.dart | 5 +- lib/src/root/statistics.dart | 13 +- lib/src/store/manage.dart | 11 +- pubspec.yaml | 9 +- 15 files changed, 197 insertions(+), 136 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d5ab134..dfb9a847 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ Many thanks to my sponsors, no matter how much or how little they donated. Spons ## [9.0.0] - 2023/XX/XX * Added support for Flutter 3.10 and Dart 3 -* Added support for flutter_map v5 +* Added support for flutter_map 'v5' * Added `StoreManagement.pruneTilesOlderThan` method * Replaced public facing `RegionType`/`type` with Dart 3 exhaustive switch statements through `BaseRegion/DownloadableRegion.when` & `RecoverableRegion.toRegion` * Improved performance and fixed bugs diff --git a/example/android/build.gradle b/example/android/build.gradle index 4e43ac75..c62a13ac 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -33,6 +33,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/example/lib/screens/main/pages/downloader/components/map_view.dart b/example/lib/screens/main/pages/downloader/components/map_view.dart index 31889aa7..558ab44a 100644 --- a/example/lib/screens/main/pages/downloader/components/map_view.dart +++ b/example/lib/screens/main/pages/downloader/components/map_view.dart @@ -31,6 +31,8 @@ class _MapViewState extends State { final _mapKey = GlobalKey>(); final MapController _mapController = MapController(); + late final DownloadProvider downloadProvider; + late final StreamSubscription _polygonVisualizerStream; late final StreamSubscription _tileCounterTriggerStream; late final StreamSubscription _manualPolygonRecalcTriggerStream; @@ -64,20 +66,29 @@ class _MapViewState extends State { void initState() { super.initState(); + downloadProvider = Provider.of(context, listen: false); + _manualPolygonRecalcTriggerStream = - Provider.of(context, listen: false) - .manualPolygonRecalcTrigger - .stream - .listen((_) { + downloadProvider.manualPolygonRecalcTrigger.stream.listen((_) { + if (downloadProvider.regionMode == RegionMode.line) { + _updateLineRegion(); + return; + } _updatePointLatLng(); _countTiles(); }); - _polygonVisualizerStream = - _mapController.mapEventStream.listen((_) => _updatePointLatLng()); + _polygonVisualizerStream = _mapController.mapEventStream.listen((_) { + if (downloadProvider.regionMode != RegionMode.line) { + _updatePointLatLng(); + } + }); + _tileCounterTriggerStream = _mapController.mapEventStream .debounce(const Duration(seconds: 1)) - .listen((_) => _countTiles()); + .listen((_) { + if (downloadProvider.regionMode != RegionMode.line) _countTiles(); + }); } @override @@ -136,6 +147,9 @@ class _MapViewState extends State { _updatePointLatLng(); _countTiles(); }, + onTap: (_, point) => _addLinePoint(point), + onSecondaryTap: (_, point) => _removeLinePoint(), + onLongPress: (_, point) => _removeLinePoint(), ), nonRotatedChildren: buildStdAttribution( urlTemplate, @@ -172,7 +186,16 @@ class _MapViewState extends State { ), ), ), - if (_coordsTopLeft != null && + if (downloadProvider.regionMode == RegionMode.line) + LineRegion( + downloadProvider.lineRegionPoints, + downloadProvider.lineRegionRadius, + ).toDrawable( + borderColor: Colors.black, + borderStrokeWidth: 2, + fillColor: Colors.green.withOpacity(2 / 3), + ) + else if (_coordsTopLeft != null && _coordsBottomRight != null && downloadProvider.regionMode != RegionMode.circle) _buildTargetPolygon( @@ -186,7 +209,9 @@ class _MapViewState extends State { _buildTargetPolygon(CircleRegion(_center!, _radius!)) ], ), - if (_crosshairsTop != null && _crosshairsBottom != null) ...[ + if (downloadProvider.regionMode != RegionMode.line && + _crosshairsTop != null && + _crosshairsBottom != null) ...[ Positioned( top: _crosshairsTop!.y, left: _crosshairsTop!.x, @@ -197,16 +222,68 @@ class _MapViewState extends State { left: _crosshairsBottom!.x, child: const Crosshairs(), ), - ] + ], + if (downloadProvider.regionMode == RegionMode.line) + Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background, + borderRadius: BorderRadius.circular(16), + ), + width: double.infinity, + height: 50, + margin: const EdgeInsets.all(16), + padding: const EdgeInsets.all(16), + child: Row( + children: [ + const Text('Radius'), + Expanded( + child: Slider( + value: downloadProvider.lineRegionRadius, + min: 100, + max: 10000, + divisions: 99, + label: + '${downloadProvider.lineRegionRadius.round()} meters', + onChanged: (val) { + downloadProvider.lineRegionRadius = val; + _updateLineRegion(); + }, + ), + ), + ], + ), + ), ], ); }, ), ); + void _updateLineRegion() { + downloadProvider.region = LineRegion( + downloadProvider.lineRegionPoints, + downloadProvider.lineRegionRadius, + ); + + _countTiles(); + } + + void _removeLinePoint() { + if (downloadProvider.regionMode == RegionMode.line) { + downloadProvider.lineRegionPoints.removeLast(); + _updateLineRegion(); + } + } + + void _addLinePoint(LatLng coord) { + if (downloadProvider.regionMode == RegionMode.line) { + downloadProvider.lineRegionPoints.add(coord); + _updateLineRegion(); + } + } + void _updatePointLatLng() { - final DownloadProvider downloadProvider = - Provider.of(context, listen: false); + if (downloadProvider.regionMode == RegionMode.line) return; final Size mapSize = _mapKey.currentContext!.size!; final bool isHeightLongestSide = mapSize.width < mapSize.height; @@ -285,6 +362,8 @@ class _MapViewState extends State { 1000; setState(() {}); break; + case RegionMode.line: + break; } if (downloadProvider.regionMode != RegionMode.circle) { @@ -307,16 +386,13 @@ class _MapViewState extends State { } Future _countTiles() async { - final DownloadProvider provider = - Provider.of(context, listen: false); - - if (provider.region != null) { - provider + if (downloadProvider.region != null) { + downloadProvider ..regionTiles = null ..regionTiles = await FMTC.instance('').download.check( - provider.region!.toDownloadable( - provider.minZoom, - provider.maxZoom, + downloadProvider.region!.toDownloadable( + downloadProvider.minZoom, + downloadProvider.maxZoom, TileLayer(), ), ); diff --git a/example/lib/screens/main/pages/downloader/components/shape_controller_popup.dart b/example/lib/screens/main/pages/downloader/components/shape_controller_popup.dart index b17d0f9c..6b228baf 100644 --- a/example/lib/screens/main/pages/downloader/components/shape_controller_popup.dart +++ b/example/lib/screens/main/pages/downloader/components/shape_controller_popup.dart @@ -7,61 +7,59 @@ import '../../../../../shared/vars/region_mode.dart'; class ShapeControllerPopup extends StatelessWidget { const ShapeControllerPopup({super.key}); - static const Map> regionShapes = { - 'Square': [ - Icons.crop_square_sharp, - RegionMode.square, - ], - 'Rectangle (Vertical)': [ - Icons.crop_portrait_sharp, - RegionMode.rectangleVertical, - ], - 'Rectangle (Horizontal)': [ - Icons.crop_landscape_sharp, - RegionMode.rectangleHorizontal, - ], - 'Circle': [ - Icons.circle_outlined, - RegionMode.circle, - ], - 'Line/Path': [ - Icons.timeline, - null, - ], + static const Map + regionShapes = { + 'Square': ( + icon: Icons.crop_square_sharp, + mode: RegionMode.square, + hint: null, + ), + 'Vertical Rectangle': ( + icon: Icons.crop_portrait_sharp, + mode: RegionMode.rectangleVertical, + hint: null, + ), + 'Horizontal Rectangle': ( + icon: Icons.crop_landscape_sharp, + mode: RegionMode.rectangleHorizontal, + hint: null, + ), + 'Circle': ( + icon: Icons.circle_outlined, + mode: RegionMode.circle, + hint: null, + ), + 'Line/Path': ( + icon: Icons.timeline, + mode: RegionMode.line, + hint: + 'Tap/click to add point to line\nHold/secondary click to remove last point from line', + ), }; @override Widget build(BuildContext context) => Padding( padding: const EdgeInsets.all(12), child: Consumer( - builder: (context, provider, _) => ListView.separated( + builder: (context, provider, _) => ListView.builder( itemCount: regionShapes.length, shrinkWrap: true, itemBuilder: (context, i) { - final String key = regionShapes.keys.toList()[i]; - final IconData icon = regionShapes.values.toList()[i][0]; - final RegionMode? mode = regionShapes.values.toList()[i][1]; - + final value = regionShapes.values.elementAt(i); return ListTile( visualDensity: VisualDensity.compact, - title: Text(key), - subtitle: i == regionShapes.length - 1 - ? const Text('Disabled in example application') - : null, - leading: Icon(icon), - trailing: - provider.regionMode == mode ? const Icon(Icons.done) : null, - onTap: i != regionShapes.length - 1 - ? () { - provider.regionMode = mode!; - Navigator.of(context).pop(); - } + title: Text(regionShapes.keys.elementAt(i)), + leading: Icon(value.icon), + trailing: provider.regionMode == value.mode + ? const Icon(Icons.done) : null, - enabled: i != regionShapes.length - 1, + subtitle: value.hint != null ? Text(value.hint!) : null, + onTap: () { + provider.regionMode = value.mode; + Navigator.of(context).pop(); + }, ); }, - separatorBuilder: (context, i) => - i == regionShapes.length - 2 ? const Divider() : Container(), ), ), ); diff --git a/example/lib/shared/state/download_provider.dart b/example/lib/shared/state/download_provider.dart index dc099b1e..8179559f 100644 --- a/example/lib/shared/state/download_provider.dart +++ b/example/lib/shared/state/download_provider.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; +import 'package:latlong2/latlong.dart'; import '../vars/region_mode.dart'; @@ -13,6 +14,20 @@ class DownloadProvider extends ChangeNotifier { notifyListeners(); } + double _lineRegionRadius = 1000; + double get lineRegionRadius => _lineRegionRadius; + set lineRegionRadius(double newNum) { + _lineRegionRadius = newNum; + notifyListeners(); + } + + List _lineRegionPoints = []; + List get lineRegionPoints => _lineRegionPoints; + set lineRegionPoints(List newList) { + _lineRegionPoints = newList; + notifyListeners(); + } + BaseRegion? _region; BaseRegion? get region => _region; set region(BaseRegion? newRegion) { diff --git a/example/lib/shared/vars/region_mode.dart b/example/lib/shared/vars/region_mode.dart index 2275027f..1af1233a 100644 --- a/example/lib/shared/vars/region_mode.dart +++ b/example/lib/shared/vars/region_mode.dart @@ -3,4 +3,5 @@ enum RegionMode { rectangleVertical, rectangleHorizontal, circle, + line, } diff --git a/example/pubspec.yaml b/example/pubspec.yaml index e99bfcfd..16177829 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -15,7 +15,7 @@ dependencies: flutter: sdk: flutter flutter_foreground_task: ^4.1.0 - flutter_map: ^4.0.0 + flutter_map: ^5.0.0-dev.1 flutter_map_tile_caching: flutter_speed_dial: ^6.0.0 fmtc_plus_background_downloading: ^8.0.0 @@ -33,9 +33,6 @@ dependencies: version: ^3.0.2 dependency_overrides: - flutter_map: - git: - url: https://github.com/fleaflet/flutter_map.git flutter_map_tile_caching: path: ../ diff --git a/lib/src/regions/base_region.dart b/lib/src/regions/base_region.dart index f5f18a7a..9359afe0 100644 --- a/lib/src/regions/base_region.dart +++ b/lib/src/regions/base_region.dart @@ -28,7 +28,7 @@ sealed class BaseRegion { /// - [LineRegion] BaseRegion({required String? name}) : name = (name?.isEmpty ?? false) - ? throw ArgumentError.value(name, 'name', 'Must not be empty.') + ? throw ArgumentError.value(name, 'name', 'Must not be empty') : name; /// The user friendly name for the region diff --git a/lib/src/regions/circle.dart b/lib/src/regions/circle.dart index 1ab365ae..6c05a981 100644 --- a/lib/src/regions/circle.dart +++ b/lib/src/regions/circle.dart @@ -16,11 +16,7 @@ class CircleRegion extends BaseRegion { /// - [DownloadableRegion] for downloading: [toDownloadable] /// - [Widget] layer to be placed in a map: [toDrawable] /// - list of [LatLng]s forming the outline: [toOutline] - CircleRegion( - this.center, - this.radius, { - super.name, - }); + CircleRegion(this.center, this.radius, {super.name}) : super(); /// Center coordinate final LatLng center; diff --git a/lib/src/regions/downloadable_region.dart b/lib/src/regions/downloadable_region.dart index aa376aa0..6f8da9ad 100644 --- a/lib/src/regions/downloadable_region.dart +++ b/lib/src/regions/downloadable_region.dart @@ -8,6 +8,9 @@ part of flutter_map_tile_caching; /// Construct via [BaseRegion.toDownloadable]. class DownloadableRegion { /// A copy of the [BaseRegion] used to form this object + /// + /// To make decisions based on the type of this region, prefer [when] over + /// switching on [R] manually. final R originalRegion; /// The minimum zoom level to fetch tiles for @@ -93,7 +96,7 @@ class DownloadableRegion { } } - /// Output a value of type [T] dependent on [originalRegion] and its type + /// Output a value of type [T] dependent on [originalRegion] and its type [R] /// /// Shortcut for [BaseRegion.when]. T when({ diff --git a/lib/src/regions/line.dart b/lib/src/regions/line.dart index 907b3d04..ae7aee72 100644 --- a/lib/src/regions/line.dart +++ b/lib/src/regions/line.dart @@ -16,11 +16,7 @@ class LineRegion extends BaseRegion { /// - [DownloadableRegion] for downloading: [toDownloadable] /// - [Widget] layer to be placed in a map: [toDrawable] /// - list of [LatLng]s forming the outline: [LineRegion.toOutlines] - LineRegion( - this.line, - this.radius, { - super.name, - }); + LineRegion(this.line, this.radius, {super.name}) : super(); /// The center line defined by a list of coordinates final List line; @@ -30,51 +26,49 @@ class LineRegion extends BaseRegion { /// Generate the list of rectangle segments formed from the locus of this line /// - /// Use the optional `overlap` argument to set the behaviour of the joints + /// Use the optional [overlap] argument to set the behaviour of the joints /// between segments: /// /// * -1: joined by closest corners (largest gap) - /// * 0 (default): joined by centers (equal gap and overlap) + /// * 0 (default): joined by centers /// * 1 (as downloaded): joined by further corners (largest overlap) List> toOutlines([int overlap = 0]) { if (overlap < -1 || overlap > 1) { throw ArgumentError('`overlap` must be between -1 and 1 inclusive'); } - const Distance dist = Distance(); - final int rad = (radius * math.pi / 4).round(); + if (line.isEmpty) return []; + + const dist = Distance(); + final rad = radius * math.pi / 4; return line.map((pos) { if ((line.indexOf(pos) + 1) >= line.length) return [LatLng(0, 0)]; - final List section = [pos, line[line.indexOf(pos) + 1]]; + final section = [pos, line[line.indexOf(pos) + 1]]; - final double bearing = dist.bearing(section[0], section[1]); - final double clockwiseRotation = + final bearing = dist.bearing(section[0], section[1]); + final clockwiseRotation = (90 + bearing) > 360 ? 360 - (90 + bearing) : (90 + bearing); - final double anticlockwiseRotation = + final anticlockwiseRotation = (bearing - 90) < 0 ? 360 + (bearing - 90) : (bearing - 90); - final LatLng offset1 = - dist.offset(section[0], rad, clockwiseRotation); // Top-right - final LatLng offset2 = - dist.offset(section[1], rad, clockwiseRotation); // Bottom-right - final LatLng offset3 = - dist.offset(section[1], rad, anticlockwiseRotation); // Bottom-left - final LatLng offset4 = - dist.offset(section[0], rad, anticlockwiseRotation); // Top-left + final topRight = dist.offset(section[0], rad, clockwiseRotation); + final bottomRight = dist.offset(section[1], rad, clockwiseRotation); + final bottomLeft = dist.offset(section[1], rad, anticlockwiseRotation); + final topLeft = dist.offset(section[0], rad, anticlockwiseRotation); - if (overlap == 0) return [offset1, offset2, offset3, offset4]; + if (overlap == 0) return [topRight, bottomRight, bottomLeft, topLeft]; - final bool r = overlap == -1; - final bool os = line.indexOf(pos) == 0; - final bool oe = line.indexOf(pos) == line.length - 2; + final r = overlap == -1; + final os = line.indexOf(pos) == 0; + final oe = line.indexOf(pos) == line.length - 2; return [ - os ? offset1 : dist.offset(offset1, r ? rad : -rad, bearing), - oe ? offset2 : dist.offset(offset2, r ? -rad : rad, bearing), - oe ? offset3 : dist.offset(offset3, r ? -rad : rad, bearing), - os ? offset4 : dist.offset(offset4, r ? rad : -rad, bearing), + os ? topRight : dist.offset(topRight, r ? rad : -rad, bearing), + oe ? bottomRight : dist.offset(bottomRight, r ? -rad : rad, bearing), + oe ? bottomLeft : dist.offset(bottomLeft, r ? -rad : rad, bearing), + os ? topLeft : dist.offset(topLeft, r ? rad : -rad, bearing), ]; }).toList() ..removeLast(); @@ -156,18 +150,15 @@ class LineRegion extends BaseRegion { /// Flattens the result of [toOutlines] - its documentation is quoted below /// - /// Prefer [toOutlines]. This method is likely to give a different result than - /// expected if used externally. - /// /// > Generate the list of rectangle segments formed from the locus of this /// > line /// > - /// > Use the optional `overlap` argument to set the behaviour of the joints + /// > Use the optional [overlap] argument to set the behaviour of the joints /// between segments: /// > /// > * -1: joined by closest corners (largest gap), - /// > * 0: joined by centers (equal gap and overlap) - /// > * 1 (default, as downloaded): joined by further corners (most overlap) + /// > * 0 (default): joined by centers + /// > * 1 (as downloaded): joined by further corners (most overlap) @override List toOutline([int overlap = 1]) => toOutlines(overlap).expand((x) => x).toList(); diff --git a/lib/src/regions/rectangle.dart b/lib/src/regions/rectangle.dart index ac0f0a7f..b737a51d 100644 --- a/lib/src/regions/rectangle.dart +++ b/lib/src/regions/rectangle.dart @@ -18,10 +18,7 @@ class RectangleRegion extends BaseRegion { /// - [DownloadableRegion] for downloading: [toDownloadable] /// - [Widget] layer to be placed in a map: [toDrawable] /// - list of [LatLng]s forming the outline: [toOutline] - RectangleRegion( - this.bounds, { - super.name, - }); + RectangleRegion(this.bounds, {super.name}) : super(); /// The coordinate bounds final LatLngBounds bounds; diff --git a/lib/src/root/statistics.dart b/lib/src/root/statistics.dart index dccbfdb7..ee5e4dc4 100644 --- a/lib/src/root/statistics.dart +++ b/lib/src/root/statistics.dart @@ -14,21 +14,14 @@ class RootStats { /// Prefer [storesAvailableAsync] to avoid blocking the UI thread. Otherwise, /// this has slightly better performance. List get storesAvailable => _registry.storeDatabases.values - .map( - (e) => StoreDirectory._( - e.descriptorSync.name, - autoCreate: false, - ), - ) + .map((e) => StoreDirectory._(e.descriptorSync.name, autoCreate: false)) .toList(); /// List all the available [StoreDirectory]s asynchronously Future> get storesAvailableAsync => Future.wait( _registry.storeDatabases.values.map( - (e) async => StoreDirectory._( - (await e.descriptor).name, - autoCreate: false, - ), + (e) async => + StoreDirectory._((await e.descriptor).name, autoCreate: false), ), ); diff --git a/lib/src/store/manage.dart b/lib/src/store/manage.dart index d0ea84b4..947fdeeb 100644 --- a/lib/src/store/manage.dart +++ b/lib/src/store/manage.dart @@ -191,9 +191,8 @@ class StoreManagement { double? size, Key? key, double scale = 1.0, - // ignore: avoid_positional_boolean_parameters - Widget Function(BuildContext, Widget, int?, bool)? frameBuilder, - Widget Function(BuildContext, Object, StackTrace?)? errorBuilder, + ImageFrameBuilder? frameBuilder, + ImageErrorWidgetBuilder? errorBuilder, String? semanticLabel, bool excludeFromSemantics = false, Color? color, @@ -251,13 +250,13 @@ class StoreManagement { /// /// This method requires the store to be [ready], else an [FMTCStoreNotReady] /// error will be raised. + /// Future tileImageAsync({ double? size, Key? key, double scale = 1.0, - // ignore: avoid_positional_boolean_parameters - Widget Function(BuildContext, Widget, int?, bool)? frameBuilder, - Widget Function(BuildContext, Object, StackTrace?)? errorBuilder, + ImageFrameBuilder? frameBuilder, + ImageErrorWidgetBuilder? errorBuilder, String? semanticLabel, bool excludeFromSemantics = false, Color? color, diff --git a/pubspec.yaml b/pubspec.yaml index 733bdbe1..e1eb053d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_map_tile_caching description: Plugin for 'flutter_map' providing advanced caching functionality, with ability to download map regions for offline use. -version: 9.0.0 +version: 9.0.0-dev.1 repository: https://github.com/JaffaKetchup/flutter_map_tile_caching issue_tracker: https://github.com/JaffaKetchup/flutter_map_tile_caching/issues documentation: https://fmtc.jaffaketchup.dev @@ -25,7 +25,7 @@ dependencies: collection: ^1.16.0 flutter: sdk: flutter - flutter_map: ^4.0.0 + flutter_map: ^5.0.0-dev.1 http: ^0.13.5 http_plus: ^0.2.2 isar: ^3.1.0+1 @@ -38,11 +38,6 @@ dependencies: stream_transform: ^2.0.0 watcher: ^1.0.2 -dependency_overrides: - flutter_map: - git: - url: https://github.com/fleaflet/flutter_map.git - dev_dependencies: build_runner: ^2.3.2 flutter_lints: ^2.0.1 From a0fdbea300763e4aaa031d08f78fb900c282e6a1 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sat, 20 May 2023 21:30:57 +0100 Subject: [PATCH 016/168] Attempt to fix workflow Former-commit-id: aad0a433242c6b8e823935cc1ef59a0472969e82 [formerly 6d9c16cf591d19e9a23e329a38f287d691ba26db] Former-commit-id: 4f19937ac0613cf48b7f6884c0231a662f6e51dd --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4bf9623e..7fba724b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -67,7 +67,7 @@ jobs: with: channel: "stable" - name: Build - run: flutter build apk --obfuscate --split-debug-info=/symbols + run: flutter build apk --obfuscate --split-debug-info=./symbols - name: Upload Artifact uses: actions/upload-artifact@v3.1.2 with: @@ -95,7 +95,7 @@ jobs: with: channel: "stable" - name: Build - run: flutter build windows --obfuscate --split-debug-info=/symbols + run: flutter build windows --obfuscate --split-debug-info=./symbols - name: Create Installer run: iscc "windowsApplicationInstallerSetup.iss" working-directory: . From b2184da03815754d686a3afc1462f207a7b38fc8 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Tue, 30 May 2023 18:57:19 +0100 Subject: [PATCH 017/168] Updated dependencies Former-commit-id: b598f87ebd6402741083a8b2ca526387a372bd80 [formerly 542408bf5b14143ee876efcd79bd7555ea38d204] Former-commit-id: 43894186ac80aa24c0a9ffbfaa80abb1d0d52d8a --- .../pages/downloader/components/map_view.dart | 18 +++++++++--------- .../lib/screens/main/pages/map/map_view.dart | 10 +++++----- example/pubspec.yaml | 8 ++++++-- lib/src/fmtc.dart | 2 +- lib/src/regions/line.dart | 2 +- lib/src/store/metadata.dart | 15 +++------------ pubspec.yaml | 8 +++++++- 7 files changed, 32 insertions(+), 31 deletions(-) diff --git a/example/lib/screens/main/pages/downloader/components/map_view.dart b/example/lib/screens/main/pages/downloader/components/map_view.dart index 558ab44a..2adb944d 100644 --- a/example/lib/screens/main/pages/downloader/components/map_view.dart +++ b/example/lib/screens/main/pages/downloader/components/map_view.dart @@ -48,10 +48,10 @@ class _MapViewState extends State { polygons: [ Polygon( points: [ - LatLng(-90, 180), - LatLng(90, 180), - LatLng(90, -180), - LatLng(-90, -180), + const LatLng(-90, 180), + const LatLng(90, 180), + const LatLng(90, -180), + const LatLng(-90, -180), ], holePointsList: [region.toOutline()], isFilled: true, @@ -130,14 +130,14 @@ class _MapViewState extends State { FlutterMap( mapController: _mapController, options: MapOptions( - center: LatLng(51.509364, -0.128928), + center: const LatLng(51.509364, -0.128928), zoom: 9.2, maxZoom: 22, maxBounds: LatLngBounds.fromPoints([ - LatLng(-90, 180), - LatLng(90, 180), - LatLng(90, -180), - LatLng(-90, -180), + const LatLng(-90, 180), + const LatLng(90, 180), + const LatLng(90, -180), + const LatLng(-90, -180), ]), interactiveFlags: InteractiveFlag.all & ~InteractiveFlag.rotate, diff --git a/example/lib/screens/main/pages/map/map_view.dart b/example/lib/screens/main/pages/map/map_view.dart index 76b053b7..542e4a16 100644 --- a/example/lib/screens/main/pages/map/map_view.dart +++ b/example/lib/screens/main/pages/map/map_view.dart @@ -41,14 +41,14 @@ class _MapPageState extends State { return FlutterMap( options: MapOptions( - center: LatLng(51.509364, -0.128928), + center: const LatLng(51.509364, -0.128928), zoom: 9.2, maxZoom: 22, maxBounds: LatLngBounds.fromPoints([ - LatLng(-90, 180), - LatLng(90, 180), - LatLng(90, -180), - LatLng(-90, -180), + const LatLng(-90, 180), + const LatLng(90, 180), + const LatLng(90, -180), + const LatLng(-90, -180), ]), interactiveFlags: InteractiveFlag.all & ~InteractiveFlag.rotate, scrollWheelVelocity: 0.002, diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 16177829..d0943deb 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -14,7 +14,7 @@ dependencies: better_open_file: ^3.6.4 flutter: sdk: flutter - flutter_foreground_task: ^4.1.0 + flutter_foreground_task: ^5.0.0 flutter_map: ^5.0.0-dev.1 flutter_map_tile_caching: flutter_speed_dial: ^6.0.0 @@ -23,7 +23,7 @@ dependencies: google_fonts: ^4.0.4 http: ^0.13.4 intl: ^0.18.0 - latlong2: ^0.8.1 + latlong2: ^0.9.0 osm_nominatim: ^2.0.1 path: ^1.8.3 provider: ^6.0.3 @@ -33,8 +33,12 @@ dependencies: version: ^3.0.2 dependency_overrides: + flutter_map: + git: + url: https://github.com/fleaflet/flutter_map.git flutter_map_tile_caching: path: ../ + http: ^1.0.0 flutter: uses-material-design: true diff --git a/lib/src/fmtc.dart b/lib/src/fmtc.dart index b381515e..8eda10aa 100644 --- a/lib/src/fmtc.dart +++ b/lib/src/fmtc.dart @@ -39,7 +39,7 @@ class FlutterMapTileCaching { required bool debugMode, }) : _debugMode = debugMode; - /// Initialise and prepare FMTC, by creating all neccessary directories/files + /// Initialise and prepare FMTC, by creating all necessary directories/files /// and configuring the [FlutterMapTileCaching] singleton /// /// Prefer to leave [rootDirectory] as `null`, which will use diff --git a/lib/src/regions/line.dart b/lib/src/regions/line.dart index ae7aee72..b1b29509 100644 --- a/lib/src/regions/line.dart +++ b/lib/src/regions/line.dart @@ -43,7 +43,7 @@ class LineRegion extends BaseRegion { final rad = radius * math.pi / 4; return line.map((pos) { - if ((line.indexOf(pos) + 1) >= line.length) return [LatLng(0, 0)]; + if ((line.indexOf(pos) + 1) >= line.length) return [const LatLng(0, 0)]; final section = [pos, line[line.indexOf(pos) + 1]]; diff --git a/lib/src/store/metadata.dart b/lib/src/store/metadata.dart index 57f47a6c..af32c903 100644 --- a/lib/src/store/metadata.dart +++ b/lib/src/store/metadata.dart @@ -14,13 +14,8 @@ class StoreMetadata extends _StoreDb { /// Add a new key-value pair to the store asynchronously /// /// Overwrites the value if the key already exists. - Future addAsync({ - required String key, - required String value, - }) => - _db.writeTxn( - () => _db.metadata.put(DbMetadata(name: key, data: value)), - ); + Future addAsync({required String key, required String value}) => + _db.writeTxn(() => _db.metadata.put(DbMetadata(name: key, data: value))); /// Add a new key-value pair to the store synchronously /// @@ -28,11 +23,7 @@ class StoreMetadata extends _StoreDb { /// /// Prefer [addAsync] to avoid blocking the UI thread. Otherwise, this has /// slightly better performance. - void add({ - required String key, - required String value, - }) => - _db.writeTxnSync( + void add({required String key, required String value}) => _db.writeTxnSync( () => _db.metadata.putSync(DbMetadata(name: key, data: value)), ); diff --git a/pubspec.yaml b/pubspec.yaml index e1eb053d..4a777005 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,7 +30,7 @@ dependencies: http_plus: ^0.2.2 isar: ^3.1.0+1 isar_flutter_libs: ^3.1.0+1 - latlong2: ^0.8.0 + latlong2: ^0.9.0 meta: ^1.7.0 path: ^1.8.2 path_provider: ^2.0.7 @@ -38,6 +38,12 @@ dependencies: stream_transform: ^2.0.0 watcher: ^1.0.2 +dependency_overrides: + flutter_map: + git: + url: https://github.com/fleaflet/flutter_map.git + http: ^1.0.0 + dev_dependencies: build_runner: ^2.3.2 flutter_lints: ^2.0.1 From bc01f936d6cbb62170cb655f15f6160609ddc2eb Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sun, 4 Jun 2023 12:45:05 +0100 Subject: [PATCH 018/168] Upgraded dependencies Absorbed 'http_plus' dependency Former-commit-id: 5f8bb841446f32a5b3dffc32a3c6713dc6620552 [formerly 61991c168b456860bc13188e8c8ae68a9c23d31f] Former-commit-id: 76a061eee9abb812d5efd4c9f1fc220f0ed21f7c --- .../pages/downloader/components/map_view.dart | 2 +- example/pubspec.yaml | 7 +- lib/flutter_map_tile_caching.dart | 2 +- lib/src/misc/http_plus.dart | 462 ++++++++++++++++++ pubspec.yaml | 20 +- 5 files changed, 476 insertions(+), 17 deletions(-) create mode 100644 lib/src/misc/http_plus.dart diff --git a/example/lib/screens/main/pages/downloader/components/map_view.dart b/example/lib/screens/main/pages/downloader/components/map_view.dart index 2adb944d..110ec0ff 100644 --- a/example/lib/screens/main/pages/downloader/components/map_view.dart +++ b/example/lib/screens/main/pages/downloader/components/map_view.dart @@ -357,7 +357,7 @@ class _MapViewState extends State { _radius = const Distance(roundResult: false).distance( _center!, _mapController - .pointToLatLng(_customPointFromPoint(calculatedTop))!, + .pointToLatLng(_customPointFromPoint(calculatedTop)), ) / 1000; setState(() {}); diff --git a/example/pubspec.yaml b/example/pubspec.yaml index d0943deb..dc3f2628 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -15,13 +15,13 @@ dependencies: flutter: sdk: flutter flutter_foreground_task: ^5.0.0 - flutter_map: ^5.0.0-dev.1 + flutter_map: ^5.0.0 flutter_map_tile_caching: flutter_speed_dial: ^6.0.0 fmtc_plus_background_downloading: ^8.0.0 fmtc_plus_sharing: ^8.0.0 google_fonts: ^4.0.4 - http: ^0.13.4 + http: ^1.0.0 intl: ^0.18.0 latlong2: ^0.9.0 osm_nominatim: ^2.0.1 @@ -33,9 +33,6 @@ dependencies: version: ^3.0.2 dependency_overrides: - flutter_map: - git: - url: https://github.com/fleaflet/flutter_map.git flutter_map_tile_caching: path: ../ http: ^1.0.0 diff --git a/lib/flutter_map_tile_caching.dart b/lib/flutter_map_tile_caching.dart index 08106afd..ff753039 100644 --- a/lib/flutter_map_tile_caching.dart +++ b/lib/flutter_map_tile_caching.dart @@ -25,7 +25,6 @@ import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/plugin_api.dart'; import 'package:http/http.dart'; import 'package:http/io_client.dart'; -import 'package:http_plus/http_plus.dart'; import 'package:isar/isar.dart'; import 'package:latlong2/latlong.dart'; import 'package:meta/meta.dart'; @@ -49,6 +48,7 @@ import 'src/errors/browsing.dart'; import 'src/errors/initialisation.dart'; import 'src/errors/store_not_ready.dart'; import 'src/misc/exts.dart'; +import 'src/misc/http_plus.dart'; import 'src/misc/typedefs.dart'; import 'src/providers/image_provider.dart'; diff --git a/lib/src/misc/http_plus.dart b/lib/src/misc/http_plus.dart new file mode 100644 index 00000000..17e7aad0 --- /dev/null +++ b/lib/src/misc/http_plus.dart @@ -0,0 +1,462 @@ +// Copyright © Luka S (JaffaKetchup) under GPL-v3 +// A full license can be found at .\LICENSE + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:http/http.dart'; +import 'package:http/io_client.dart'; +import 'package:http2/http2.dart'; +import 'package:http_parser/http_parser.dart'; + +////////////////////////////////////////////////////////////////////////////////// +// Originally maintained at github.com/daadu/http_plus // +// Now maintained internally due to incompatbility with the latest dependencies // +////////////////////////////////////////////////////////////////////////////////// + +/// HttpClient that supports HTTP2. +class HttpPlusClient extends BaseClient { + /// Flag to enable HTTP2. + /// + /// Defaults to `true`. + final bool enableHttp2; + + /// HTTP1 client that should be used. + /// + /// If null then new object of [IOClient] with appropriate configuration is + /// used. + final BaseClient http1Client; + + /// [SecurityContext] used when calling [SecureSocket.connect]. + final SecurityContext? context; + + /// [BadCertificateCallback] used when calling [SecureSocket.connect]. + final BadCertificateCallback? badCertificateCallback; + + /// Timeout [Duration] used when calling [SecureSocket.connect]. + final Duration? connectionTimeout; + + /// Automatically decompress response payload. + /// + /// If set to `true` and value of [HttpHeaders.contentEncodingHeader] in + /// response headers is either `gzip` or `deflate` then [GZipCodec.decoder] and + /// [ZLibCodec.decoder] is used respectively to transform the response body. + /// + /// Defaults to `true`. + final bool autoUncompress; + + /// Keep connections to the server open. + /// + /// If set to `false`, then the connection is closed immediately after the + /// response is collected. + /// + /// Defaults to `true`. + final bool maintainOpenConnections; + + /// Maximum number of connection that can be open at a time. + /// + /// Set to `-1` to have no limit on number of connections. + /// + /// If it is set to positive number and the connection limit is reached, then + /// the oldest connection is closed and removed. + /// + /// Defaults to `-1`. + final int maxOpenConnections; + + /// Enable logging. + /// + /// Defaults to `false`. + final bool enableLogging; + + /// Default client instance used by top-level functions. + /// + /// This client is shared across all top-level HTTP-method functions provided + /// by the library. + /// + /// [maxOpenConnections] is set to `8`, to limit resources. + static final HttpPlusClient defaultClient = + HttpPlusClient(maxOpenConnections: 8); + + final Map _h2Connections = {}; + + /// Create [HttpPlusClient] object. + HttpPlusClient({ + this.enableHttp2 = true, + BaseClient? http1Client, + this.context, + this.badCertificateCallback, + this.connectionTimeout, + this.autoUncompress = true, + this.maintainOpenConnections = true, + this.maxOpenConnections = -1, + this.enableLogging = false, + }) : assert( + maxOpenConnections == -1 || maxOpenConnections > 0, + 'maxOpenConnections must be -1, or > 0.', + ), + http1Client = http1Client ?? + IOClient( + HttpClient(context: context) + ..badCertificateCallback = badCertificateCallback + ..connectionTimeout = connectionTimeout + ..autoUncompress = autoUncompress, + ); + + @override + Future send(BaseRequest request) async => + _send(request, []); + + Future _send( + BaseRequest request, + List redirects, + ) async { + // if not-enabled or non-HTTPS -> HTTP 1.x + if (!enableHttp2 || request.url.scheme != 'https') { + return _sendHttp1(request); + } + + // get-or-create HTTP2 connection + final h2Connection = + await _getOrCreateHttp2Connection(request.url.host, request.url.port); + + // if no h2Connection - then fallback to HTTP 1.x + if (h2Connection == null) return _sendHttp1(request); + + // make HTTP2 request + return _sendHttp2(request, h2Connection, redirects); + } + + Future _getOrCreateHttp2Connection( + String host, + int port, + ) async { + // get an existing (if any) HTTP2 connection + var connection = _h2Connections[host]; + + // return if connection exists and is open + if (connection?.isOpen ?? false) return connection; + + // if connection exists - then reset and remove it from _connections + if (connection != null) { + connection = null; + _h2Connections.remove(host); + } + + // create new socket + const http2Protocol = 'h2'; + final socket = await SecureSocket.connect( + host, + port, + supportedProtocols: [http2Protocol], + onBadCertificate: badCertificateCallback != null + ? (cert) => badCertificateCallback!.call(cert, host, port) + : null, + context: context, + timeout: connectionTimeout, + ); + + // if HTTP2 not selected - then close and return null + if (socket.selectedProtocol != http2Protocol) { + await socket.close(); + return null; + } + + // if maxOpenConnections limit reached -> close some connections + if (maxOpenConnections > -1) { + while (_h2Connections.length >= maxOpenConnections) { + final oldConnection = _h2Connections.remove(_h2Connections.keys.first)!; + await oldConnection.finish(); + } + } + + // create connection from socket, save it for future use, and return it + connection = ClientTransportConnection.viaSocket(socket); + if (maintainOpenConnections) _h2Connections[host] = connection; + return connection; + } + + Future _sendHttp1(BaseRequest request) => + http1Client.send(request); + + Future _sendHttp2( + BaseRequest request, + ClientTransportConnection connection, + List redirects, + ) async { + // finalize request + final requestStream = request.finalize(); + + // make headers + final headers = [ + Header.ascii(':method', request.method), + Header.ascii(':path', _fullUrlPath(request.url)), + Header.ascii(':scheme', request.url.scheme), + Header.ascii(':authority', request.url.host), + // if method-with-data and no content-length and transfer-encoding != chunked + // -> then add `contentLengthHeader` + if ({'PUT', 'POST', 'PATCH'}.contains(request.method) && + !request.headers.containsKey(HttpHeaders.contentLengthHeader) && + request.headers[HttpHeaders.transferEncodingHeader] != 'chunked') + Header.ascii( + HttpHeaders.contentLengthHeader, + request.contentLength.toString(), + ), + ...request.headers.keys.map( + (key) => Header.ascii( + key.toLowerCase(), + request.headers[key] ?? '', + ), + ), + ]; + + // create outgoing stream + final stream = connection.makeRequest(headers); + + // stream request data to stream - and then close outgoing sink + await requestStream.forEach(stream.sendData); + await stream.outgoingMessages.close(); + + // make StreamedResponse + final response = await _makeResponse(request, stream, redirects); + + // if not maintainOpenConnections - then close connection + if (!maintainOpenConnections) await connection.finish(); + + // return response + return response; + } + + Future _makeResponse( + BaseRequest request, + ClientTransportStream stream, + List redirects, + ) { + // initialize - header, body + final headers = CaseInsensitiveMap(); + final body = StreamController>(); + final responseCompleter = Completer(); + + void complete() { + // ignore if already completed + if (responseCompleter.isCompleted) return; + + // check if status is present + final statusCode = int.tryParse(headers.remove(':status').toString()); + if (statusCode == null) { + return responseCompleter.completeError( + StateError( + 'Server ${request.url} did not send a response status code.', + ), + ); + } + + // follow redirects - if has locationHeader + if (request.followRedirects && + headers.containsKey(HttpHeaders.locationHeader)) { + // check if not exceeding maxRedirects + if (redirects.length >= request.maxRedirects) { + return responseCompleter.completeError( + RedirectException( + 'max redirect count of ${request.maxRedirects} exceeded', + redirects, + ), + ); + } + + // get location and create new - redirects and request + final location = + request.url.resolve(headers[HttpHeaders.locationHeader]!); + final newRedirects = List.from(redirects) + ..add(_RedirectInfo(request.method, statusCode, location)); + final newRequest = Request( + statusCode == HttpStatus.temporaryRedirect ? request.method : 'GET', + location, + ) + ..followRedirects = request.followRedirects + ..headers.addAll(request.headers) + ..maxRedirects = request.maxRedirects; + if (request is Request) { + newRequest.encoding = request.encoding; + if (statusCode == 307) { + newRequest.bodyBytes = request.bodyBytes; + } + } + if (request.contentLength != null) { + newRequest.headers[HttpHeaders.contentLengthHeader] = + request.contentLength.toString(); + } + // call _send with new request and redirect and complete with it + return responseCompleter.complete(_send(newRequest, newRedirects)); + } + + // transform stream if compressed + var responseStream = body.stream; + if (autoUncompress) { + if (headers[HttpHeaders.contentEncodingHeader] == 'gzip') { + responseStream = responseStream.transform(gzip.decoder); + } else if (headers[HttpHeaders.contentEncodingHeader] == 'deflate') { + responseStream = responseStream.transform(zlib.decoder); + } + } + responseCompleter.complete( + StreamedResponse( + responseStream, + statusCode, + contentLength: + int.tryParse(headers[HttpHeaders.contentLengthHeader].toString()), + headers: headers, + request: request, + reasonPhrase: _findReasonPhrase(statusCode), + isRedirect: headers.containsKey(HttpHeaders.locationHeader), + ), + ); + } + + stream.incomingMessages.listen( + (message) { + if (message is HeadersStreamMessage) { + for (final header in message.headers) { + headers[utf8.decode(header.name)] = utf8.decode(header.value); + } + } else if (message is DataStreamMessage) { + body.add(message.bytes); + } else if (!responseCompleter.isCompleted) { + responseCompleter.completeError( + ArgumentError.value( + message, + 'message', + 'must be HeadersStreamMessage or DataStreamMessage', + ), + ); + } + }, + cancelOnError: true, + onDone: () { + complete(); + body.close(); + }, + onError: (e, s) { + if (!responseCompleter.isCompleted) { + responseCompleter.completeError(e, s); + } + }, + ); + + return responseCompleter.future; + } + + @override + void close() { + super.close(); + http1Client.close(); + for (final socket in _h2Connections.values) { + socket.finish(); + } + } +} + +class _RedirectInfo implements RedirectInfo { + @override + final String method; + @override + final int statusCode; + @override + final Uri location; + + _RedirectInfo(this.method, this.statusCode, this.location); +} + +String _fullUrlPath(Uri uri, {bool withFragment = false}) => Uri( + fragment: uri.hasFragment && withFragment ? uri.fragment : null, + query: uri.hasQuery ? uri.query : null, + path: uri.path, + ).toString(); + +/// Taken from `dart:_http`. Finds the HTTP reason phrase for a given [statusCode]. +String _findReasonPhrase(int statusCode) { + switch (statusCode) { + case HttpStatus.continue_: + return 'Continue'; + case HttpStatus.switchingProtocols: + return 'Switching Protocols'; + case HttpStatus.ok: + return 'OK'; + case HttpStatus.created: + return 'Created'; + case HttpStatus.accepted: + return 'Accepted'; + case HttpStatus.nonAuthoritativeInformation: + return 'Non-Authoritative Information'; + case HttpStatus.noContent: + return 'No Content'; + case HttpStatus.resetContent: + return 'Reset Content'; + case HttpStatus.partialContent: + return 'Partial Content'; + case HttpStatus.multipleChoices: + return 'Multiple Choices'; + case HttpStatus.movedPermanently: + return 'Moved Permanently'; + case HttpStatus.found: + return 'Found'; + case HttpStatus.seeOther: + return 'See Other'; + case HttpStatus.notModified: + return 'Not Modified'; + case HttpStatus.useProxy: + return 'Use Proxy'; + case HttpStatus.temporaryRedirect: + return 'Temporary Redirect'; + case HttpStatus.badRequest: + return 'Bad Request'; + case HttpStatus.unauthorized: + return 'Unauthorized'; + case HttpStatus.paymentRequired: + return 'Payment Required'; + case HttpStatus.forbidden: + return 'Forbidden'; + case HttpStatus.notFound: + return 'Not Found'; + case HttpStatus.methodNotAllowed: + return 'Method Not Allowed'; + case HttpStatus.notAcceptable: + return 'Not Acceptable'; + case HttpStatus.proxyAuthenticationRequired: + return 'Proxy Authentication Required'; + case HttpStatus.requestTimeout: + return 'Request Time-out'; + case HttpStatus.conflict: + return 'Conflict'; + case HttpStatus.gone: + return 'Gone'; + case HttpStatus.lengthRequired: + return 'Length Required'; + case HttpStatus.preconditionFailed: + return 'Precondition Failed'; + case HttpStatus.requestEntityTooLarge: + return 'Request Entity Too Large'; + case HttpStatus.requestUriTooLong: + return 'Request-URI Too Long'; + case HttpStatus.unsupportedMediaType: + return 'Unsupported Media Type'; + case HttpStatus.requestedRangeNotSatisfiable: + return 'Requested range not satisfiable'; + case HttpStatus.expectationFailed: + return 'Expectation Failed'; + case HttpStatus.internalServerError: + return 'Internal Server Error'; + case HttpStatus.notImplemented: + return 'Not Implemented'; + case HttpStatus.badGateway: + return 'Bad Gateway'; + case HttpStatus.serviceUnavailable: + return 'Service Unavailable'; + case HttpStatus.gatewayTimeout: + return 'Gateway Time-out'; + case HttpStatus.httpVersionNotSupported: + return 'Http Version not supported'; + default: + return 'Status $statusCode'; + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 4a777005..032aac34 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_map_tile_caching description: Plugin for 'flutter_map' providing advanced caching functionality, with ability to download map regions for offline use. -version: 9.0.0-dev.1 +version: 9.0.0-dev.2 repository: https://github.com/JaffaKetchup/flutter_map_tile_caching issue_tracker: https://github.com/JaffaKetchup/flutter_map_tile_caching/issues documentation: https://fmtc.jaffaketchup.dev @@ -9,6 +9,11 @@ documentation: https://fmtc.jaffaketchup.dev funding: - https://github.com/sponsors/JaffaKetchup +topics: + - flutter-map + - map + - fmtc + platforms: android: ios: @@ -25,9 +30,10 @@ dependencies: collection: ^1.16.0 flutter: sdk: flutter - flutter_map: ^5.0.0-dev.1 - http: ^0.13.5 - http_plus: ^0.2.2 + flutter_map: ^5.0.0 + http: ^1.0.0 + http2: ^2.0.1 + http_parser: ^4.0.2 isar: ^3.1.0+1 isar_flutter_libs: ^3.1.0+1 latlong2: ^0.9.0 @@ -38,12 +44,6 @@ dependencies: stream_transform: ^2.0.0 watcher: ^1.0.2 -dependency_overrides: - flutter_map: - git: - url: https://github.com/fleaflet/flutter_map.git - http: ^1.0.0 - dev_dependencies: build_runner: ^2.3.2 flutter_lints: ^2.0.1 From 91b7dbf73db0b405ec4302728e03e4052edd0ab1 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Tue, 27 Jun 2023 20:19:20 +0100 Subject: [PATCH 019/168] Added secondary check to tile images retrieved across the network in `FMTCImageProvider` Improved error handling and definitions in `FMTCImageProvider` Upgraded dependencies Former-commit-id: d9d949f1d969049212db517d8710db942da2c95a [formerly 008058f57f72bfc29d3b04e5f56e65fb87337ec6] Former-commit-id: 9ff847ac9117b4995eb5e1ab919c2dbdb7012111 --- .../pages/downloader/components/map_view.dart | 39 +- .../lib/screens/main/pages/map/map_view.dart | 26 +- example/pubspec.yaml | 10 +- lib/flutter_map_tile_caching.dart | 2 +- lib/src/bulk_download/downloader.dart | 2 +- lib/src/errors/browsing.dart | 154 +++++- lib/src/misc/http_plus.dart | 462 ------------------ lib/src/providers/image_provider.dart | 194 +++++--- lib/src/providers/tile_provider.dart | 6 +- lib/src/store/directory.dart | 6 +- lib/src/store/download.dart | 6 +- pubspec.yaml | 5 +- 12 files changed, 307 insertions(+), 605 deletions(-) delete mode 100644 lib/src/misc/http_plus.dart diff --git a/example/lib/screens/main/pages/downloader/components/map_view.dart b/example/lib/screens/main/pages/downloader/components/map_view.dart index 110ec0ff..d75aab83 100644 --- a/example/lib/screens/main/pages/downloader/components/map_view.dart +++ b/example/lib/screens/main/pages/downloader/components/map_view.dart @@ -3,6 +3,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map/plugin_api.dart'; import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; import 'package:latlong2/latlong.dart'; import 'package:provider/provider.dart'; @@ -130,19 +131,21 @@ class _MapViewState extends State { FlutterMap( mapController: _mapController, options: MapOptions( - center: const LatLng(51.509364, -0.128928), - zoom: 9.2, + initialCenter: const LatLng(51.509364, -0.128928), + initialZoom: 9.2, maxZoom: 22, - maxBounds: LatLngBounds.fromPoints([ - const LatLng(-90, 180), - const LatLng(90, 180), - const LatLng(90, -180), - const LatLng(-90, -180), - ]), - interactiveFlags: - InteractiveFlag.all & ~InteractiveFlag.rotate, - scrollWheelVelocity: 0.002, - keepAlive: true, + cameraConstraint: CameraConstraint.contain( + bounds: LatLngBounds.fromPoints([ + const LatLng(-90, 180), + const LatLng(90, 180), + const LatLng(90, -180), + const LatLng(-90, -180), + ]), + ), + interactionOptions: const InteractionOptions( + flags: InteractiveFlag.all & ~InteractiveFlag.rotate, + scrollWheelVelocity: 0.002, + ), onMapReady: () { _updatePointLatLng(); _countTiles(); @@ -150,6 +153,7 @@ class _MapViewState extends State { onTap: (_, point) => _addLinePoint(point), onSecondaryTap: (_, point) => _removeLinePoint(), onLongPress: (_, point) => _removeLinePoint(), + keepAlive: true, ), nonRotatedChildren: buildStdAttribution( urlTemplate, @@ -352,11 +356,12 @@ class _MapViewState extends State { _crosshairsTop = calculatedTop - _crosshairsMovement; _crosshairsBottom = centerNormal - _crosshairsMovement; - _center = - _mapController.pointToLatLng(_customPointFromPoint(centerNormal)); + _center = MapController.of(context) + .camera + .pointToLatLng(_customPointFromPoint(centerNormal)); _radius = const Distance(roundResult: false).distance( _center!, - _mapController + _mapController.camera .pointToLatLng(_customPointFromPoint(calculatedTop)), ) / 1000; @@ -370,9 +375,9 @@ class _MapViewState extends State { _crosshairsTop = calculatedTopLeft - _crosshairsMovement; _crosshairsBottom = calculatedBottomRight - _crosshairsMovement; - _coordsTopLeft = _mapController + _coordsTopLeft = _mapController.camera .pointToLatLng(_customPointFromPoint(calculatedTopLeft)); - _coordsBottomRight = _mapController + _coordsBottomRight = _mapController.camera .pointToLatLng(_customPointFromPoint(calculatedBottomRight)); setState(() {}); diff --git a/example/lib/screens/main/pages/map/map_view.dart b/example/lib/screens/main/pages/map/map_view.dart index 542e4a16..96e9ae95 100644 --- a/example/lib/screens/main/pages/map/map_view.dart +++ b/example/lib/screens/main/pages/map/map_view.dart @@ -41,17 +41,21 @@ class _MapPageState extends State { return FlutterMap( options: MapOptions( - center: const LatLng(51.509364, -0.128928), - zoom: 9.2, + initialCenter: const LatLng(51.509364, -0.128928), + initialZoom: 9.2, maxZoom: 22, - maxBounds: LatLngBounds.fromPoints([ - const LatLng(-90, 180), - const LatLng(90, 180), - const LatLng(90, -180), - const LatLng(-90, -180), - ]), - interactiveFlags: InteractiveFlag.all & ~InteractiveFlag.rotate, - scrollWheelVelocity: 0.002, + cameraConstraint: CameraConstraint.contain( + bounds: LatLngBounds.fromPoints([ + const LatLng(-90, 180), + const LatLng(90, 180), + const LatLng(90, -180), + const LatLng(-90, -180), + ]), + ), + interactionOptions: const InteractionOptions( + flags: InteractiveFlag.all & ~InteractiveFlag.rotate, + scrollWheelVelocity: 0.002, + ), keepAlive: true, ), nonRotatedChildren: buildStdAttribution(urlTemplate), @@ -60,7 +64,7 @@ class _MapPageState extends State { urlTemplate: urlTemplate, tileProvider: provider.currentStore != null ? FMTC.instance(provider.currentStore!).getTileProvider( - FMTCTileProviderSettings( + settings: FMTCTileProviderSettings( behavior: CacheBehavior.values .byName(metadata.data!['behaviour']!), cachedValidDuration: int.parse( diff --git a/example/pubspec.yaml b/example/pubspec.yaml index dc3f2628..9fcababd 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -14,13 +14,13 @@ dependencies: better_open_file: ^3.6.4 flutter: sdk: flutter - flutter_foreground_task: ^5.0.0 + flutter_foreground_task: ^6.0.0+1 flutter_map: ^5.0.0 flutter_map_tile_caching: - flutter_speed_dial: ^6.0.0 + flutter_speed_dial: ^7.0.0 fmtc_plus_background_downloading: ^8.0.0 fmtc_plus_sharing: ^8.0.0 - google_fonts: ^4.0.4 + google_fonts: ^5.1.0 http: ^1.0.0 intl: ^0.18.0 latlong2: ^0.9.0 @@ -33,6 +33,10 @@ dependencies: version: ^3.0.2 dependency_overrides: + flutter_map: + git: + url: https://github.com/rorystephenson/flutter_map.git + ref: flutter-map-state-refactor flutter_map_tile_caching: path: ../ http: ^1.0.0 diff --git a/lib/flutter_map_tile_caching.dart b/lib/flutter_map_tile_caching.dart index ff753039..08106afd 100644 --- a/lib/flutter_map_tile_caching.dart +++ b/lib/flutter_map_tile_caching.dart @@ -25,6 +25,7 @@ import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/plugin_api.dart'; import 'package:http/http.dart'; import 'package:http/io_client.dart'; +import 'package:http_plus/http_plus.dart'; import 'package:isar/isar.dart'; import 'package:latlong2/latlong.dart'; import 'package:meta/meta.dart'; @@ -48,7 +49,6 @@ import 'src/errors/browsing.dart'; import 'src/errors/initialisation.dart'; import 'src/errors/store_not_ready.dart'; import 'src/misc/exts.dart'; -import 'src/misc/http_plus.dart'; import 'src/misc/typedefs.dart'; import 'src/providers/image_provider.dart'; diff --git a/lib/src/bulk_download/downloader.dart b/lib/src/bulk_download/downloader.dart index a8bb187c..d6b3cbcc 100644 --- a/lib/src/bulk_download/downloader.dart +++ b/lib/src/bulk_download/downloader.dart @@ -30,7 +30,7 @@ Future> bulkDownloader({ required FMTCTileProvider provider, required Uint8List? seaTileBytes, required InternalProgressTimingManagement progressManagement, - required BaseClient client, + required Client client, }) async { final tiles = FMTCRegistry.instance(provider.storeDirectory.storeName); diff --git a/lib/src/errors/browsing.dart b/lib/src/errors/browsing.dart index ace1fcd3..832e9691 100644 --- a/lib/src/errors/browsing.dart +++ b/lib/src/errors/browsing.dart @@ -1,10 +1,12 @@ // Copyright © Luka S (JaffaKetchup) under GPL-v3 // A full license can be found at .\LICENSE +import 'package:flutter_map/flutter_map.dart'; +import 'package:http/http.dart'; +import 'package:http/io_client.dart'; import 'package:meta/meta.dart'; import '../../flutter_map_tile_caching.dart'; -import '../providers/image_provider.dart'; /// An [Exception] indicating that there was an error retrieving tiles to be /// displayed on the map @@ -13,16 +15,50 @@ import '../providers/image_provider.dart'; /// through of all valid/possible cases, but you may wish to handle them /// anyway using [FMTCTileProviderSettings.errorHandler]. /// -/// Always thrown from within [FMTCImageProvider] generated from -/// [FMTCTileProvider]. The [message] further indicates the reason, and will -/// depend on the current caching behaviour. The [type] represents the same -/// message in a way that is easy to parse/handle. +/// Use [type] to establish the condition that threw this exception, and +/// [message] for a user-friendly English description of this exception. Also +/// see the other properties for more information. class FMTCBrowsingError implements Exception { - /// Friendly message + /// Defines the condition that threw this exception + /// + /// See [message] for a user friendly description of this value. + final FMTCBrowsingErrorType type; + + /// A user-friendly English description of the [type] of this exception, + /// suitable for UI display, also with some hints at a potential resolution + /// or debugging step. + /// + /// Need just the description, or just the resolution step? See + /// [FMTCBrowsingErrorType.explanation] & [FMTCBrowsingErrorType.resolution]. final String message; - /// Programmatic error descriptor - final FMTCBrowsingErrorType type; + /// Generated network URL at which the tile was requested from + final String networkUrl; + + /// Generated URL that was used to find potential existing cached tiles, + /// taking into account [FMTCTileProviderSettings.obscuredQueryParams]. + final String matcherUrl; + + /// If available, the attempted HTTP request + /// + /// Will be available if [type] is not + /// [FMTCBrowsingErrorType.missingInCacheOnlyMode]. + final Request? request; + + /// If available, the HTTP response streamed from the server + /// + /// Will be available if [type] is + /// [FMTCBrowsingErrorType.negativeFetchResponse] or + /// [FMTCBrowsingErrorType.invalidImageData]. + final StreamedResponse? response; + + /// If available, the error object that was caught when attempting the HTTP + /// request + /// + /// Will be available if [type] is + /// [FMTCBrowsingErrorType.noConnectionDuringFetch] or + /// [FMTCBrowsingErrorType.unknownFetchException]. + final Object? originalError; /// An [Exception] indicating that there was an error retrieving tiles to be /// displayed on the map @@ -31,33 +67,97 @@ class FMTCBrowsingError implements Exception { /// through of all valid/possible cases, but you may wish to handle them /// anyway using [FMTCTileProviderSettings.errorHandler]. /// - /// Always thrown from within [FMTCImageProvider] generated from - /// [FMTCTileProvider]. The [message] further indicates the reason, and will - /// depend on the current caching behaviour. The [type] represents the same - /// message in a way that is easy to parse/handle. + /// Use [type] to establish the condition that threw this exception, and + /// [message] for a user-friendly English description of this exception. Also + /// see the other properties for more information. @internal - FMTCBrowsingError(this.message, this.type); + FMTCBrowsingError({ + required this.type, + required this.networkUrl, + required this.matcherUrl, + this.request, + this.response, + this.originalError, + }) : message = '${type.explanation} ${type.resolution}'; @override - String toString() => 'FMTCBrowsingError: $message'; + String toString() => 'FMTCBrowsingError ($type): $message'; } -/// Pragmatic error descriptor for a [FMTCBrowsingError.message] +/// Defines the type of issue that a [FMTCBrowsingError] is reporting /// -/// See documentation on that object for more information. +/// See [explanation] and [resolution] for more information about each type. +/// [FMTCBrowsingError.message] is formed from the concatenation of these two +/// properties. enum FMTCBrowsingErrorType { - /// Paired with friendly message: - /// "Failed to load the tile from the cache because it was missing." - missingInCacheOnlyMode, + /// Failed to load the tile from the cache because it was missing + /// + /// Ensure that tiles are cached before using [CacheBehavior.cacheOnly]. + missingInCacheOnlyMode( + 'Failed to load the tile from the cache because it was missing.', + 'Ensure that tiles are cached before using `CacheBehavior.cacheOnly`.', + ), - /// Paired with friendly message: - /// "Failed to load the tile from the cache or the network because it was + /// Failed to load the tile from the cache or the network because it was /// missing from the cache and a connection to the server could not be - /// established." - noConnectionDuringFetch, + /// established + /// + /// Check your Internet connection. + noConnectionDuringFetch( + 'Failed to load the tile from the cache or the network because it was missing from the cache and a connection to the server could not be established.', + 'Check your Internet connection.', + ), + + /// Failed to load the tile from the cache or network because it was missing + /// from the cache and there was an unexpected error when requesting from the + /// server + /// + /// Try specifying a normal HTTP/1.1 [IOClient] when using + /// [StoreDirectory.getTileProvider]. Check that the [TileLayer.urlTemplate] is + /// correct, that any necessary authorization data is correctly included, and + /// that the server serves the viewed region. + unknownFetchException( + 'Failed to load the tile from the cache or network because it was missing from the cache and there was an unexpected error when requesting from the server.', + 'Try specifying a normal HTTP/1.1 `IOClient` when using `getTileProvider`. Check that the `TileLayer.urlTemplate` is correct, that any necessary authorization data is correctly included, and that the server serves the viewed region.', + ), + + /// Failed to load the tile from the cache or the network because it was + /// missing from the cache and the server responded with a HTTP code other than + /// 200 OK + /// + /// Check that the [TileLayer.urlTemplate] is correct, that any necessary + /// authorization data is correctly included, and that the server serves the + /// viewed region. + negativeFetchResponse( + 'Failed to load the tile from the cache or the network because it was missing from the cache and the server responded with a HTTP code other than 200 OK.', + 'Check that the `TileLayer.urlTemplate` is correct, that any necessary authorization data is correctly included, and that the server serves the viewed region.', + ), + + /// Failed to load the tile from the network because it responded with an HTTP + /// code of 200 OK but an invalid image data + /// + /// Your server may be misconfigured and returning an error message or blank + /// response under 200 OK. Check that the `TileLayer.urlTemplate` is correct, + /// that any necessary authorization data is correctly included, and that the + /// server serves the viewed region. + invalidImageData( + 'Failed to load the tile from the network because it responded with an HTTP code of 200 OK but an invalid image data.', + 'Your server may be misconfigured and returning an error message or blank response under 200 OK. Check that the `TileLayer.urlTemplate` is correct, that any necessary authorization data is correctly included, and that the server serves the viewed region.', + ); + + /// Defines the type of issue that a [FMTCBrowsingError] is reporting + /// + /// See [explanation] and [resolution] for more information about each type. + /// [FMTCBrowsingError.message] is formed from the concatenation of these two + /// properties. + @internal + const FMTCBrowsingErrorType(this.explanation, this.resolution); + + /// A user-friendly English description of this exception, suitable for UI + /// display + final String explanation; - /// Paired with friendly message: - /// "Failed to load the tile from the cache or the network because it was - /// missing from the cache and the server responded with a HTTP code of <$>." - negativeFetchResponse, + /// Guidance (in user-friendly English) for how this exception might be + /// resolved, or at least a first debugging step + final String resolution; } diff --git a/lib/src/misc/http_plus.dart b/lib/src/misc/http_plus.dart deleted file mode 100644 index 17e7aad0..00000000 --- a/lib/src/misc/http_plus.dart +++ /dev/null @@ -1,462 +0,0 @@ -// Copyright © Luka S (JaffaKetchup) under GPL-v3 -// A full license can be found at .\LICENSE - -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:http/http.dart'; -import 'package:http/io_client.dart'; -import 'package:http2/http2.dart'; -import 'package:http_parser/http_parser.dart'; - -////////////////////////////////////////////////////////////////////////////////// -// Originally maintained at github.com/daadu/http_plus // -// Now maintained internally due to incompatbility with the latest dependencies // -////////////////////////////////////////////////////////////////////////////////// - -/// HttpClient that supports HTTP2. -class HttpPlusClient extends BaseClient { - /// Flag to enable HTTP2. - /// - /// Defaults to `true`. - final bool enableHttp2; - - /// HTTP1 client that should be used. - /// - /// If null then new object of [IOClient] with appropriate configuration is - /// used. - final BaseClient http1Client; - - /// [SecurityContext] used when calling [SecureSocket.connect]. - final SecurityContext? context; - - /// [BadCertificateCallback] used when calling [SecureSocket.connect]. - final BadCertificateCallback? badCertificateCallback; - - /// Timeout [Duration] used when calling [SecureSocket.connect]. - final Duration? connectionTimeout; - - /// Automatically decompress response payload. - /// - /// If set to `true` and value of [HttpHeaders.contentEncodingHeader] in - /// response headers is either `gzip` or `deflate` then [GZipCodec.decoder] and - /// [ZLibCodec.decoder] is used respectively to transform the response body. - /// - /// Defaults to `true`. - final bool autoUncompress; - - /// Keep connections to the server open. - /// - /// If set to `false`, then the connection is closed immediately after the - /// response is collected. - /// - /// Defaults to `true`. - final bool maintainOpenConnections; - - /// Maximum number of connection that can be open at a time. - /// - /// Set to `-1` to have no limit on number of connections. - /// - /// If it is set to positive number and the connection limit is reached, then - /// the oldest connection is closed and removed. - /// - /// Defaults to `-1`. - final int maxOpenConnections; - - /// Enable logging. - /// - /// Defaults to `false`. - final bool enableLogging; - - /// Default client instance used by top-level functions. - /// - /// This client is shared across all top-level HTTP-method functions provided - /// by the library. - /// - /// [maxOpenConnections] is set to `8`, to limit resources. - static final HttpPlusClient defaultClient = - HttpPlusClient(maxOpenConnections: 8); - - final Map _h2Connections = {}; - - /// Create [HttpPlusClient] object. - HttpPlusClient({ - this.enableHttp2 = true, - BaseClient? http1Client, - this.context, - this.badCertificateCallback, - this.connectionTimeout, - this.autoUncompress = true, - this.maintainOpenConnections = true, - this.maxOpenConnections = -1, - this.enableLogging = false, - }) : assert( - maxOpenConnections == -1 || maxOpenConnections > 0, - 'maxOpenConnections must be -1, or > 0.', - ), - http1Client = http1Client ?? - IOClient( - HttpClient(context: context) - ..badCertificateCallback = badCertificateCallback - ..connectionTimeout = connectionTimeout - ..autoUncompress = autoUncompress, - ); - - @override - Future send(BaseRequest request) async => - _send(request, []); - - Future _send( - BaseRequest request, - List redirects, - ) async { - // if not-enabled or non-HTTPS -> HTTP 1.x - if (!enableHttp2 || request.url.scheme != 'https') { - return _sendHttp1(request); - } - - // get-or-create HTTP2 connection - final h2Connection = - await _getOrCreateHttp2Connection(request.url.host, request.url.port); - - // if no h2Connection - then fallback to HTTP 1.x - if (h2Connection == null) return _sendHttp1(request); - - // make HTTP2 request - return _sendHttp2(request, h2Connection, redirects); - } - - Future _getOrCreateHttp2Connection( - String host, - int port, - ) async { - // get an existing (if any) HTTP2 connection - var connection = _h2Connections[host]; - - // return if connection exists and is open - if (connection?.isOpen ?? false) return connection; - - // if connection exists - then reset and remove it from _connections - if (connection != null) { - connection = null; - _h2Connections.remove(host); - } - - // create new socket - const http2Protocol = 'h2'; - final socket = await SecureSocket.connect( - host, - port, - supportedProtocols: [http2Protocol], - onBadCertificate: badCertificateCallback != null - ? (cert) => badCertificateCallback!.call(cert, host, port) - : null, - context: context, - timeout: connectionTimeout, - ); - - // if HTTP2 not selected - then close and return null - if (socket.selectedProtocol != http2Protocol) { - await socket.close(); - return null; - } - - // if maxOpenConnections limit reached -> close some connections - if (maxOpenConnections > -1) { - while (_h2Connections.length >= maxOpenConnections) { - final oldConnection = _h2Connections.remove(_h2Connections.keys.first)!; - await oldConnection.finish(); - } - } - - // create connection from socket, save it for future use, and return it - connection = ClientTransportConnection.viaSocket(socket); - if (maintainOpenConnections) _h2Connections[host] = connection; - return connection; - } - - Future _sendHttp1(BaseRequest request) => - http1Client.send(request); - - Future _sendHttp2( - BaseRequest request, - ClientTransportConnection connection, - List redirects, - ) async { - // finalize request - final requestStream = request.finalize(); - - // make headers - final headers = [ - Header.ascii(':method', request.method), - Header.ascii(':path', _fullUrlPath(request.url)), - Header.ascii(':scheme', request.url.scheme), - Header.ascii(':authority', request.url.host), - // if method-with-data and no content-length and transfer-encoding != chunked - // -> then add `contentLengthHeader` - if ({'PUT', 'POST', 'PATCH'}.contains(request.method) && - !request.headers.containsKey(HttpHeaders.contentLengthHeader) && - request.headers[HttpHeaders.transferEncodingHeader] != 'chunked') - Header.ascii( - HttpHeaders.contentLengthHeader, - request.contentLength.toString(), - ), - ...request.headers.keys.map( - (key) => Header.ascii( - key.toLowerCase(), - request.headers[key] ?? '', - ), - ), - ]; - - // create outgoing stream - final stream = connection.makeRequest(headers); - - // stream request data to stream - and then close outgoing sink - await requestStream.forEach(stream.sendData); - await stream.outgoingMessages.close(); - - // make StreamedResponse - final response = await _makeResponse(request, stream, redirects); - - // if not maintainOpenConnections - then close connection - if (!maintainOpenConnections) await connection.finish(); - - // return response - return response; - } - - Future _makeResponse( - BaseRequest request, - ClientTransportStream stream, - List redirects, - ) { - // initialize - header, body - final headers = CaseInsensitiveMap(); - final body = StreamController>(); - final responseCompleter = Completer(); - - void complete() { - // ignore if already completed - if (responseCompleter.isCompleted) return; - - // check if status is present - final statusCode = int.tryParse(headers.remove(':status').toString()); - if (statusCode == null) { - return responseCompleter.completeError( - StateError( - 'Server ${request.url} did not send a response status code.', - ), - ); - } - - // follow redirects - if has locationHeader - if (request.followRedirects && - headers.containsKey(HttpHeaders.locationHeader)) { - // check if not exceeding maxRedirects - if (redirects.length >= request.maxRedirects) { - return responseCompleter.completeError( - RedirectException( - 'max redirect count of ${request.maxRedirects} exceeded', - redirects, - ), - ); - } - - // get location and create new - redirects and request - final location = - request.url.resolve(headers[HttpHeaders.locationHeader]!); - final newRedirects = List.from(redirects) - ..add(_RedirectInfo(request.method, statusCode, location)); - final newRequest = Request( - statusCode == HttpStatus.temporaryRedirect ? request.method : 'GET', - location, - ) - ..followRedirects = request.followRedirects - ..headers.addAll(request.headers) - ..maxRedirects = request.maxRedirects; - if (request is Request) { - newRequest.encoding = request.encoding; - if (statusCode == 307) { - newRequest.bodyBytes = request.bodyBytes; - } - } - if (request.contentLength != null) { - newRequest.headers[HttpHeaders.contentLengthHeader] = - request.contentLength.toString(); - } - // call _send with new request and redirect and complete with it - return responseCompleter.complete(_send(newRequest, newRedirects)); - } - - // transform stream if compressed - var responseStream = body.stream; - if (autoUncompress) { - if (headers[HttpHeaders.contentEncodingHeader] == 'gzip') { - responseStream = responseStream.transform(gzip.decoder); - } else if (headers[HttpHeaders.contentEncodingHeader] == 'deflate') { - responseStream = responseStream.transform(zlib.decoder); - } - } - responseCompleter.complete( - StreamedResponse( - responseStream, - statusCode, - contentLength: - int.tryParse(headers[HttpHeaders.contentLengthHeader].toString()), - headers: headers, - request: request, - reasonPhrase: _findReasonPhrase(statusCode), - isRedirect: headers.containsKey(HttpHeaders.locationHeader), - ), - ); - } - - stream.incomingMessages.listen( - (message) { - if (message is HeadersStreamMessage) { - for (final header in message.headers) { - headers[utf8.decode(header.name)] = utf8.decode(header.value); - } - } else if (message is DataStreamMessage) { - body.add(message.bytes); - } else if (!responseCompleter.isCompleted) { - responseCompleter.completeError( - ArgumentError.value( - message, - 'message', - 'must be HeadersStreamMessage or DataStreamMessage', - ), - ); - } - }, - cancelOnError: true, - onDone: () { - complete(); - body.close(); - }, - onError: (e, s) { - if (!responseCompleter.isCompleted) { - responseCompleter.completeError(e, s); - } - }, - ); - - return responseCompleter.future; - } - - @override - void close() { - super.close(); - http1Client.close(); - for (final socket in _h2Connections.values) { - socket.finish(); - } - } -} - -class _RedirectInfo implements RedirectInfo { - @override - final String method; - @override - final int statusCode; - @override - final Uri location; - - _RedirectInfo(this.method, this.statusCode, this.location); -} - -String _fullUrlPath(Uri uri, {bool withFragment = false}) => Uri( - fragment: uri.hasFragment && withFragment ? uri.fragment : null, - query: uri.hasQuery ? uri.query : null, - path: uri.path, - ).toString(); - -/// Taken from `dart:_http`. Finds the HTTP reason phrase for a given [statusCode]. -String _findReasonPhrase(int statusCode) { - switch (statusCode) { - case HttpStatus.continue_: - return 'Continue'; - case HttpStatus.switchingProtocols: - return 'Switching Protocols'; - case HttpStatus.ok: - return 'OK'; - case HttpStatus.created: - return 'Created'; - case HttpStatus.accepted: - return 'Accepted'; - case HttpStatus.nonAuthoritativeInformation: - return 'Non-Authoritative Information'; - case HttpStatus.noContent: - return 'No Content'; - case HttpStatus.resetContent: - return 'Reset Content'; - case HttpStatus.partialContent: - return 'Partial Content'; - case HttpStatus.multipleChoices: - return 'Multiple Choices'; - case HttpStatus.movedPermanently: - return 'Moved Permanently'; - case HttpStatus.found: - return 'Found'; - case HttpStatus.seeOther: - return 'See Other'; - case HttpStatus.notModified: - return 'Not Modified'; - case HttpStatus.useProxy: - return 'Use Proxy'; - case HttpStatus.temporaryRedirect: - return 'Temporary Redirect'; - case HttpStatus.badRequest: - return 'Bad Request'; - case HttpStatus.unauthorized: - return 'Unauthorized'; - case HttpStatus.paymentRequired: - return 'Payment Required'; - case HttpStatus.forbidden: - return 'Forbidden'; - case HttpStatus.notFound: - return 'Not Found'; - case HttpStatus.methodNotAllowed: - return 'Method Not Allowed'; - case HttpStatus.notAcceptable: - return 'Not Acceptable'; - case HttpStatus.proxyAuthenticationRequired: - return 'Proxy Authentication Required'; - case HttpStatus.requestTimeout: - return 'Request Time-out'; - case HttpStatus.conflict: - return 'Conflict'; - case HttpStatus.gone: - return 'Gone'; - case HttpStatus.lengthRequired: - return 'Length Required'; - case HttpStatus.preconditionFailed: - return 'Precondition Failed'; - case HttpStatus.requestEntityTooLarge: - return 'Request Entity Too Large'; - case HttpStatus.requestUriTooLong: - return 'Request-URI Too Long'; - case HttpStatus.unsupportedMediaType: - return 'Unsupported Media Type'; - case HttpStatus.requestedRangeNotSatisfiable: - return 'Requested range not satisfiable'; - case HttpStatus.expectationFailed: - return 'Expectation Failed'; - case HttpStatus.internalServerError: - return 'Internal Server Error'; - case HttpStatus.notImplemented: - return 'Not Implemented'; - case HttpStatus.badGateway: - return 'Bad Gateway'; - case HttpStatus.serviceUnavailable: - return 'Service Unavailable'; - case HttpStatus.gatewayTimeout: - return 'Gateway Time-out'; - case HttpStatus.httpVersionNotSupported: - return 'Http Version not supported'; - default: - return 'Status $statusCode'; - } -} diff --git a/lib/src/providers/image_provider.dart b/lib/src/providers/image_provider.dart index dd51e697..96cc3f59 100644 --- a/lib/src/providers/image_provider.dart +++ b/lib/src/providers/image_provider.dart @@ -2,6 +2,8 @@ // A full license can be found at .\LICENSE import 'dart:async'; +import 'dart:io'; +import 'dart:typed_data'; import 'dart:ui'; import 'package:flutter/foundation.dart'; @@ -72,47 +74,25 @@ class FMTCImageProvider extends ImageProvider { StreamController chunkEvents, ImageDecoderCallback decode, ) async { - Future cacheHitMiss({required bool hit}) => - (hit ? _cacheHitsQueue : _cacheMissesQueue).add(() async { - if (db.isOpen) { - await db.writeTxn(() async { - final store = db.isOpen ? await db.descriptor : null; - if (store == null) return; - if (hit) store.hits += 1; - if (!hit) store.misses += 1; - await db.storeDescriptor.put(store); - }); - } - }); - - Future finish({ - List? bytes, - String? throwError, - FMTCBrowsingErrorType? throwErrorType, - bool? cacheHit, - }) async { + Future finishWithError(FMTCBrowsingError err) async { scheduleMicrotask(() => PaintingBinding.instance.imageCache.evict(key)); unawaited(chunkEvents.close()); + await evict(); - if (cacheHit != null) unawaited(cacheHitMiss(hit: cacheHit)); - - if (throwError != null) { - await evict(); - - final error = FMTCBrowsingError(throwError, throwErrorType!); - provider.settings.errorHandler?.call(error); - throw error; - } + provider.settings.errorHandler?.call(err); + throw err; + } - if (bytes != null) { - return decode( - await ImmutableBuffer.fromUint8List(Uint8List.fromList(bytes)), - ); - } + Future finishSuccessfully({ + required Uint8List bytes, + required bool cacheHit, + }) async { + scheduleMicrotask(() => PaintingBinding.instance.imageCache.evict(key)); + unawaited(chunkEvents.close()); + await evict(); - throw ArgumentError( - '`finish` was called with an invalid combination of arguments, or a fall-through situation occurred.', - ); + unawaited(_cacheHitMiss(hit: cacheHit)); + return decode(await ImmutableBuffer.fromUint8List(bytes)); } final networkUrl = provider.getTileUrl(coords, options); @@ -128,68 +108,124 @@ class FMTCImageProvider extends ImageProvider { existingTile.lastModified.millisecondsSinceEpoch > provider.settings.cachedValidDuration.inMilliseconds)); - List? bytes; + // Prepare a list of image bytes and prefill if there's already a cached + // tile available + Uint8List? bytes; if (!needsCreating) bytes = Uint8List.fromList(existingTile.bytes); + // If there is a cached tile that's in date available, use it + if (!needsCreating && !needsUpdating) { + return finishSuccessfully(bytes: bytes!, cacheHit: true); + } + + // If a tile is not available and cache only mode is in use, just fail + // before attempting a network call if (provider.settings.behavior == CacheBehavior.cacheOnly && needsCreating) { - return finish( - throwError: - 'Failed to load the tile from the cache because it was missing.', - throwErrorType: FMTCBrowsingErrorType.missingInCacheOnlyMode, - cacheHit: false, + return finishWithError( + FMTCBrowsingError( + type: FMTCBrowsingErrorType.missingInCacheOnlyMode, + networkUrl: networkUrl, + matcherUrl: matcherUrl, + ), ); } - if (!needsCreating && !needsUpdating) { - return finish(bytes: bytes, cacheHit: true); - } + // From this point, a tile must exist (but it may be outdated). However, an + // outdated tile is better than no tile at all, so in the event of an error, + // always return the existing tile's bytes + // Setup a network request for the tile & handle network exceptions + final request = Request('GET', Uri.parse(networkUrl)) + ..headers.addAll(provider.headers); final StreamedResponse response; - try { - response = await provider.httpClient.send( - Request('GET', Uri.parse(networkUrl))..headers.addAll(provider.headers), - ); - } catch (_) { - return finish( - bytes: !needsCreating ? bytes : null, - throwError: needsCreating - ? 'Failed to load the tile from the cache or the network because it was missing from the cache and a connection to the server could not be established.' - : null, - throwErrorType: FMTCBrowsingErrorType.noConnectionDuringFetch, - cacheHit: false, + response = await provider.httpClient.send(request); + } catch (e) { + if (!needsCreating) { + return finishSuccessfully(bytes: bytes!, cacheHit: false); + } + return finishWithError( + FMTCBrowsingError( + type: e is SocketException + ? FMTCBrowsingErrorType.noConnectionDuringFetch + : FMTCBrowsingErrorType.unknownFetchException, + networkUrl: networkUrl, + matcherUrl: matcherUrl, + request: request, + originalError: e, + ), ); } + // Check whether the network response is not 200 OK if (response.statusCode != 200) { - return finish( - bytes: !needsCreating ? bytes : null, - throwError: needsCreating - ? 'Failed to load the tile from the cache or the network because it was missing from the cache and the server responded with a HTTP code of ${response.statusCode}' - : null, - throwErrorType: FMTCBrowsingErrorType.negativeFetchResponse, - cacheHit: false, + if (!needsCreating) { + return finishSuccessfully(bytes: bytes!, cacheHit: false); + } + return finishWithError( + FMTCBrowsingError( + type: FMTCBrowsingErrorType.negativeFetchResponse, + networkUrl: networkUrl, + matcherUrl: matcherUrl, + request: request, + response: response, + ), ); } - int bytesReceivedLength = 0; - bytes = []; + // Extract the image bytes from the streamed network response + final bytesBuilder = BytesBuilder(copy: false); await for (final byte in response.stream) { - bytesReceivedLength += byte.length; - bytes.addAll(byte); + bytesBuilder.add(byte); chunkEvents.add( ImageChunkEvent( - cumulativeBytesLoaded: bytesReceivedLength, + cumulativeBytesLoaded: bytesBuilder.length, expectedTotalBytes: response.contentLength, ), ); } + final responseBytes = bytesBuilder.takeBytes(); + // Perform a secondary check to ensure that the bytes recieved actually + // encode a valid image + late final bool isValidImageData; + try { + isValidImageData = (await (await instantiateImageCodec( + responseBytes, + targetWidth: 8, + targetHeight: 8, + )) + .getNextFrame()) + .image + .width > + 0; + } catch (e) { + isValidImageData = false; + } + if (!isValidImageData) { + if (!needsCreating) { + return finishSuccessfully(bytes: bytes!, cacheHit: false); + } + return finishWithError( + FMTCBrowsingError( + type: FMTCBrowsingErrorType.invalidImageData, + networkUrl: networkUrl, + matcherUrl: matcherUrl, + request: request, + response: response, + ), + ); + } + + // Cache the tile retrieved from the network response unawaited( - db.writeTxn(() => db.tiles.put(DbTile(url: matcherUrl, bytes: bytes!))), + db.writeTxn( + () => db.tiles.put(DbTile(url: matcherUrl, bytes: responseBytes)), + ), ); + // Clear out old tiles if the maximum store length has been exceeded if (needsCreating && provider.settings.maxStoreLength != 0) { unawaited( _removeOldestQueue.add( @@ -205,9 +241,25 @@ class FMTCImageProvider extends ImageProvider { ); } - return finish(bytes: bytes, cacheHit: false); + return finishSuccessfully(bytes: responseBytes, cacheHit: false); } + Future _cacheHitMiss({required bool hit}) => + (hit ? _cacheHitsQueue : _cacheMissesQueue).add(() async { + if (db.isOpen) { + await db.writeTxn(() async { + final store = db.isOpen ? await db.descriptor : null; + if (store == null) return; + if (hit) { + store.hits += 1; + } else { + store.misses += 1; + } + await db.storeDescriptor.put(store); + }); + } + }); + @override Future obtainKey(ImageConfiguration configuration) => SynchronousFuture(this); diff --git a/lib/src/providers/tile_provider.dart b/lib/src/providers/tile_provider.dart index 5f113d36..59237713 100644 --- a/lib/src/providers/tile_provider.dart +++ b/lib/src/providers/tile_provider.dart @@ -16,18 +16,18 @@ class FMTCTileProvider extends TileProvider { /// [FlutterMapTileCaching]. final FMTCTileProviderSettings settings; - /// [BaseClient] (such as a [HttpClient]) used to make all network requests + /// [Client] (such as a [IOClient]) used to make all network requests /// /// Defaults to a [HttpPlusClient] which supports HTTP/2 and falls back to a /// standard [IOClient]/[HttpClient] for HTTP/1.1 servers. Timeout is set to /// 5 seconds by default. - final BaseClient httpClient; + final Client httpClient; FMTCTileProvider._({ required this.storeDirectory, required FMTCTileProviderSettings? settings, Map headers = const {}, - BaseClient? httpClient, + Client? httpClient, }) : settings = settings ?? FMTC.instance.settings.defaultTileProviderSettings, httpClient = httpClient ?? diff --git a/lib/src/store/directory.dart b/lib/src/store/directory.dart index 5de7d083..24c814b0 100644 --- a/lib/src/store/directory.dart +++ b/lib/src/store/directory.dart @@ -54,11 +54,11 @@ class StoreDirectory { /// Uses [FMTCSettings.defaultTileProviderSettings] by default (and it's /// default if unspecified). Alternatively, override [settings] for this get /// only. - FMTCTileProvider getTileProvider([ + FMTCTileProvider getTileProvider({ FMTCTileProviderSettings? settings, Map? headers, - BaseClient? httpClient, - ]) => + Client? httpClient, + }) => FMTCTileProvider._( storeDirectory: this, settings: settings, diff --git a/lib/src/store/download.dart b/lib/src/store/download.dart index 423cf139..e35765fb 100644 --- a/lib/src/store/download.dart +++ b/lib/src/store/download.dart @@ -19,7 +19,7 @@ class DownloadManagement { Completer? _cancelRequestSignal; Completer? _cancelCompleteSignal; InternalProgressTimingManagement? _progressManagement; - BaseClient? _httpClient; + Client? _httpClient; factory DownloadManagement._(StoreDirectory storeDirectory) { if (!_instances.keys.contains(storeDirectory)) { @@ -66,7 +66,7 @@ class DownloadManagement { bool disableRecovery = false, DownloadBufferMode bufferMode = DownloadBufferMode.disabled, int? bufferLimit, - BaseClient? httpClient, + Client? httpClient, }) async* { // Start recovery _recoveryId = DateTime.now().millisecondsSinceEpoch; @@ -83,7 +83,7 @@ class DownloadManagement { // Get the tile provider final FMTCTileProvider tileProvider = - _storeDirectory.getTileProvider(tileProviderSettings); + _storeDirectory.getTileProvider(settings: tileProviderSettings); // Initialise HTTP client _httpClient = httpClient ?? diff --git a/pubspec.yaml b/pubspec.yaml index 032aac34..890e1fda 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -31,9 +31,8 @@ dependencies: flutter: sdk: flutter flutter_map: ^5.0.0 - http: ^1.0.0 - http2: ^2.0.1 - http_parser: ^4.0.2 + http: ^1.1.0 + http_plus: ^0.2.3 isar: ^3.1.0+1 isar_flutter_libs: ^3.1.0+1 latlong2: ^0.9.0 From db0482a8fdf3875511355a9e3523d92d49e8be0f Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sat, 1 Jul 2023 15:11:14 +0100 Subject: [PATCH 020/168] Minor changes Former-commit-id: a2020cbd9ee08fd70e6ec7536161ab5726f0f62a [formerly a98f5bcdd702e639ef87ae0808c4fe1b301ac95c] Former-commit-id: 13c729d0a6e041af760189e3b93d4cb98744a8be --- .../main/pages/downloader/components/map_view.dart | 1 + lib/src/regions/base_region.dart | 2 +- lib/src/root/migrator.dart | 10 ++++------ 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/example/lib/screens/main/pages/downloader/components/map_view.dart b/example/lib/screens/main/pages/downloader/components/map_view.dart index d75aab83..7fb138da 100644 --- a/example/lib/screens/main/pages/downloader/components/map_view.dart +++ b/example/lib/screens/main/pages/downloader/components/map_view.dart @@ -165,6 +165,7 @@ class _MapViewState extends State { maxZoom: 20, reset: generalProvider.resetController.stream, keepBuffer: 5, + userAgentPackageName: 'dev.org.fmtc.example.app', backgroundColor: const Color(0xFFaad3df), tileBuilder: (context, widget, tile) => FutureBuilder( diff --git a/lib/src/regions/base_region.dart b/lib/src/regions/base_region.dart index 9359afe0..bcfc6434 100644 --- a/lib/src/regions/base_region.dart +++ b/lib/src/regions/base_region.dart @@ -91,7 +91,7 @@ sealed class BaseRegion { identical(this, other) || (other is BaseRegion && other.name == name); @override - @mustBeOverridden @mustCallSuper + @mustBeOverridden int get hashCode => name.hashCode; } diff --git a/lib/src/root/migrator.dart b/lib/src/root/migrator.dart index f21e970d..b49d404d 100644 --- a/lib/src/root/migrator.dart +++ b/lib/src/root/migrator.dart @@ -63,12 +63,10 @@ class RootMigrator { ]; // Search for the previous structure - final Directory normal = - (await getApplicationDocumentsDirectory()) >> 'fmtc'; - final Directory temporary = (await getTemporaryDirectory()) >> 'fmtc'; - final Directory? custom = - customDirectory == null ? null : customDirectory >> 'fmtc'; - final Directory? root = await normal.exists() + final normal = (await getApplicationDocumentsDirectory()) >> 'fmtc'; + final temporary = (await getTemporaryDirectory()) >> 'fmtc'; + final custom = customDirectory == null ? null : customDirectory >> 'fmtc'; + final root = await normal.exists() ? normal : await temporary.exists() ? temporary From 025e188527841203783f3ebd295e39ef9266afff Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Mon, 3 Jul 2023 14:34:01 +0100 Subject: [PATCH 021/168] Reimplemented bulk downloading Improved performance and stability significantly Former-commit-id: 4b06a9f3bad71c1401d6c92cfdbb7bff73892a0f [formerly ce2ef8291fd7bc86965d25d6d15e9e4d6bc359a3] Former-commit-id: d844fd6da00f4953e54a7a385f5d8b0d415c6ecc --- example/.metadata | 10 +- example/lib/main.dart | 74 ++-- .../download_region/download_region.dart | 2 +- lib/flutter_map_tile_caching.dart | 8 +- lib/src/bulk_download/bulk_tile_writer.dart | 232 ------------ lib/src/bulk_download/download_progress.dart | 270 ++++---------- lib/src/bulk_download/downloader.dart | 199 ---------- lib/src/bulk_download/instance.dart | 20 + lib/src/bulk_download/manager.dart | 213 +++++++++++ lib/src/bulk_download/thread.dart | 156 ++++++++ lib/src/bulk_download/tile_event.dart | 111 ++++++ lib/src/bulk_download/tile_loops/count.dart | 10 +- .../bulk_download/tile_loops/generate.dart | 10 +- lib/src/bulk_download/tile_loops/shared.dart | 4 +- lib/src/bulk_download/tile_progress.dart | 32 -- lib/src/db/defs/recovery.dart | 7 - lib/src/db/defs/recovery.g.dart | 275 ++------------ lib/src/misc/int_extremes.dart | 2 + lib/src/regions/base_region.dart | 3 - lib/src/regions/circle.dart | 7 - lib/src/regions/downloadable_region.dart | 59 +-- lib/src/regions/line.dart | 7 - lib/src/regions/recovered_region.dart | 24 -- lib/src/regions/rectangle.dart | 7 - lib/src/root/recovery.dart | 11 +- lib/src/store/download.dart | 352 ++++++++---------- lib/src/store/statistics.dart | 4 - 27 files changed, 812 insertions(+), 1297 deletions(-) delete mode 100644 lib/src/bulk_download/bulk_tile_writer.dart delete mode 100644 lib/src/bulk_download/downloader.dart create mode 100644 lib/src/bulk_download/instance.dart create mode 100644 lib/src/bulk_download/manager.dart create mode 100644 lib/src/bulk_download/thread.dart create mode 100644 lib/src/bulk_download/tile_event.dart delete mode 100644 lib/src/bulk_download/tile_progress.dart create mode 100644 lib/src/misc/int_extremes.dart diff --git a/example/.metadata b/example/.metadata index 6884bd8c..05fece53 100644 --- a/example/.metadata +++ b/example/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled. version: - revision: f72efea43c3013323d1b95cff571f3c1caa37583 + revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff channel: stable project_type: app @@ -13,11 +13,11 @@ project_type: app migration: platforms: - platform: root - create_revision: f72efea43c3013323d1b95cff571f3c1caa37583 - base_revision: f72efea43c3013323d1b95cff571f3c1caa37583 + create_revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff + base_revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff - platform: windows - create_revision: f72efea43c3013323d1b95cff571f3c1caa37583 - base_revision: f72efea43c3013323d1b95cff571f3c1caa37583 + create_revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff + base_revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff # User provided section diff --git a/example/lib/main.dart b/example/lib/main.dart index 2cd86983..261e3b9f 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,16 +1,8 @@ -import 'dart:io'; - import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; -import 'package:google_fonts/google_fonts.dart'; -import 'package:path/path.dart' as p; -import 'package:provider/provider.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -import 'screens/main/main.dart'; -import 'shared/state/download_provider.dart'; -import 'shared/state/general_provider.dart'; +import 'package:latlong2/latlong.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -21,33 +13,64 @@ void main() async { ), ); - final SharedPreferences prefs = await SharedPreferences.getInstance(); + //final SharedPreferences prefs = await SharedPreferences.getInstance(); - String? damagedDatabaseDeleted; + // String? damagedDatabaseDeleted; await FlutterMapTileCaching.initialise( - errorHandler: (error) => damagedDatabaseDeleted = error.message, + // errorHandler: (error) => damagedDatabaseDeleted = error.message, debugMode: true, ); - await FMTC.instance.rootDirectory.migrator.fromV6(urlTemplates: []); + // await FMTC.instance.rootDirectory.migrator.fromV6(urlTemplates: []); - if (prefs.getBool('reset') ?? false) { - await FMTC.instance.rootDirectory.manage.reset(); - } + //if (prefs.getBool('reset') ?? false) { + // await FMTC.instance.rootDirectory.manage.reset(); + // } - final File newAppVersionFile = File( - p.join( - // ignore: invalid_use_of_internal_member, invalid_use_of_protected_member - FMTC.instance.rootDirectory.directory.absolute.path, - 'newAppVersion.${Platform.isWindows ? 'exe' : 'apk'}', + //final File newAppVersionFile = File( + // p.join( + // // ignore: invalid_use_of_internal_member, invalid_use_of_protected_member + // FMTC.instance.rootDirectory.directory.absolute.path, + // 'newAppVersion.${Platform.isWindows ? 'exe' : 'apk'}', + // ), + // ); + // if (await newAppVersionFile.exists()) await newAppVersionFile.delete(); + + final region = + RectangleRegion(LatLngBounds(const LatLng(1, 1), const LatLng(-1, -1))) + .toDownloadable( + 1, + 12, + TileLayer( + urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + userAgentPackageName: 'dev.jaffaketchup.fmtc.demo1', ), ); - if (await newAppVersionFile.exists()) await newAppVersionFile.delete(); - runApp(AppContainer(damagedDatabaseDeleted: damagedDatabaseDeleted)); + FMTC.instance['hello'].download + .startForeground( + region: region, + pruneExistingTiles: false, + pruneSeaTiles: false, + maxBufferLength: 200, + ) + .listen( + (progress) => print( + '${progress.successfulTiles} tiles, ${progress.duration}, ${progress.lastTileEvent?.result}', + ), + ); + print('DOWNLOAD COMPLETE'); + + await Future.delayed(const Duration(seconds: 1)); + + print('REQUEST CANCEL'); + await FMTC.instance['hello'].download.cancel(); + print('DOWNLOAD CANCELLED'); + + //runApp(AppContainer(damagedDatabaseDeleted: damagedDatabaseDeleted)); } -class AppContainer extends StatelessWidget { +/*class AppContainer extends StatelessWidget { const AppContainer({ super.key, required this.damagedDatabaseDeleted, @@ -87,3 +110,4 @@ class AppContainer extends StatelessWidget { ), ); } +*/ \ No newline at end of file diff --git a/example/lib/screens/download_region/download_region.dart b/example/lib/screens/download_region/download_region.dart index a54471a5..1220c8cb 100644 --- a/example/lib/screens/download_region/download_region.dart +++ b/example/lib/screens/download_region/download_region.dart @@ -126,7 +126,7 @@ class _DownloadRegionPopupState extends State { downloadProvider.disableRecovery, bufferMode: downloadProvider.bufferMode, - bufferLimit: downloadProvider + maxBufferLength: downloadProvider .bufferMode == DownloadBufferMode.tiles ? downloadProvider.bufferingAmount diff --git a/lib/flutter_map_tile_caching.dart b/lib/flutter_map_tile_caching.dart index 08106afd..1fff1c75 100644 --- a/lib/flutter_map_tile_caching.dart +++ b/lib/flutter_map_tile_caching.dart @@ -14,6 +14,7 @@ library flutter_map_tile_caching; import 'dart:async'; import 'dart:io'; +import 'dart:isolate'; import 'dart:math' as math; import 'package:async/async.dart'; @@ -34,11 +35,9 @@ import 'package:path_provider/path_provider.dart'; import 'package:stream_transform/stream_transform.dart'; import 'package:watcher/watcher.dart'; -import 'src/bulk_download/bulk_tile_writer.dart'; -import 'src/bulk_download/downloader.dart'; -import 'src/bulk_download/internal_timing_progress_management.dart'; +import 'src/bulk_download/instance.dart'; +import 'src/bulk_download/manager.dart'; import 'src/bulk_download/tile_loops/shared.dart'; -import 'src/bulk_download/tile_progress.dart'; import 'src/db/defs/metadata.dart'; import 'src/db/defs/recovery.dart'; import 'src/db/defs/store_descriptor.dart'; @@ -59,6 +58,7 @@ export 'src/errors/store_not_ready.dart'; export 'src/misc/typedefs.dart'; part 'src/bulk_download/download_progress.dart'; +part 'src/bulk_download/tile_event.dart'; part 'src/fmtc.dart'; part 'src/misc/store_db_impl.dart'; part 'src/providers/tile_provider.dart'; diff --git a/lib/src/bulk_download/bulk_tile_writer.dart b/lib/src/bulk_download/bulk_tile_writer.dart deleted file mode 100644 index 68df7135..00000000 --- a/lib/src/bulk_download/bulk_tile_writer.dart +++ /dev/null @@ -1,232 +0,0 @@ -// Copyright © Luka S (JaffaKetchup) under GPL-v3 -// A full license can be found at .\LICENSE - -import 'dart:async'; -import 'dart:isolate'; - -import 'package:async/async.dart'; -import 'package:flutter/foundation.dart'; -import 'package:isar/isar.dart'; -import 'package:meta/meta.dart'; - -import '../../flutter_map_tile_caching.dart'; -import '../db/defs/metadata.dart'; -import '../db/defs/store_descriptor.dart'; -import '../db/defs/tile.dart'; -import '../db/tools.dart'; -import 'tile_progress.dart'; - -/// Handles tile writing during a bulk download -/// -/// Note that this is designed for performance, relying on isolate workers to -/// carry out expensive operations. -@internal -class BulkTileWriter { - BulkTileWriter._(); - - static BulkTileWriter? _instance; - static BulkTileWriter get instance => _instance!; - - late ReceivePort _recievePort; - late DownloadBufferMode _bufferMode; - late StreamController _downloadStream; - - late SendPort sendPort; - late StreamQueue events; - - static Future start({ - required FMTCTileProvider provider, - required DownloadBufferMode bufferMode, - required int? bufferLimit, - required String directory, - required StreamController streamController, - }) async { - final btw = BulkTileWriter._() - .._recievePort = ReceivePort() - .._bufferMode = bufferMode - .._downloadStream = streamController; - - await Isolate.spawn( - bufferMode == DownloadBufferMode.disabled - ? _instantWorker - : _bufferWorker, - btw._recievePort.sendPort, - ); - - btw - ..events = StreamQueue(btw._recievePort) - ..sendPort = await btw.events.next - ..sendPort.send( - bufferMode == DownloadBufferMode.disabled - ? [ - provider.storeDirectory.storeName, - directory, - ] - : [ - provider.storeDirectory.storeName, - directory, - bufferMode, - bufferLimit ?? - (bufferMode == DownloadBufferMode.tiles ? 500 : 2000000), - ], - ); - - _instance = btw; - } - - static Future stop(Uint8List? tileImage) async { - if (_instance == null) return; - - instance.sendPort.send(null); - if (instance._bufferMode != DownloadBufferMode.disabled) { - instance._downloadStream.add( - TileProgress( - failedUrl: null, - tileImage: tileImage, - wasSeaTile: false, - wasExistingTile: false, - wasCancelOperation: true, - bulkTileWriterResponse: await instance.events.next, - ), - ); - } - await instance.events.cancel(immediate: true); - - _instance = null; - } -} - -/// Isolate ('worker') for [BulkTileWriter] that supports buffered tile writing -/// -/// Starting this worker will send its recieve port to the specified [sendPort], -/// to be used for further communication, as described below: -/// -/// The first incoming message is expected to contain setup information, namely -/// the store name that will be targeted, the buffer mode, and the buffer limit, -/// as a list. No response is sent to this message. -/// -/// Following incoming messages are expected to either contain tile information -/// or to signal the end of the worker's lifespan. Should the message not be -/// null, a list will be expected where the first element is the tile URL and the -/// second element is the tile bytes. Should the message be null, the worker will -/// be terminated as described below. Responses are defined below. -/// -/// On reciept of a tile descriptor, it will be added to the buffer. If the total -/// buffer size then exceeds the limit defined in the setup message, the buffer -/// will be written to the database, then cleared. In this case, the response -/// will be a list of the total number of tiles and the total number of bytes -/// now written to the database. If the limit is not exceeded, the tile will not -/// be written, and the response will be null, indicating that there is no more -/// information, but the tile was processed correctly. -/// -/// On reciept of the `null` termination message, the buffer will be written, -/// regardless of it's length. This worker will then be killed, with a response -/// of the total number of tiles written, regardless of whether any tiles were -/// just written. -/// -/// It is illegal to kill this isolate externally, as this may lead to data loss. -/// Always terminate by sending the termination (`null`) message. -/// -/// It is illegal to send corrupted/invalid/unknown messages, as this will likely -/// crash the worker, leading to data loss. No validation is performed on -/// incoming data. -Future _bufferWorker(SendPort sendPort) async { - final rp = ReceivePort(); - sendPort.send(rp.sendPort); - final recievePort = rp.asBroadcastStream(); - - final setupInfo = await recievePort.first as List; - final db = Isar.openSync( - [DbStoreDescriptorSchema, DbTileSchema, DbMetadataSchema], - name: DatabaseTools.hash(setupInfo[0]).toString(), - directory: setupInfo[1], - inspector: false, - ); - - final bufferMode = setupInfo[2] as DownloadBufferMode; - final bufferLimit = setupInfo[3] as int; - - final tileBuffer = {}; - - int totalTilesWritten = 0; - int totalBytesWritten = 0; - - int currentTilesBuffered = 0; - int currentBytesBuffered = 0; - - void writeBuffer() { - db.writeTxnSync(() => tileBuffer.forEach(db.tiles.putSync)); - - totalBytesWritten += currentBytesBuffered; - totalTilesWritten += currentTilesBuffered; - - tileBuffer.clear(); - currentBytesBuffered = 0; - currentTilesBuffered = 0; - } - - recievePort.where((i) => i == null).listen((_) { - writeBuffer(); - Isolate.exit(sendPort, [totalTilesWritten, totalBytesWritten]); - }); - recievePort.where((i) => i != null).listen((info) { - currentBytesBuffered += Uint8List.fromList((info as List)[1]).lengthInBytes; - currentTilesBuffered++; - tileBuffer.add(DbTile(url: info[0], bytes: info[1])); - if ((bufferMode == DownloadBufferMode.tiles - ? currentTilesBuffered - : currentBytesBuffered) > - bufferLimit) { - writeBuffer(); - sendPort.send([totalTilesWritten, totalBytesWritten]); - } else { - sendPort.send(null); - } - }); -} - -/// Isolate ('worker') for [BulkTileWriter] that doesn't supports buffered tile -/// writing -/// -/// Starting this worker will send its recieve port to the specified [sendPort], -/// to be used for further communication, as described below: -/// -/// The first incoming message is expected to contain setup information, namely -/// the store name that will be targeted. No response is sent to this message. -/// -/// Following incoming messages are expected to either contain tile information -/// or to signal the end of the worker's lifespan. Should the message not be -/// null, a list will be expected where the first element is the tile URL and the -/// second element is the tile bytes. Should the message be null, the worker will -/// be terminated immediatley without a response. -/// -/// On reciept of a tile descriptor, the tile will be written to the database, -/// and the response will be `null`. -/// -/// It is not recommended to kill this isolate externally. Prefer termination by -/// sending the termination (`null`) message. -/// -/// It is illegal to send corrupted/invalid/unknown messages, as this will likely -/// crash the worker, leading to data loss. No validation is performed on -/// incoming data. -Future _instantWorker(SendPort sendPort) async { - final rp = ReceivePort(); - sendPort.send(rp.sendPort); - final recievePort = rp.asBroadcastStream(); - - final setupInfo = await recievePort.first as List; - final db = Isar.openSync( - [DbStoreDescriptorSchema, DbTileSchema, DbMetadataSchema], - name: DatabaseTools.hash(setupInfo[0]).toString(), - directory: setupInfo[1], - inspector: false, - ); - - recievePort.where((i) => i == null).listen((_) => Isolate.exit()); - recievePort.where((i) => i != null).listen((info) { - db.writeTxnSync( - () => db.tiles.putSync(DbTile(url: (info as List)[0], bytes: info[1])), - ); - sendPort.send(null); - }); -} diff --git a/lib/src/bulk_download/download_progress.dart b/lib/src/bulk_download/download_progress.dart index 4c6eed24..729f5298 100644 --- a/lib/src/bulk_download/download_progress.dart +++ b/lib/src/bulk_download/download_progress.dart @@ -1,230 +1,86 @@ // Copyright © Luka S (JaffaKetchup) under GPL-v3 // A full license can be found at .\LICENSE -part of '../../flutter_map_tile_caching.dart'; +part of flutter_map_tile_caching; -/// Represents the progress of an ongoing or finished (if [percentageProgress] -/// is 100%) bulk download -/// -/// Should avoid manual construction, use named constructor -/// [DownloadProgress.empty] to generate placeholders. +@immutable class DownloadProgress { - /// Identification number of the corresponding download - /// - /// A zero identification denotes that there is no corresponding download yet, - /// usually due to the initialisation with [DownloadProgress.empty]. - final int downloadID; + final TileEvent? lastTileEvent; - /// Class for managing the average tiles per second - /// ([InternalProgressTimingManagement.averageTPS]) measurement of a download - final InternalProgressTimingManagement _progressManagement; + final int cachedTiles; + final int prunedTiles; + final int failedTiles; - /// Number of tiles downloaded successfully - /// - /// If using buffering, it is important to note that these tiles will be - /// written successfully (becoming part of [persistedTiles]), except in the - /// event of a crash. In this case, the tiles that fall under the difference - /// of [persistedTiles] - [successfulTiles] will be lost. - /// - /// For more information about buffering, see the - /// [appropriate docs page](https://fmtc.jaffaketchup.dev/bulk-downloading/foreground/buffering). - final int successfulTiles; - - /// Number of tiles persisted successfully (only significant when using - /// buffering) - /// - /// This value will not necessarily change with every change to - /// [successfulTiles]. It will only change after the buffer has been written. - /// - /// Tiles that fall within this number are safely persisted to the database and - /// cannot be lost as a consequence of a crash, unlike [successfulTiles]. - /// - /// For more information about buffering, see the - /// [appropriate docs page](https://fmtc.jaffaketchup.dev/bulk-downloading/foreground/buffering). - final int persistedTiles; - - /// List of URLs of failed tiles - final List failedTiles; - - /// Approximate total number of tiles to be downloaded final int maxTiles; - /// Number of kibibytes successfully downloaded - /// - /// If using buffering, it is important to note that these tiles will be - /// written successfully (becoming part of [persistedSize]), except in the - /// event of a crash. In this case, the tiles that fall under the difference - /// of [persistedSize] - [successfulSize] will be lost. - /// - /// For more information about buffering, see the - /// [appropriate docs page](https://fmtc.jaffaketchup.dev/bulk-downloading/foreground/buffering). - final double successfulSize; - - /// Number of kibibytes successfully persisted (only significant when using - /// buffering) - /// - /// This value will not necessarily change with every change to - /// [successfulSize]. It will only change after the buffer has been written. - /// - /// Tiles that fall within this number are safely persisted to the database and - /// cannot be lost as a consequence of a crash, unlike [successfulSize]. - /// - /// For more information about buffering, see the - /// [appropriate docs page](https://fmtc.jaffaketchup.dev/bulk-downloading/foreground/buffering). - final double persistedSize; - - /// Number of tiles removed because they were entirely sea (these also make up - /// part of [successfulTiles]) - /// - /// Only applicable if sea tile removal is enabled, otherwise this value is - /// always 0. - final int seaTiles; - - /// Number of tiles not downloaded because they already existed (these also - /// make up part of [successfulTiles]) - /// - /// Only applicable if redownload prevention is enabled, otherwise this value - /// is always 0. - final int existingTiles; - - /// Elapsed duration since start of download process - final Duration duration; - - /// Get the [ImageProvider] of the last tile that was downloaded - /// - /// Is `null` if the last tile failed, or the tile already existed and - /// `preventRedownload` is enabled. - final MemoryImage? tileImage; - - /// The [DownloadBufferMode] in use - /// - /// If [DownloadBufferMode.disabled], then [persistedSize] and [persistedTiles] - /// will remain 0. - final DownloadBufferMode bufferMode; - - /// Number of attempted tile downloads, including failure - /// - /// Is equal to `successfulTiles + failedTiles.length`. - int get attemptedTiles => successfulTiles + failedTiles.length; - - /// Approximate number of tiles remaining to be downloaded - /// - /// Is equal to `approxMaxTiles - attemptedTiles`. + int get successfulTiles => cachedTiles + prunedTiles; + int get attemptedTiles => successfulTiles + failedTiles; int get remainingTiles => maxTiles - attemptedTiles; - /// Percentage of tiles saved by using sea tile removal (ie. discount) - /// - /// Only applicable if sea tile removal is enabled, otherwise this value is - /// always 0. - /// - /// Is equal to - /// `100 - ((((successfulTiles - existingTiles) - seaTiles) / successfulTiles) * 100)`. - double get seaTilesDiscount => seaTiles == 0 - ? 0 - : 100 - - ((((successfulTiles - existingTiles) - seaTiles) / successfulTiles) * - 100); - - /// Percentage of tiles saved by using redownload prevention (ie. discount) - /// - /// Only applicable if redownload prevention is enabled, otherwise this value - /// is always 0. - /// - /// Is equal to - /// `100 - ((((successfulTiles - seaTiles) - existingTiles) / successfulTiles) * 100)`. - double get existingTilesDiscount => existingTiles == 0 - ? 0 - : 100 - - ((((successfulTiles - seaTiles) - existingTiles) / successfulTiles) * - 100); + final Duration duration; - /// Approximate percentage of process complete - /// - /// Is equal to `(attemptedTiles / maxTiles) * 100`. double get percentageProgress => (attemptedTiles / maxTiles) * 100; + bool get isFinished => attemptedTiles == maxTiles; - /// Retrieve the average number of tiles per second that are being downloaded - /// - /// Uses an exponentially smoothed moving average algorithm instead of a linear - /// average algorithm. This should lead to more accurate estimations based on - /// this data. The full original algorithm (written in Python) can be found at - /// https://stackoverflow.com/a/54264570/11846040. - double get averageTPS => _progressManagement.averageTPS; - - /// Estimate duration for entire download process, using [averageTPS] - /// - /// Uses an exponentially smoothed moving average algorithm instead of a linear - /// average algorithm. This should lead to more accurate duration calculations, - /// but may not return the same result as expected. The full original algorithm - /// (written in Python) can be found at - /// https://stackoverflow.com/a/54264570/11846040. - Duration get estTotalDuration => Duration( - seconds: (maxTiles / averageTPS.clamp(1, double.infinity)).round(), - ); - - /// Estimate remaining duration until the end of the download process - /// - /// Uses an exponentially smoothed moving average algorithm instead of a linear - /// average algorithm. This should lead to more accurate duration calculations, - /// but may not return the same result as expected. The full original algorithm - /// (written in Python) can be found at - /// https://stackoverflow.com/a/54264570/11846040. - Duration get estRemainingDuration => estTotalDuration - duration; - - /// Avoid construction using this method. Use [DownloadProgress.empty] to generate empty placeholders where necessary. - DownloadProgress._({ - required this.downloadID, - required this.successfulTiles, - required this.persistedTiles, + const DownloadProgress._({ + required this.lastTileEvent, + required this.cachedTiles, + required this.prunedTiles, required this.failedTiles, required this.maxTiles, - required this.successfulSize, - required this.persistedSize, - required this.seaTiles, - required this.existingTiles, required this.duration, - required this.tileImage, - required this.bufferMode, - required InternalProgressTimingManagement progressManagement, - }) : _progressManagement = progressManagement; - - /// Create an empty placeholder (all values set to 0 or empty) [DownloadProgress], useful for `initialData` in a [StreamBuilder] - DownloadProgress.empty() - : downloadID = 0, - successfulTiles = 0, - persistedTiles = 0, - failedTiles = [], - successfulSize = 0, - persistedSize = 0, - maxTiles = 1, - seaTiles = 0, - existingTiles = 0, - duration = Duration.zero, - tileImage = null, - bufferMode = DownloadBufferMode.disabled, - _progressManagement = InternalProgressTimingManagement(); + }); + + factory DownloadProgress.initial({required int maxTiles}) => + DownloadProgress._( + lastTileEvent: null, + cachedTiles: 0, + prunedTiles: 0, + failedTiles: 0, + maxTiles: maxTiles, + duration: Duration.zero, + ); - //! GENERAL OBJECT STUFF !// + DownloadProgress update({ + TileEvent? newTileEvent, + required Duration newDuration, + }) => + DownloadProgress._( + lastTileEvent: newTileEvent ?? lastTileEvent, + cachedTiles: cachedTiles + + (newTileEvent?.result.category == TileEventResultCategory.cached + ? 1 + : 0), + prunedTiles: prunedTiles + + (newTileEvent?.result.category == TileEventResultCategory.pruned + ? 1 + : 0), + failedTiles: failedTiles + + (newTileEvent?.result.category == TileEventResultCategory.failed + ? 1 + : 0), + maxTiles: maxTiles, + duration: newDuration, + ); @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - - return other is DownloadProgress && - other.successfulTiles == successfulTiles && - other.failedTiles == failedTiles && - other.maxTiles == maxTiles && - other.seaTiles == seaTiles && - other.existingTiles == existingTiles && - other.duration == duration; - } + bool operator ==(Object other) => + identical(this, other) || + (other is DownloadProgress && + lastTileEvent == other.lastTileEvent && + successfulTiles == other.successfulTiles && + prunedTiles == other.prunedTiles && + failedTiles == other.failedTiles && + maxTiles == other.maxTiles && + duration == other.duration); @override - int get hashCode => - successfulTiles.hashCode ^ - failedTiles.hashCode ^ - maxTiles.hashCode ^ - seaTiles.hashCode ^ - existingTiles.hashCode ^ - duration.hashCode; + int get hashCode => Object.hashAllUnordered([ + lastTileEvent.hashCode, + successfulTiles.hashCode, + prunedTiles.hashCode, + failedTiles.hashCode, + maxTiles.hashCode, + duration.hashCode, + ]); } diff --git a/lib/src/bulk_download/downloader.dart b/lib/src/bulk_download/downloader.dart deleted file mode 100644 index d6b3cbcc..00000000 --- a/lib/src/bulk_download/downloader.dart +++ /dev/null @@ -1,199 +0,0 @@ -// Copyright © Luka S (JaffaKetchup) under GPL-v3 -// A full license can be found at .\LICENSE - -import 'dart:async'; -import 'dart:io'; -import 'dart:isolate'; -import 'dart:typed_data'; - -import 'package:async/async.dart'; -import 'package:collection/collection.dart'; -import 'package:flutter_map/flutter_map.dart'; -import 'package:http/http.dart'; -import 'package:meta/meta.dart'; - -import '../../flutter_map_tile_caching.dart'; -import '../db/defs/tile.dart'; -import '../db/registry.dart'; -import '../db/tools.dart'; -import 'bulk_tile_writer.dart'; -import 'internal_timing_progress_management.dart'; -import 'tile_loops/shared.dart'; -import 'tile_progress.dart'; - -@internal -Future> bulkDownloader({ - required StreamController streamController, - required Completer cancelRequestSignal, - required Completer cancelCompleteSignal, - required DownloadableRegion region, - required FMTCTileProvider provider, - required Uint8List? seaTileBytes, - required InternalProgressTimingManagement progressManagement, - required Client client, -}) async { - final tiles = FMTCRegistry.instance(provider.storeDirectory.storeName); - - final recievePort = ReceivePort(); - final tileIsolate = await Isolate.spawn( - region.when( - rectangle: (_) => TilesGenerator.rectangleTiles, - circle: (_) => TilesGenerator.circleTiles, - line: (_) => TilesGenerator.lineTiles, - ), - (sendPort: recievePort.sendPort, region: region), - onExit: recievePort.sendPort, - ); - final tileQueue = StreamQueue( - recievePort.skip(region.start).take( - region.end == null - ? 9223372036854775807 - : (region.end! - region.start), - ), - ); - final requestTilePort = (await tileQueue.next) as SendPort; - - final threadCompleters = List.generate( - region.parallelThreads, - (_) => Completer(), - growable: false, - ); - - for (final threadCompleter in threadCompleters) { - unawaited(() async { - while (true) { - requestTilePort.send(null); - - final (int, int, int)? coords; - try { - coords = await tileQueue.next; - // ignore: avoid_catching_errors - } on StateError { - threadCompleter.complete(); - break; - } - - if (cancelRequestSignal.isCompleted) { - await tileQueue.cancel(immediate: true); - - tileIsolate.kill(priority: Isolate.immediate); - recievePort.close(); - - await BulkTileWriter.stop(null); - unawaited(streamController.close()); - - cancelCompleteSignal.complete(); - break; - } - - if (coords == null) { - await tileQueue.cancel(); - - threadCompleter.complete(); - await Future.wait(threadCompleters.map((e) => e.future)); - - recievePort.close(); - - await BulkTileWriter.stop(null); - unawaited(streamController.close()); - - break; - } - - final coord = TileCoordinates(coords.$1, coords.$2, coords.$3); - - final url = provider.getTileUrl(coord, region.options); - final existingTile = await tiles.tiles.get(DatabaseTools.hash(url)); - - try { - final List bytes = []; - - if (region.preventRedownload && existingTile != null) { - streamController.add( - TileProgress( - failedUrl: null, - tileImage: null, - wasSeaTile: false, - wasExistingTile: true, - wasCancelOperation: false, - bulkTileWriterResponse: null, - ), - ); - } - - final uri = Uri.parse(url); - final response = await client - .send(Request('GET', uri)..headers.addAll(provider.headers)); - - if (response.statusCode != 200) { - throw HttpException( - '[${response.statusCode}] ${response.reasonPhrase ?? 'Unknown Reason'}', - uri: uri, - ); - } - - int received = 0; - await for (final List evt in response.stream) { - bytes.addAll(evt); - received += evt.length; - progressManagement.registerEvent( - url, - TimestampProgress( - DateTime.now(), - received / (response.contentLength ?? 0), - ), - ); - } - - if (existingTile != null && - seaTileBytes != null && - const ListEquality().equals(bytes, seaTileBytes)) { - streamController.add( - TileProgress( - failedUrl: null, - tileImage: Uint8List.fromList(bytes), - wasSeaTile: true, - wasExistingTile: false, - wasCancelOperation: false, - bulkTileWriterResponse: null, - ), - ); - } - - BulkTileWriter.instance.sendPort.send( - List.unmodifiable( - [provider.settings.obscureQueryParams(url), bytes], - ), - ); - - streamController.add( - TileProgress( - failedUrl: null, - tileImage: Uint8List.fromList(bytes), - wasSeaTile: false, - wasExistingTile: false, - wasCancelOperation: false, - bulkTileWriterResponse: await BulkTileWriter.instance.events.next, - ), - ); - } catch (e) { - region.errorHandler?.call(e); - if (!streamController.isClosed) { - streamController.add( - TileProgress( - failedUrl: url, - tileImage: null, - wasSeaTile: false, - wasExistingTile: false, - wasCancelOperation: false, - bulkTileWriterResponse: null, - ), - ); - } - } - } - }()); - } - - return streamController.stream; -} diff --git a/lib/src/bulk_download/instance.dart b/lib/src/bulk_download/instance.dart new file mode 100644 index 00000000..e6baeada --- /dev/null +++ b/lib/src/bulk_download/instance.dart @@ -0,0 +1,20 @@ +// Copyright © Luka S (JaffaKetchup) under GPL-v3 +// A full license can be found at .\LICENSE + +import 'package:meta/meta.dart'; + +@internal +class DownloadInstance { + DownloadInstance._(this.id); + static final Map _instances = {}; + + static DownloadInstance? registerIfAvailable(Object id) => + _instances.containsKey(id) + ? null + : _instances[id] ??= DownloadInstance._(id); + static bool unregister(Object id) => _instances.remove(id) != null; + static DownloadInstance? get(Object id) => _instances[id]; + + final Object id; + Future Function()? cancelDownloadRequest; +} diff --git a/lib/src/bulk_download/manager.dart b/lib/src/bulk_download/manager.dart new file mode 100644 index 00000000..ee750721 --- /dev/null +++ b/lib/src/bulk_download/manager.dart @@ -0,0 +1,213 @@ +// Copyright © Luka S (JaffaKetchup) under GPL-v3 +// A full license can be found at .\LICENSE + +import 'dart:async'; +import 'dart:isolate'; +import 'dart:typed_data'; + +import 'package:async/async.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:http/http.dart' as http; +import 'package:meta/meta.dart'; + +import '../../flutter_map_tile_caching.dart'; +import '../db/tools.dart'; +import '../misc/int_extremes.dart'; +import 'thread.dart'; +import 'tile_loops/shared.dart'; + +@internal +Future downloadManager( + ({ + SendPort sendPort, + String rootDirectory, + DownloadableRegion region, + FMTCTileProvider tileProvider, + int parallelThreads, + int maxBufferLength, + bool pruneExistingTiles, + bool pruneSeaTiles, + Duration? maxReportInterval, + }) input, +) async { + // Count number of tiles + final maxTiles = input.region.when( + rectangle: (_) => TilesCounter.rectangleTiles, + circle: (_) => TilesCounter.circleTiles, + line: (_) => TilesCounter.lineTiles, + )(input.region); + + // Setup sea tile removal system + Uint8List? seaTileBytes; + if (input.pruneSeaTiles) { + try { + seaTileBytes = await http.readBytes( + Uri.parse( + input.tileProvider.getTileUrl( + const TileCoordinates(0, 0, 17), + input.region.options, + ), + ), + headers: input.tileProvider.headers, + ); + } catch (_) { + seaTileBytes = null; + } + } + + // Setup tile generator isolate + final tileRecievePort = ReceivePort(); + final tileIsolate = await Isolate.spawn( + input.region.when( + rectangle: (_) => TilesGenerator.rectangleTiles, + circle: (_) => TilesGenerator.circleTiles, + line: (_) => TilesGenerator.lineTiles, + ), + (sendPort: tileRecievePort.sendPort, region: input.region), + onExit: tileRecievePort.sendPort, + debugName: '[FMTC] Tile Coords Generator Thread', + ); + final tileQueue = StreamQueue( + tileRecievePort.skip(input.region.start).take( + input.region.end == null + ? largestInt + : (input.region.end! - input.region.start), + ), + ); + final requestTilePort = await tileQueue.next as SendPort; + + // Setup two-way communications with root + final rootRecievePort = ReceivePort(); + void send(Object? m) => input.sendPort.send(m); + send(rootRecievePort.sendPort); + + // Setup cancel signal handling + final cancelSignal = Completer(); + unawaited( + rootRecievePort.firstWhere((e) => e == null).then((_) { + try { + cancelSignal.complete(); + // ignore: avoid_catching_errors, empty_catches + } on StateError {} + }), + ); + + // Start progress tracking + final downloadDuration = Stopwatch()..start(); + var lastDownloadProgress = DownloadProgress.initial(maxTiles: maxTiles); + + // Setup progress report fallback + final Timer? fallbackReportTimer; + if (input.maxReportInterval case final maxReportInterval?) { + fallbackReportTimer = Timer.periodic( + maxReportInterval, + (_) => send( + lastDownloadProgress = + lastDownloadProgress.update(newDuration: downloadDuration.elapsed), + ), + ); + } else { + fallbackReportTimer = null; + } + + // Start download threads + final downloadThreads = List.generate( + input.parallelThreads, + (threadNo) async { + if (cancelSignal.isCompleted) return; + + // Start thread worker isolate & setup two-way communications + final downloadThreadRecievePort = ReceivePort(); + await Isolate.spawn( + singleDownloadThread, + ( + sendPort: downloadThreadRecievePort.sendPort, + storeId: + DatabaseTools.hash(input.tileProvider.storeDirectory.storeName) + .toString(), + rootDirectory: input.rootDirectory, + region: input.region, + tileProvider: input.tileProvider, + maxBufferLength: + (input.maxBufferLength / input.parallelThreads).ceil(), + pruneExistingTiles: input.pruneExistingTiles, + seaTileBytes: seaTileBytes, + ), + onExit: downloadThreadRecievePort.sendPort, + debugName: '[FMTC] Bulk Download Thread #$threadNo', + ); + late final SendPort sendPort; + final sendPortCompleter = Completer(); + + // Prevent completion of this function until the thread is shutdown + final threadKilled = Completer(); + + // When one thread is complete, or the manual cancel signal is sent, + // kill all threads + unawaited( + cancelSignal.future + .then((_) async => (await sendPortCompleter.future).send(null)), + ); + + downloadThreadRecievePort.listen( + (evt) async { + // Thread is sending tile data + if (evt is TileEvent) { + send( + lastDownloadProgress = lastDownloadProgress.update( + newTileEvent: evt, + newDuration: downloadDuration.elapsed, + ), + ); + return; + } + + // Thread is requesting new tile coords + if (evt is int) { + requestTilePort.send(null); + try { + sendPort.send(await tileQueue.next); + // ignore: avoid_catching_errors + } on StateError { + sendPort.send(null); + } + return; + } + + // Thread is establishing comms + if (evt is SendPort) { + sendPortCompleter.complete(evt); + sendPort = evt; + return; + } + + // Thread ended, goto `onDone` + if (evt == null) return downloadThreadRecievePort.close(); + }, + onDone: () { + try { + cancelSignal.complete(); + // ignore: avoid_catching_errors, empty_catches + } on StateError {} + + threadKilled.complete(); + }, + ); + + // Prevent completion of this function until the thread is shutdown + await threadKilled.future; + }, + growable: false, + ); + + // Wait for download to complete/be fully cancelled + await Future.wait(downloadThreads); + + // Cleanup resources and shutdown + fallbackReportTimer?.cancel(); + downloadDuration.stop(); + await tileQueue.cancel(immediate: true); + tileIsolate.kill(priority: Isolate.immediate); + rootRecievePort.close(); + Isolate.exit(); +} diff --git a/lib/src/bulk_download/thread.dart b/lib/src/bulk_download/thread.dart new file mode 100644 index 00000000..5e1d1199 --- /dev/null +++ b/lib/src/bulk_download/thread.dart @@ -0,0 +1,156 @@ +// Copyright © Luka S (JaffaKetchup) under GPL-v3 +// A full license can be found at .\LICENSE + +import 'dart:async'; +import 'dart:io'; +import 'dart:isolate'; +import 'dart:typed_data'; + +import 'package:async/async.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:http/http.dart'; +import 'package:isar/isar.dart'; +import 'package:meta/meta.dart'; + +import '../../flutter_map_tile_caching.dart'; +import '../db/defs/metadata.dart'; +import '../db/defs/store_descriptor.dart'; +import '../db/defs/tile.dart'; +import '../db/tools.dart'; + +@internal +Future singleDownloadThread( + ({ + SendPort sendPort, + String storeId, + String rootDirectory, + DownloadableRegion region, + FMTCTileProvider tileProvider, + int maxBufferLength, + bool pruneExistingTiles, + Uint8List? seaTileBytes, + }) input, +) async { + // Setup two-way communications + final recievePort = ReceivePort(); + void send(Object m) => input.sendPort.send(m); + send(recievePort.sendPort); + + // Setup tile queue + final tileQueue = StreamQueue(recievePort); + + // Open a reference to the Isar DB for the current store + final db = Isar.openSync( + [DbStoreDescriptorSchema, DbTileSchema, DbMetadataSchema], + name: input.storeId, + directory: input.rootDirectory, + inspector: false, + ); + + final httpClient = Client(); + + final tileBuffer = []; + + while (true) { + // Write tile buffer if necessary + if (tileBuffer.length > input.maxBufferLength) { + db.writeTxnSync(() => db.tiles.putAllSync(tileBuffer)); + tileBuffer.clear(); + } + + // Request new tile coords + send(0); + final coords = (await tileQueue.next) as (int, int, int)?; + + // Cleanup resources and shutdown if no more coords available + if (coords == null) { + recievePort.close(); + await tileQueue.cancel(immediate: true); + + httpClient.close(); + + if (tileBuffer.isNotEmpty) { + db.writeTxnSync(() => db.tiles.putAllSync(tileBuffer)); + } + await db.close(); + + Isolate.exit(); + } + + final url = input.tileProvider.getTileUrl( + TileCoordinates(coords.$1, coords.$2, coords.$3), + input.region.options, + ); + final existingTile = await db.tiles.get(DatabaseTools.hash(url)); + + if (input.pruneExistingTiles && existingTile != null) { + send( + TileEvent( + TileEventResult.alreadyExisting, + url: url, + tileImage: Uint8List.fromList(existingTile.bytes), + ), + ); + continue; + } + + final Response response; + try { + response = await httpClient.get( + Uri.parse(url), + headers: input.tileProvider.headers, + ); + } catch (e) { + send( + TileEvent( + e is SocketException + ? TileEventResult.noConnectionDuringFetch + : TileEventResult.unknownFetchException, + url: url, + fetchError: e, + ), + ); + continue; + } + + if (response.statusCode != 200) { + send( + TileEvent( + TileEventResult.negativeFetchResponse, + url: url, + fetchResponse: response, + ), + ); + continue; + } + + if (const ListEquality().equals(response.bodyBytes, input.seaTileBytes)) { + send( + TileEvent( + TileEventResult.isSeaTile, + url: url, + tileImage: response.bodyBytes, + fetchResponse: response, + ), + ); + continue; + } + + final tile = DbTile(url: url, bytes: response.bodyBytes); + if (input.maxBufferLength == 0) { + db.writeTxnSync(() => db.tiles.putSync(tile)); + } else { + tileBuffer.add(tile); + } + + send( + TileEvent( + TileEventResult.success, + url: url, + tileImage: response.bodyBytes, + fetchResponse: response, + ), + ); + } +} diff --git a/lib/src/bulk_download/tile_event.dart b/lib/src/bulk_download/tile_event.dart new file mode 100644 index 00000000..df800a15 --- /dev/null +++ b/lib/src/bulk_download/tile_event.dart @@ -0,0 +1,111 @@ +// Copyright © Luka S (JaffaKetchup) under GPL-v3 +// A full license can be found at .\LICENSE + +part of flutter_map_tile_caching; + +/// A generalized category for [TileEventResult] +enum TileEventResultCategory { + /// The associated tile has been successfully downloaded and cached + /// + /// Independent category for [TileEventResult.success] only. + cached, + + /// The associated tile may have been downloaded, but was not cached + /// intentionally + /// + /// This may be because it: + /// - already existed & `pruneExistingTiles` was `true`: + /// [TileEventResult.alreadyExisting] + /// - was a sea tile & `pruneSeaTiles` was `true`: [TileEventResult.isSeaTile] + pruned, + + /// The associated tile was not successfully downloaded, potentially for a + /// variety of reasons. + /// + /// Category for [TileEventResult.negativeFetchResponse], + /// [TileEventResult.noConnectionDuringFetch], and + /// [TileEventResult.unknownFetchException]. + failed; +} + +/// The result of attempting to cache the associated tile/[TileEvent] +enum TileEventResult { + /// The associated tile was successfully downloaded and cached + success(TileEventResultCategory.cached), + + /// The associated tile was not downloaded (intentionally), becuase it already + /// existed & `pruneExistingTiles` was `true` + alreadyExisting(TileEventResultCategory.pruned), + + /// The associated tile was downloaded, but was not cached (intentionally), + /// because it was a sea tile & `pruneSeaTiles` was `true` + isSeaTile(TileEventResultCategory.pruned), + + /// The associated tile was not successfully downloaded because the tile server + /// responded with a status code other than HTTP 200 OK + negativeFetchResponse(TileEventResultCategory.failed), + + /// The associated tile was not successfully downloaded because a connection + /// could not be made to the tile server + noConnectionDuringFetch(TileEventResultCategory.failed), + + /// The associated tile was not successfully downloaded because of an unknown + /// exception when fetching the tile from the tile server + unknownFetchException(TileEventResultCategory.failed); + + /// The result of attempting to cache the associated tile/[TileEvent] + const TileEventResult(this.category); + + /// A generalized category for this event + final TileEventResultCategory category; +} + +/// The raw result of a tile download during bulk downloading +/// +/// Does not contain information about the download as a whole, that is +/// [DownloadProgress]' responsibility. +class TileEvent { + /// The status of this event, the result of attempting to cache this tile + /// + /// See [TileEventResult.category] ([TileEventResultCategory]) for + /// categorization of this result into 3 categories: + /// + /// - [TileEventResultCategory.cached] (tile was downloaded and cached) + /// - [TileEventResultCategory.pruned] (tile was not cached, but intentionally) + /// - [TileEventResultCategory.failed] (tile was not cached, due to an error) + final TileEventResult result; + + /// The URL used to request the tile + final String url; + + /// The raw bytes that were fetched from the [url], if available + /// + /// Not available if the result category is [TileEventResultCategory.failed]. + final Uint8List? tileImage; + + /// The raw [Response] from the [url], if available + /// + /// Not available if [result] is [TileEventResult.noConnectionDuringFetch], + /// [TileEventResult.unknownFetchException], or + /// [TileEventResult.alreadyExisting]. + final Response? fetchResponse; + + /// The raw error thrown when fetching from the [url], if available + /// + /// Only available if [result] is [TileEventResult.noConnectionDuringFetch] or + /// [TileEventResult.unknownFetchException]. + final Object? fetchError; + + /// The raw result of a tile download during bulk downloading + /// + /// Does not contain information about the download as a whole, that is + /// [DownloadProgress]' responsibility. + @internal + TileEvent( + this.result, { + required this.url, + this.tileImage, + this.fetchResponse, + this.fetchError, + }); +} diff --git a/lib/src/bulk_download/tile_loops/count.dart b/lib/src/bulk_download/tile_loops/count.dart index ebc81689..695d81af 100644 --- a/lib/src/bulk_download/tile_loops/count.dart +++ b/lib/src/bulk_download/tile_loops/count.dart @@ -58,7 +58,7 @@ class TilesCounter { .unscaleBy(tileSize) .floor(); - outlineTileNums[zoomLvl]![tile.x] ??= [_largestInt, _smallestInt]; + outlineTileNums[zoomLvl]![tile.x] ??= [largestInt, smallestInt]; outlineTileNums[zoomLvl]![tile.x] = [ tile.y < outlineTileNums[zoomLvl]![tile.x]![0] ? tile.y @@ -99,16 +99,16 @@ class TilesCounter { final normal = CustomPoint(p2.y - p1.y, p1.x - p2.x); - var minA = _largestInt; - var maxA = _smallestInt; + var minA = largestInt; + var maxA = smallestInt; for (final p in a.points) { final projected = normal.x * p.x + normal.y * p.y; if (projected < minA) minA = projected; if (projected > maxA) maxA = projected; } - var minB = _largestInt; - var maxB = _smallestInt; + var minB = largestInt; + var maxB = smallestInt; for (final p in b.points) { final projected = normal.x * p.x + normal.y * p.y; if (projected < minB) minB = projected; diff --git a/lib/src/bulk_download/tile_loops/generate.dart b/lib/src/bulk_download/tile_loops/generate.dart index d5b74831..4920bfd7 100644 --- a/lib/src/bulk_download/tile_loops/generate.dart +++ b/lib/src/bulk_download/tile_loops/generate.dart @@ -72,7 +72,7 @@ class TilesGenerator { .unscaleBy(tileSize) .floor(); - outlineTileNums[zoomLvl]![tile.x] ??= [_largestInt, _smallestInt]; + outlineTileNums[zoomLvl]![tile.x] ??= [largestInt, smallestInt]; outlineTileNums[zoomLvl]![tile.x] = [ tile.y < outlineTileNums[zoomLvl]![tile.x]![0] ? tile.y @@ -118,16 +118,16 @@ class TilesGenerator { final normal = CustomPoint(p2.y - p1.y, p1.x - p2.x); - var minA = _largestInt; - var maxA = _smallestInt; + var minA = largestInt; + var maxA = smallestInt; for (final p in a.points) { final projected = normal.x * p.x + normal.y * p.y; if (projected < minA) minA = projected; if (projected > maxA) maxA = projected; } - var minB = _largestInt; - var maxB = _smallestInt; + var minB = largestInt; + var maxB = smallestInt; for (final p in b.points) { final projected = normal.x * p.x + normal.y * p.y; if (projected < minB) minB = projected; diff --git a/lib/src/bulk_download/tile_loops/shared.dart b/lib/src/bulk_download/tile_loops/shared.dart index 46dc49a9..586f5113 100644 --- a/lib/src/bulk_download/tile_loops/shared.dart +++ b/lib/src/bulk_download/tile_loops/shared.dart @@ -10,6 +10,7 @@ import 'package:flutter_map/flutter_map.dart' hide Polygon; import 'package:latlong2/latlong.dart'; import '../../../flutter_map_tile_caching.dart'; +import '../../misc/int_extremes.dart'; part 'count.dart'; part 'generate.dart'; @@ -25,8 +26,5 @@ class _Polygon { List> get points => [nw, ne, se, sw]; } -const _largestInt = 9223372036854775807; -const _smallestInt = -9223372036854775808; - CustomPoint _getTileSize(DownloadableRegion region) => CustomPoint(region.options.tileSize, region.options.tileSize); diff --git a/lib/src/bulk_download/tile_progress.dart b/lib/src/bulk_download/tile_progress.dart deleted file mode 100644 index fd7f51ff..00000000 --- a/lib/src/bulk_download/tile_progress.dart +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright © Luka S (JaffaKetchup) under GPL-v3 -// A full license can be found at .\LICENSE - -import 'dart:typed_data'; - -import 'package:meta/meta.dart'; - -@internal -class TileProgress { - final String? failedUrl; - final Uint8List? tileImage; - - final bool wasSeaTile; - final bool wasExistingTile; - final int sizeBytes; - - final bool wasCancelOperation; - final List? bulkTileWriterResponse; - - TileProgress({ - required this.failedUrl, - required this.tileImage, - required this.wasSeaTile, - required this.wasExistingTile, - required this.wasCancelOperation, - required this.bulkTileWriterResponse, - }) : sizeBytes = tileImage?.lengthInBytes ?? 0; - - @override - String toString() => - 'Tile Progress Report (${failedUrl != null ? 'Failed' : 'Successful'}):\n - `failedUrl`: $failedUrl\n - Has `tileImage`: ${tileImage != null}\n - `wasSeaTile`: $wasSeaTile\n - `wasExistingTile`: $wasExistingTile\n - `sizeBytes`: $sizeBytes\n - `wasCancelOperation`: $wasCancelOperation\n - `bulkTileWriterResponse`: $bulkTileWriterResponse'; -} diff --git a/lib/src/db/defs/recovery.dart b/lib/src/db/defs/recovery.dart index 14079ab4..dfda92da 100644 --- a/lib/src/db/defs/recovery.dart +++ b/lib/src/db/defs/recovery.dart @@ -24,10 +24,6 @@ class DbRecoverableRegion { final short start; final short? end; - final byte parallelThreads; - final bool preventRedownload; - final bool seaTileRemoval; - final float? nwLat; final float? nwLng; final float? seLat; @@ -50,9 +46,6 @@ class DbRecoverableRegion { required this.maxZoom, required this.start, this.end, - required this.parallelThreads, - required this.preventRedownload, - required this.seaTileRemoval, this.nwLat, this.nwLng, this.seLat, diff --git a/lib/src/db/defs/recovery.g.dart b/lib/src/db/defs/recovery.g.dart index 614f2c1f..6122c7cc 100644 --- a/lib/src/db/defs/recovery.g.dart +++ b/lib/src/db/defs/recovery.g.dart @@ -72,48 +72,33 @@ const DbRecoverableRegionSchema = CollectionSchema( name: r'nwLng', type: IsarType.float, ), - r'parallelThreads': PropertySchema( - id: 11, - name: r'parallelThreads', - type: IsarType.byte, - ), - r'preventRedownload': PropertySchema( - id: 12, - name: r'preventRedownload', - type: IsarType.bool, - ), r'seLat': PropertySchema( - id: 13, + id: 11, name: r'seLat', type: IsarType.float, ), r'seLng': PropertySchema( - id: 14, + id: 12, name: r'seLng', type: IsarType.float, ), - r'seaTileRemoval': PropertySchema( - id: 15, - name: r'seaTileRemoval', - type: IsarType.bool, - ), r'start': PropertySchema( - id: 16, + id: 13, name: r'start', type: IsarType.int, ), r'storeName': PropertySchema( - id: 17, + id: 14, name: r'storeName', type: IsarType.string, ), r'time': PropertySchema( - id: 18, + id: 15, name: r'time', type: IsarType.dateTime, ), r'type': PropertySchema( - id: 19, + id: 16, name: r'type', type: IsarType.byte, enumMap: _DbRecoverableRegiontypeEnumValueMap, @@ -172,15 +157,12 @@ void _dbRecoverableRegionSerialize( writer.writeByte(offsets[8], object.minZoom); writer.writeFloat(offsets[9], object.nwLat); writer.writeFloat(offsets[10], object.nwLng); - writer.writeByte(offsets[11], object.parallelThreads); - writer.writeBool(offsets[12], object.preventRedownload); - writer.writeFloat(offsets[13], object.seLat); - writer.writeFloat(offsets[14], object.seLng); - writer.writeBool(offsets[15], object.seaTileRemoval); - writer.writeInt(offsets[16], object.start); - writer.writeString(offsets[17], object.storeName); - writer.writeDateTime(offsets[18], object.time); - writer.writeByte(offsets[19], object.type.index); + writer.writeFloat(offsets[11], object.seLat); + writer.writeFloat(offsets[12], object.seLng); + writer.writeInt(offsets[13], object.start); + writer.writeString(offsets[14], object.storeName); + writer.writeDateTime(offsets[15], object.time); + writer.writeByte(offsets[16], object.type.index); } DbRecoverableRegion _dbRecoverableRegionDeserialize( @@ -202,16 +184,13 @@ DbRecoverableRegion _dbRecoverableRegionDeserialize( minZoom: reader.readByte(offsets[8]), nwLat: reader.readFloatOrNull(offsets[9]), nwLng: reader.readFloatOrNull(offsets[10]), - parallelThreads: reader.readByte(offsets[11]), - preventRedownload: reader.readBool(offsets[12]), - seLat: reader.readFloatOrNull(offsets[13]), - seLng: reader.readFloatOrNull(offsets[14]), - seaTileRemoval: reader.readBool(offsets[15]), - start: reader.readInt(offsets[16]), - storeName: reader.readString(offsets[17]), - time: reader.readDateTime(offsets[18]), + seLat: reader.readFloatOrNull(offsets[11]), + seLng: reader.readFloatOrNull(offsets[12]), + start: reader.readInt(offsets[13]), + storeName: reader.readString(offsets[14]), + time: reader.readDateTime(offsets[15]), type: _DbRecoverableRegiontypeValueEnumMap[ - reader.readByteOrNull(offsets[19])] ?? + reader.readByteOrNull(offsets[16])] ?? RegionType.rectangle, ); return object; @@ -247,22 +226,16 @@ P _dbRecoverableRegionDeserializeProp

( case 10: return (reader.readFloatOrNull(offset)) as P; case 11: - return (reader.readByte(offset)) as P; - case 12: - return (reader.readBool(offset)) as P; - case 13: return (reader.readFloatOrNull(offset)) as P; - case 14: + case 12: return (reader.readFloatOrNull(offset)) as P; - case 15: - return (reader.readBool(offset)) as P; - case 16: + case 13: return (reader.readInt(offset)) as P; - case 17: + case 14: return (reader.readString(offset)) as P; - case 18: + case 15: return (reader.readDateTime(offset)) as P; - case 19: + case 16: return (_DbRecoverableRegiontypeValueEnumMap[ reader.readByteOrNull(offset)] ?? RegionType.rectangle) as P; @@ -1468,72 +1441,6 @@ extension DbRecoverableRegionQueryFilter on QueryBuilder - parallelThreadsEqualTo(int value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'parallelThreads', - value: value, - )); - }); - } - - QueryBuilder - parallelThreadsGreaterThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'parallelThreads', - value: value, - )); - }); - } - - QueryBuilder - parallelThreadsLessThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'parallelThreads', - value: value, - )); - }); - } - - QueryBuilder - parallelThreadsBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'parallelThreads', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - )); - }); - } - - QueryBuilder - preventRedownloadEqualTo(bool value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'preventRedownload', - value: value, - )); - }); - } - QueryBuilder seLatIsNull() { return QueryBuilder.apply(this, (query) { @@ -1702,16 +1609,6 @@ extension DbRecoverableRegionQueryFilter on QueryBuilder - seaTileRemovalEqualTo(bool value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'seaTileRemoval', - value: value, - )); - }); - } - QueryBuilder startEqualTo(int value) { return QueryBuilder.apply(this, (query) { @@ -2151,34 +2048,6 @@ extension DbRecoverableRegionQuerySortBy }); } - QueryBuilder - sortByParallelThreads() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'parallelThreads', Sort.asc); - }); - } - - QueryBuilder - sortByParallelThreadsDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'parallelThreads', Sort.desc); - }); - } - - QueryBuilder - sortByPreventRedownload() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'preventRedownload', Sort.asc); - }); - } - - QueryBuilder - sortByPreventRedownloadDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'preventRedownload', Sort.desc); - }); - } - QueryBuilder sortBySeLat() { return QueryBuilder.apply(this, (query) { @@ -2207,20 +2076,6 @@ extension DbRecoverableRegionQuerySortBy }); } - QueryBuilder - sortBySeaTileRemoval() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'seaTileRemoval', Sort.asc); - }); - } - - QueryBuilder - sortBySeaTileRemovalDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'seaTileRemoval', Sort.desc); - }); - } - QueryBuilder sortByStart() { return QueryBuilder.apply(this, (query) { @@ -2420,34 +2275,6 @@ extension DbRecoverableRegionQuerySortThenBy }); } - QueryBuilder - thenByParallelThreads() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'parallelThreads', Sort.asc); - }); - } - - QueryBuilder - thenByParallelThreadsDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'parallelThreads', Sort.desc); - }); - } - - QueryBuilder - thenByPreventRedownload() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'preventRedownload', Sort.asc); - }); - } - - QueryBuilder - thenByPreventRedownloadDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'preventRedownload', Sort.desc); - }); - } - QueryBuilder thenBySeLat() { return QueryBuilder.apply(this, (query) { @@ -2476,20 +2303,6 @@ extension DbRecoverableRegionQuerySortThenBy }); } - QueryBuilder - thenBySeaTileRemoval() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'seaTileRemoval', Sort.asc); - }); - } - - QueryBuilder - thenBySeaTileRemovalDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'seaTileRemoval', Sort.desc); - }); - } - QueryBuilder thenByStart() { return QueryBuilder.apply(this, (query) { @@ -2626,20 +2439,6 @@ extension DbRecoverableRegionQueryWhereDistinct }); } - QueryBuilder - distinctByParallelThreads() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'parallelThreads'); - }); - } - - QueryBuilder - distinctByPreventRedownload() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'preventRedownload'); - }); - } - QueryBuilder distinctBySeLat() { return QueryBuilder.apply(this, (query) { @@ -2654,13 +2453,6 @@ extension DbRecoverableRegionQueryWhereDistinct }); } - QueryBuilder - distinctBySeaTileRemoval() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'seaTileRemoval'); - }); - } - QueryBuilder distinctByStart() { return QueryBuilder.apply(this, (query) { @@ -2770,20 +2562,6 @@ extension DbRecoverableRegionQueryProperty }); } - QueryBuilder - parallelThreadsProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'parallelThreads'); - }); - } - - QueryBuilder - preventRedownloadProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'preventRedownload'); - }); - } - QueryBuilder seLatProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'seLat'); @@ -2796,13 +2574,6 @@ extension DbRecoverableRegionQueryProperty }); } - QueryBuilder - seaTileRemovalProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'seaTileRemoval'); - }); - } - QueryBuilder startProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'start'); diff --git a/lib/src/misc/int_extremes.dart b/lib/src/misc/int_extremes.dart new file mode 100644 index 00000000..7af86ba4 --- /dev/null +++ b/lib/src/misc/int_extremes.dart @@ -0,0 +1,2 @@ +const largestInt = 9223372036854775807; +const smallestInt = -9223372036854775808; diff --git a/lib/src/regions/base_region.dart b/lib/src/regions/base_region.dart index bcfc6434..7448a147 100644 --- a/lib/src/regions/base_region.dart +++ b/lib/src/regions/base_region.dart @@ -62,9 +62,6 @@ sealed class BaseRegion { int minZoom, int maxZoom, TileLayer options, { - int parallelThreads = 10, - bool preventRedownload = false, - bool seaTileRemoval = false, int start = 0, int? end, Crs crs = const Epsg3857(), diff --git a/lib/src/regions/circle.dart b/lib/src/regions/circle.dart index 6c05a981..941d42de 100644 --- a/lib/src/regions/circle.dart +++ b/lib/src/regions/circle.dart @@ -29,9 +29,6 @@ class CircleRegion extends BaseRegion { int minZoom, int maxZoom, TileLayer options, { - int parallelThreads = 10, - bool preventRedownload = false, - bool seaTileRemoval = false, int start = 0, int? end, Crs crs = const Epsg3857(), @@ -42,13 +39,9 @@ class CircleRegion extends BaseRegion { minZoom: minZoom, maxZoom: maxZoom, options: options, - parallelThreads: parallelThreads, - preventRedownload: preventRedownload, - seaTileRemoval: seaTileRemoval, start: start, end: end, crs: crs, - errorHandler: errorHandler, ); @override diff --git a/lib/src/regions/downloadable_region.dart b/lib/src/regions/downloadable_region.dart index 6f8da9ad..8d38585c 100644 --- a/lib/src/regions/downloadable_region.dart +++ b/lib/src/regions/downloadable_region.dart @@ -22,38 +22,6 @@ class DownloadableRegion { /// The options used to fetch tiles final TileLayer options; - /// The number of download threads allowed to run simultaneously - /// - /// This will significantly increase speed, at the expense of faster battery - /// drain. Note that some servers may forbid multithreading, in which case this - /// should be set to 1, unless another limit is specified. - /// - /// Set to 1 to disable multithreading. Defaults to 10. - final int parallelThreads; - - /// Whether to skip downloading tiles that already exist - /// - /// Defaults to `false`, so that existing tiles will be updated. - final bool preventRedownload; - - /// Whether to remove tiles that are entirely sea - /// - /// The checks are conducted by comparing the bytes of the tile at x:0, y:0, - /// and z:19 to the bytes of the currently downloading tile. If they match, the - /// tile is deleted, otherwise the tile is kept. - /// - /// This option is therefore not supported when using satellite tiles (because - /// of the variations from tile to tile), on maps where the tile 0/0/19 is not - /// entirely sea, or on servers where zoom level 19 is not supported. If not - /// supported, set this to `false` to avoid wasting unnecessary time and to - /// avoid errors. - /// - /// This is a storage saving feature, not a time saving or data saving feature: - /// tiles still have to be fully downloaded before they can be checked. - /// - /// Set to `false` to keep sea tiles, which is the default. - final bool seaTileRemoval; - /// Optionally skip past a number of tiles 'at the start' of a region /// /// Set to 0 to skip none, which is the default. @@ -67,33 +35,20 @@ class DownloadableRegion { /// The map projection to use to calculate tiles. Defaults to [Epsg3857]. final Crs crs; - /// A function that takes any type of error as an argument to be called in the - /// event a tile fetch fails - final void Function(Object?)? errorHandler; - DownloadableRegion._( this.originalRegion, { required this.minZoom, required this.maxZoom, required this.options, - required this.parallelThreads, - required this.preventRedownload, - required this.seaTileRemoval, required this.start, required this.end, required this.crs, - required this.errorHandler, }) { if (minZoom > maxZoom) { throw ArgumentError( '`minZoom` should be less than or equal to `maxZoom`', ); } - if (parallelThreads < 1) { - throw ArgumentError( - '`parallelThreads` should be more than or equal to 1. Set to 1 to disable multithreading', - ); - } } /// Output a value of type [T] dependent on [originalRegion] and its type [R] @@ -114,13 +69,9 @@ class DownloadableRegion { other.minZoom == minZoom && other.maxZoom == maxZoom && other.options == options && - other.parallelThreads == parallelThreads && - other.preventRedownload == preventRedownload && - other.seaTileRemoval == seaTileRemoval && other.start == start && other.end == end && - other.crs == crs && - other.errorHandler == errorHandler); + other.crs == crs); @override int get hashCode => Object.hashAllUnordered([ @@ -128,16 +79,8 @@ class DownloadableRegion { minZoom.hashCode, maxZoom.hashCode, options.hashCode, - parallelThreads.hashCode, - preventRedownload.hashCode, - seaTileRemoval.hashCode, start.hashCode, end.hashCode, crs.hashCode, - errorHandler.hashCode, ]); - - @override - String toString() => - 'DownloadableRegion(originalRegion: $originalRegion, minZoom: $minZoom, maxZoom: $maxZoom, options: $options, parallelThreads: $parallelThreads, preventRedownload: $preventRedownload, seaTileRemoval: $seaTileRemoval, start: $start, end: $end, crs: $crs, errorHandler: $errorHandler)'; } diff --git a/lib/src/regions/line.dart b/lib/src/regions/line.dart index b1b29509..707ea6c5 100644 --- a/lib/src/regions/line.dart +++ b/lib/src/regions/line.dart @@ -79,9 +79,6 @@ class LineRegion extends BaseRegion { int minZoom, int maxZoom, TileLayer options, { - int parallelThreads = 10, - bool preventRedownload = false, - bool seaTileRemoval = false, int start = 0, int? end, Crs crs = const Epsg3857(), @@ -92,13 +89,9 @@ class LineRegion extends BaseRegion { minZoom: minZoom, maxZoom: maxZoom, options: options, - parallelThreads: parallelThreads, - preventRedownload: preventRedownload, - seaTileRemoval: seaTileRemoval, start: start, end: end, crs: crs, - errorHandler: errorHandler, ); @override diff --git a/lib/src/regions/recovered_region.dart b/lib/src/regions/recovered_region.dart index 19cc17e2..44bed3a4 100644 --- a/lib/src/regions/recovered_region.dart +++ b/lib/src/regions/recovered_region.dart @@ -60,23 +60,6 @@ class RecoveredRegion { /// Optionally skip a number of tiles 'at the end' of a region final int? end; - /// The number of download threads allowed to run simultaneously - /// - /// This will significantly increase speed, at the expense of faster battery drain. Note that some servers may forbid multithreading, in which case this should be set to 1, unless another limit is specified. - final int parallelThreads; - - /// Whether to skip downloading tiles that already exist - final bool preventRedownload; - - /// Whether to remove tiles that are entirely sea - /// - /// The checks are conducted by comparing the bytes of the tile at x:0, y:0, and z:19 to the bytes of the currently downloading tile. If they match, the tile is deleted, otherwise the tile is kept. - /// - /// This option is therefore not supported when using satellite tiles (because of the variations from tile to tile), on maps where the tile 0/0/19 is not entirely sea, or on servers where zoom level 19 is not supported. If not supported, set this to `false` to avoid wasting unnecessary time and to avoid errors. - /// - /// This is a storage saving feature, not a time saving or data saving feature: tiles still have to be fully downloaded before they can be checked. - final bool seaTileRemoval; - RecoveredRegion._({ required this.id, required this.storeName, @@ -90,9 +73,6 @@ class RecoveredRegion { required this.maxZoom, required this.start, required this.end, - required this.parallelThreads, - required this.preventRedownload, - required this.seaTileRemoval, }) : _type = type; /// Convert this region into it's original [BaseRegion], calling the respective @@ -119,12 +99,8 @@ class RecoveredRegion { minZoom: minZoom, maxZoom: maxZoom, options: options, - parallelThreads: parallelThreads, - preventRedownload: preventRedownload, - seaTileRemoval: seaTileRemoval, start: start, end: end, crs: crs, - errorHandler: errorHandler, ); } diff --git a/lib/src/regions/rectangle.dart b/lib/src/regions/rectangle.dart index b737a51d..ca9ed2e4 100644 --- a/lib/src/regions/rectangle.dart +++ b/lib/src/regions/rectangle.dart @@ -28,9 +28,6 @@ class RectangleRegion extends BaseRegion { int minZoom, int maxZoom, TileLayer options, { - int parallelThreads = 10, - bool preventRedownload = false, - bool seaTileRemoval = false, int start = 0, int? end, Crs crs = const Epsg3857(), @@ -41,13 +38,9 @@ class RectangleRegion extends BaseRegion { minZoom: minZoom, maxZoom: maxZoom, options: options, - parallelThreads: parallelThreads, - preventRedownload: preventRedownload, - seaTileRemoval: seaTileRemoval, start: start, end: end, crs: crs, - errorHandler: errorHandler, ); @override diff --git a/lib/src/root/recovery.dart b/lib/src/root/recovery.dart index dc8c3464..5358477f 100644 --- a/lib/src/root/recovery.dart +++ b/lib/src/root/recovery.dart @@ -60,9 +60,6 @@ class RootRecovery { maxZoom: r.maxZoom, start: r.start, end: r.end, - parallelThreads: r.parallelThreads, - preventRedownload: r.preventRedownload, - seaTileRemoval: r.seaTileRemoval, ), ) .toList(); @@ -108,9 +105,6 @@ class RootRecovery { maxZoom: region.maxZoom, start: region.start, end: region.end, - parallelThreads: region.parallelThreads, - preventRedownload: region.preventRedownload, - seaTileRemoval: region.seaTileRemoval, nwLat: region.originalRegion is RectangleRegion ? (region.originalRegion as RectangleRegion) .bounds @@ -166,7 +160,8 @@ class RootRecovery { /// Safely cancel a recoverable region Future cancel(int id) async { - _downloadsOngoing.remove(id); - return _recovery.writeTxn(() => _recovery.recovery.delete(id)); + if (_downloadsOngoing.remove(id)) { + return _recovery.writeTxn(() => _recovery.recovery.delete(id)); + } } } diff --git a/lib/src/store/download.dart b/lib/src/store/download.dart index e35765fb..e83368e2 100644 --- a/lib/src/store/download.dart +++ b/lib/src/store/download.dart @@ -5,202 +5,184 @@ part of flutter_map_tile_caching; /// Provides tools to manage bulk downloading to a specific [StoreDirectory] /// -/// Is a singleton to ensure functioning as expected. -/// /// The 'fmtc_plus_background_downloading' module must be installed to add the /// background downloading functionality. +/// +/// {@template num_instances} +/// By default, only one download per store at the same time is tolerated. +/// Attempting to start more than one at the same time may cause poor +/// performance or other poor behaviour. +/// +/// However, if necessary, multiple can be started by setting methods' +/// `instanceId` argument to a unique value on methods. Note that this unique +/// value must be known and remembered to control the state of the download. +/// {@endtemplate} +/// +/// Does not keep state. State and download instances are held internally by +/// [DownloadInstance]. +@immutable class DownloadManagement { - /// The store directory to provide access paths to + const DownloadManagement._(this._storeDirectory); final StoreDirectory _storeDirectory; - int? _recoveryId; - // ignore: close_sinks - StreamController? _tileProgressStreamController; - Completer? _cancelRequestSignal; - Completer? _cancelCompleteSignal; - InternalProgressTimingManagement? _progressManagement; - Client? _httpClient; - - factory DownloadManagement._(StoreDirectory storeDirectory) { - if (!_instances.keys.contains(storeDirectory)) { - _instances[storeDirectory] = DownloadManagement.__(storeDirectory); - } - return _instances[storeDirectory]!; - } - - DownloadManagement.__(this._storeDirectory); - - /// Contains the intialised instances of [DownloadManagement]s - static final Map _instances = {}; - /// Download a specified [DownloadableRegion] in the foreground, with a - /// recovery session + /// recovery session by default /// /// To check the number of tiles that need to be downloaded before using this /// function, use [check]. /// - /// [httpClient] defaults to a [HttpPlusClient] which supports HTTP/2 and falls - /// back to a standard [IOClient]/[HttpClient] for HTTP/1.1 servers. Timeout is - /// set to 5 seconds by default. - /// /// Streams a [DownloadProgress] object containing statistics and information - /// about the download's progression status. This must be listened to. + /// about the download's progression status, once per tile and at intervals + /// of no longer than [maxReportInterval]. This must be listened to, otherwise + /// the download will not start. /// /// --- /// - /// [bufferMode] and [bufferLimit] control how this download will use - /// buffering. For information about buffering, and it's advantages and - /// disadvantages, see - /// [this docs page](https://fmtc.jaffaketchup.dev/bulk-downloading/foreground/buffering). - /// Also see [DownloadBufferMode]'s documentation. + /// 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 100 | 0 to disable): number of tiles to + /// temporarily buffer before writing to the store (split evenly between + /// [parallelThreads]) + /// - [pruneExistingTiles] (defaults to `true`): whether to skip downloading + /// tiles that are already cached + /// - [pruneSeaTiles] (defaults to `true`): whether to skip caching tiles that + /// are entirely sea (this is decided based on a comparison to the tile at + /// x0y0z17) + /// + /// Using too many parallel threads may place significant strain on the tile + /// server, so check your tile server's ToS for more information. + /// + /// 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. + /// The output stream's statistics do not account for buffering. + /// + /// --- /// - /// - If [bufferMode] is [DownloadBufferMode.disabled] (default), [bufferLimit] - /// will be ignored - /// - If [bufferMode] is [DownloadBufferMode.tiles], [bufferLimit] will default - /// to 500 - /// - If [bufferMode] is [DownloadBufferMode.bytes], [bufferLimit] will default - /// to 2000000 (2 MB) + /// {@macro num_instances} + @useResult Stream startForeground({ required DownloadableRegion region, FMTCTileProviderSettings? tileProviderSettings, + int parallelThreads = 5, + int maxBufferLength = 100, + bool pruneExistingTiles = true, + bool pruneSeaTiles = true, + Duration? maxReportInterval = const Duration(seconds: 1), bool disableRecovery = false, - DownloadBufferMode bufferMode = DownloadBufferMode.disabled, - int? bufferLimit, - Client? httpClient, + Object instanceId = 0, }) async* { - // Start recovery - _recoveryId = DateTime.now().millisecondsSinceEpoch; - if (!disableRecovery) { - await FMTC.instance.rootDirectory.recovery._start( - id: _recoveryId!, - storeName: _storeDirectory.storeName, - region: region, + // 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', ); } - // Count number of tiles - final maxTiles = await check(region); - - // Get the tile provider - final FMTCTileProvider tileProvider = - _storeDirectory.getTileProvider(settings: tileProviderSettings); - - // Initialise HTTP client - _httpClient = httpClient ?? - HttpPlusClient( - http1Client: IOClient( - HttpClient() - ..connectionTimeout = const Duration(seconds: 5) - ..userAgent = null, - ), - connectionTimeout: const Duration(seconds: 5), - ); + if (region.options.tileProvider.headers['User-Agent'] == + 'flutter_map (unknown)') { + throw ArgumentError( + "`.toDownloadable`'s `TileLayer` argument must specify an appropriate `userAgentPackageName` or other `headers['User-Agent']`", + 'region.options.userAgentPackageName', + ); + } - // Initialise the sea tile removal system - Uint8List? seaTileBytes; - if (region.seaTileRemoval) { - try { - seaTileBytes = (await _httpClient!.get( - Uri.parse( - tileProvider.getTileUrl( - const TileCoordinates(0, 0, 17), - region.options, - ), - ), - )) - .bodyBytes; - } catch (e) { - seaTileBytes = null; - } + if (parallelThreads < 1) { + throw ArgumentError.value( + parallelThreads, + 'parallelThreads', + 'must be greater than 0', + ); } - // Initialise variables - final List failedTiles = []; - int bufferedTiles = 0; - int bufferedSize = 0; - int persistedTiles = 0; - int persistedSize = 0; - int seaTiles = 0; - int existingTiles = 0; - _tileProgressStreamController = StreamController(); - _cancelRequestSignal = Completer(); - _cancelCompleteSignal = Completer(); + if (maxBufferLength < 0) { + throw ArgumentError.value( + maxBufferLength, + 'maxBufferLength', + 'must be greater than -1', + ); + } - // Start progress management - final DateTime startTime = DateTime.now(); - _progressManagement = InternalProgressTimingManagement()..start(); + // Create download instance + final instance = DownloadInstance.registerIfAvailable(instanceId); + if (instance == null) { + throw StateError( + "Download instance with ID $instanceId already exists\nIf you're sure you want to start multiple downloads, use a unique `instanceId`.", + ); + } - // Start writing isolates - await BulkTileWriter.start( - provider: tileProvider, - bufferMode: bufferMode, - bufferLimit: bufferLimit, - directory: FMTC.instance.rootDirectory.directory.absolute.path, - streamController: _tileProgressStreamController!, - ); + // Start recovery system (unless disabled) + final recoveryId = + Object.hash(instanceId, DateTime.now().millisecondsSinceEpoch); + if (!disableRecovery) { + await FMTC.instance.rootDirectory.recovery._start( + id: recoveryId, + storeName: _storeDirectory.storeName, + region: region, + ); + } - // Start the bulk downloader - final Stream downloadStream = await bulkDownloader( - streamController: _tileProgressStreamController!, - cancelRequestSignal: _cancelRequestSignal!, - cancelCompleteSignal: _cancelCompleteSignal!, - region: region, - provider: tileProvider, - seaTileBytes: seaTileBytes, - progressManagement: _progressManagement!, - client: _httpClient!, + // Start download thread + final recievePort = ReceivePort(); + await Isolate.spawn( + downloadManager, + ( + sendPort: recievePort.sendPort, + rootDirectory: FMTC.instance.rootDirectory.directory.absolute.path, + region: region, + tileProvider: _storeDirectory.getTileProvider( + settings: tileProviderSettings, + headers: region.options.tileProvider.headers, + ), + parallelThreads: parallelThreads, + maxBufferLength: maxBufferLength, + pruneExistingTiles: pruneExistingTiles, + pruneSeaTiles: pruneSeaTiles, + maxReportInterval: maxReportInterval, + ), + onExit: recievePort.sendPort, + debugName: '[FMTC] Master Bulk Download Thread', ); - // Listen to download progress, and report results - await for (final TileProgress evt in downloadStream) { - if (evt.failedUrl == null) { - if (!evt.wasCancelOperation) { - bufferedTiles++; - } else { - bufferedTiles = 0; - } - bufferedSize += evt.sizeBytes; - if (evt.bulkTileWriterResponse != null) { - persistedTiles = evt.bulkTileWriterResponse![0]; - persistedSize = evt.bulkTileWriterResponse![1]; - } - } else { - failedTiles.add(evt.failedUrl!); - } + // Setup (part 1) cancel request mechanism + final cancelCompleter = Completer(); - if (evt.wasSeaTile) seaTiles += 1; - if (evt.wasExistingTile) existingTiles += 1; + await for (final evt in recievePort) { + // Handle shutdown (both normal and cancellation) + if (evt == null) break; - final DownloadProgress prog = DownloadProgress._( - downloadID: _recoveryId!, - maxTiles: maxTiles, - successfulTiles: bufferedTiles, - persistedTiles: persistedTiles, - failedTiles: failedTiles, - successfulSize: bufferedSize / 1024, - persistedSize: persistedSize / 1024, - seaTiles: seaTiles, - existingTiles: existingTiles, - duration: DateTime.now().difference(startTime), - tileImage: evt.tileImage == null ? null : MemoryImage(evt.tileImage!), - bufferMode: bufferMode, - progressManagement: _progressManagement!, - ); + // Setup (part 2) cancel request mechanism + if (evt is SendPort) { + instance.cancelDownloadRequest = () { + evt.send(null); + return cancelCompleter.future; + }; + continue; + } - yield prog; - if (prog.percentageProgress >= 100) break; + // Ensure message is of `DownloadProgress` in all other cases + evt as DownloadProgress; + yield evt; } - _internalCancel(); + // Handle shutdown (both normal and cancellation) + instance.cancelDownloadRequest = null; + recievePort.close(); + await FMTC.instance.rootDirectory.recovery.cancel(recoveryId); + DownloadInstance.unregister(instanceId); + cancelCompleter.complete(); } - /// Check approximately how many downloadable tiles are within a specified - /// [DownloadableRegion] + /// Check how many downloadable tiles are within a specified region /// - /// This does not take into account sea tile removal or redownload prevention, - /// as these are handled in the download area of the code. + /// This does not take into account sea tile removal or redownload prevention. /// - /// Returns an `int` which is the number of tiles. + /// Returns the number of tiles. Future check(DownloadableRegion region) => compute( region.when( rectangle: (_) => TilesCounter.rectangleTiles, @@ -210,55 +192,21 @@ class DownloadManagement { region, ); - /// Cancels the ongoing foreground download and recovery session (within the - /// current object) + /// Cancels the ongoing foreground download and recovery session + /// + /// Will return once the cancellation is complete. Note that all running + /// parallel download threads will be allowed to finish their *current* tile + /// download, and tiles will be written. There is no facility to cancel the + /// download immediately, as this would likely cause unwanted behaviour. + /// + /// {@macro num_instances} + /// + /// Does nothing (returns immediately) if there is no ongoing download. /// /// Do not use to cancel background downloads, return `true` from the /// background download callback to cancel a background download. Background /// download cancellations require a few more 'shut-down' steps that can create /// unexpected issues and memory leaks if not carried out. - /// - /// Note that another instance of this object must be retrieved before another - /// download is attempted, as this one is destroyed. - Future cancel() async { - _cancelRequestSignal?.complete(); - await _cancelCompleteSignal?.future; - - _internalCancel(); - } - - void _internalCancel() { - _progressManagement?.stop(); - - if (_recoveryId != null) { - FMTC.instance.rootDirectory.recovery.cancel(_recoveryId!); - } - _httpClient?.close(); - - _instances.remove(_storeDirectory); - } -} - -/// Describes the buffering mode during a bulk download -/// -/// For information about buffering, and it's advantages and disadvantages, see -/// [this docs page](https://fmtc.jaffaketchup.dev/bulk-downloading/foreground/buffering). -enum DownloadBufferMode { - /// Disable the buffer (use direct writing) - /// - /// Tiles will be written directly to the database as soon as they are - /// downloaded. - disabled, - - /// Set the limit of the buffer in terms of the number of tiles it holds - /// - /// Tiles will be written to an intermediate memory buffer, then bulk written - /// to the database once there are more tiles than specified. - tiles, - - /// Set the limit of the buffer in terms of the number of bytes it holds - /// - /// Tiles will be written to an intermediate memory buffer, then bulk written - /// to the database once there are more bytes than specified. - bytes, + Future cancel({Object instanceId = 0}) async => + await DownloadInstance.get(instanceId)?.cancelDownloadRequest?.call(); } diff --git a/lib/src/store/statistics.dart b/lib/src/store/statistics.dart index c2f53c85..5344c919 100644 --- a/lib/src/store/statistics.dart +++ b/lib/src/store/statistics.dart @@ -1,10 +1,6 @@ // Copyright © Luka S (JaffaKetchup) under GPL-v3 // A full license can be found at .\LICENSE -// Not sure why the hell this triggers! It triggers on a documentation comment, -// and doesn't go away no matter what I do. -// ignore_for_file: use_late_for_private_fields_and_variables - part of flutter_map_tile_caching; /// Provides statistics about a [StoreDirectory] From d416c6edd265729db4f987d36d9c110187b0be2c Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Tue, 4 Jul 2023 22:33:18 +0100 Subject: [PATCH 022/168] Added pause and resume functionality to bulk downloading Added/improved buffering functionality to bulk downloading Improved bulk downloading performance Removed support for HTTP/2 clients Removed support for bulk download buffering by storage capacity (bytes) limit Improved bulk downloading screen in example app Improved documentation Former-commit-id: 05c208cfc58adbbbff846af3e9919deffe2fe5e1 [formerly 2f625f7efe384ed1e30ecf7cebb6bb5ce32f7ff1] Former-commit-id: 693bf701e10371a71bc1cdb19f5112d141e2b372 --- example/lib/main.dart | 82 ++-- .../components/buffering_configuration.dart | 71 +--- .../download_region/download_region.dart | 195 +++------- .../components/download_layout.dart | 363 ++++++++++++++++++ .../pages/downloading/components/header.dart | 60 --- .../components/horizontal_layout.dart | 159 -------- .../multi_linear_progress_indicator.dart | 88 +++++ .../downloading/components/stat_display.dart | 28 +- .../downloading/components/tile_image.dart | 47 --- .../components/vertical_layout.dart | 128 ------ .../main/pages/downloading/downloading.dart | 119 ++++-- .../components/recovery_start_button.dart | 2 - .../lib/shared/state/download_provider.dart | 12 +- lib/flutter_map_tile_caching.dart | 8 +- lib/src/bulk_download/download_progress.dart | 126 ++++-- lib/src/bulk_download/instance.dart | 7 +- lib/src/bulk_download/manager.dart | 307 +++++++++------ lib/src/bulk_download/thread.dart | 59 ++- lib/src/bulk_download/tile_event.dart | 37 +- lib/src/misc/int_extremes.dart | 3 + lib/src/providers/tile_provider.dart | 20 +- lib/src/store/directory.dart | 2 +- lib/src/store/download.dart | 100 +++-- pubspec.yaml | 1 - 24 files changed, 1086 insertions(+), 938 deletions(-) create mode 100644 example/lib/screens/main/pages/downloading/components/download_layout.dart delete mode 100644 example/lib/screens/main/pages/downloading/components/header.dart delete mode 100644 example/lib/screens/main/pages/downloading/components/horizontal_layout.dart create mode 100644 example/lib/screens/main/pages/downloading/components/multi_linear_progress_indicator.dart delete mode 100644 example/lib/screens/main/pages/downloading/components/tile_image.dart delete mode 100644 example/lib/screens/main/pages/downloading/components/vertical_layout.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index 261e3b9f..f0be4c88 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,8 +1,16 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; -import 'package:latlong2/latlong.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:path/path.dart' as p; +import 'package:provider/provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'screens/main/main.dart'; +import 'shared/state/download_provider.dart'; +import 'shared/state/general_provider.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -13,64 +21,33 @@ void main() async { ), ); - //final SharedPreferences prefs = await SharedPreferences.getInstance(); + final SharedPreferences prefs = await SharedPreferences.getInstance(); - // String? damagedDatabaseDeleted; + String? damagedDatabaseDeleted; await FlutterMapTileCaching.initialise( - // errorHandler: (error) => damagedDatabaseDeleted = error.message, + errorHandler: (error) => damagedDatabaseDeleted = error.message, debugMode: true, ); - // await FMTC.instance.rootDirectory.migrator.fromV6(urlTemplates: []); - - //if (prefs.getBool('reset') ?? false) { - // await FMTC.instance.rootDirectory.manage.reset(); - // } + await FMTC.instance.rootDirectory.migrator.fromV6(urlTemplates: []); - //final File newAppVersionFile = File( - // p.join( - // // ignore: invalid_use_of_internal_member, invalid_use_of_protected_member - // FMTC.instance.rootDirectory.directory.absolute.path, - // 'newAppVersion.${Platform.isWindows ? 'exe' : 'apk'}', - // ), - // ); - // if (await newAppVersionFile.exists()) await newAppVersionFile.delete(); + if (prefs.getBool('reset') ?? false) { + await FMTC.instance.rootDirectory.manage.reset(); + } - final region = - RectangleRegion(LatLngBounds(const LatLng(1, 1), const LatLng(-1, -1))) - .toDownloadable( - 1, - 12, - TileLayer( - urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', - userAgentPackageName: 'dev.jaffaketchup.fmtc.demo1', + final newAppVersionFile = File( + p.join( + // ignore: invalid_use_of_internal_member, invalid_use_of_protected_member + FMTC.instance.rootDirectory.directory.absolute.path, + 'newAppVersion.${Platform.isWindows ? 'exe' : 'apk'}', ), ); + if (await newAppVersionFile.exists()) await newAppVersionFile.delete(); - FMTC.instance['hello'].download - .startForeground( - region: region, - pruneExistingTiles: false, - pruneSeaTiles: false, - maxBufferLength: 200, - ) - .listen( - (progress) => print( - '${progress.successfulTiles} tiles, ${progress.duration}, ${progress.lastTileEvent?.result}', - ), - ); - print('DOWNLOAD COMPLETE'); - - await Future.delayed(const Duration(seconds: 1)); - - print('REQUEST CANCEL'); - await FMTC.instance['hello'].download.cancel(); - print('DOWNLOAD CANCELLED'); - - //runApp(AppContainer(damagedDatabaseDeleted: damagedDatabaseDeleted)); + runApp(AppContainer(damagedDatabaseDeleted: damagedDatabaseDeleted)); } -/*class AppContainer extends StatelessWidget { +class AppContainer extends StatelessWidget { const AppContainer({ super.key, required this.damagedDatabaseDeleted, @@ -81,12 +58,8 @@ void main() async { @override Widget build(BuildContext context) => MultiProvider( providers: [ - ChangeNotifierProvider( - create: (context) => GeneralProvider(), - ), - ChangeNotifierProvider( - create: (context) => DownloadProvider(), - ), + ChangeNotifierProvider(create: (context) => GeneralProvider()), + ChangeNotifierProvider(create: (context) => DownloadProvider()), ], child: MaterialApp( title: 'FMTC Example', @@ -110,4 +83,3 @@ void main() async { ), ); } -*/ \ No newline at end of file diff --git a/example/lib/screens/download_region/components/buffering_configuration.dart b/example/lib/screens/download_region/components/buffering_configuration.dart index 1fa99fef..9d85f1fd 100644 --- a/example/lib/screens/download_region/components/buffering_configuration.dart +++ b/example/lib/screens/download_region/components/buffering_configuration.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; import 'package:provider/provider.dart'; import '../../../shared/state/download_provider.dart'; @@ -13,64 +12,24 @@ class BufferingConfiguration extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('BUFFERING CONFIGURATION'), - const SizedBox(height: 15), if (provider.regionTiles == null) const CircularProgressIndicator() - else ...[ - Row( - children: [ - SegmentedButton( - segments: const [ - ButtonSegment( - value: DownloadBufferMode.disabled, - label: Text('Disabled'), - icon: Icon(Icons.cancel), - ), - ButtonSegment( - value: DownloadBufferMode.tiles, - label: Text('Tiles'), - icon: Icon(Icons.flip_to_front_outlined), - ), - ButtonSegment( - value: DownloadBufferMode.bytes, - label: Text('Size (kB)'), - icon: Icon(Icons.storage_rounded), - ), - ], - selected: {provider.bufferMode}, - onSelectionChanged: (s) => provider.bufferMode = s.single, - ), - const SizedBox(width: 20), - provider.bufferMode == DownloadBufferMode.disabled - ? const SizedBox.shrink() - : Text( - provider.bufferMode == DownloadBufferMode.tiles && - provider.bufferingAmount >= - provider.regionTiles! - ? 'Write Once' - : '${provider.bufferingAmount} ${provider.bufferMode == DownloadBufferMode.tiles ? 'tiles' : 'kB'}', - ), - ], + else + Builder( + builder: (context) { + final max = (provider.regionTiles! / 2).floorToDouble(); + return Slider( + value: provider.bufferingAmount.clamp(0, max).toDouble(), + max: max, + divisions: (max / 10).ceil(), + onChanged: (value) => + provider.bufferingAmount = value.clamp(0, max).round(), + label: provider.bufferingAmount == 0 + ? 'Disabled' + : 'Write every ${provider.bufferingAmount} tiles', + ); + }, ), - const SizedBox(height: 5), - provider.bufferMode == DownloadBufferMode.disabled - ? const Slider(value: 0.5, onChanged: null) - : Slider( - value: provider.bufferMode == DownloadBufferMode.tiles - ? provider.bufferingAmount - .clamp(10, provider.regionTiles!) - .roundToDouble() - : provider.bufferingAmount.roundToDouble(), - min: provider.bufferMode == DownloadBufferMode.tiles - ? 10 - : 500, - max: provider.bufferMode == DownloadBufferMode.tiles - ? provider.regionTiles!.toDouble() - : 10000, - onChanged: (value) => - provider.bufferingAmount = value.round(), - ), - ], ], ), ); diff --git a/example/lib/screens/download_region/download_region.dart b/example/lib/screens/download_region/download_region.dart index 1220c8cb..385e45af 100644 --- a/example/lib/screens/download_region/download_region.dart +++ b/example/lib/screens/download_region/download_region.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; -import 'package:fmtc_plus_background_downloading/fmtc_plus_background_downloading.dart'; +//import 'package:fmtc_plus_background_downloading/fmtc_plus_background_downloading.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -57,140 +57,71 @@ class _DownloadRegionPopupState extends State { } @override - Widget build(BuildContext context) => Scaffold( - appBar: AppBar( - title: const Text('Download Region'), - ), - body: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - RegionInformation( - widget: widget, - circleRegion: circleRegion, - rectangleRegion: rectangleRegion, - ), - const SectionSeparator(), - const StoreSelector(), - const SectionSeparator(), - const OptionalFunctionality(), - const SectionSeparator(), - const BufferingConfiguration(), - const SectionSeparator(), - const BackgroundDownloadBatteryOptimizationsInfo(), - const SectionSeparator(), - const UsageWarning(), - const SectionSeparator(), - const Text('START DOWNLOAD IN'), - Consumer2( - builder: (context, downloadProvider, generalProvider, _) => - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Expanded( - child: ElevatedButton( - onPressed: downloadProvider.selectedStore == null - ? null - : () async { - final Map metadata = - await downloadProvider - .selectedStore!.metadata.readAsync; - - downloadProvider.setDownloadProgress( - downloadProvider.selectedStore!.download - .startForeground( - region: widget.region.toDownloadable( - downloadProvider.minZoom, - downloadProvider.maxZoom, - TileLayer( - urlTemplate: - metadata['sourceURL'], - ), - preventRedownload: downloadProvider - .preventRedownload, - seaTileRemoval: - downloadProvider.seaTileRemoval, - parallelThreads: - (await SharedPreferences - .getInstance()) - .getBool( - 'bypassDownloadThreadsLimitation', - ) ?? - false - ? 10 - : 2, - ), - disableRecovery: - downloadProvider.disableRecovery, - bufferMode: - downloadProvider.bufferMode, - maxBufferLength: downloadProvider - .bufferMode == - DownloadBufferMode.tiles - ? downloadProvider.bufferingAmount - : downloadProvider - .bufferingAmount * - 1000, - ) - .asBroadcastStream(), - ); + Widget build(BuildContext context) => Consumer( + builder: (context, provider, _) => Scaffold( + appBar: AppBar(title: const Text('Configure Bulk Download')), + floatingActionButton: provider.selectedStore == null + ? null + : FloatingActionButton.extended( + onPressed: () async { + final Map metadata = + await provider.selectedStore!.metadata.readAsync; - if (mounted) Navigator.of(context).pop(); - }, - child: const Text('Foreground'), - ), - ), - const SizedBox(width: 10), - Expanded( - child: OutlinedButton( - onPressed: downloadProvider.selectedStore == null - ? null - : () async { - final Map metadata = - await downloadProvider - .selectedStore!.metadata.readAsync; + provider.setDownloadProgress( + provider.selectedStore!.download + .startForeground( + region: widget.region.toDownloadable( + provider.minZoom, + provider.maxZoom, + TileLayer( + urlTemplate: metadata['sourceURL'], + userAgentPackageName: + 'dev.jaffaketchup.fmtc.demo', + ), + ), + parallelThreads: + (await SharedPreferences.getInstance()).getBool( + 'bypassDownloadThreadsLimitation', + ) ?? + false + ? 10 + : 2, + maxBufferLength: provider.bufferingAmount, + pruneExistingTiles: provider.preventRedownload, + pruneSeaTiles: provider.seaTileRemoval, + disableRecovery: provider.disableRecovery, + ) + .asBroadcastStream(), + ); - await downloadProvider.selectedStore!.download - .startBackground( - region: widget.region.toDownloadable( - downloadProvider.minZoom, - downloadProvider.maxZoom, - TileLayer( - urlTemplate: metadata['sourceURL'], - ), - preventRedownload: - downloadProvider.preventRedownload, - seaTileRemoval: - downloadProvider.seaTileRemoval, - parallelThreads: (await SharedPreferences - .getInstance()) - .getBool( - 'bypassDownloadThreadsLimitation', - ) ?? - false - ? 10 - : 2, - ), - disableRecovery: - downloadProvider.disableRecovery, - backgroundNotificationIcon: - const AndroidResource( - name: 'ic_notification_icon', - defType: 'mipmap', - ), - ); - - if (mounted) Navigator.of(context).pop(); - }, - child: const Text('Background'), - ), - ), - ], - ), + if (mounted) Navigator.of(context).pop(); + }, + label: const Text('Start Download'), + icon: const Icon(Icons.save), ), - ], + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RegionInformation( + widget: widget, + circleRegion: circleRegion, + rectangleRegion: rectangleRegion, + ), + const SectionSeparator(), + const StoreSelector(), + const SectionSeparator(), + const OptionalFunctionality(), + const SectionSeparator(), + const BufferingConfiguration(), + const SectionSeparator(), + const BackgroundDownloadBatteryOptimizationsInfo(), + const SectionSeparator(), + const UsageWarning(), + ], + ), ), ), ), diff --git a/example/lib/screens/main/pages/downloading/components/download_layout.dart b/example/lib/screens/main/pages/downloading/components/download_layout.dart new file mode 100644 index 00000000..39577394 --- /dev/null +++ b/example/lib/screens/main/pages/downloading/components/download_layout.dart @@ -0,0 +1,363 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:provider/provider.dart'; + +import '../../../../../shared/state/download_provider.dart'; +import '../../../../../shared/vars/size_formatter.dart'; +import 'multi_linear_progress_indicator.dart'; +import 'stat_display.dart'; + +class DownloadLayout extends StatelessWidget { + const DownloadLayout({ + super.key, + required this.storeDirectory, + required this.download, + }); + + final StoreDirectory storeDirectory; + final DownloadProgress download; + + @override + Widget build(BuildContext context) => Column( + children: [ + IntrinsicHeight( + child: Row( + children: [ + RepaintBoundary( + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: SizedBox.square( + dimension: 256 / 1.25, + child: download.lastTileEvent.tileImage != null + ? Image.memory( + download.lastTileEvent.tileImage!, + gaplessPlayback: true, + ) + : const Center( + child: CircularProgressIndicator.adaptive(), + ), + ), + ), + ), + const SizedBox(width: 32), + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + StatDisplay( + statistic: + '${download.percentageProgress.toStringAsFixed(2)}%', + description: 'percentage attempted', + ), + StatDisplay( + statistic: download.duration.toString().split('.')[0], + description: 'elapsed duration', + ), + const SizedBox(height: 16), + if (!download.hasFinished) + RepaintBoundary( + child: Row( + children: [ + IconButton.outlined( + onPressed: storeDirectory.download.isPaused() + ? () => storeDirectory.download.resume() + : () => storeDirectory.download.pause(), + icon: Icon( + storeDirectory.download.isPaused() + ? Icons.play_arrow + : Icons.pause, + ), + ), + const SizedBox(width: 8), + IconButton.outlined( + onPressed: () => storeDirectory.download.cancel(), + icon: const Icon(Icons.cancel), + ) + ], + ), + ), + if (download.hasFinished) + OutlinedButton( + onPressed: () { + WidgetsBinding.instance.addPostFrameCallback( + (_) => Provider.of( + context, + listen: false, + ).setDownloadProgress(null), + ); + }, + child: const Padding( + padding: EdgeInsets.symmetric(horizontal: 32), + child: Text('Exit'), + ), + ), + ], + ), + const SizedBox(width: 48), + const VerticalDivider(), + const SizedBox(width: 16), + Expanded( + child: Table( + children: [ + TableRow( + children: [ + StatDisplay( + statistic: '${download.attemptedTiles}', + description: 'attempted tiles', + ), + StatDisplay( + statistic: + '${download.cachedTiles - download.bufferedTiles} + ${download.bufferedTiles}', + description: 'cached + buffered tiles', + ), + StatDisplay( + statistic: + '${((download.cachedSize - download.bufferedSize) * 1024).asReadableSize} + ${(download.bufferedSize * 1024).asReadableSize}', + description: 'cached + buffered size', + ), + ], + ), + TableRow( + children: [ + StatDisplay( + statistic: '${download.remainingTiles}', + description: 'remaining tiles', + ), + StatDisplay( + statistic: + '${download.prunedTiles} (${download.prunedTiles == 0 ? 0 : (100 - ((download.cachedTiles - download.prunedTiles) / download.cachedTiles) * 100).toStringAsFixed(1)}%)', + description: 'pruned tiles (% saving)', + ), + StatDisplay( + statistic: + '${(download.prunedSize * 1024).asReadableSize} (${download.prunedTiles == 0 ? 0 : (100 - ((download.cachedSize - download.prunedSize) / download.cachedSize) * 100).toStringAsFixed(1)}%)', + description: 'pruned size (% saving)', + ), + ], + ), + TableRow( + children: [ + StatDisplay( + statistic: '${download.maxTiles}', + description: 'total tiles', + ), + RepaintBoundary( + child: Column( + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + download.failedTiles.toString(), + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: download.failedTiles == 0 + ? null + : Colors.red, + ), + ), + if (download.failedTiles != 0) ...[ + const SizedBox(width: 8), + const Icon( + Icons.warning_amber, + color: Colors.red, + ), + ] + ], + ), + Text( + 'failed tiles', + style: TextStyle( + fontSize: 16, + color: download.failedTiles == 0 + ? null + : Colors.red, + ), + ), + ], + ), + ), + const SizedBox.shrink(), + ], + ), + ], + ), + ), + ], + ), + ), + const SizedBox(height: 30), + MulitLinearProgressIndicator( + maxValue: download.maxTiles, + backgroundChild: Text( + '${download.remainingTiles}', + style: const TextStyle(color: Colors.white), + ), + progresses: [ + ( + value: download.cachedTiles + + download.prunedTiles + + download.failedTiles, + color: Colors.red, + child: Text( + '${download.failedTiles}', + style: const TextStyle(color: Colors.black), + ) + ), + ( + value: download.cachedTiles + download.prunedTiles, + color: Colors.yellow, + child: Text( + '${download.prunedTiles}', + style: const TextStyle(color: Colors.black), + ) + ), + ( + value: download.cachedTiles, + color: Colors.green[300]!, + child: Text( + '${download.bufferedTiles}', + style: const TextStyle(color: Colors.black), + ) + ), + ( + value: download.cachedTiles - download.bufferedTiles, + color: Colors.green, + child: Text( + '${download.cachedTiles - download.bufferedTiles}', + style: const TextStyle(color: Colors.white), + ) + ), + ], + ), + const SizedBox(height: 32), + Expanded( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RotatedBox( + quarterTurns: 3, + child: Text( + 'FAILED TILES', + style: GoogleFonts.ubuntu( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + ), + Expanded( + child: RepaintBoundary( + child: Consumer( + builder: (context, provider, _) => provider + .failedTiles.isEmpty + ? const Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.task_alt, size: 48), + SizedBox(height: 10), + Text('Any failed tiles will appear here'), + ], + ) + : ListView.builder( + reverse: true, + addRepaintBoundaries: false, + itemCount: provider.failedTiles.length, + itemBuilder: (context, index) => ListTile( + leading: Icon( + switch (provider.failedTiles[index].result) { + TileEventResult.noConnectionDuringFetch => + Icons.wifi_off, + TileEventResult.unknownFetchException => + Icons.error, + TileEventResult.negativeFetchResponse => + Icons.reply, + _ => Icons.abc, + }, + ), + title: Text(provider.failedTiles[index].url), + subtitle: Text( + switch (provider.failedTiles[index].result) { + TileEventResult.noConnectionDuringFetch => + 'Failed to establish a connection to the network. Check your Internet connection!', + TileEventResult.unknownFetchException => + 'There was an unknown error when trying to download this tile, of type ${provider.failedTiles[index].fetchError.runtimeType}', + TileEventResult.negativeFetchResponse => + 'The tile server responded with an HTTP status code of ${provider.failedTiles[index].fetchResponse!.statusCode} (${provider.failedTiles[index].fetchResponse!.reasonPhrase})', + _ => '', + }, + ), + ), + ), + ), + ), + ), + ], + ), + ), + /*Stack( + children: [ + LinearProgressIndicator( + value: data.percentageProgress / 100, + minHeight: 12, + backgroundColor: Colors.grey[300], + valueColor: AlwaysStoppedAnimation( + Theme.of(context).colorScheme.primary.withOpacity(0.5), + ), + ), + LinearProgressIndicator( + value: data.persistedTiles / data.maxTiles, + minHeight: 12, + backgroundColor: Colors.transparent, + valueColor: AlwaysStoppedAnimation( + Theme.of(context).colorScheme.primary, + ), + ), + ], + ), + const SizedBox(height: 30), + Expanded( + child: data.failedTiles.isEmpty + ? const Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.report_off, size: 36), + SizedBox(height: 10), + Text('No Failed Tiles'), + ], + ) + : Row( + children: [ + const SizedBox(width: 30), + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.warning, + size: 36, + ), + const SizedBox(height: 10), + StatDisplay( + statistic: data.failedTiles.length.toString(), + description: 'failed tiles', + ), + ], + ), + const SizedBox(width: 30), + Expanded( + child: ListView.builder( + itemCount: data.failedTiles.length, + itemBuilder: (context, index) => ListTile( + title: Text( + data.failedTiles[index], + textAlign: TextAlign.end, + ), + ), + ), + ), + ], + ), + ),*/ + ], + ); +} diff --git a/example/lib/screens/main/pages/downloading/components/header.dart b/example/lib/screens/main/pages/downloading/components/header.dart deleted file mode 100644 index 64d35a98..00000000 --- a/example/lib/screens/main/pages/downloading/components/header.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; -import 'package:google_fonts/google_fonts.dart'; -import 'package:provider/provider.dart'; - -import '../../../../../shared/state/download_provider.dart'; - -class Header extends StatefulWidget { - const Header({ - super.key, - }); - - @override - State

createState() => _HeaderState(); -} - -class _HeaderState extends State
{ - bool cancelled = false; - - @override - Widget build(BuildContext context) => Consumer( - builder: (context, provider, _) => Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Downloading', - style: GoogleFonts.openSans( - fontWeight: FontWeight.bold, - fontSize: 24, - ), - ), - Text( - 'Downloading To: ${provider.selectedStore?.storeName ?? ''}', - overflow: TextOverflow.fade, - softWrap: false, - ), - ], - ), - ), - const SizedBox(width: 15), - IconButton( - icon: const Icon(Icons.cancel), - tooltip: 'Cancel Download', - onPressed: cancelled - ? null - : () async { - await FMTC - .instance(provider.selectedStore!.storeName) - .download - .cancel(); - setState(() => cancelled = true); - }, - ), - ], - ), - ); -} diff --git a/example/lib/screens/main/pages/downloading/components/horizontal_layout.dart b/example/lib/screens/main/pages/downloading/components/horizontal_layout.dart deleted file mode 100644 index 3b313826..00000000 --- a/example/lib/screens/main/pages/downloading/components/horizontal_layout.dart +++ /dev/null @@ -1,159 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; - -import '../../../../../shared/vars/size_formatter.dart'; -import 'stat_display.dart'; -import 'tile_image.dart'; - -class HorizontalLayout extends StatelessWidget { - const HorizontalLayout({ - super.key, - required this.data, - }); - - final DownloadProgress data; - - @override - Widget build(BuildContext context) => Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - tileImage(data: data), - const SizedBox(width: 15), - Column( - children: [ - StatDisplay( - statistic: data.bufferMode == DownloadBufferMode.disabled - ? data.successfulTiles.toString() - : '${data.successfulTiles} (${data.successfulTiles - data.persistedTiles})', - description: 'successful tiles', - ), - const SizedBox(height: 5), - StatDisplay( - statistic: data.bufferMode == DownloadBufferMode.disabled - ? (data.successfulSize * 1024).asReadableSize - : '${(data.successfulSize * 1024).asReadableSize} (${((data.successfulSize - data.persistedSize) * 1024).asReadableSize})', - description: 'successful size', - ), - const SizedBox(height: 5), - StatDisplay( - statistic: data.maxTiles.toString(), - description: 'total tiles', - ), - ], - ), - const SizedBox(width: 30), - Column( - mainAxisSize: MainAxisSize.min, - children: [ - StatDisplay( - statistic: data.averageTPS.toStringAsFixed(2), - description: 'average tps', - ), - const SizedBox(height: 5), - StatDisplay( - statistic: data.duration - .toString() - .split('.') - .first - .padLeft(8, '0'), - description: 'duration taken', - ), - const SizedBox(height: 5), - StatDisplay( - statistic: data.estRemainingDuration - .toString() - .split('.') - .first - .padLeft(8, '0'), - description: 'est remaining duration', - ), - ], - ), - const SizedBox(width: 30), - Column( - mainAxisSize: MainAxisSize.min, - children: [ - StatDisplay( - statistic: - '${data.existingTiles} (${data.existingTilesDiscount.ceil()}%)', - description: 'existing tiles', - ), - const SizedBox(height: 5), - StatDisplay( - statistic: - '${data.seaTiles} (${data.seaTilesDiscount.ceil()}%)', - description: 'sea tiles', - ), - ], - ), - ], - ), - const SizedBox(height: 30), - Stack( - children: [ - LinearProgressIndicator( - value: data.percentageProgress / 100, - minHeight: 12, - backgroundColor: Colors.grey[300], - valueColor: AlwaysStoppedAnimation( - Theme.of(context).colorScheme.primary.withOpacity(0.5), - ), - ), - LinearProgressIndicator( - value: data.persistedTiles / data.maxTiles, - minHeight: 12, - backgroundColor: Colors.transparent, - valueColor: AlwaysStoppedAnimation( - Theme.of(context).colorScheme.primary, - ), - ), - ], - ), - const SizedBox(height: 30), - Expanded( - child: data.failedTiles.isEmpty - ? const Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.report_off, size: 36), - SizedBox(height: 10), - Text('No Failed Tiles'), - ], - ) - : Row( - children: [ - const SizedBox(width: 30), - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon( - Icons.warning, - size: 36, - ), - const SizedBox(height: 10), - StatDisplay( - statistic: data.failedTiles.length.toString(), - description: 'failed tiles', - ), - ], - ), - const SizedBox(width: 30), - Expanded( - child: ListView.builder( - itemCount: data.failedTiles.length, - itemBuilder: (context, index) => ListTile( - title: Text( - data.failedTiles[index], - textAlign: TextAlign.end, - ), - ), - ), - ), - ], - ), - ), - ], - ); -} diff --git a/example/lib/screens/main/pages/downloading/components/multi_linear_progress_indicator.dart b/example/lib/screens/main/pages/downloading/components/multi_linear_progress_indicator.dart new file mode 100644 index 00000000..8fe16e63 --- /dev/null +++ b/example/lib/screens/main/pages/downloading/components/multi_linear_progress_indicator.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; + +typedef IndividualProgress = ({num value, Color color, Widget? child}); + +class MulitLinearProgressIndicator extends StatefulWidget { + const MulitLinearProgressIndicator({ + super.key, + required this.progresses, + this.maxValue = 1, + this.backgroundChild, + this.height = 24, + this.radius, + this.childAlignment = Alignment.centerRight, + this.animationDuration = const Duration(milliseconds: 500), + }); + + final List progresses; + final num maxValue; + final Widget? backgroundChild; + final double height; + final BorderRadiusGeometry? radius; + final AlignmentGeometry childAlignment; + final Duration animationDuration; + + @override + State createState() => + _MulitLinearProgressIndicatorState(); +} + +class _MulitLinearProgressIndicatorState + extends State { + @override + Widget build(BuildContext context) => RepaintBoundary( + child: LayoutBuilder( + builder: (context, constraints) => ClipRRect( + borderRadius: + widget.radius ?? BorderRadius.circular(widget.height / 2), + child: SizedBox( + height: widget.height, + width: constraints.maxWidth, + child: Stack( + children: [ + Positioned.fill( + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background, + borderRadius: widget.radius ?? + BorderRadius.circular(widget.height / 2), + border: Border.all( + color: Theme.of(context).colorScheme.onBackground, + ), + ), + padding: EdgeInsets.symmetric( + vertical: 2, + horizontal: widget.height / 2, + ), + alignment: widget.childAlignment, + child: widget.backgroundChild, + ), + ), + ...widget.progresses.map( + (e) => AnimatedPositioned( + height: widget.height, + left: 0, + width: (constraints.maxWidth / widget.maxValue) * e.value, + duration: widget.animationDuration, + child: Container( + decoration: BoxDecoration( + color: e.color, + borderRadius: widget.radius ?? + BorderRadius.circular(widget.height / 2), + ), + padding: EdgeInsets.symmetric( + vertical: 2, + horizontal: widget.height / 2, + ), + alignment: widget.childAlignment, + child: e.child, + ), + ), + ), + ], + ), + ), + ), + ), + ); +} diff --git a/example/lib/screens/main/pages/downloading/components/stat_display.dart b/example/lib/screens/main/pages/downloading/components/stat_display.dart index 998f8263..6ce752c0 100644 --- a/example/lib/screens/main/pages/downloading/components/stat_display.dart +++ b/example/lib/screens/main/pages/downloading/components/stat_display.dart @@ -11,19 +11,21 @@ class StatDisplay extends StatelessWidget { final String description; @override - Widget build(BuildContext context) => Column( - children: [ - Text( - statistic, - style: const TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, + Widget build(BuildContext context) => RepaintBoundary( + child: Column( + children: [ + Text( + statistic, + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + ), ), - ), - Text( - description, - style: const TextStyle(fontSize: 16), - ), - ], + Text( + description, + style: const TextStyle(fontSize: 16), + ), + ], + ), ); } diff --git a/example/lib/screens/main/pages/downloading/components/tile_image.dart b/example/lib/screens/main/pages/downloading/components/tile_image.dart deleted file mode 100644 index 13197b23..00000000 --- a/example/lib/screens/main/pages/downloading/components/tile_image.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; - -Widget tileImage({ - required DownloadProgress data, - double tileImageSize = 256 / 1.25, -}) => - data.tileImage != null - ? Stack( - alignment: Alignment.center, - children: [ - Container( - foregroundDecoration: BoxDecoration( - color: data.percentageProgress != 100 - ? null - : Colors.white.withOpacity(0.75), - ), - child: Image( - image: data.tileImage!, - height: tileImageSize, - width: tileImageSize, - gaplessPlayback: true, - ), - ), - if (data.percentageProgress == 100) - const Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.done_all, - size: 36, - color: Colors.green, - ), - SizedBox(height: 10), - Text('Download Complete'), - ], - ), - ], - ) - : SizedBox( - height: tileImageSize, - width: tileImageSize, - child: const Center( - child: CircularProgressIndicator(), - ), - ); diff --git a/example/lib/screens/main/pages/downloading/components/vertical_layout.dart b/example/lib/screens/main/pages/downloading/components/vertical_layout.dart deleted file mode 100644 index a7cbd559..00000000 --- a/example/lib/screens/main/pages/downloading/components/vertical_layout.dart +++ /dev/null @@ -1,128 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; - -import '../../../../../shared/vars/size_formatter.dart'; -import 'stat_display.dart'; - -class VerticalLayout extends StatelessWidget { - const VerticalLayout({ - super.key, - required this.data, - }); - - final DownloadProgress data; - - @override - Widget build(BuildContext context) => Column( - children: [ - Column( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - StatDisplay( - statistic: - '${data.successfulTiles} / ${data.maxTiles} (${data.averageTPS.round()} avg tps)', - description: 'successful / total tiles', - ), - const SizedBox(height: 2), - StatDisplay( - statistic: (data.successfulSize * 1024).asReadableSize, - description: 'downloaded size', - ), - const SizedBox(height: 2), - StatDisplay( - statistic: - data.duration.toString().split('.').first.padLeft(8, '0'), - description: 'duration taken', - ), - const SizedBox(height: 2), - StatDisplay( - statistic: data.estRemainingDuration - .toString() - .split('.') - .first - .padLeft(8, '0'), - description: 'est remaining duration', - ), - const SizedBox(height: 2), - StatDisplay( - statistic: data.estTotalDuration - .toString() - .split('.') - .first - .padLeft(8, '0'), - description: 'est total duration', - ), - const SizedBox(height: 2), - StatDisplay( - statistic: - '${data.existingTiles} (${data.existingTilesDiscount.ceil()}%) | ${data.seaTiles} (${data.seaTilesDiscount.ceil()}%)', - description: 'existing tiles | sea tiles', - ), - ], - ), - const SizedBox(height: 15), - Stack( - children: [ - LinearProgressIndicator( - value: data.percentageProgress / 100, - minHeight: 8, - backgroundColor: Colors.grey[300], - valueColor: AlwaysStoppedAnimation( - Theme.of(context).colorScheme.primary.withOpacity(0.5), - ), - ), - LinearProgressIndicator( - value: data.persistedTiles / data.maxTiles, - minHeight: 8, - backgroundColor: Colors.transparent, - valueColor: AlwaysStoppedAnimation( - Theme.of(context).colorScheme.primary, - ), - ), - ], - ), - const SizedBox(height: 15), - Expanded( - child: data.failedTiles.isEmpty - ? const Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.report_off, size: 36), - SizedBox(height: 10), - Text('No Failed Tiles'), - ], - ) - : Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - data.failedTiles.length.toString(), - style: const TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(width: 10), - const Text( - 'failed tiles', - style: TextStyle(fontSize: 16), - ), - ], - ), - const SizedBox(height: 15), - Expanded( - child: ListView.builder( - itemCount: data.failedTiles.length, - itemBuilder: (context, index) => ListTile( - title: Text(data.failedTiles[index]), - ), - ), - ), - ], - ), - ), - ], - ); -} diff --git a/example/lib/screens/main/pages/downloading/downloading.dart b/example/lib/screens/main/pages/downloading/downloading.dart index 7a2c668e..cc194779 100644 --- a/example/lib/screens/main/pages/downloading/downloading.dart +++ b/example/lib/screens/main/pages/downloading/downloading.dart @@ -1,11 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; +import 'package:google_fonts/google_fonts.dart'; import 'package:provider/provider.dart'; import '../../../../shared/state/download_provider.dart'; -import 'components/header.dart'; -import 'components/horizontal_layout.dart'; -import 'components/vertical_layout.dart'; +import 'components/download_layout.dart'; class DownloadingPage extends StatefulWidget { const DownloadingPage({super.key}); @@ -14,50 +13,86 @@ class DownloadingPage extends StatefulWidget { State createState() => _DownloadingPageState(); } -class _DownloadingPageState extends State { +class _DownloadingPageState extends State + with AutomaticKeepAliveClientMixin { @override - Widget build(BuildContext context) => Scaffold( - body: SafeArea( - child: Padding( - padding: const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Header(), - const SizedBox(height: 12), - Expanded( - child: Padding( - padding: const EdgeInsets.all(6), - child: Consumer( - builder: (context, provider, _) => - StreamBuilder( - stream: provider.downloadProgress, - initialData: DownloadProgress.empty(), - builder: (context, snapshot) { - if (snapshot.connectionState == - ConnectionState.done) { - WidgetsBinding.instance.addPostFrameCallback( - (_) => provider.setDownloadProgress( - null, - notify: false, - ), - ); - } - - return LayoutBuilder( - builder: (context, constraints) => - constraints.maxWidth > 725 - ? HorizontalLayout(data: snapshot.data!) - : VerticalLayout(data: snapshot.data!), - ); - }, + Widget build(BuildContext context) { + super.build(context); + return Scaffold( + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Consumer( + builder: (context, provider, _) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Downloading', + style: GoogleFonts.openSans( + fontWeight: FontWeight.bold, + fontSize: 24, ), ), + Text( + 'Downloading To: ${provider.selectedStore!.storeName}', + overflow: TextOverflow.fade, + softWrap: false, + ), + ], + ), + ), + const SizedBox(height: 12), + Expanded( + child: Padding( + padding: const EdgeInsets.all(6), + child: Consumer( + builder: (context, provider, _) => + StreamBuilder( + stream: provider.downloadProgress, + builder: (context, snapshot) { + if (snapshot.data == null) { + return const Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + CircularProgressIndicator(), + SizedBox(height: 16), + Text( + 'Taking a while?', + style: TextStyle(fontWeight: FontWeight.bold), + ), + Text( + 'Please wait for the download to start...', + ), + ], + ), + ); + } + + if (snapshot.data!.lastTileEvent.result.category == + TileEventResultCategory.failed) { + provider.addFailedTile(snapshot.data!.lastTileEvent); + } + + return DownloadLayout( + storeDirectory: provider.selectedStore!, + download: snapshot.data!, + ); + }, + ), ), ), - ], - ), + ), + ], ), ), - ); + ), + ); + } + + @override + bool get wantKeepAlive => true; } diff --git a/example/lib/screens/main/pages/recovery/components/recovery_start_button.dart b/example/lib/screens/main/pages/recovery/components/recovery_start_button.dart index 498473da..235f875f 100644 --- a/example/lib/screens/main/pages/recovery/components/recovery_start_button.dart +++ b/example/lib/screens/main/pages/recovery/components/recovery_start_button.dart @@ -45,8 +45,6 @@ class RecoveryStartButton extends StatelessWidget { ) ..minZoom = region.minZoom ..maxZoom = region.maxZoom - ..preventRedownload = region.preventRedownload - ..seaTileRemoval = region.seaTileRemoval ..setSelectedStore( FMTC.instance(region.storeName), ) diff --git a/example/lib/shared/state/download_provider.dart b/example/lib/shared/state/download_provider.dart index 8179559f..b22f695e 100644 --- a/example/lib/shared/state/download_provider.dart +++ b/example/lib/shared/state/download_provider.dart @@ -100,18 +100,14 @@ class DownloadProvider extends ChangeNotifier { notifyListeners(); } - DownloadBufferMode _bufferMode = DownloadBufferMode.tiles; - DownloadBufferMode get bufferMode => _bufferMode; - set bufferMode(DownloadBufferMode newMode) { - _bufferMode = newMode; - _bufferingAmount = newMode == DownloadBufferMode.tiles ? 500 : 5000; - notifyListeners(); - } - int _bufferingAmount = 500; int get bufferingAmount => _bufferingAmount; set bufferingAmount(int newNum) { _bufferingAmount = newNum; notifyListeners(); } + + final List _failedTiles = []; + List get failedTiles => _failedTiles; + void addFailedTile(TileEvent e) => _failedTiles.add(e); } diff --git a/lib/flutter_map_tile_caching.dart b/lib/flutter_map_tile_caching.dart index 1fff1c75..fd091afb 100644 --- a/lib/flutter_map_tile_caching.dart +++ b/lib/flutter_map_tile_caching.dart @@ -24,9 +24,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/plugin_api.dart'; -import 'package:http/http.dart'; +import 'package:http/http.dart' as http; import 'package:http/io_client.dart'; -import 'package:http_plus/http_plus.dart'; import 'package:isar/isar.dart'; import 'package:latlong2/latlong.dart'; import 'package:meta/meta.dart'; @@ -36,7 +35,7 @@ import 'package:stream_transform/stream_transform.dart'; import 'package:watcher/watcher.dart'; import 'src/bulk_download/instance.dart'; -import 'src/bulk_download/manager.dart'; + import 'src/bulk_download/tile_loops/shared.dart'; import 'src/db/defs/metadata.dart'; import 'src/db/defs/recovery.dart'; @@ -48,6 +47,7 @@ import 'src/errors/browsing.dart'; import 'src/errors/initialisation.dart'; import 'src/errors/store_not_ready.dart'; import 'src/misc/exts.dart'; +import 'src/misc/int_extremes.dart'; import 'src/misc/typedefs.dart'; import 'src/providers/image_provider.dart'; @@ -58,6 +58,8 @@ export 'src/errors/store_not_ready.dart'; export 'src/misc/typedefs.dart'; part 'src/bulk_download/download_progress.dart'; +part 'src/bulk_download/manager.dart'; +part 'src/bulk_download/thread.dart'; part 'src/bulk_download/tile_event.dart'; part 'src/fmtc.dart'; part 'src/misc/store_db_impl.dart'; diff --git a/lib/src/bulk_download/download_progress.dart b/lib/src/bulk_download/download_progress.dart index 729f5298..c256fdb9 100644 --- a/lib/src/bulk_download/download_progress.dart +++ b/lib/src/bulk_download/download_progress.dart @@ -5,82 +5,148 @@ part of flutter_map_tile_caching; @immutable class DownloadProgress { - final TileEvent? lastTileEvent; + /// The result of the last attempted tile + /// + /// May be used for UI display, error handling, or debugging purposes. + TileEvent get lastTileEvent => _lastTileEvent!; + final TileEvent? _lastTileEvent; + /// The number of new tiles successfully downloaded and cached, that is those + /// tiles with the result of [TileEventResult.success] + /// ([TileEventResultCategory.cached]) final int cachedTiles; + final double cachedSize; + final int bufferedTiles; + final double bufferedSize; final int prunedTiles; + final double prunedSize; final int failedTiles; - final int maxTiles; + final Duration duration; + final bool hasFinished; int get successfulTiles => cachedTiles + prunedTiles; + double get successfulSize => cachedSize + prunedSize; int get attemptedTiles => successfulTiles + failedTiles; int get remainingTiles => maxTiles - attemptedTiles; - final Duration duration; - double get percentageProgress => (attemptedTiles / maxTiles) * 100; - bool get isFinished => attemptedTiles == maxTiles; - const DownloadProgress._({ - required this.lastTileEvent, + const DownloadProgress.__({ + required TileEvent? lastTileEvent, required this.cachedTiles, + required this.cachedSize, + required this.bufferedTiles, + required this.bufferedSize, required this.prunedTiles, + required this.prunedSize, required this.failedTiles, required this.maxTiles, required this.duration, - }); + required this.hasFinished, + }) : _lastTileEvent = lastTileEvent; - factory DownloadProgress.initial({required int maxTiles}) => - DownloadProgress._( + factory DownloadProgress._initial({required int maxTiles}) => + DownloadProgress.__( lastTileEvent: null, cachedTiles: 0, + cachedSize: 0, + bufferedTiles: 0, + bufferedSize: 0, prunedTiles: 0, + prunedSize: 0, failedTiles: 0, maxTiles: maxTiles, duration: Duration.zero, + hasFinished: false, + ); + + DownloadProgress _updateDuration( + Duration newDuration, + ) => + DownloadProgress.__( + lastTileEvent: lastTileEvent, + cachedTiles: cachedTiles, + cachedSize: cachedSize, + bufferedTiles: bufferedTiles, + bufferedSize: bufferedSize, + prunedTiles: prunedTiles, + prunedSize: prunedSize, + failedTiles: failedTiles, + maxTiles: maxTiles, + duration: newDuration, + hasFinished: false, ); - DownloadProgress update({ - TileEvent? newTileEvent, + DownloadProgress _update({ + required TileEvent? newTileEvent, + required int newBufferedTiles, + required double newBufferedSize, required Duration newDuration, + bool hasFinished = false, }) => - DownloadProgress._( + DownloadProgress.__( lastTileEvent: newTileEvent ?? lastTileEvent, - cachedTiles: cachedTiles + - (newTileEvent?.result.category == TileEventResultCategory.cached - ? 1 - : 0), - prunedTiles: prunedTiles + - (newTileEvent?.result.category == TileEventResultCategory.pruned - ? 1 - : 0), - failedTiles: failedTiles + - (newTileEvent?.result.category == TileEventResultCategory.failed - ? 1 - : 0), + cachedTiles: newTileEvent == null + ? cachedTiles + : newTileEvent.result.category == TileEventResultCategory.cached + ? cachedTiles + 1 + : cachedTiles, + cachedSize: newTileEvent == null + ? cachedSize + : newTileEvent.result.category == TileEventResultCategory.cached + ? cachedSize + (newTileEvent.tileImage!.lengthInBytes / 1024) + : cachedSize, + bufferedTiles: newBufferedTiles, + bufferedSize: newBufferedSize, + prunedTiles: newTileEvent == null + ? prunedTiles + : newTileEvent.result.category == TileEventResultCategory.pruned + ? prunedTiles + 1 + : prunedTiles, + prunedSize: newTileEvent == null + ? prunedSize + : newTileEvent.result.category == TileEventResultCategory.pruned + ? prunedSize + (newTileEvent.tileImage!.lengthInBytes / 1024) + : prunedSize, + failedTiles: newTileEvent == null + ? failedTiles + : newTileEvent.result.category == TileEventResultCategory.failed + ? failedTiles + 1 + : failedTiles, maxTiles: maxTiles, duration: newDuration, + hasFinished: hasFinished, ); @override bool operator ==(Object other) => identical(this, other) || (other is DownloadProgress && - lastTileEvent == other.lastTileEvent && - successfulTiles == other.successfulTiles && + _lastTileEvent == other._lastTileEvent && + cachedTiles == other.cachedTiles && + cachedSize == other.cachedSize && + bufferedTiles == other.bufferedTiles && + bufferedSize == other.bufferedSize && prunedTiles == other.prunedTiles && + prunedSize == other.prunedSize && failedTiles == other.failedTiles && maxTiles == other.maxTiles && - duration == other.duration); + duration == other.duration && + hasFinished == other.hasFinished); @override int get hashCode => Object.hashAllUnordered([ - lastTileEvent.hashCode, - successfulTiles.hashCode, + _lastTileEvent.hashCode, + cachedTiles.hashCode, + cachedSize.hashCode, + bufferedTiles.hashCode, + bufferedSize.hashCode, prunedTiles.hashCode, + prunedSize.hashCode, failedTiles.hashCode, maxTiles.hashCode, duration.hashCode, + hasFinished.hashCode, ]); } diff --git a/lib/src/bulk_download/instance.dart b/lib/src/bulk_download/instance.dart index e6baeada..760540c3 100644 --- a/lib/src/bulk_download/instance.dart +++ b/lib/src/bulk_download/instance.dart @@ -16,5 +16,10 @@ class DownloadInstance { static DownloadInstance? get(Object id) => _instances[id]; final Object id; - Future Function()? cancelDownloadRequest; + + Future Function()? requestCancel; + + bool isPaused = false; + Future Function()? requestPause; + void Function()? requestResume; } diff --git a/lib/src/bulk_download/manager.dart b/lib/src/bulk_download/manager.dart index ee750721..ad04c9d6 100644 --- a/lib/src/bulk_download/manager.dart +++ b/lib/src/bulk_download/manager.dart @@ -1,23 +1,9 @@ // Copyright © Luka S (JaffaKetchup) under GPL-v3 // A full license can be found at .\LICENSE -import 'dart:async'; -import 'dart:isolate'; -import 'dart:typed_data'; - -import 'package:async/async.dart'; -import 'package:flutter_map/flutter_map.dart'; -import 'package:http/http.dart' as http; -import 'package:meta/meta.dart'; - -import '../../flutter_map_tile_caching.dart'; -import '../db/tools.dart'; -import '../misc/int_extremes.dart'; -import 'thread.dart'; -import 'tile_loops/shared.dart'; - -@internal -Future downloadManager( +part of flutter_map_tile_caching; + +Future _downloadManager( ({ SendPort sendPort, String rootDirectory, @@ -55,6 +41,16 @@ Future downloadManager( } } + // Setup thread buffer tracking + late final List<({double size, int tiles})> threadBuffers; + if (input.maxBufferLength != 0) { + threadBuffers = List.generate( + input.parallelThreads, + (_) => (tiles: 0, size: 0.0), + growable: false, + ); + } + // Setup tile generator isolate final tileRecievePort = ReceivePort(); final tileIsolate = await Isolate.spawn( @@ -81,133 +77,202 @@ Future downloadManager( void send(Object? m) => input.sendPort.send(m); send(rootRecievePort.sendPort); - // Setup cancel signal handling - final cancelSignal = Completer(); - unawaited( - rootRecievePort.firstWhere((e) => e == null).then((_) { - try { - cancelSignal.complete(); - // ignore: avoid_catching_errors, empty_catches - } on StateError {} - }), - ); - // Start progress tracking final downloadDuration = Stopwatch()..start(); - var lastDownloadProgress = DownloadProgress.initial(maxTiles: maxTiles); + var lastDownloadProgress = DownloadProgress._initial(maxTiles: maxTiles); + + // Setup cancel, pause, and resume handling + final threadPausedStates = List.generate( + input.parallelThreads, + (_) => Completer(), + growable: false, + ); + final cancelSignal = Completer(); + Completer pauseResumeSignal = Completer()..complete(); + rootRecievePort.listen( + (e) async { + if (e == null) { + try { + cancelSignal.complete(); + // ignore: avoid_catching_errors, empty_catches + } on StateError {} + } else if (e == 1) { + pauseResumeSignal = Completer(); + for (int i = 0; i < input.parallelThreads; i++) { + threadPausedStates[i] = Completer(); + } + await Future.wait(threadPausedStates.map((e) => e.future)); + downloadDuration.stop(); + send(1); + } else if (e == 2) { + pauseResumeSignal.complete(); + downloadDuration.start(); + } + }, + ); // Setup progress report fallback final Timer? fallbackReportTimer; if (input.maxReportInterval case final maxReportInterval?) { fallbackReportTimer = Timer.periodic( maxReportInterval, - (_) => send( - lastDownloadProgress = - lastDownloadProgress.update(newDuration: downloadDuration.elapsed), - ), + (_) { + if (lastDownloadProgress != + DownloadProgress._initial(maxTiles: maxTiles) && + pauseResumeSignal.isCompleted) { + send( + lastDownloadProgress = + lastDownloadProgress._updateDuration(downloadDuration.elapsed), + ); + } + }, ); } else { fallbackReportTimer = null; } - // Start download threads - final downloadThreads = List.generate( - input.parallelThreads, - (threadNo) async { - if (cancelSignal.isCompleted) return; - - // Start thread worker isolate & setup two-way communications - final downloadThreadRecievePort = ReceivePort(); - await Isolate.spawn( - singleDownloadThread, - ( - sendPort: downloadThreadRecievePort.sendPort, - storeId: - DatabaseTools.hash(input.tileProvider.storeDirectory.storeName) - .toString(), - rootDirectory: input.rootDirectory, - region: input.region, - tileProvider: input.tileProvider, - maxBufferLength: - (input.maxBufferLength / input.parallelThreads).ceil(), - pruneExistingTiles: input.pruneExistingTiles, - seaTileBytes: seaTileBytes, - ), - onExit: downloadThreadRecievePort.sendPort, - debugName: '[FMTC] Bulk Download Thread #$threadNo', - ); - late final SendPort sendPort; - final sendPortCompleter = Completer(); + // Start download threads & wait for download to complete/cancelled + await Future.wait( + List.generate( + input.parallelThreads, + (threadNo) async { + if (cancelSignal.isCompleted) return; - // Prevent completion of this function until the thread is shutdown - final threadKilled = Completer(); + // Start thread worker isolate & setup two-way communications + final downloadThreadRecievePort = ReceivePort(); + await Isolate.spawn( + _singleDownloadThread, + ( + sendPort: downloadThreadRecievePort.sendPort, + storeId: + DatabaseTools.hash(input.tileProvider.storeDirectory.storeName) + .toString(), + rootDirectory: input.rootDirectory, + region: input.region, + tileProvider: input.tileProvider, + maxBufferLength: + (input.maxBufferLength / input.parallelThreads).ceil(), + pruneExistingTiles: input.pruneExistingTiles, + seaTileBytes: seaTileBytes, + ), + onExit: downloadThreadRecievePort.sendPort, + debugName: '[FMTC] Bulk Download Thread #$threadNo', + ); + late final SendPort sendPort; + final sendPortCompleter = Completer(); - // When one thread is complete, or the manual cancel signal is sent, - // kill all threads - unawaited( - cancelSignal.future - .then((_) async => (await sendPortCompleter.future).send(null)), - ); + // Prevent completion of this function until the thread is shutdown + final threadKilled = Completer(); - downloadThreadRecievePort.listen( - (evt) async { - // Thread is sending tile data - if (evt is TileEvent) { - send( - lastDownloadProgress = lastDownloadProgress.update( - newTileEvent: evt, - newDuration: downloadDuration.elapsed, - ), - ); - return; - } - - // Thread is requesting new tile coords - if (evt is int) { - requestTilePort.send(null); - try { - sendPort.send(await tileQueue.next); - // ignore: avoid_catching_errors - } on StateError { - sendPort.send(null); + // When one thread is complete, or the manual cancel signal is sent, + // kill all threads + unawaited( + cancelSignal.future + .then((_) async => (await sendPortCompleter.future).send(null)), + ); + + downloadThreadRecievePort.listen( + (evt) async { + // Thread is sending tile data + if (evt is TileEvent) { + if (input.maxBufferLength != 0) { + if (evt.result == TileEventResult.success) { + threadBuffers[threadNo] = ( + tiles: evt._wasBufferReset + ? 0 + : threadBuffers[threadNo].tiles + 1, + size: evt._wasBufferReset + ? 0 + : threadBuffers[threadNo].size + + (evt.tileImage!.lengthInBytes / 1024) + ); + } + + send( + lastDownloadProgress = lastDownloadProgress._update( + newTileEvent: evt, + newBufferedTiles: threadBuffers + .map((e) => e.tiles) + .reduce((a, b) => a + b), + newBufferedSize: threadBuffers + .map((e) => e.size) + .reduce((a, b) => a + b), + newDuration: downloadDuration.elapsed, + ), + ); + } else { + send( + lastDownloadProgress = lastDownloadProgress._update( + newTileEvent: evt, + newBufferedTiles: 0, + newBufferedSize: 0, + newDuration: downloadDuration.elapsed, + ), + ); + } + return; } - return; - } - - // Thread is establishing comms - if (evt is SendPort) { - sendPortCompleter.complete(evt); - sendPort = evt; - return; - } - - // Thread ended, goto `onDone` - if (evt == null) return downloadThreadRecievePort.close(); - }, - onDone: () { - try { - cancelSignal.complete(); - // ignore: avoid_catching_errors, empty_catches - } on StateError {} - - threadKilled.complete(); - }, - ); - // Prevent completion of this function until the thread is shutdown - await threadKilled.future; - }, - growable: false, + // Thread is requesting new tile coords + if (evt is int) { + if (!pauseResumeSignal.isCompleted) { + threadPausedStates[threadNo].complete(); + await pauseResumeSignal.future; + } + + requestTilePort.send(null); + try { + sendPort.send(await tileQueue.next); + // ignore: avoid_catching_errors + } on StateError { + sendPort.send(null); + } + return; + } + + // Thread is establishing comms + if (evt is SendPort) { + sendPortCompleter.complete(evt); + sendPort = evt; + return; + } + + // Thread ended, goto `onDone` + if (evt == null) return downloadThreadRecievePort.close(); + }, + onDone: () { + try { + cancelSignal.complete(); + // ignore: avoid_catching_errors, empty_catches + } on StateError {} + + threadKilled.complete(); + }, + ); + + // Prevent completion of this function until the thread is shutdown + await threadKilled.future; + }, + growable: false, + ), ); - // Wait for download to complete/be fully cancelled - await Future.wait(downloadThreads); + // Send final buffer cleared progress report + fallbackReportTimer?.cancel(); + send( + lastDownloadProgress = lastDownloadProgress._update( + newTileEvent: null, + newBufferedTiles: 0, + newBufferedSize: 0, + newDuration: downloadDuration.elapsed, + hasFinished: true, + ), + ); // Cleanup resources and shutdown - fallbackReportTimer?.cancel(); downloadDuration.stop(); - await tileQueue.cancel(immediate: true); - tileIsolate.kill(priority: Isolate.immediate); rootRecievePort.close(); + tileIsolate.kill(priority: Isolate.immediate); + await tileQueue.cancel(immediate: true); Isolate.exit(); } diff --git a/lib/src/bulk_download/thread.dart b/lib/src/bulk_download/thread.dart index 5e1d1199..f57742bf 100644 --- a/lib/src/bulk_download/thread.dart +++ b/lib/src/bulk_download/thread.dart @@ -1,26 +1,9 @@ // Copyright © Luka S (JaffaKetchup) under GPL-v3 // A full license can be found at .\LICENSE -import 'dart:async'; -import 'dart:io'; -import 'dart:isolate'; -import 'dart:typed_data'; - -import 'package:async/async.dart'; -import 'package:collection/collection.dart'; -import 'package:flutter_map/flutter_map.dart'; -import 'package:http/http.dart'; -import 'package:isar/isar.dart'; -import 'package:meta/meta.dart'; - -import '../../flutter_map_tile_caching.dart'; -import '../db/defs/metadata.dart'; -import '../db/defs/store_descriptor.dart'; -import '../db/defs/tile.dart'; -import '../db/tools.dart'; - -@internal -Future singleDownloadThread( +part of flutter_map_tile_caching; + +Future _singleDownloadThread( ({ SendPort sendPort, String storeId, @@ -48,17 +31,13 @@ Future singleDownloadThread( inspector: false, ); - final httpClient = Client(); + // Initialise a long lasting HTTP client + final httpClient = http.Client(); + // Initialise the tile buffer array final tileBuffer = []; while (true) { - // Write tile buffer if necessary - if (tileBuffer.length > input.maxBufferLength) { - db.writeTxnSync(() => db.tiles.putAllSync(tileBuffer)); - tileBuffer.clear(); - } - // Request new tile coords send(0); final coords = (await tileQueue.next) as (int, int, int)?; @@ -78,15 +57,17 @@ Future singleDownloadThread( Isolate.exit(); } + // Get new tile URL & any existing tile final url = input.tileProvider.getTileUrl( TileCoordinates(coords.$1, coords.$2, coords.$3), input.region.options, ); final existingTile = await db.tiles.get(DatabaseTools.hash(url)); + // Skip if tile already exists and user demands existing tile pruning if (input.pruneExistingTiles && existingTile != null) { send( - TileEvent( + TileEvent._( TileEventResult.alreadyExisting, url: url, tileImage: Uint8List.fromList(existingTile.bytes), @@ -95,7 +76,8 @@ Future singleDownloadThread( continue; } - final Response response; + // Fetch new tile from URL + final http.Response response; try { response = await httpClient.get( Uri.parse(url), @@ -103,7 +85,7 @@ Future singleDownloadThread( ); } catch (e) { send( - TileEvent( + TileEvent._( e is SocketException ? TileEventResult.noConnectionDuringFetch : TileEventResult.unknownFetchException, @@ -116,7 +98,7 @@ Future singleDownloadThread( if (response.statusCode != 200) { send( - TileEvent( + TileEvent._( TileEventResult.negativeFetchResponse, url: url, fetchResponse: response, @@ -125,9 +107,10 @@ Future singleDownloadThread( continue; } + // Skip if tile is a sea tile & user demands sea tile pruning if (const ListEquality().equals(response.bodyBytes, input.seaTileBytes)) { send( - TileEvent( + TileEvent._( TileEventResult.isSeaTile, url: url, tileImage: response.bodyBytes, @@ -137,6 +120,7 @@ Future singleDownloadThread( continue; } + // Write tile directly to database or place in buffer queue final tile = DbTile(url: url, bytes: response.bodyBytes); if (input.maxBufferLength == 0) { db.writeTxnSync(() => db.tiles.putSync(tile)); @@ -144,12 +128,21 @@ Future singleDownloadThread( tileBuffer.add(tile); } + // Write buffer to database if necessary + final wasBufferReset = tileBuffer.length >= input.maxBufferLength; + if (wasBufferReset) { + db.writeTxnSync(() => db.tiles.putAllSync(tileBuffer)); + tileBuffer.clear(); + } + + // Return successful response to user send( - TileEvent( + TileEvent._( TileEventResult.success, url: url, tileImage: response.bodyBytes, fetchResponse: response, + wasBufferReset: wasBufferReset, ), ); } diff --git a/lib/src/bulk_download/tile_event.dart b/lib/src/bulk_download/tile_event.dart index df800a15..519c43d8 100644 --- a/lib/src/bulk_download/tile_event.dart +++ b/lib/src/bulk_download/tile_event.dart @@ -83,12 +83,12 @@ class TileEvent { /// Not available if the result category is [TileEventResultCategory.failed]. final Uint8List? tileImage; - /// The raw [Response] from the [url], if available + /// The raw [http.Response] from the [url], if available /// /// Not available if [result] is [TileEventResult.noConnectionDuringFetch], /// [TileEventResult.unknownFetchException], or /// [TileEventResult.alreadyExisting]. - final Response? fetchResponse; + final http.Response? fetchResponse; /// The raw error thrown when fetching from the [url], if available /// @@ -96,16 +96,35 @@ class TileEvent { /// [TileEventResult.unknownFetchException]. final Object? fetchError; - /// The raw result of a tile download during bulk downloading - /// - /// Does not contain information about the download as a whole, that is - /// [DownloadProgress]' responsibility. - @internal - TileEvent( + final bool _wasBufferReset; + + TileEvent._( this.result, { required this.url, this.tileImage, this.fetchResponse, this.fetchError, - }); + bool wasBufferReset = false, + }) : _wasBufferReset = wasBufferReset; + + @override + bool operator ==(Object other) => + identical(this, other) || + (other is TileEvent && + result == other.result && + url == other.url && + tileImage == other.tileImage && + fetchResponse == other.fetchResponse && + fetchError == other.fetchError && + _wasBufferReset == other._wasBufferReset); + + @override + int get hashCode => Object.hashAllUnordered([ + result.hashCode, + url.hashCode, + tileImage.hashCode, + fetchResponse.hashCode, + fetchError.hashCode, + _wasBufferReset.hashCode, + ]); } diff --git a/lib/src/misc/int_extremes.dart b/lib/src/misc/int_extremes.dart index 7af86ba4..51587c62 100644 --- a/lib/src/misc/int_extremes.dart +++ b/lib/src/misc/int_extremes.dart @@ -1,2 +1,5 @@ +// Copyright © Luka S (JaffaKetchup) under GPL-v3 +// A full license can be found at .\LICENSE + const largestInt = 9223372036854775807; const smallestInt = -9223372036854775808; diff --git a/lib/src/providers/tile_provider.dart b/lib/src/providers/tile_provider.dart index 59237713..0a9f4bc3 100644 --- a/lib/src/providers/tile_provider.dart +++ b/lib/src/providers/tile_provider.dart @@ -16,29 +16,19 @@ class FMTCTileProvider extends TileProvider { /// [FlutterMapTileCaching]. final FMTCTileProviderSettings settings; - /// [Client] (such as a [IOClient]) used to make all network requests + /// [http.Client] (such as a [IOClient]) used to make all network requests /// - /// Defaults to a [HttpPlusClient] which supports HTTP/2 and falls back to a - /// standard [IOClient]/[HttpClient] for HTTP/1.1 servers. Timeout is set to - /// 5 seconds by default. - final Client httpClient; + /// Defaults to a standard [IOClient]/[HttpClient] for HTTP/1.1 servers. + final http.Client httpClient; FMTCTileProvider._({ required this.storeDirectory, required FMTCTileProviderSettings? settings, Map headers = const {}, - Client? httpClient, + http.Client? httpClient, }) : settings = settings ?? FMTC.instance.settings.defaultTileProviderSettings, - httpClient = httpClient ?? - HttpPlusClient( - http1Client: IOClient( - HttpClient() - ..connectionTimeout = const Duration(seconds: 5) - ..userAgent = null, - ), - connectionTimeout: const Duration(seconds: 5), - ), + httpClient = httpClient ?? IOClient(HttpClient()..userAgent = null), super( headers: { ...headers, diff --git a/lib/src/store/directory.dart b/lib/src/store/directory.dart index 24c814b0..a3b73d05 100644 --- a/lib/src/store/directory.dart +++ b/lib/src/store/directory.dart @@ -57,7 +57,7 @@ class StoreDirectory { FMTCTileProvider getTileProvider({ FMTCTileProviderSettings? settings, Map? headers, - Client? httpClient, + http.Client? httpClient, }) => FMTCTileProvider._( storeDirectory: this, diff --git a/lib/src/store/download.dart b/lib/src/store/download.dart index e83368e2..e2ae4652 100644 --- a/lib/src/store/download.dart +++ b/lib/src/store/download.dart @@ -33,8 +33,8 @@ class DownloadManagement { /// /// 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]. This must be listened to, otherwise - /// the download will not start. + /// of no longer than [maxReportInterval] (after the first tile). This must be + /// listened to, otherwise the download will not start. /// /// --- /// @@ -130,7 +130,7 @@ class DownloadManagement { // Start download thread final recievePort = ReceivePort(); await Isolate.spawn( - downloadManager, + _downloadManager, ( sendPort: recievePort.sendPort, rootDirectory: FMTC.instance.rootDirectory.directory.absolute.path, @@ -149,29 +149,47 @@ class DownloadManagement { debugName: '[FMTC] Master Bulk Download Thread', ); - // Setup (part 1) cancel request mechanism + // Setup control mechanisms (completers) final cancelCompleter = Completer(); + Completer? pauseCompleter; await for (final evt in recievePort) { + // Handle new progress message + if (evt is DownloadProgress) { + yield evt; + continue; + } + // Handle shutdown (both normal and cancellation) if (evt == null) break; - // Setup (part 2) cancel request mechanism - if (evt is SendPort) { - instance.cancelDownloadRequest = () { - evt.send(null); - return cancelCompleter.future; - }; + // Handle pause comms + if (evt == 1) { + pauseCompleter?.complete(); continue; } - // Ensure message is of `DownloadProgress` in all other cases - evt as DownloadProgress; - yield evt; + // 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; + } } // Handle shutdown (both normal and cancellation) - instance.cancelDownloadRequest = null; recievePort.close(); await FMTC.instance.rootDirectory.recovery.cancel(recoveryId); DownloadInstance.unregister(instanceId); @@ -192,21 +210,59 @@ class DownloadManagement { region, ); - /// Cancels the ongoing foreground download and recovery session + /// Cancel the ongoing foreground download and recovery session /// /// Will return once the cancellation is complete. Note that all running /// parallel download threads will be allowed to finish their *current* tile - /// download, and tiles will be written. There is no facility to cancel the - /// download immediately, as this would likely cause unwanted behaviour. + /// download, and buffered tiles will be written. There is no facility to + /// cancel the download immediately, as this would likely cause unwanted + /// behaviour. /// /// {@macro num_instances} /// /// Does nothing (returns immediately) if there is no ongoing download. /// - /// Do not use to cancel background downloads, return `true` from the - /// background download callback to cancel a background download. Background - /// download cancellations require a few more 'shut-down' steps that can create - /// unexpected issues and memory leaks if not carried out. + /// Do not use to interact with background downloads. Future cancel({Object instanceId = 0}) async => - await DownloadInstance.get(instanceId)?.cancelDownloadRequest?.call(); + await DownloadInstance.get(instanceId)?.requestCancel?.call(); + + /// Pause the ongoing foreground download + /// + /// Use [resume] to resume the download. It is also safe to use [cancel] + /// without resuming first. + /// + /// Will return once the pause operation is complete. Note that all running + /// parallel download threads will be allowed to finish their *current* tile + /// download. Any buffered tiles are not written. + /// + /// {@macro num_instances} + /// + /// Does nothing (returns immediately) if there is no ongoing download or the + /// download is already paused. + /// + /// Do not use to interact with background downloads. + Future pause({Object instanceId = 0}) async => + await DownloadInstance.get(instanceId)?.requestPause?.call(); + + /// Resume (after a [pause]) the ongoing foreground download + /// + /// {@macro num_instances} + /// + /// Does nothing if there is no ongoing download or the download is already + /// running. + /// + /// Do not use to interact with background downloads. + void resume({Object instanceId = 0}) => + DownloadInstance.get(instanceId)?.requestResume?.call(); + + /// Whether the ongoing foreground download is currently paused after a call + /// to [pause] (and prior to [resume]) + /// + /// {@macro num_instances} + /// + /// Also returns `false` if there is no ongoing download. + /// + /// Do not use to interact with background downloads. + bool isPaused({Object instanceId = 0}) => + DownloadInstance.get(instanceId)?.isPaused ?? false; } diff --git a/pubspec.yaml b/pubspec.yaml index 890e1fda..9a1cb94c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -32,7 +32,6 @@ dependencies: sdk: flutter flutter_map: ^5.0.0 http: ^1.1.0 - http_plus: ^0.2.3 isar: ^3.1.0+1 isar_flutter_libs: ^3.1.0+1 latlong2: ^0.9.0 From fdeb4ad38949e88d2a76ceb44c25f7f74f589be9 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 6 Jul 2023 14:16:52 +0100 Subject: [PATCH 023/168] Added rate limiting to bulk downloading Added progress estimation to bulk downloading Other general improvements Improved example application Former-commit-id: a47765e7072c483637680dbd41bfbdd396625874 [formerly 4a7c4cc2e1a7f6977584033db9ebdb028fe6eada] Former-commit-id: 7fbe33f9c319e1de539ffef8f2b1e4565d195113 --- example/lib/main.dart | 7 - .../bd_battery_optimizations_info.dart | 74 ---- .../components/optional_functionality.dart | 12 +- .../components/region_information.dart | 116 ++++-- .../components/usage_warning.dart | 25 -- .../download_region/download_region.dart | 371 ++++++++++++++---- example/lib/screens/main/main.dart | 6 - .../pages/downloader/components/map_view.dart | 88 ++++- .../main/pages/downloader/downloader.dart | 4 +- .../components/download_layout.dart | 150 +------ .../components/main_statistics.dart | 132 +++++++ .../settingsAndAbout/components/header.dart | 20 - .../settingsAndAbout/settings_and_about.dart | 136 ------- .../lib/shared/state/download_provider.dart | 44 ++- example/pubspec.yaml | 3 +- lib/flutter_map_tile_caching.dart | 1 + lib/src/bulk_download/download_progress.dart | 135 ++++--- .../internal_timing_progress_management.dart | 88 ----- lib/src/bulk_download/manager.dart | 76 +++- .../bulk_download/rate_limited_stream.dart | 96 +++++ lib/src/bulk_download/thread.dart | 4 +- lib/src/bulk_download/tile_event.dart | 28 +- lib/src/providers/image_provider.dart | 6 +- lib/src/providers/tile_provider.dart | 8 +- lib/src/regions/base_region.dart | 1 + lib/src/regions/circle.dart | 2 +- lib/src/regions/downloadable_region.dart | 14 +- lib/src/regions/line.dart | 2 +- lib/src/regions/rectangle.dart | 2 +- lib/src/root/migrator.dart | 4 +- lib/src/settings/tile_provider_settings.dart | 10 +- lib/src/store/download.dart | 38 +- 32 files changed, 935 insertions(+), 768 deletions(-) delete mode 100644 example/lib/screens/download_region/components/bd_battery_optimizations_info.dart delete mode 100644 example/lib/screens/download_region/components/usage_warning.dart create mode 100644 example/lib/screens/main/pages/downloading/components/main_statistics.dart delete mode 100644 example/lib/screens/main/pages/settingsAndAbout/components/header.dart delete mode 100644 example/lib/screens/main/pages/settingsAndAbout/settings_and_about.dart delete mode 100644 lib/src/bulk_download/internal_timing_progress_management.dart create mode 100644 lib/src/bulk_download/rate_limited_stream.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index f0be4c88..39f89fb1 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -6,7 +6,6 @@ import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:path/path.dart' as p; import 'package:provider/provider.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'screens/main/main.dart'; import 'shared/state/download_provider.dart'; @@ -21,8 +20,6 @@ void main() async { ), ); - final SharedPreferences prefs = await SharedPreferences.getInstance(); - String? damagedDatabaseDeleted; await FlutterMapTileCaching.initialise( errorHandler: (error) => damagedDatabaseDeleted = error.message, @@ -31,10 +28,6 @@ void main() async { await FMTC.instance.rootDirectory.migrator.fromV6(urlTemplates: []); - if (prefs.getBool('reset') ?? false) { - await FMTC.instance.rootDirectory.manage.reset(); - } - final newAppVersionFile = File( p.join( // ignore: invalid_use_of_internal_member, invalid_use_of_protected_member diff --git a/example/lib/screens/download_region/components/bd_battery_optimizations_info.dart b/example/lib/screens/download_region/components/bd_battery_optimizations_info.dart deleted file mode 100644 index ac14bb86..00000000 --- a/example/lib/screens/download_region/components/bd_battery_optimizations_info.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; -import 'package:fmtc_plus_background_downloading/fmtc_plus_background_downloading.dart'; - -class BackgroundDownloadBatteryOptimizationsInfo extends StatefulWidget { - const BackgroundDownloadBatteryOptimizationsInfo({ - super.key, - }); - - @override - State createState() => - _BackgroundDownloadBatteryOptimizationsInfoState(); -} - -class _BackgroundDownloadBatteryOptimizationsInfoState - extends State { - @override - Widget build(BuildContext context) => FutureBuilder( - future: FMTC - .instance('') - .download - .requestIgnoreBatteryOptimizations(requestIfDenied: false), - builder: (context, snapshot) => Row( - children: [ - Icon( - snapshot.data == null || !snapshot.data! - ? Icons.warning_amber - : Icons.done, - color: snapshot.data == null || !snapshot.data! - ? Colors.amber - : Colors.green, - size: 36, - ), - const SizedBox(width: 15), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - "Apps that support background downloading can request extra permissions to help prevent the background process being stopped by the system. Specifically, the 'ignore battery optimisations' permission helps most. The API has a method to manage this permission.", - textAlign: TextAlign.justify, - ), - const SizedBox(height: 10), - Text( - snapshot.hasError - ? 'This platform currently does not support this API: it is only supported on Android.' - : snapshot.data == null - ? 'Checking if this permission is currently granted to this application...' - : (!snapshot.data! - ? 'This application does not have this permission granted to it currently. Tap the button below to use the API method to request the permission.' - : 'This application does currently have this permission granted to it.'), - textAlign: TextAlign.justify, - ), - if (!(snapshot.data ?? true)) - SizedBox( - width: double.infinity, - child: OutlinedButton( - onPressed: () async { - await FMTC - .instance('') - .download - .requestIgnoreBatteryOptimizations(); - setState(() {}); - }, - child: const Text('Request Permission'), - ), - ), - ], - ), - ), - ], - ), - ); -} diff --git a/example/lib/screens/download_region/components/optional_functionality.dart b/example/lib/screens/download_region/components/optional_functionality.dart index 4ecebf98..dbdde164 100644 --- a/example/lib/screens/download_region/components/optional_functionality.dart +++ b/example/lib/screens/download_region/components/optional_functionality.dart @@ -16,7 +16,7 @@ class OptionalFunctionality extends StatelessWidget { children: [ Row( children: [ - const Text('Only Download New Tiles'), + const Text('Skip Existing Tiles'), const Spacer(), IconButton( onPressed: () { @@ -32,8 +32,8 @@ class OptionalFunctionality extends StatelessWidget { icon: const Icon(Icons.help_outline), ), Switch( - value: provider.preventRedownload, - onChanged: (val) => provider.preventRedownload = val, + value: provider.skipExistingTiles, + onChanged: (val) => provider.skipExistingTiles = val, activeColor: Theme.of(context).colorScheme.primary, ) ], @@ -41,7 +41,7 @@ class OptionalFunctionality extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text('Remove Sea Tiles'), + const Text('Skip Sea Tiles'), const Spacer(), IconButton( onPressed: () { @@ -57,8 +57,8 @@ class OptionalFunctionality extends StatelessWidget { icon: const Icon(Icons.help_outline), ), Switch( - value: provider.seaTileRemoval, - onChanged: (val) => provider.seaTileRemoval = val, + value: provider.skipSeaTiles, + onChanged: (val) => provider.skipSeaTiles = val, activeColor: Theme.of(context).colorScheme.primary, ) ], diff --git a/example/lib/screens/download_region/components/region_information.dart b/example/lib/screens/download_region/components/region_information.dart index 5b16556c..0cdae024 100644 --- a/example/lib/screens/download_region/components/region_information.dart +++ b/example/lib/screens/download_region/components/region_information.dart @@ -1,22 +1,18 @@ import 'package:flutter/material.dart'; import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; import 'package:intl/intl.dart'; +import 'package:latlong2/latlong.dart'; import 'package:provider/provider.dart'; import '../../../shared/state/download_provider.dart'; -import '../download_region.dart'; class RegionInformation extends StatelessWidget { const RegionInformation({ super.key, - required this.widget, - required this.circleRegion, - required this.rectangleRegion, + required this.region, }); - final DownloadRegionPopup widget; - final CircleRegion? circleRegion; - final RectangleRegion? rectangleRegion; + final BaseRegion region; @override Widget build(BuildContext context) => Column( @@ -29,43 +25,83 @@ class RegionInformation extends StatelessWidget { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (widget.region is CircleRegion) ...[ - const Text('APPROX. CENTER'), - Text( - '${circleRegion!.center.latitude.toStringAsFixed(3)}, ${circleRegion!.center.longitude.toStringAsFixed(3)}', - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 24, + ...region.when( + rectangle: (rectangle) => [ + const Text('APPROX. NORTH WEST'), + Text( + '${rectangle.bounds.northWest.latitude.toStringAsFixed(3)}, ${rectangle.bounds.northWest.longitude.toStringAsFixed(3)}', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 24, + ), ), - ), - const SizedBox(height: 10), - const Text('RADIUS'), - Text( - '${circleRegion!.radius.toStringAsFixed(2)} km', - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 24, + const SizedBox(height: 10), + const Text('APPROX. SOUTH EAST'), + Text( + '${rectangle.bounds.southEast.latitude.toStringAsFixed(3)}, ${rectangle.bounds.southEast.longitude.toStringAsFixed(3)}', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 24, + ), ), - ), - ] else ...[ - const Text('APPROX. NORTH WEST'), - Text( - '${rectangleRegion!.bounds.northWest.latitude.toStringAsFixed(3)}, ${rectangleRegion!.bounds.northWest.longitude.toStringAsFixed(3)}', - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 24, + ], + circle: (circle) => [ + const Text('APPROX. CENTER'), + Text( + '${circle.center.latitude.toStringAsFixed(3)}, ${circle.center.longitude.toStringAsFixed(3)}', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 24, + ), ), - ), - const SizedBox(height: 10), - const Text('APPROX. SOUTH EAST'), - Text( - '${rectangleRegion!.bounds.southEast.latitude.toStringAsFixed(3)}, ${rectangleRegion!.bounds.southEast.longitude.toStringAsFixed(3)}', - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 24, + const SizedBox(height: 10), + const Text('RADIUS'), + Text( + '${circle.radius.toStringAsFixed(2)} km', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 24, + ), ), - ), - ], + ], + line: (line) { + const distCalc = Distance(roundResult: false); + double totalDistance = 0; + for (int i = 0; i < line.line.length - 1; i++) { + totalDistance += + distCalc.distance(line.line[i], line.line[i + 1]); + } + + return [ + const Text('LINE LENGTH'), + Text( + '${(totalDistance / 1000).toStringAsFixed(3)} km', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 24, + ), + ), + const SizedBox(height: 10), + const Text('FIRST COORD'), + Text( + '${line.line[0].latitude.toStringAsFixed(3)}, ${line.line[0].longitude.toStringAsFixed(3)}', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 24, + ), + ), + const SizedBox(height: 10), + const Text('LAST COORD'), + Text( + '${line.line.last.latitude.toStringAsFixed(3)}, ${line.line.last.longitude.toStringAsFixed(3)}', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 24, + ), + ), + ]; + }, + ), const SizedBox(height: 10), const Text('MIN/MAX ZOOM LEVELS'), Consumer( diff --git a/example/lib/screens/download_region/components/usage_warning.dart b/example/lib/screens/download_region/components/usage_warning.dart deleted file mode 100644 index c0ba4742..00000000 --- a/example/lib/screens/download_region/components/usage_warning.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:flutter/material.dart'; - -class UsageWarning extends StatelessWidget { - const UsageWarning({ - super.key, - }); - - @override - Widget build(BuildContext context) => const Row( - children: [ - Icon( - Icons.warning_amber, - color: Colors.red, - size: 36, - ), - SizedBox(width: 15), - Expanded( - child: Text( - "You must abide by your tile server's Terms of Service when Bulk Downloading. Many servers will forbid or heavily restrict this action, as it places extra strain on resources. Be respectful, and note that you use this functionality at your own risk.\nThis example application is limited to a maximum of 2 simultaneous download threads by default.", - textAlign: TextAlign.justify, - ), - ), - ], - ); -} diff --git a/example/lib/screens/download_region/download_region.dart b/example/lib/screens/download_region/download_region.dart index 385e45af..8624e899 100644 --- a/example/lib/screens/download_region/download_region.dart +++ b/example/lib/screens/download_region/download_region.dart @@ -1,19 +1,15 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; -//import 'package:fmtc_plus_background_downloading/fmtc_plus_background_downloading.dart'; import 'package:provider/provider.dart'; -import 'package:shared_preferences/shared_preferences.dart'; +//import 'package:shared_preferences/shared_preferences.dart'; import '../../shared/state/download_provider.dart'; import '../../shared/state/general_provider.dart'; -import 'components/bd_battery_optimizations_info.dart'; -import 'components/buffering_configuration.dart'; -import 'components/optional_functionality.dart'; import 'components/region_information.dart'; import 'components/section_separator.dart'; import 'components/store_selector.dart'; -import 'components/usage_warning.dart'; class DownloadRegionPopup extends StatefulWidget { const DownloadRegionPopup({ @@ -28,21 +24,7 @@ class DownloadRegionPopup extends StatefulWidget { } class _DownloadRegionPopupState extends State { - late final CircleRegion? circleRegion; - late final RectangleRegion? rectangleRegion; - - @override - void initState() { - if (widget.region is CircleRegion) { - circleRegion = widget.region as CircleRegion; - rectangleRegion = null; - } else { - rectangleRegion = widget.region as RectangleRegion; - circleRegion = null; - } - - super.initState(); - } + bool isReady = false; @override void didChangeDependencies() { @@ -62,68 +44,303 @@ class _DownloadRegionPopupState extends State { appBar: AppBar(title: const Text('Configure Bulk Download')), floatingActionButton: provider.selectedStore == null ? null - : FloatingActionButton.extended( - onPressed: () async { - final Map metadata = - await provider.selectedStore!.metadata.readAsync; - - provider.setDownloadProgress( - provider.selectedStore!.download - .startForeground( - region: widget.region.toDownloadable( - provider.minZoom, - provider.maxZoom, - TileLayer( - urlTemplate: metadata['sourceURL'], - userAgentPackageName: - 'dev.jaffaketchup.fmtc.demo', - ), + : Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + AnimatedScale( + scale: isReady ? 1 : 0, + duration: const Duration(milliseconds: 200), + curve: Curves.easeInCubic, + alignment: Alignment.bottomRight, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.onBackground, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(12), + topRight: Radius.circular(12), + bottomLeft: Radius.circular(12), + ), + ), + margin: const EdgeInsets.only(right: 12, left: 32), + padding: const EdgeInsets.all(12), + constraints: const BoxConstraints(maxWidth: 500), + child: const Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + "You must abide by your tile server's Terms of Service when bulk downloading. Many servers will forbid or heavily restrict this action, as it places extra strain on resources. Be respectful, and note that you use this functionality at your own risk.", + textAlign: TextAlign.end, + style: TextStyle(color: Colors.black), + ), + SizedBox(height: 8), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'CAUTION', + style: TextStyle( + color: Colors.red, + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + SizedBox(width: 8), + Icon( + Icons.report, + color: Colors.red, + size: 32, + ), + ], ), - parallelThreads: - (await SharedPreferences.getInstance()).getBool( - 'bypassDownloadThreadsLimitation', - ) ?? - false - ? 10 - : 2, - maxBufferLength: provider.bufferingAmount, - pruneExistingTiles: provider.preventRedownload, - pruneSeaTiles: provider.seaTileRemoval, - disableRecovery: provider.disableRecovery, - ) - .asBroadcastStream(), - ); + ], + ), + ), + ), + const SizedBox(height: 16), + FloatingActionButton.extended( + onPressed: () async { + if (!isReady) { + setState(() => isReady = true); + return; + } + final Map metadata = + await provider.selectedStore!.metadata.readAsync; - if (mounted) Navigator.of(context).pop(); - }, - label: const Text('Start Download'), - icon: const Icon(Icons.save), + provider.setDownloadProgress( + provider.selectedStore!.download + .startForeground( + region: widget.region.toDownloadable( + provider.minZoom, + provider.maxZoom, + TileLayer( + urlTemplate: metadata['sourceURL'], + userAgentPackageName: + 'dev.jaffaketchup.fmtc.demo', + ), + ), + parallelThreads: provider.parallelThreads, + maxBufferLength: provider.bufferingAmount, + skipExistingTiles: provider.skipExistingTiles, + skipSeaTiles: provider.skipSeaTiles, + rateLimit: provider.rateLimit, + disableRecovery: provider.disableRecovery, + ) + .asBroadcastStream(), + ); + + if (mounted) Navigator.of(context).pop(); + }, + label: const Text('Start Download'), + icon: Icon(isReady ? Icons.save : Icons.arrow_forward), + ), + ], + ), + body: Stack( + children: [ + Positioned.fill( + left: 12, + top: 12, + right: 12, + bottom: 12, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RegionInformation(region: widget.region), + const SectionSeparator(), + const StoreSelector(), + const SectionSeparator(), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('CONFIGURE DOWNLOAD OPTIONS'), + const SizedBox(height: 16), + Row( + children: [ + const Text('Parallel Threads'), + const Spacer(), + Icon( + Icons.lock, + color: provider.parallelThreads == 10 + ? Colors.amber + : Colors.white.withOpacity(0.2), + ), + const SizedBox(width: 16), + IntrinsicWidth( + child: TextFormField( + initialValue: '5', + textAlign: TextAlign.end, + keyboardType: TextInputType.number, + decoration: const InputDecoration( + isDense: true, + counterText: '', + suffixText: ' threads', + ), + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + const NumericalRangeFormatter( + min: 1, + max: 10, + ), + ], + onChanged: (value) => + provider.parallelThreads = + int.tryParse(value) ?? + provider.parallelThreads, + ), + ), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + const Text('Rate Limit'), + const Spacer(), + Icon( + Icons.lock, + color: provider.rateLimit == 100 + ? Colors.amber + : Colors.white.withOpacity(0.2), + ), + const SizedBox(width: 16), + IntrinsicWidth( + child: TextFormField( + initialValue: '100', + textAlign: TextAlign.end, + keyboardType: TextInputType.number, + decoration: const InputDecoration( + isDense: true, + counterText: '', + suffixText: ' max. tiles/second', + ), + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + const NumericalRangeFormatter( + min: 1, + max: 100, + ), + ], + onChanged: (value) => provider.rateLimit = + int.tryParse(value) ?? provider.rateLimit, + ), + ), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + const Text('Tile Buffer Length'), + const Spacer(), + IntrinsicWidth( + child: TextFormField( + initialValue: '200', + textAlign: TextAlign.end, + keyboardType: TextInputType.number, + decoration: const InputDecoration( + isDense: true, + counterText: '', + suffixText: ' tiles', + ), + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + const NumericalRangeFormatter( + min: 0, + max: 9223372036854775807, + ), + ], + onChanged: (value) => + provider.bufferingAmount = + int.tryParse(value) ?? + provider.bufferingAmount, + ), + ), + ], + ), + const SizedBox(height: 16), + Row( + children: [ + const Text('Skip Existing Tiles'), + const Spacer(), + Switch( + value: provider.skipExistingTiles, + onChanged: (val) => + provider.skipExistingTiles = val, + activeColor: + Theme.of(context).colorScheme.primary, + ) + ], + ), + const SizedBox(height: 6), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Skip Sea Tiles'), + const Spacer(), + Switch( + value: provider.skipSeaTiles, + onChanged: (val) => provider.skipSeaTiles = val, + activeColor: + Theme.of(context).colorScheme.primary, + ) + ], + ), + ], + ), + ], + ), ), - body: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - RegionInformation( - widget: widget, - circleRegion: circleRegion, - rectangleRegion: rectangleRegion, + ), + Positioned.fill( + child: IgnorePointer( + ignoring: !isReady, + child: GestureDetector( + onTap: + isReady ? () => setState(() => isReady = false) : null, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + curve: Curves.easeInCubic, + color: isReady + ? Colors.black.withOpacity(2 / 3) + : Colors.transparent, + ), ), - const SectionSeparator(), - const StoreSelector(), - const SectionSeparator(), - const OptionalFunctionality(), - const SectionSeparator(), - const BufferingConfiguration(), - const SectionSeparator(), - const BackgroundDownloadBatteryOptimizationsInfo(), - const SectionSeparator(), - const UsageWarning(), - ], + ), ), - ), + ], ), ), ); } + +class NumericalRangeFormatter extends TextInputFormatter { + const NumericalRangeFormatter({required this.min, required this.max}); + final int min; + final int max; + + @override + TextEditingValue formatEditUpdate( + TextEditingValue oldValue, + TextEditingValue newValue, + ) { + if (newValue.text.isEmpty) return newValue; + + final int parsed = int.parse(newValue.text); + + if (parsed < min) { + return TextEditingValue.empty.copyWith( + text: min.toString(), + selection: TextSelection.collapsed(offset: min.toString().length), + ); + } + if (parsed > max) { + return TextEditingValue.empty.copyWith( + text: max.toString(), + selection: TextSelection.collapsed(offset: max.toString().length), + ); + } + + return newValue; + } +} diff --git a/example/lib/screens/main/main.dart b/example/lib/screens/main/main.dart index 2a447f49..cc18ee78 100644 --- a/example/lib/screens/main/main.dart +++ b/example/lib/screens/main/main.dart @@ -9,7 +9,6 @@ import 'pages/downloader/downloader.dart'; import 'pages/downloading/downloading.dart'; import 'pages/map/map_view.dart'; import 'pages/recovery/recovery.dart'; -import 'pages/settingsAndAbout/settings_and_about.dart'; import 'pages/stores/stores.dart'; class MainScreen extends StatefulWidget { @@ -62,10 +61,6 @@ class _MainScreenState extends State { ), label: 'Recover', ), - const NavigationDestination( - icon: Icon(Icons.settings), - label: 'Settings', - ), ]; List get _pages => [ @@ -77,7 +72,6 @@ class _MainScreenState extends State { : const DownloadingPage(), ), RecoveryPage(moveToDownloadPage: () => _onDestinationSelected(2)), - const SettingsAndAboutPage(), ]; void _onDestinationSelected(int index) { diff --git a/example/lib/screens/main/pages/downloader/components/map_view.dart b/example/lib/screens/main/pages/downloader/components/map_view.dart index 7fb138da..c7257567 100644 --- a/example/lib/screens/main/pages/downloader/components/map_view.dart +++ b/example/lib/screens/main/pages/downloader/components/map_view.dart @@ -1,10 +1,13 @@ import 'dart:async'; +import 'dart:io'; import 'dart:math'; +import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/plugin_api.dart'; import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; +import 'package:gpx/gpx.dart'; import 'package:latlong2/latlong.dart'; import 'package:provider/provider.dart'; import 'package:stream_transform/stream_transform.dart'; @@ -235,27 +238,75 @@ class _MapViewState extends State { borderRadius: BorderRadius.circular(16), ), width: double.infinity, - height: 50, margin: const EdgeInsets.all(16), - padding: const EdgeInsets.all(16), - child: Row( - children: [ - const Text('Radius'), - Expanded( - child: Slider( - value: downloadProvider.lineRegionRadius, - min: 100, - max: 10000, - divisions: 99, - label: - '${downloadProvider.lineRegionRadius.round()} meters', - onChanged: (val) { - downloadProvider.lineRegionRadius = val; + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + child: IntrinsicHeight( + child: Row( + children: [ + const Text('Radius'), + Expanded( + child: Slider( + value: downloadProvider.lineRegionRadius, + min: 50, + max: 5000, + divisions: 99, + label: + '${downloadProvider.lineRegionRadius.round()} meters', + onChanged: (val) { + downloadProvider.lineRegionRadius = val; + _updateLineRegion(); + }, + ), + ), + const VerticalDivider(), + IconButton( + onPressed: () async { + final result = + await FilePicker.platform.pickFiles( + dialogTitle: 'Open GPX', + type: FileType.custom, + allowedExtensions: ['gpx', 'kml'], + allowMultiple: true, + ); + + if (result != null) { + final gpxReader = GpxReader(); + for (final path + in result.files.map((e) => e.path)) { + downloadProvider.lineRegionPoints.addAll( + gpxReader + .fromString( + await File(path!).readAsString(), + ) + .trks + .map( + (e) => e.trksegs.map( + (e) => e.trkpts.map( + (e) => LatLng(e.lat!, e.lon!), + ), + ), + ) + .expand((e) => e) + .expand((e) => e), + ); + _updateLineRegion(); + } + } + }, + icon: const Icon(Icons.download), + tooltip: 'Import from GPX', + ), + IconButton( + onPressed: () { + downloadProvider.lineRegionPoints.clear(); _updateLineRegion(); }, + icon: const Icon(Icons.cancel), + tooltip: 'Clear existing points', ), - ), - ], + ], + ), ), ), ], @@ -357,8 +408,7 @@ class _MapViewState extends State { _crosshairsTop = calculatedTop - _crosshairsMovement; _crosshairsBottom = centerNormal - _crosshairsMovement; - _center = MapController.of(context) - .camera + _center = _mapController.camera .pointToLatLng(_customPointFromPoint(centerNormal)); _radius = const Distance(roundResult: false).distance( _center!, diff --git a/example/lib/screens/main/pages/downloader/downloader.dart b/example/lib/screens/main/pages/downloader/downloader.dart index 6e133fb3..4ab5f121 100644 --- a/example/lib/screens/main/pages/downloader/downloader.dart +++ b/example/lib/screens/main/pages/downloader/downloader.dart @@ -41,7 +41,9 @@ class _DownloaderPageState extends State { ), floatingActionButton: Consumer( builder: (context, provider, _) => FloatingActionButton.extended( - onPressed: provider.region == null || provider.regionTiles == null + onPressed: provider.region == null || + provider.regionTiles == null || + provider.regionTiles == 0 ? () {} : () => Navigator.of(context).push( MaterialPageRoute( diff --git a/example/lib/screens/main/pages/downloading/components/download_layout.dart b/example/lib/screens/main/pages/downloading/components/download_layout.dart index 39577394..a46ec4ae 100644 --- a/example/lib/screens/main/pages/downloading/components/download_layout.dart +++ b/example/lib/screens/main/pages/downloading/components/download_layout.dart @@ -5,6 +5,7 @@ import 'package:provider/provider.dart'; import '../../../../../shared/state/download_provider.dart'; import '../../../../../shared/vars/size_formatter.dart'; +import 'main_statistics.dart'; import 'multi_linear_progress_indicator.dart'; import 'stat_display.dart'; @@ -28,7 +29,7 @@ class DownloadLayout extends StatelessWidget { child: ClipRRect( borderRadius: BorderRadius.circular(16), child: SizedBox.square( - dimension: 256 / 1.25, + dimension: 256, child: download.lastTileEvent.tileImage != null ? Image.memory( download.lastTileEvent.tileImage!, @@ -41,59 +42,11 @@ class DownloadLayout extends StatelessWidget { ), ), const SizedBox(width: 32), - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - StatDisplay( - statistic: - '${download.percentageProgress.toStringAsFixed(2)}%', - description: 'percentage attempted', - ), - StatDisplay( - statistic: download.duration.toString().split('.')[0], - description: 'elapsed duration', - ), - const SizedBox(height: 16), - if (!download.hasFinished) - RepaintBoundary( - child: Row( - children: [ - IconButton.outlined( - onPressed: storeDirectory.download.isPaused() - ? () => storeDirectory.download.resume() - : () => storeDirectory.download.pause(), - icon: Icon( - storeDirectory.download.isPaused() - ? Icons.play_arrow - : Icons.pause, - ), - ), - const SizedBox(width: 8), - IconButton.outlined( - onPressed: () => storeDirectory.download.cancel(), - icon: const Icon(Icons.cancel), - ) - ], - ), - ), - if (download.hasFinished) - OutlinedButton( - onPressed: () { - WidgetsBinding.instance.addPostFrameCallback( - (_) => Provider.of( - context, - listen: false, - ).setDownloadProgress(null), - ); - }, - child: const Padding( - padding: EdgeInsets.symmetric(horizontal: 32), - child: Text('Exit'), - ), - ), - ], + MainStatistics( + download: download, + storeDirectory: storeDirectory, ), - const SizedBox(width: 48), + const SizedBox(width: 32), const VerticalDivider(), const SizedBox(width: 16), Expanded( @@ -101,10 +54,6 @@ class DownloadLayout extends StatelessWidget { children: [ TableRow( children: [ - StatDisplay( - statistic: '${download.attemptedTiles}', - description: 'attempted tiles', - ), StatDisplay( statistic: '${download.cachedTiles - download.bufferedTiles} + ${download.bufferedTiles}', @@ -119,28 +68,20 @@ class DownloadLayout extends StatelessWidget { ), TableRow( children: [ - StatDisplay( - statistic: '${download.remainingTiles}', - description: 'remaining tiles', - ), StatDisplay( statistic: - '${download.prunedTiles} (${download.prunedTiles == 0 ? 0 : (100 - ((download.cachedTiles - download.prunedTiles) / download.cachedTiles) * 100).toStringAsFixed(1)}%)', - description: 'pruned tiles (% saving)', + '${download.skippedTiles} (${download.skippedTiles == 0 ? 0 : (100 - ((download.cachedTiles - download.skippedTiles) / download.cachedTiles) * 100).toStringAsFixed(1)}%)', + description: 'skipped tiles (% saving)', ), StatDisplay( statistic: - '${(download.prunedSize * 1024).asReadableSize} (${download.prunedTiles == 0 ? 0 : (100 - ((download.cachedSize - download.prunedSize) / download.cachedSize) * 100).toStringAsFixed(1)}%)', - description: 'pruned size (% saving)', + '${(download.skippedSize * 1024).asReadableSize} (${download.skippedTiles == 0 ? 0 : (100 - ((download.cachedSize - download.skippedSize) / download.cachedSize) * 100).toStringAsFixed(1)}%)', + description: 'skipped size (% saving)', ), ], ), TableRow( children: [ - StatDisplay( - statistic: '${download.maxTiles}', - description: 'total tiles', - ), RepaintBoundary( child: Column( children: [ @@ -163,7 +104,7 @@ class DownloadLayout extends StatelessWidget { Icons.warning_amber, color: Colors.red, ), - ] + ], ], ), Text( @@ -197,7 +138,7 @@ class DownloadLayout extends StatelessWidget { progresses: [ ( value: download.cachedTiles + - download.prunedTiles + + download.skippedTiles + download.failedTiles, color: Colors.red, child: Text( @@ -206,10 +147,10 @@ class DownloadLayout extends StatelessWidget { ) ), ( - value: download.cachedTiles + download.prunedTiles, + value: download.cachedTiles + download.skippedTiles, color: Colors.yellow, child: Text( - '${download.prunedTiles}', + '${download.skippedTiles}', style: const TextStyle(color: Colors.black), ) ), @@ -295,69 +236,6 @@ class DownloadLayout extends StatelessWidget { ], ), ), - /*Stack( - children: [ - LinearProgressIndicator( - value: data.percentageProgress / 100, - minHeight: 12, - backgroundColor: Colors.grey[300], - valueColor: AlwaysStoppedAnimation( - Theme.of(context).colorScheme.primary.withOpacity(0.5), - ), - ), - LinearProgressIndicator( - value: data.persistedTiles / data.maxTiles, - minHeight: 12, - backgroundColor: Colors.transparent, - valueColor: AlwaysStoppedAnimation( - Theme.of(context).colorScheme.primary, - ), - ), - ], - ), - const SizedBox(height: 30), - Expanded( - child: data.failedTiles.isEmpty - ? const Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.report_off, size: 36), - SizedBox(height: 10), - Text('No Failed Tiles'), - ], - ) - : Row( - children: [ - const SizedBox(width: 30), - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon( - Icons.warning, - size: 36, - ), - const SizedBox(height: 10), - StatDisplay( - statistic: data.failedTiles.length.toString(), - description: 'failed tiles', - ), - ], - ), - const SizedBox(width: 30), - Expanded( - child: ListView.builder( - itemCount: data.failedTiles.length, - itemBuilder: (context, index) => ListTile( - title: Text( - data.failedTiles[index], - textAlign: TextAlign.end, - ), - ), - ), - ), - ], - ), - ),*/ ], ); } diff --git a/example/lib/screens/main/pages/downloading/components/main_statistics.dart b/example/lib/screens/main/pages/downloading/components/main_statistics.dart new file mode 100644 index 00000000..a2015a4c --- /dev/null +++ b/example/lib/screens/main/pages/downloading/components/main_statistics.dart @@ -0,0 +1,132 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; +import 'package:provider/provider.dart'; + +import '../../../../../shared/state/download_provider.dart'; +import 'stat_display.dart'; + +class MainStatistics extends StatefulWidget { + const MainStatistics({ + super.key, + required this.download, + required this.storeDirectory, + }); + + final DownloadProgress download; + final StoreDirectory storeDirectory; + + @override + State createState() => _MainStatisticsState(); +} + +class _MainStatisticsState extends State { + @override + Widget build(BuildContext context) => IntrinsicWidth( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + RepaintBoundary( + child: Text( + '${widget.download.attemptedTiles}/${widget.download.maxTiles} (${widget.download.percentageProgress.toStringAsFixed(2)}%)', + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox(height: 16), + StatDisplay( + statistic: + '${widget.download.elapsedDuration.toString().split('.')[0]} / ${widget.download.estTotalDuration.toString().split('.')[0]}', + description: 'elapsed / estimated total duration', + ), + StatDisplay( + statistic: + widget.download.estRemainingDuration.toString().split('.')[0], + description: 'estimated remaining duration', + ), + RepaintBoundary( + child: Column( + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + widget.download.tilesPerSecond.toStringAsFixed(2), + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: widget.download.isTPSArtificiallyCapped + ? Colors.amber + : null, + ), + ), + if (widget.download.isTPSArtificiallyCapped) ...[ + const SizedBox(width: 8), + const Icon(Icons.lock_clock, color: Colors.amber), + ], + ], + ), + Text( + 'approx. tiles per second', + style: TextStyle( + fontSize: 16, + color: widget.download.isTPSArtificiallyCapped + ? Colors.amber + : null, + ), + ), + ], + ), + ), + const SizedBox(height: 24), + if (!widget.download.isComplete) + RepaintBoundary( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton.outlined( + onPressed: () async { + if (widget.storeDirectory.download.isPaused()) { + widget.storeDirectory.download.resume(); + } else { + await widget.storeDirectory.download.pause(); + } + setState(() {}); + }, + icon: Icon( + widget.storeDirectory.download.isPaused() + ? Icons.play_arrow + : Icons.pause, + ), + ), + const SizedBox(width: 12), + IconButton.outlined( + onPressed: () => widget.storeDirectory.download.cancel(), + icon: const Icon(Icons.cancel), + ), + ], + ), + ), + if (widget.download.isComplete) + SizedBox( + width: double.infinity, + child: OutlinedButton( + onPressed: () { + WidgetsBinding.instance.addPostFrameCallback( + (_) => Provider.of( + context, + listen: false, + ).setDownloadProgress(null), + ); + }, + child: const Padding( + padding: EdgeInsets.symmetric(vertical: 8), + child: Text('Exit'), + ), + ), + ), + ], + ), + ); +} diff --git a/example/lib/screens/main/pages/settingsAndAbout/components/header.dart b/example/lib/screens/main/pages/settingsAndAbout/components/header.dart deleted file mode 100644 index b3efb980..00000000 --- a/example/lib/screens/main/pages/settingsAndAbout/components/header.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:google_fonts/google_fonts.dart'; - -class Header extends StatelessWidget { - const Header({ - super.key, - required this.title, - }); - - final String title; - - @override - Widget build(BuildContext context) => Text( - title, - style: GoogleFonts.openSans( - fontWeight: FontWeight.bold, - fontSize: 24, - ), - ); -} diff --git a/example/lib/screens/main/pages/settingsAndAbout/settings_and_about.dart b/example/lib/screens/main/pages/settingsAndAbout/settings_and_about.dart deleted file mode 100644 index 4387eb8e..00000000 --- a/example/lib/screens/main/pages/settingsAndAbout/settings_and_about.dart +++ /dev/null @@ -1,136 +0,0 @@ -import 'dart:io'; - -import 'package:flutter/material.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import '../../../../shared/components/loading_indicator.dart'; -import 'components/header.dart'; - -class SettingsAndAboutPage extends StatefulWidget { - const SettingsAndAboutPage({super.key}); - - @override - State createState() => _SettingsAndAboutPageState(); -} - -class _SettingsAndAboutPageState extends State { - final creditsScrollController = ScrollController(); - final Map _settings = { - 'Reset FMTC On Every Startup\nDefaults to disabled': 'reset', - "Bypass Download Threads Limitation\nBy default, only 2 simultaneous bulk download threads can be used in the example application\nEnabling this increases the number to 10, which is only to be used in compliance with the tile server's TOS": - 'bypassDownloadThreadsLimitation', - }; - - @override - Widget build(BuildContext context) => Scaffold( - body: SafeArea( - child: Padding( - padding: const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Header( - title: 'Settings', - ), - const SizedBox(height: 12), - FutureBuilder( - future: SharedPreferences.getInstance(), - builder: (context, prefs) => prefs.hasData - ? ListView.builder( - itemCount: _settings.length, - shrinkWrap: true, - itemBuilder: (context, index) { - final List info = - _settings.keys.toList()[index].split('\n'); - - return SwitchListTile( - title: Text(info[0]), - subtitle: info.length >= 2 - ? Text( - info.getRange(1, info.length).join('\n'), - ) - : null, - onChanged: (newBool) async { - await prefs.data!.setBool( - _settings.values.toList()[index], - newBool, - ); - setState(() {}); - }, - value: prefs.data!.getBool( - _settings.values.toList()[index], - ) ?? - false, - ); - }, - ) - : const LoadingIndicator( - message: 'Loading Settings...', - ), - ), - const SizedBox(height: 24), - Row( - children: [ - const Header( - title: 'App Credits', - ), - const SizedBox(width: 12), - TextButton.icon( - onPressed: () { - showLicensePage( - context: context, - applicationName: 'FMTC Demo', - applicationVersion: - 'for v9.0.0\n(on ${Platform().operatingSystemFormatted})', - applicationIcon: Image.asset( - 'assets/icons/ProjectIcon.png', - height: 48, - ), - ); - }, - icon: const Icon(Icons.info), - label: const Text('Show Licenses'), - ), - ], - ), - const SizedBox(height: 12), - Expanded( - child: SingleChildScrollView( - controller: creditsScrollController, - child: const Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "An example application for the 'flutter_map_tile_caching' project, built by Luka S (JaffaKetchup). Tap on the above button to show more detailed information.\n", - ), - Text( - "Many thanks go to all my donors, whom can be found on the documentation website. If you want to support me, any amount is appriciated! Please visit the GitHub repository for donation/sponsorship options.\n\nYou can see all the dependenices used in this application by tapping the 'Show Licenses' button above. In addition to the packages listed there, thanks also go to:\n - Nominatim: their services are used to retrieve the location of a recoverable download on the 'Recover' screen\n - OpenStreetMap: their tiles are the default throughout the application\n - Inno Setup: their software provides the installer for the Windows version of this application", - ), - ], - ), - ), - ), - ], - ), - ), - ), - ); -} - -extension on Platform { - String get operatingSystemFormatted { - switch (Platform.operatingSystem) { - case 'android': - return 'Android'; - case 'ios': - return 'iOS'; - case 'linux': - return 'Linux'; - case 'macos': - return 'MacOS'; - case 'windows': - return 'Windows'; - default: - return 'Unknown Operating System'; - } - } -} diff --git a/example/lib/shared/state/download_provider.dart b/example/lib/shared/state/download_provider.dart index b22f695e..fecf63ce 100644 --- a/example/lib/shared/state/download_provider.dart +++ b/example/lib/shared/state/download_provider.dart @@ -79,17 +79,38 @@ class DownloadProvider extends ChangeNotifier { if (notify) notifyListeners(); } - bool _preventRedownload = false; - bool get preventRedownload => _preventRedownload; - set preventRedownload(bool newBool) { - _preventRedownload = newBool; + int _parallelThreads = 5; + int get parallelThreads => _parallelThreads; + set parallelThreads(int newNum) { + _parallelThreads = newNum; notifyListeners(); } - bool _seaTileRemoval = true; - bool get seaTileRemoval => _seaTileRemoval; - set seaTileRemoval(bool newBool) { - _seaTileRemoval = newBool; + int _bufferingAmount = 100; + int get bufferingAmount => _bufferingAmount; + set bufferingAmount(int newNum) { + _bufferingAmount = newNum; + notifyListeners(); + } + + bool _skipExistingTiles = false; + bool get skipExistingTiles => _skipExistingTiles; + set skipExistingTiles(bool newBool) { + _skipExistingTiles = newBool; + notifyListeners(); + } + + bool _skipSeaTiles = true; + bool get skipSeaTiles => _skipSeaTiles; + set skipSeaTiles(bool newBool) { + _skipSeaTiles = newBool; + notifyListeners(); + } + + int? _rateLimit = 100; + int? get rateLimit => _rateLimit; + set rateLimit(int? newNum) { + _rateLimit = newNum; notifyListeners(); } @@ -100,13 +121,6 @@ class DownloadProvider extends ChangeNotifier { notifyListeners(); } - int _bufferingAmount = 500; - int get bufferingAmount => _bufferingAmount; - set bufferingAmount(int newNum) { - _bufferingAmount = newNum; - notifyListeners(); - } - final List _failedTiles = []; List get failedTiles => _failedTiles; void addFailedTile(TileEvent e) => _failedTiles.add(e); diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 9fcababd..3c6d5b58 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -12,6 +12,7 @@ environment: dependencies: badges: ^3.0.2 better_open_file: ^3.6.4 + file_picker: ^5.2.10 flutter: sdk: flutter flutter_foreground_task: ^6.0.0+1 @@ -21,13 +22,13 @@ dependencies: fmtc_plus_background_downloading: ^8.0.0 fmtc_plus_sharing: ^8.0.0 google_fonts: ^5.1.0 + gpx: ^2.2.1 http: ^1.0.0 intl: ^0.18.0 latlong2: ^0.9.0 osm_nominatim: ^2.0.1 path: ^1.8.3 provider: ^6.0.3 - shared_preferences: ^2.0.15 stream_transform: ^2.0.0 validators: ^3.0.0 version: ^3.0.2 diff --git a/lib/flutter_map_tile_caching.dart b/lib/flutter_map_tile_caching.dart index fd091afb..2a759d2e 100644 --- a/lib/flutter_map_tile_caching.dart +++ b/lib/flutter_map_tile_caching.dart @@ -36,6 +36,7 @@ import 'package:watcher/watcher.dart'; import 'src/bulk_download/instance.dart'; +import 'src/bulk_download/rate_limited_stream.dart'; import 'src/bulk_download/tile_loops/shared.dart'; import 'src/db/defs/metadata.dart'; import 'src/db/defs/recovery.dart'; diff --git a/lib/src/bulk_download/download_progress.dart b/lib/src/bulk_download/download_progress.dart index c256fdb9..938c8ab3 100644 --- a/lib/src/bulk_download/download_progress.dart +++ b/lib/src/bulk_download/download_progress.dart @@ -18,32 +18,51 @@ class DownloadProgress { final double cachedSize; final int bufferedTiles; final double bufferedSize; - final int prunedTiles; - final double prunedSize; + final int skippedTiles; + final double skippedSize; final int failedTiles; final int maxTiles; - final Duration duration; - final bool hasFinished; - int get successfulTiles => cachedTiles + prunedTiles; - double get successfulSize => cachedSize + prunedSize; + final Duration elapsedDuration; + final double tilesPerSecond; + final bool isTPSArtificiallyCapped; + + final bool isComplete; + + int get successfulTiles => cachedTiles + skippedTiles; + double get successfulSize => cachedSize + skippedSize; int get attemptedTiles => successfulTiles + failedTiles; int get remainingTiles => maxTiles - attemptedTiles; double get percentageProgress => (attemptedTiles / maxTiles) * 100; + Duration get estTotalDuration => isComplete + ? elapsedDuration + : Duration( + seconds: + (((maxTiles / tilesPerSecond.clamp(1, largestInt)) / 10).round() * + 10) + .clamp(elapsedDuration.inSeconds, largestInt), + ); + Duration get estRemainingDuration => + estTotalDuration - elapsedDuration < Duration.zero + ? Duration.zero + : estTotalDuration - elapsedDuration; + const DownloadProgress.__({ required TileEvent? lastTileEvent, required this.cachedTiles, required this.cachedSize, required this.bufferedTiles, required this.bufferedSize, - required this.prunedTiles, - required this.prunedSize, + required this.skippedTiles, + required this.skippedSize, required this.failedTiles, required this.maxTiles, - required this.duration, - required this.hasFinished, + required this.elapsedDuration, + required this.tilesPerSecond, + required this.isTPSArtificiallyCapped, + required this.isComplete, }) : _lastTileEvent = lastTileEvent; factory DownloadProgress._initial({required int maxTiles}) => @@ -53,37 +72,46 @@ class DownloadProgress { cachedSize: 0, bufferedTiles: 0, bufferedSize: 0, - prunedTiles: 0, - prunedSize: 0, + skippedTiles: 0, + skippedSize: 0, failedTiles: 0, maxTiles: maxTiles, - duration: Duration.zero, - hasFinished: false, + elapsedDuration: Duration.zero, + tilesPerSecond: 0, + isTPSArtificiallyCapped: false, + isComplete: false, ); - DownloadProgress _updateDuration( - Duration newDuration, - ) => + DownloadProgress _updateProgress({ + required Duration newDuration, + required double tilesPerSecond, + required int? rateLimit, + }) => DownloadProgress.__( lastTileEvent: lastTileEvent, cachedTiles: cachedTiles, cachedSize: cachedSize, bufferedTiles: bufferedTiles, bufferedSize: bufferedSize, - prunedTiles: prunedTiles, - prunedSize: prunedSize, + skippedTiles: skippedTiles, + skippedSize: skippedSize, failedTiles: failedTiles, maxTiles: maxTiles, - duration: newDuration, - hasFinished: false, + elapsedDuration: newDuration, + tilesPerSecond: tilesPerSecond, + isTPSArtificiallyCapped: + tilesPerSecond >= (rateLimit ?? double.infinity) - 0.5, + isComplete: false, ); - DownloadProgress _update({ + DownloadProgress _updateProgressWithTile({ required TileEvent? newTileEvent, required int newBufferedTiles, required double newBufferedSize, required Duration newDuration, - bool hasFinished = false, + required double tilesPerSecond, + required int? rateLimit, + bool isComplete = false, }) => DownloadProgress.__( lastTileEvent: newTileEvent ?? lastTileEvent, @@ -99,24 +127,27 @@ class DownloadProgress { : cachedSize, bufferedTiles: newBufferedTiles, bufferedSize: newBufferedSize, - prunedTiles: newTileEvent == null - ? prunedTiles - : newTileEvent.result.category == TileEventResultCategory.pruned - ? prunedTiles + 1 - : prunedTiles, - prunedSize: newTileEvent == null - ? prunedSize - : newTileEvent.result.category == TileEventResultCategory.pruned - ? prunedSize + (newTileEvent.tileImage!.lengthInBytes / 1024) - : prunedSize, + skippedTiles: newTileEvent == null + ? skippedTiles + : newTileEvent.result.category == TileEventResultCategory.skipped + ? skippedTiles + 1 + : skippedTiles, + skippedSize: newTileEvent == null + ? skippedSize + : newTileEvent.result.category == TileEventResultCategory.skipped + ? skippedSize + (newTileEvent.tileImage!.lengthInBytes / 1024) + : skippedSize, failedTiles: newTileEvent == null ? failedTiles : newTileEvent.result.category == TileEventResultCategory.failed ? failedTiles + 1 : failedTiles, maxTiles: maxTiles, - duration: newDuration, - hasFinished: hasFinished, + elapsedDuration: newDuration, + tilesPerSecond: tilesPerSecond, + isTPSArtificiallyCapped: + tilesPerSecond >= (rateLimit ?? double.infinity) - 0.5, + isComplete: isComplete, ); @override @@ -128,25 +159,29 @@ class DownloadProgress { cachedSize == other.cachedSize && bufferedTiles == other.bufferedTiles && bufferedSize == other.bufferedSize && - prunedTiles == other.prunedTiles && - prunedSize == other.prunedSize && + skippedTiles == other.skippedTiles && + skippedSize == other.skippedSize && failedTiles == other.failedTiles && maxTiles == other.maxTiles && - duration == other.duration && - hasFinished == other.hasFinished); + elapsedDuration == other.elapsedDuration && + tilesPerSecond == other.tilesPerSecond && + isTPSArtificiallyCapped == other.isTPSArtificiallyCapped && + isComplete == other.isComplete); @override int get hashCode => Object.hashAllUnordered([ - _lastTileEvent.hashCode, - cachedTiles.hashCode, - cachedSize.hashCode, - bufferedTiles.hashCode, - bufferedSize.hashCode, - prunedTiles.hashCode, - prunedSize.hashCode, - failedTiles.hashCode, - maxTiles.hashCode, - duration.hashCode, - hasFinished.hashCode, + _lastTileEvent, + cachedTiles, + cachedSize, + bufferedTiles, + bufferedSize, + skippedTiles, + skippedSize, + failedTiles, + maxTiles, + elapsedDuration, + tilesPerSecond, + isTPSArtificiallyCapped, + isComplete, ]); } diff --git a/lib/src/bulk_download/internal_timing_progress_management.dart b/lib/src/bulk_download/internal_timing_progress_management.dart deleted file mode 100644 index 47b44403..00000000 --- a/lib/src/bulk_download/internal_timing_progress_management.dart +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright © Luka S (JaffaKetchup) under GPL-v3 -// A full license can be found at .\LICENSE - -import 'dart:async'; - -import 'package:collection/collection.dart'; - -/// Object containing the [timestamp] of the measurement and the percentage -/// [progress] (0-1) of the applicable tile -class TimestampProgress { - /// Time at which the measurement of progress was taken - final DateTime timestamp; - - /// Percentage progress (0-1) of the applicable tile - final double progress; - - /// Object containing the [timestamp] of the measurement and the percentage - /// [progress] (0-1) of the applicable tile - TimestampProgress( - this.timestamp, - this.progress, - ); - - @override - String toString() => - 'TileTimestampProgress(timestamp: $timestamp, progress: $progress)'; - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - - return other is TimestampProgress && - other.timestamp == timestamp && - other.progress == progress; - } - - @override - int get hashCode => timestamp.hashCode ^ progress.hashCode; -} - -/// Internal class for managing the tiles per second ([averageTPS]) measurement -/// of a download -class InternalProgressTimingManagement { - static const double _smoothing = 0.05; - - late Timer _timer; - final List _tpsMeasurements = []; - final Map _rawProgresses = {}; - double _averageTPS = 0; - - /// Retrieve the number of tiles per second - /// - /// Always 0 if [start] has not been called or [stop] has been called - double get averageTPS => _averageTPS; - - /// Calculate the number of tiles that are being downloaded per second on - /// average, and write to [averageTPS] - /// - /// Uses an exponentially smoothed moving average algorithm instead of a linear - /// average algorithm. This should lead to more accurate estimations based on - /// this data. The full original algorithm (written in Python) can be found at - /// https://stackoverflow.com/a/54264570/11846040. - void start() => _timer = Timer.periodic(const Duration(seconds: 1), (_) { - _rawProgresses.removeWhere( - (_, v) => v.timestamp - .isBefore(DateTime.now().subtract(const Duration(seconds: 1))), - ); - _tpsMeasurements.add(_rawProgresses.values.map((e) => e.progress).sum); - - _averageTPS = _tpsMeasurements.length == 1 - ? _tpsMeasurements[0] - : (_smoothing * _tpsMeasurements.last) + - ((1 - _smoothing) * - _tpsMeasurements.sum / - _tpsMeasurements.length); - }); - - /// Stop calculating the [averageTPS] measurement - void stop() { - _timer.cancel(); - _averageTPS = 0; - } - - /// Insert a new tile progress event into [_rawProgresses], to be accounted for - /// by [averageTPS] - void registerEvent(String url, TimestampProgress progress) => - _rawProgresses[url] = progress; -} diff --git a/lib/src/bulk_download/manager.dart b/lib/src/bulk_download/manager.dart index ad04c9d6..20409ac1 100644 --- a/lib/src/bulk_download/manager.dart +++ b/lib/src/bulk_download/manager.dart @@ -11,9 +11,10 @@ Future _downloadManager( FMTCTileProvider tileProvider, int parallelThreads, int maxBufferLength, - bool pruneExistingTiles, - bool pruneSeaTiles, + bool skipExistingTiles, + bool skipSeaTiles, Duration? maxReportInterval, + int? rateLimit, }) input, ) async { // Count number of tiles @@ -25,7 +26,7 @@ Future _downloadManager( // Setup sea tile removal system Uint8List? seaTileBytes; - if (input.pruneSeaTiles) { + if (input.skipSeaTiles) { try { seaTileBytes = await http.readBytes( Uri.parse( @@ -64,11 +65,22 @@ Future _downloadManager( debugName: '[FMTC] Tile Coords Generator Thread', ); final tileQueue = StreamQueue( - tileRecievePort.skip(input.region.start).take( - input.region.end == null - ? largestInt - : (input.region.end! - input.region.start), - ), + input.rateLimit == null + ? tileRecievePort.skip(input.region.start).take( + input.region.end == null + ? largestInt + : (input.region.end! - input.region.start), + ) + : RateLimitedStream.fromSourceStream( + emitEvery: Duration( + microseconds: ((1 / input.rateLimit!) * 1000000).ceil(), + ), + sourceStream: tileRecievePort.skip(input.region.start).take( + input.region.end == null + ? largestInt + : (input.region.end! - input.region.start), + ), + ).stream, ); final requestTilePort = await tileQueue.next as SendPort; @@ -80,6 +92,10 @@ Future _downloadManager( // Start progress tracking final downloadDuration = Stopwatch()..start(); var lastDownloadProgress = DownloadProgress._initial(maxTiles: maxTiles); + int mptTileIndex = -1; + int mptSlots = 50; + final microsecondsPerTile = List.filled(mptSlots, null, growable: true); + final tpsRates = List.filled(20, null); // Setup cancel, pause, and resume handling final threadPausedStates = List.generate( @@ -120,9 +136,13 @@ Future _downloadManager( if (lastDownloadProgress != DownloadProgress._initial(maxTiles: maxTiles) && pauseResumeSignal.isCompleted) { + final tps = tpsRates.nonNulls.average; send( - lastDownloadProgress = - lastDownloadProgress._updateDuration(downloadDuration.elapsed), + lastDownloadProgress = lastDownloadProgress._updateProgress( + newDuration: downloadDuration.elapsed, + tilesPerSecond: tps, + rateLimit: input.rateLimit, + ), ); } }, @@ -131,6 +151,9 @@ Future _downloadManager( fallbackReportTimer = null; } + // TODO: This might be the wrong place to put this + final tileTimer = Stopwatch(); + // Start download threads & wait for download to complete/cancelled await Future.wait( List.generate( @@ -152,7 +175,7 @@ Future _downloadManager( tileProvider: input.tileProvider, maxBufferLength: (input.maxBufferLength / input.parallelThreads).ceil(), - pruneExistingTiles: input.pruneExistingTiles, + skipExistingTiles: input.skipExistingTiles, seaTileBytes: seaTileBytes, ), onExit: downloadThreadRecievePort.sendPort, @@ -175,6 +198,17 @@ Future _downloadManager( (evt) async { // Thread is sending tile data if (evt is TileEvent) { + // Handle progress estimation measurements + mptTileIndex++; + microsecondsPerTile[mptTileIndex % mptSlots] = + (tileTimer..stop()).elapsedMicroseconds; + final rawTPS = + (1 / (microsecondsPerTile.nonNulls.average)) * 1000000; + microsecondsPerTile.length = mptSlots = rawTPS.ceil() * 2; + tpsRates[mptTileIndex % 20] = rawTPS; + final tps = tpsRates.nonNulls.average; + + // If buffering is in use, send a progress update with buffer info if (input.maxBufferLength != 0) { if (evt.result == TileEventResult.success) { threadBuffers[threadNo] = ( @@ -189,7 +223,8 @@ Future _downloadManager( } send( - lastDownloadProgress = lastDownloadProgress._update( + lastDownloadProgress = + lastDownloadProgress._updateProgressWithTile( newTileEvent: evt, newBufferedTiles: threadBuffers .map((e) => e.tiles) @@ -198,15 +233,20 @@ Future _downloadManager( .map((e) => e.size) .reduce((a, b) => a + b), newDuration: downloadDuration.elapsed, + tilesPerSecond: tps, + rateLimit: input.rateLimit, ), ); } else { send( - lastDownloadProgress = lastDownloadProgress._update( + lastDownloadProgress = + lastDownloadProgress._updateProgressWithTile( newTileEvent: evt, newBufferedTiles: 0, newBufferedSize: 0, newDuration: downloadDuration.elapsed, + tilesPerSecond: tps, + rateLimit: input.rateLimit, ), ); } @@ -220,6 +260,10 @@ Future _downloadManager( await pauseResumeSignal.future; } + tileTimer + ..reset() + ..start(); + requestTilePort.send(null); try { sendPort.send(await tileQueue.next); @@ -260,12 +304,14 @@ Future _downloadManager( // Send final buffer cleared progress report fallbackReportTimer?.cancel(); send( - lastDownloadProgress = lastDownloadProgress._update( + lastDownloadProgress = lastDownloadProgress._updateProgressWithTile( newTileEvent: null, newBufferedTiles: 0, newBufferedSize: 0, newDuration: downloadDuration.elapsed, - hasFinished: true, + tilesPerSecond: 0, + rateLimit: input.rateLimit, + isComplete: true, ), ); diff --git a/lib/src/bulk_download/rate_limited_stream.dart b/lib/src/bulk_download/rate_limited_stream.dart new file mode 100644 index 00000000..5ffa2cd2 --- /dev/null +++ b/lib/src/bulk_download/rate_limited_stream.dart @@ -0,0 +1,96 @@ +// Copyright © Luka S (JaffaKetchup) under GPL-v3 +// A full license can be found at .\LICENSE + +import 'dart:async'; + +/// Transforms a series of events to an output [stream] where a delay of at least +/// [emitEvery] is inserted between every event +/// +/// There are 3 ways to contruct this: +/// - [RateLimitedStream] : No initial stream, remains open until [close] called +/// - [RateLimitedStream.withInitialStream] : Uses initial stream, remains open +/// until [close] called +/// - [RateLimitedStream.fromSourceStream] : One-shot from source stream, closes +/// automatically after output completes +/// +/// Remember, input streams are likely to close before the output [stream]. +/// +/// Do not call [close] if input streams are still outputting or new events are +/// being [add]ed. +/// +/// Optionally pass in a `customStreamController`. If passed in, use only this +/// object's methods to manipulate the stream, not the passed in controller's +/// methods. +/// +/// Illustration of [stream], where one decimal is 500ms, and [emitEvery] is set +/// to 1s: +/// ``` +/// Input: .ABC....DE..F........GH +/// Output: .A..B..C..D..E..F....G..H +/// ``` +class RateLimitedStream { + RateLimitedStream({ + required this.emitEvery, + this.cancelOnError = false, + StreamController? customStreamController, + }) : _streamController = customStreamController ?? StreamController(); + + RateLimitedStream.withInitialStream({ + required this.emitEvery, + required Stream initialStream, + this.cancelOnError = false, + StreamController? customStreamController, + }) : _streamController = customStreamController ?? StreamController() { + _streamController.addStream(initialStream, cancelOnError: cancelOnError); + } + + RateLimitedStream.fromSourceStream({ + required this.emitEvery, + required Stream sourceStream, + this.cancelOnError = false, + StreamController? customStreamController, + }) : _streamController = customStreamController ?? StreamController() { + _streamController + .addStream(sourceStream, cancelOnError: cancelOnError) + .then((_) => close()); + } + + final Duration emitEvery; + final bool cancelOnError; + + final StreamController _streamController; + + void add(E event) => _streamController.sink.add(event); + Future addStream(Stream stream) => + _streamController.sink.addStream(stream); + void addError(Object error, [StackTrace? stackTrace]) => + _streamController.sink.addError(error, stackTrace); + Future close() => _streamController.sink.close(); + Future get done => _streamController.sink.done; + + Stream get stream { + Completer emitEvt = Completer()..complete(); + Timer.periodic(emitEvery, (_) { + if (!emitEvt.isCompleted) emitEvt.complete(); + }); + + return _streamController.stream + .transform>( + StreamTransformer.fromHandlers( + handleData: (data, sink) => sink.add( + (() async { + await emitEvt.future; + emitEvt = Completer(); + return data; + })(), + ), + handleError: (error, stackTrace, sink) { + sink.addError(error, stackTrace); + if (cancelOnError) sink.close(); + }, + handleDone: (sink) => sink.close(), + ), + ) + .asyncMap((e) => e); + } +} diff --git a/lib/src/bulk_download/thread.dart b/lib/src/bulk_download/thread.dart index f57742bf..57f3f6da 100644 --- a/lib/src/bulk_download/thread.dart +++ b/lib/src/bulk_download/thread.dart @@ -11,7 +11,7 @@ Future _singleDownloadThread( DownloadableRegion region, FMTCTileProvider tileProvider, int maxBufferLength, - bool pruneExistingTiles, + bool skipExistingTiles, Uint8List? seaTileBytes, }) input, ) async { @@ -65,7 +65,7 @@ Future _singleDownloadThread( final existingTile = await db.tiles.get(DatabaseTools.hash(url)); // Skip if tile already exists and user demands existing tile pruning - if (input.pruneExistingTiles && existingTile != null) { + if (input.skipExistingTiles && existingTile != null) { send( TileEvent._( TileEventResult.alreadyExisting, diff --git a/lib/src/bulk_download/tile_event.dart b/lib/src/bulk_download/tile_event.dart index 519c43d8..9adc9e8a 100644 --- a/lib/src/bulk_download/tile_event.dart +++ b/lib/src/bulk_download/tile_event.dart @@ -14,10 +14,10 @@ enum TileEventResultCategory { /// intentionally /// /// This may be because it: - /// - already existed & `pruneExistingTiles` was `true`: + /// - already existed & `skipExistingTiles` was `true`: /// [TileEventResult.alreadyExisting] - /// - was a sea tile & `pruneSeaTiles` was `true`: [TileEventResult.isSeaTile] - pruned, + /// - was a sea tile & `skipSeaTiles` was `true`: [TileEventResult.isSeaTile] + skipped, /// The associated tile was not successfully downloaded, potentially for a /// variety of reasons. @@ -34,12 +34,12 @@ enum TileEventResult { success(TileEventResultCategory.cached), /// The associated tile was not downloaded (intentionally), becuase it already - /// existed & `pruneExistingTiles` was `true` - alreadyExisting(TileEventResultCategory.pruned), + /// existed & `skipExistingTiles` was `true` + alreadyExisting(TileEventResultCategory.skipped), /// The associated tile was downloaded, but was not cached (intentionally), - /// because it was a sea tile & `pruneSeaTiles` was `true` - isSeaTile(TileEventResultCategory.pruned), + /// because it was a sea tile & `skipSeaTiles` was `true` + isSeaTile(TileEventResultCategory.skipped), /// The associated tile was not successfully downloaded because the tile server /// responded with a status code other than HTTP 200 OK @@ -71,7 +71,7 @@ class TileEvent { /// categorization of this result into 3 categories: /// /// - [TileEventResultCategory.cached] (tile was downloaded and cached) - /// - [TileEventResultCategory.pruned] (tile was not cached, but intentionally) + /// - [TileEventResultCategory.skipped] (tile was not cached, but intentionally) /// - [TileEventResultCategory.failed] (tile was not cached, due to an error) final TileEventResult result; @@ -120,11 +120,11 @@ class TileEvent { @override int get hashCode => Object.hashAllUnordered([ - result.hashCode, - url.hashCode, - tileImage.hashCode, - fetchResponse.hashCode, - fetchError.hashCode, - _wasBufferReset.hashCode, + result, + url, + tileImage, + fetchResponse, + fetchError, + _wasBufferReset, ]); } diff --git a/lib/src/providers/image_provider.dart b/lib/src/providers/image_provider.dart index 96cc3f59..f5078bbf 100644 --- a/lib/src/providers/image_provider.dart +++ b/lib/src/providers/image_provider.dart @@ -274,11 +274,7 @@ class FMTCImageProvider extends ImageProvider { other.options == options); @override - int get hashCode => Object.hashAllUnordered([ - coords.hashCode, - provider.hashCode, - options.hashCode, - ]); + int get hashCode => Object.hash(coords, provider, options); } Future _removeOldestTile(List args) async { diff --git a/lib/src/providers/tile_provider.dart b/lib/src/providers/tile_provider.dart index 0a9f4bc3..1aad3d90 100644 --- a/lib/src/providers/tile_provider.dart +++ b/lib/src/providers/tile_provider.dart @@ -93,10 +93,6 @@ class FMTCTileProvider extends TileProvider { other.headers == headers); @override - int get hashCode => Object.hashAllUnordered([ - httpClient.hashCode, - settings.hashCode, - storeDirectory.hashCode, - headers.hashCode, - ]); + int get hashCode => + Object.hash(httpClient, settings, storeDirectory, headers); } diff --git a/lib/src/regions/base_region.dart b/lib/src/regions/base_region.dart index 7448a147..e4af1914 100644 --- a/lib/src/regions/base_region.dart +++ b/lib/src/regions/base_region.dart @@ -41,6 +41,7 @@ sealed class BaseRegion { /// /// _This property is currently redundant, but usage is planned in future /// versions._ + @experimental final String? name; /// Output a value of type [T] dependent on `this` and its type diff --git a/lib/src/regions/circle.dart b/lib/src/regions/circle.dart index 941d42de..81281b2e 100644 --- a/lib/src/regions/circle.dart +++ b/lib/src/regions/circle.dart @@ -110,5 +110,5 @@ class CircleRegion extends BaseRegion { super == other); @override - int get hashCode => Object.hashAllUnordered([center, radius, super.hashCode]); + int get hashCode => Object.hash(center, radius, super.hashCode); } diff --git a/lib/src/regions/downloadable_region.dart b/lib/src/regions/downloadable_region.dart index 8d38585c..907ceca7 100644 --- a/lib/src/regions/downloadable_region.dart +++ b/lib/src/regions/downloadable_region.dart @@ -75,12 +75,12 @@ class DownloadableRegion { @override int get hashCode => Object.hashAllUnordered([ - originalRegion.hashCode, - minZoom.hashCode, - maxZoom.hashCode, - options.hashCode, - start.hashCode, - end.hashCode, - crs.hashCode, + originalRegion, + minZoom, + maxZoom, + options, + start, + end, + crs, ]); } diff --git a/lib/src/regions/line.dart b/lib/src/regions/line.dart index 707ea6c5..a5d7e7b4 100644 --- a/lib/src/regions/line.dart +++ b/lib/src/regions/line.dart @@ -166,5 +166,5 @@ class LineRegion extends BaseRegion { super == other); @override - int get hashCode => Object.hashAllUnordered([line, radius, super.hashCode]); + int get hashCode => Object.hash(line, radius, super.hashCode); } diff --git a/lib/src/regions/rectangle.dart b/lib/src/regions/rectangle.dart index ca9ed2e4..502d0754 100644 --- a/lib/src/regions/rectangle.dart +++ b/lib/src/regions/rectangle.dart @@ -79,5 +79,5 @@ class RectangleRegion extends BaseRegion { (other is RectangleRegion && other.bounds == bounds && super == other); @override - int get hashCode => Object.hashAllUnordered([bounds, super.hashCode]); + int get hashCode => Object.hash(bounds, super.hashCode); } diff --git a/lib/src/root/migrator.dart b/lib/src/root/migrator.dart index b49d404d..ab9eb231 100644 --- a/lib/src/root/migrator.dart +++ b/lib/src/root/migrator.dart @@ -161,7 +161,7 @@ class RootMigrator { }, ), )) - .whereNotNull() + .nonNulls .toList(), ), ); @@ -261,5 +261,5 @@ class _FilesystemSanitiserResult { } @override - int get hashCode => validOutput.hashCode ^ errorMessages.hashCode; + int get hashCode => Object.hash(validOutput, errorMessages); } diff --git a/lib/src/settings/tile_provider_settings.dart b/lib/src/settings/tile_provider_settings.dart index f5cf1842..6bbd8172 100644 --- a/lib/src/settings/tile_provider_settings.dart +++ b/lib/src/settings/tile_provider_settings.dart @@ -100,10 +100,10 @@ class FMTCTileProviderSettings { @override int get hashCode => Object.hashAllUnordered([ - behavior.hashCode, - cachedValidDuration.hashCode, - maxStoreLength.hashCode, - errorHandler.hashCode, - obscuredQueryParams.hashCode, + behavior, + cachedValidDuration, + maxStoreLength, + errorHandler, + obscuredQueryParams, ]); } diff --git a/lib/src/store/download.dart b/lib/src/store/download.dart index e2ae4652..5f07493e 100644 --- a/lib/src/store/download.dart +++ b/lib/src/store/download.dart @@ -46,9 +46,9 @@ class DownloadManagement { /// - [maxBufferLength] (defaults to 100 | 0 to disable): number of tiles to /// temporarily buffer before writing to the store (split evenly between /// [parallelThreads]) - /// - [pruneExistingTiles] (defaults to `true`): whether to skip downloading + /// - [skipExistingTiles] (defaults to `true`): whether to skip downloading /// tiles that are already cached - /// - [pruneSeaTiles] (defaults to `true`): whether to skip caching tiles that + /// - [skipSeaTiles] (defaults to `true`): whether to skip caching tiles that /// are entirely sea (this is decided based on a comparison to the tile at /// x0y0z17) /// @@ -62,6 +62,18 @@ class DownloadManagement { /// /// --- /// + /// Although disabled `null` by default, [rateLimit] can be used to impose a + /// limit on the maximum number of tiles requests that can be made per second. + /// This is useful to avoid placing too much strain on tile servers and avoid + /// rate limiting. Note that the real number of requests per second may exceed + /// [rateLimit] by a very small amount. + /// + /// To check whether the current [DownloadProgress.tilesPerSecond] statistic is + /// currently limited by [rateLimit], check + /// [DownloadProgress.isTPSArtificiallyCapped]. + /// + /// --- + /// /// {@macro num_instances} @useResult Stream startForeground({ @@ -69,9 +81,10 @@ class DownloadManagement { FMTCTileProviderSettings? tileProviderSettings, int parallelThreads = 5, int maxBufferLength = 100, - bool pruneExistingTiles = true, - bool pruneSeaTiles = true, + bool skipExistingTiles = true, + bool skipSeaTiles = true, Duration? maxReportInterval = const Duration(seconds: 1), + int? rateLimit, bool disableRecovery = false, Object instanceId = 0, }) async* { @@ -96,7 +109,7 @@ class DownloadManagement { throw ArgumentError.value( parallelThreads, 'parallelThreads', - 'must be greater than 0', + 'must be 1 or greater', ); } @@ -104,7 +117,15 @@ class DownloadManagement { throw ArgumentError.value( maxBufferLength, 'maxBufferLength', - 'must be greater than -1', + 'must be 0 or greater', + ); + } + + if ((rateLimit ?? 2) < 1) { + throw ArgumentError.value( + rateLimit, + 'rateLimit', + 'must be 1 or greater, or null', ); } @@ -141,9 +162,10 @@ class DownloadManagement { ), parallelThreads: parallelThreads, maxBufferLength: maxBufferLength, - pruneExistingTiles: pruneExistingTiles, - pruneSeaTiles: pruneSeaTiles, + skipExistingTiles: skipExistingTiles, + skipSeaTiles: skipSeaTiles, maxReportInterval: maxReportInterval, + rateLimit: rateLimit, ), onExit: recievePort.sendPort, debugName: '[FMTC] Master Bulk Download Thread', From 8912d9d8ce9eb368cc07b4636697e1d5d1ac9a9c Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 6 Jul 2023 14:17:22 +0100 Subject: [PATCH 024/168] Removed TODO comment Former-commit-id: ea0897d5ae276aa5821fe27c4d7c6befe084d35b [formerly bdea586a8c593cb723d6e28a905e74bcba32e124] Former-commit-id: 2b3ddedbd6e67a4518ff36b57721cd626e50faac --- lib/src/bulk_download/manager.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/bulk_download/manager.dart b/lib/src/bulk_download/manager.dart index 20409ac1..84ad81e9 100644 --- a/lib/src/bulk_download/manager.dart +++ b/lib/src/bulk_download/manager.dart @@ -151,7 +151,7 @@ Future _downloadManager( fallbackReportTimer = null; } - // TODO: This might be the wrong place to put this + // This might be the wrong place to put this? final tileTimer = Stopwatch(); // Start download threads & wait for download to complete/cancelled From 6d8071ed23ded7635ba9c9af2d847e558c75d2c3 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 6 Jul 2023 14:21:00 +0100 Subject: [PATCH 025/168] Fixed dependency Former-commit-id: 72e672f696b33de74a8757e7fc29e4749a8aa6a3 [formerly 332b1f22e4adb72872293cb30771522c1512aa45] Former-commit-id: 2de7f08db408933241e375fae78196d20133b05a --- example/pubspec.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 3c6d5b58..feb1b753 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -36,8 +36,7 @@ dependencies: dependency_overrides: flutter_map: git: - url: https://github.com/rorystephenson/flutter_map.git - ref: flutter-map-state-refactor + url: https://github.com/fleaflet/flutter_map.git flutter_map_tile_caching: path: ../ http: ^1.0.0 From 89110c3cf31c594772244bebfe615b336ee0883e Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 6 Jul 2023 23:07:52 +0100 Subject: [PATCH 026/168] Reduced redundant tiles generated in `LineRegion` Improved example application Former-commit-id: 605158adde90e2f182f23cdb71854e4162ca27d0 [formerly 0f8282761c86d0dd6f55dbead0ba404366dd5c2e] Former-commit-id: 2f78652bdc9f5fe447d4cd53f18136c24e4433c4 --- .../components/buffering_configuration.dart | 36 ------ .../components/optional_functionality.dart | 108 ------------------ .../download_region/download_region.dart | 6 +- .../pages/downloader/components/map_view.dart | 27 +++-- lib/src/bulk_download/tile_loops/count.dart | 17 ++- .../bulk_download/tile_loops/generate.dart | 19 +-- lib/src/bulk_download/tile_loops/shared.dart | 13 ++- 7 files changed, 56 insertions(+), 170 deletions(-) delete mode 100644 example/lib/screens/download_region/components/buffering_configuration.dart delete mode 100644 example/lib/screens/download_region/components/optional_functionality.dart diff --git a/example/lib/screens/download_region/components/buffering_configuration.dart b/example/lib/screens/download_region/components/buffering_configuration.dart deleted file mode 100644 index 9d85f1fd..00000000 --- a/example/lib/screens/download_region/components/buffering_configuration.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -import '../../../shared/state/download_provider.dart'; - -class BufferingConfiguration extends StatelessWidget { - const BufferingConfiguration({super.key}); - - @override - Widget build(BuildContext context) => Consumer( - builder: (context, provider, _) => Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text('BUFFERING CONFIGURATION'), - if (provider.regionTiles == null) - const CircularProgressIndicator() - else - Builder( - builder: (context) { - final max = (provider.regionTiles! / 2).floorToDouble(); - return Slider( - value: provider.bufferingAmount.clamp(0, max).toDouble(), - max: max, - divisions: (max / 10).ceil(), - onChanged: (value) => - provider.bufferingAmount = value.clamp(0, max).round(), - label: provider.bufferingAmount == 0 - ? 'Disabled' - : 'Write every ${provider.bufferingAmount} tiles', - ); - }, - ), - ], - ), - ); -} diff --git a/example/lib/screens/download_region/components/optional_functionality.dart b/example/lib/screens/download_region/components/optional_functionality.dart deleted file mode 100644 index dbdde164..00000000 --- a/example/lib/screens/download_region/components/optional_functionality.dart +++ /dev/null @@ -1,108 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -import '../../../shared/state/download_provider.dart'; - -class OptionalFunctionality extends StatelessWidget { - const OptionalFunctionality({super.key}); - - @override - Widget build(BuildContext context) => Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text('OPTIONAL FUNCTIONALITY'), - Consumer( - builder: (context, provider, _) => Column( - children: [ - Row( - children: [ - const Text('Skip Existing Tiles'), - const Spacer(), - IconButton( - onPressed: () { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - '`preventRedownload` within API. Controls whether the script will re-download tiles that already exist or not.', - ), - duration: Duration(seconds: 8), - ), - ); - }, - icon: const Icon(Icons.help_outline), - ), - Switch( - value: provider.skipExistingTiles, - onChanged: (val) => provider.skipExistingTiles = val, - activeColor: Theme.of(context).colorScheme.primary, - ) - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text('Skip Sea Tiles'), - const Spacer(), - IconButton( - onPressed: () { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - '`seaTileRemoval` within API. Deletes tiles that are pure sea - tiles that match the tile at x=0, y=0, z=19 exactly. Note that this saves storage space, but not time or data: tiles still have to be downloaded to be matched. Not supported on satelite servers.', - ), - duration: Duration(seconds: 8), - ), - ); - }, - icon: const Icon(Icons.help_outline), - ), - Switch( - value: provider.skipSeaTiles, - onChanged: (val) => provider.skipSeaTiles = val, - activeColor: Theme.of(context).colorScheme.primary, - ) - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text('Disable Recovery'), - const Spacer(), - IconButton( - onPressed: () { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - 'Disables automatic recovery. Use only for testing or in special circumstances.', - ), - duration: Duration(seconds: 8), - ), - ); - }, - icon: const Icon(Icons.help_outline), - ), - Switch( - value: provider.disableRecovery, - onChanged: (val) async { - if (val) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - 'This option is not recommended, use with caution', - ), - duration: Duration(seconds: 8), - ), - ); - } - provider.disableRecovery = val; - }, - activeColor: Colors.amber, - ) - ], - ), - ], - ), - ), - ], - ); -} diff --git a/example/lib/screens/download_region/download_region.dart b/example/lib/screens/download_region/download_region.dart index 8624e899..06a55950 100644 --- a/example/lib/screens/download_region/download_region.dart +++ b/example/lib/screens/download_region/download_region.dart @@ -200,14 +200,14 @@ class _DownloadRegionPopupState extends State { const Spacer(), Icon( Icons.lock, - color: provider.rateLimit == 100 + color: provider.rateLimit == 200 ? Colors.amber : Colors.white.withOpacity(0.2), ), const SizedBox(width: 16), IntrinsicWidth( child: TextFormField( - initialValue: '100', + initialValue: '200', textAlign: TextAlign.end, keyboardType: TextInputType.number, decoration: const InputDecoration( @@ -219,7 +219,7 @@ class _DownloadRegionPopupState extends State { FilteringTextInputFormatter.digitsOnly, const NumericalRangeFormatter( min: 1, - max: 100, + max: 200, ), ], onChanged: (value) => provider.rateLimit = diff --git a/example/lib/screens/main/pages/downloader/components/map_view.dart b/example/lib/screens/main/pages/downloader/components/map_view.dart index c7257567..e3401d0a 100644 --- a/example/lib/screens/main/pages/downloader/components/map_view.dart +++ b/example/lib/screens/main/pages/downloader/components/map_view.dart @@ -4,6 +4,7 @@ import 'dart:math'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/plugin_api.dart'; import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; @@ -253,7 +254,10 @@ class _MapViewState extends State { divisions: 99, label: '${downloadProvider.lineRegionRadius.round()} meters', - onChanged: (val) { + onChanged: (val) => setState( + () => downloadProvider.lineRegionRadius = val, + ), + onChangeEnd: (val) { downloadProvider.lineRegionRadius = val; _updateLineRegion(); }, @@ -262,13 +266,20 @@ class _MapViewState extends State { const VerticalDivider(), IconButton( onPressed: () async { - final result = - await FilePicker.platform.pickFiles( - dialogTitle: 'Open GPX', - type: FileType.custom, - allowedExtensions: ['gpx', 'kml'], - allowMultiple: true, - ); + late final FilePickerResult? result; + try { + result = await FilePicker.platform.pickFiles( + dialogTitle: 'Parse From GPX', + type: FileType.custom, + allowedExtensions: ['gpx', 'kml'], + allowMultiple: true, + ); + } on PlatformException catch (_) { + result = await FilePicker.platform.pickFiles( + dialogTitle: 'Parse From GPX', + allowMultiple: true, + ); + } if (result != null) { final gpxReader = GpxReader(); diff --git a/lib/src/bulk_download/tile_loops/count.dart b/lib/src/bulk_download/tile_loops/count.dart index 695d81af..8b78bdeb 100644 --- a/lib/src/bulk_download/tile_loops/count.dart +++ b/lib/src/bulk_download/tile_loops/count.dart @@ -130,6 +130,8 @@ class TilesCounter { for (double zoomLvl = region.minZoom.toDouble(); zoomLvl <= region.maxZoom; zoomLvl++) { + final generatedTiles = []; + for (final rect in lineOutline) { final rotatedRectangle = ( bottomLeft: rect[0], @@ -190,6 +192,13 @@ class TilesCounter { for (int x = straightRectangleNW.x; x <= straightRectangleSE.x; x++) { bool foundOverlappingTile = false; for (int y = straightRectangleNW.y; y <= straightRectangleSE.y; y++) { + final tile = _Polygon( + CustomPoint(x, y), + CustomPoint(x + 1, y), + CustomPoint(x + 1, y + 1), + CustomPoint(x, y + 1), + ); + if (generatedTiles.contains(tile.hashCode)) continue; if (overlap( _Polygon( rotatedRectangleNW, @@ -197,14 +206,10 @@ class TilesCounter { rotatedRectangleSE, rotatedRectangleSW, ), - _Polygon( - CustomPoint(x, y), - CustomPoint(x + 1, y), - CustomPoint(x + 1, y + 1), - CustomPoint(x, y + 1), - ), + tile, )) { numberOfTiles++; + generatedTiles.add(tile.hashCode); foundOverlappingTile = true; } else if (foundOverlappingTile) { break; diff --git a/lib/src/bulk_download/tile_loops/generate.dart b/lib/src/bulk_download/tile_loops/generate.dart index 4920bfd7..3b8eac29 100644 --- a/lib/src/bulk_download/tile_loops/generate.dart +++ b/lib/src/bulk_download/tile_loops/generate.dart @@ -152,6 +152,8 @@ class TilesGenerator { for (double zoomLvl = region.minZoom.toDouble(); zoomLvl <= region.maxZoom; zoomLvl++) { + final generatedTiles = []; + for (final rect in lineOutline) { final rotatedRectangle = ( bottomLeft: rect[0], @@ -212,6 +214,13 @@ class TilesGenerator { for (int x = straightRectangleNW.x; x <= straightRectangleSE.x; x++) { bool foundOverlappingTile = false; for (int y = straightRectangleNW.y; y <= straightRectangleSE.y; y++) { + final tile = _Polygon( + CustomPoint(x, y), + CustomPoint(x + 1, y), + CustomPoint(x + 1, y + 1), + CustomPoint(x, y + 1), + ); + if (generatedTiles.contains(tile.hashCode)) continue; if (overlap( _Polygon( rotatedRectangleNW, @@ -219,16 +228,12 @@ class TilesGenerator { rotatedRectangleSE, rotatedRectangleSW, ), - _Polygon( - CustomPoint(x, y), - CustomPoint(x + 1, y), - CustomPoint(x + 1, y + 1), - CustomPoint(x, y + 1), - ), + tile, )) { + generatedTiles.add(tile.hashCode); + foundOverlappingTile = true; await requestQueue.next; input.sendPort.send((x, y, zoomLvl.toInt())); - foundOverlappingTile = true; } else if (foundOverlappingTile) { break; } diff --git a/lib/src/bulk_download/tile_loops/shared.dart b/lib/src/bulk_download/tile_loops/shared.dart index 586f5113..b624934b 100644 --- a/lib/src/bulk_download/tile_loops/shared.dart +++ b/lib/src/bulk_download/tile_loops/shared.dart @@ -16,14 +16,23 @@ part 'count.dart'; part 'generate.dart'; class _Polygon { + _Polygon(this.nw, this.ne, this.se, this.sw) : points = [nw, ne, se, sw] { + hashCode = Object.hashAllUnordered(points); + } + final CustomPoint nw; final CustomPoint ne; final CustomPoint se; final CustomPoint sw; + final List> points; - _Polygon(this.nw, this.ne, this.se, this.sw); + @override + late final int hashCode; - List> get points => [nw, ne, se, sw]; + @override + bool operator ==(Object other) => + identical(this, other) || + (other is _Polygon && hashCode == other.hashCode); } CustomPoint _getTileSize(DownloadableRegion region) => From 0606ef43b585297c2c7346e92b6513509d5e9be6 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Tue, 11 Jul 2023 16:05:21 +0200 Subject: [PATCH 027/168] Updated CHANGELOG Former-commit-id: e1f5b51ba595b923f3271657450a0b5f3f3a2fb8 [formerly 57c54e4019eaca5a0193f96740f788a8af55df33] Former-commit-id: 0ed6e7587839d73a5d7d1d08836b7a396d6b7a13 --- CHANGELOG.md | 17 +++++++++++++---- pubspec.yaml | 2 +- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dfb9a847..ae7d0893 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,12 +16,21 @@ Many thanks to my sponsors, no matter how much or how little they donated. Spons ## [9.0.0] - 2023/XX/XX -* Added support for Flutter 3.10 and Dart 3 -* Added support for flutter_map 'v5' +* Migrated to Flutter 3.10 and Dart 3 +* Migrated to flutter_map v5 +* Added support for Isar v4 (bug fixes & stability improvements) +* Bulk downloading reimplementation + * Improved download speed significantly + * Added pause and resume functionality (to accompany existing cancel functionality) + * Added rate limiting support + * Fixed instability and bugs when cancelling buffering downloads + * Fixed generation of `LineRegion` tiles by reducing number of redundant duplicate tiles + * Removed support for bulk download buffering by size capacity +* Added secondary check to `FMTCImageProvider` to ensure responses are valid images * Added `StoreManagement.pruneTilesOlderThan` method * Replaced public facing `RegionType`/`type` with Dart 3 exhaustive switch statements through `BaseRegion/DownloadableRegion.when` & `RecoverableRegion.toRegion` -* Improved performance and fixed bugs -* Example application improvements +* Removed HTTP/2 support +* Improved & simplified example application ## [8.0.0] - 2023/05/05 diff --git a/pubspec.yaml b/pubspec.yaml index 9a1cb94c..683f81c2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_map_tile_caching description: Plugin for 'flutter_map' providing advanced caching functionality, with ability to download map regions for offline use. -version: 9.0.0-dev.2 +version: 9.0.0-dev.3 repository: https://github.com/JaffaKetchup/flutter_map_tile_caching issue_tracker: https://github.com/JaffaKetchup/flutter_map_tile_caching/issues documentation: https://fmtc.jaffaketchup.dev From 29ce0734c59b2f7552e32884f2c22bfce5df930f Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Tue, 11 Jul 2023 19:23:36 +0200 Subject: [PATCH 028/168] Changed `startForeground` arguments Changed `toDownloadable` arguments Removed background module from example app Updated CHANGELOG Former-commit-id: 0b9739842ab3774a9e2416abe426ac00f4b04f55 [formerly af08233d118f72dfcd813c4f8046535baba4d857] Former-commit-id: e9d08d00b505d6c7320940e15c5eb48a977ed5da --- CHANGELOG.md | 3 + .../download_region/download_region.dart | 12 +- example/lib/screens/main/main.dart | 135 +++++++++--------- .../pages/downloader/components/map_view.dart | 6 +- .../lib/shared/state/download_provider.dart | 4 +- example/pubspec.yaml | 1 - .../flutter/generated_plugin_registrant.cc | 3 - .../windows/flutter/generated_plugins.cmake | 1 - lib/flutter_map_tile_caching.dart | 1 + lib/src/bulk_download/manager.dart | 52 ++++--- lib/src/bulk_download/thread.dart | 31 ++-- lib/src/misc/obscure_query_params.dart | 16 +++ lib/src/providers/image_provider.dart | 6 +- lib/src/providers/tile_provider.dart | 10 +- lib/src/regions/base_region.dart | 8 +- lib/src/regions/circle.dart | 8 +- lib/src/regions/line.dart | 8 +- lib/src/regions/rectangle.dart | 8 +- lib/src/settings/tile_provider_settings.dart | 12 -- lib/src/store/download.dart | 29 ++-- pubspec.yaml | 2 +- windowsApplicationInstallerSetup.iss | 1 - 22 files changed, 194 insertions(+), 163 deletions(-) create mode 100644 lib/src/misc/obscure_query_params.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index ae7d0893..4f51f388 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,9 +23,12 @@ Many thanks to my sponsors, no matter how much or how little they donated. Spons * Improved download speed significantly * Added pause and resume functionality (to accompany existing cancel functionality) * Added rate limiting support + * Added support for multiple simultaneous downloads * Fixed instability and bugs when cancelling buffering downloads * Fixed generation of `LineRegion` tiles by reducing number of redundant duplicate tiles + * Fixed usage of `obscuredQueryParams` * Removed support for bulk download buffering by size capacity + * Removed support for custom `HttpClient`s * Added secondary check to `FMTCImageProvider` to ensure responses are valid images * Added `StoreManagement.pruneTilesOlderThan` method * Replaced public facing `RegionType`/`type` with Dart 3 exhaustive switch statements through `BaseRegion/DownloadableRegion.when` & `RecoverableRegion.toRegion` diff --git a/example/lib/screens/download_region/download_region.dart b/example/lib/screens/download_region/download_region.dart index 06a55950..120f2ac8 100644 --- a/example/lib/screens/download_region/download_region.dart +++ b/example/lib/screens/download_region/download_region.dart @@ -3,7 +3,6 @@ import 'package:flutter/services.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; import 'package:provider/provider.dart'; -//import 'package:shared_preferences/shared_preferences.dart'; import '../../shared/state/download_provider.dart'; import '../../shared/state/general_provider.dart'; @@ -112,9 +111,9 @@ class _DownloadRegionPopupState extends State { provider.selectedStore!.download .startForeground( region: widget.region.toDownloadable( - provider.minZoom, - provider.maxZoom, - TileLayer( + minZoom: provider.minZoom, + maxZoom: provider.maxZoom, + options: TileLayer( urlTemplate: metadata['sourceURL'], userAgentPackageName: 'dev.jaffaketchup.fmtc.demo', @@ -170,7 +169,8 @@ class _DownloadRegionPopupState extends State { const SizedBox(width: 16), IntrinsicWidth( child: TextFormField( - initialValue: '5', + initialValue: + provider.parallelThreads.toString(), textAlign: TextAlign.end, keyboardType: TextInputType.number, decoration: const InputDecoration( @@ -207,7 +207,7 @@ class _DownloadRegionPopupState extends State { const SizedBox(width: 16), IntrinsicWidth( child: TextFormField( - initialValue: '200', + initialValue: provider.rateLimit.toString(), textAlign: TextAlign.end, keyboardType: TextInputType.number, decoration: const InputDecoration( diff --git a/example/lib/screens/main/main.dart b/example/lib/screens/main/main.dart index cc18ee78..02733fba 100644 --- a/example/lib/screens/main/main.dart +++ b/example/lib/screens/main/main.dart @@ -1,7 +1,6 @@ import 'package:badges/badges.dart'; import 'package:flutter/material.dart' hide Badge; import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; -import 'package:fmtc_plus_background_downloading/fmtc_plus_background_downloading.dart'; import 'package:provider/provider.dart'; import '../../shared/state/download_provider.dart'; @@ -107,80 +106,78 @@ class _MainScreenState extends State { } @override - Widget build(BuildContext context) => FMTCBackgroundDownload( - child: Scaffold( - bottomNavigationBar: MediaQuery.of(context).size.width > 950 - ? null - : NavigationBar( - backgroundColor: - Theme.of(context).navigationBarTheme.backgroundColor, - onDestinationSelected: _onDestinationSelected, - selectedIndex: _currentPageIndex, - destinations: _destinations, - labelBehavior: MediaQuery.of(context).size.width > 450 - ? null - : NavigationDestinationLabelBehavior.alwaysHide, - height: 70, - ), - body: Row( - children: [ - if (MediaQuery.of(context).size.width > 950) - NavigationRail( - onDestinationSelected: _onDestinationSelected, - selectedIndex: _currentPageIndex, - groupAlignment: 0, - extended: extended, - destinations: _destinations - .map( - (d) => NavigationRailDestination( - icon: d.icon, - label: Text(d.label), - padding: const EdgeInsets.all(10), - ), - ) - .toList(), - leading: Row( - children: [ - AnimatedContainer( - width: extended ? 205 : 0, - duration: kThemeAnimationDuration, - curve: Curves.easeInOut, + Widget build(BuildContext context) => Scaffold( + bottomNavigationBar: MediaQuery.of(context).size.width > 950 + ? null + : NavigationBar( + backgroundColor: + Theme.of(context).navigationBarTheme.backgroundColor, + onDestinationSelected: _onDestinationSelected, + selectedIndex: _currentPageIndex, + destinations: _destinations, + labelBehavior: MediaQuery.of(context).size.width > 450 + ? null + : NavigationDestinationLabelBehavior.alwaysHide, + height: 70, + ), + body: Row( + children: [ + if (MediaQuery.of(context).size.width > 950) + NavigationRail( + onDestinationSelected: _onDestinationSelected, + selectedIndex: _currentPageIndex, + groupAlignment: 0, + extended: extended, + destinations: _destinations + .map( + (d) => NavigationRailDestination( + icon: d.icon, + label: Text(d.label), + padding: const EdgeInsets.all(10), ), - IconButton( - icon: AnimatedSwitcher( - duration: kThemeAnimationDuration, - switchInCurve: Curves.easeInOut, - switchOutCurve: Curves.easeInOut, - child: Icon( - extended ? Icons.menu_open : Icons.menu, - key: UniqueKey(), - ), + ) + .toList(), + leading: Row( + children: [ + AnimatedContainer( + width: extended ? 205 : 0, + duration: kThemeAnimationDuration, + curve: Curves.easeInOut, + ), + IconButton( + icon: AnimatedSwitcher( + duration: kThemeAnimationDuration, + switchInCurve: Curves.easeInOut, + switchOutCurve: Curves.easeInOut, + child: Icon( + key: UniqueKey(), + extended ? Icons.menu_open : Icons.menu, ), - onPressed: () => setState(() => extended = !extended), - tooltip: !extended ? 'Extend Menu' : 'Collapse Menu', ), - ], - ), + onPressed: () => setState(() => extended = !extended), + tooltip: !extended ? 'Extend Menu' : 'Collapse Menu', + ), + ], ), - Expanded( - child: ClipRRect( - borderRadius: BorderRadius.only( - topLeft: MediaQuery.of(context).size.width > 950 - ? const Radius.circular(16) - : Radius.zero, - bottomLeft: MediaQuery.of(context).size.width > 950 - ? const Radius.circular(16) - : Radius.zero, - ), - child: PageView( - controller: _pageController, - physics: const NeverScrollableScrollPhysics(), - children: _pages, - ), + ), + Expanded( + child: ClipRRect( + borderRadius: BorderRadius.only( + topLeft: MediaQuery.of(context).size.width > 950 + ? const Radius.circular(16) + : Radius.zero, + bottomLeft: MediaQuery.of(context).size.width > 950 + ? const Radius.circular(16) + : Radius.zero, + ), + child: PageView( + controller: _pageController, + physics: const NeverScrollableScrollPhysics(), + children: _pages, ), ), - ], - ), + ), + ], ), ); } diff --git a/example/lib/screens/main/pages/downloader/components/map_view.dart b/example/lib/screens/main/pages/downloader/components/map_view.dart index e3401d0a..f6d9831a 100644 --- a/example/lib/screens/main/pages/downloader/components/map_view.dart +++ b/example/lib/screens/main/pages/downloader/components/map_view.dart @@ -458,9 +458,9 @@ class _MapViewState extends State { ..regionTiles = null ..regionTiles = await FMTC.instance('').download.check( downloadProvider.region!.toDownloadable( - downloadProvider.minZoom, - downloadProvider.maxZoom, - TileLayer(), + minZoom: downloadProvider.minZoom, + maxZoom: downloadProvider.maxZoom, + options: TileLayer(), ), ); } diff --git a/example/lib/shared/state/download_provider.dart b/example/lib/shared/state/download_provider.dart index fecf63ce..92af8e65 100644 --- a/example/lib/shared/state/download_provider.dart +++ b/example/lib/shared/state/download_provider.dart @@ -93,7 +93,7 @@ class DownloadProvider extends ChangeNotifier { notifyListeners(); } - bool _skipExistingTiles = false; + bool _skipExistingTiles = true; bool get skipExistingTiles => _skipExistingTiles; set skipExistingTiles(bool newBool) { _skipExistingTiles = newBool; @@ -107,7 +107,7 @@ class DownloadProvider extends ChangeNotifier { notifyListeners(); } - int? _rateLimit = 100; + int? _rateLimit = 200; int? get rateLimit => _rateLimit; set rateLimit(int? newNum) { _rateLimit = newNum; diff --git a/example/pubspec.yaml b/example/pubspec.yaml index feb1b753..deb9e549 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -19,7 +19,6 @@ dependencies: flutter_map: ^5.0.0 flutter_map_tile_caching: flutter_speed_dial: ^7.0.0 - fmtc_plus_background_downloading: ^8.0.0 fmtc_plus_sharing: ^8.0.0 google_fonts: ^5.1.0 gpx: ^2.2.1 diff --git a/example/windows/flutter/generated_plugin_registrant.cc b/example/windows/flutter/generated_plugin_registrant.cc index 50c5a45b..6d420e2b 100644 --- a/example/windows/flutter/generated_plugin_registrant.cc +++ b/example/windows/flutter/generated_plugin_registrant.cc @@ -7,15 +7,12 @@ #include "generated_plugin_registrant.h" #include -#include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { IsarFlutterLibsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("IsarFlutterLibsPlugin")); - PermissionHandlerWindowsPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); SharePlusWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); UrlLauncherWindowsRegisterWithRegistrar( diff --git a/example/windows/flutter/generated_plugins.cmake b/example/windows/flutter/generated_plugins.cmake index 200e5670..759d9b5d 100644 --- a/example/windows/flutter/generated_plugins.cmake +++ b/example/windows/flutter/generated_plugins.cmake @@ -4,7 +4,6 @@ list(APPEND FLUTTER_PLUGIN_LIST isar_flutter_libs - permission_handler_windows share_plus url_launcher_windows ) diff --git a/lib/flutter_map_tile_caching.dart b/lib/flutter_map_tile_caching.dart index 2a759d2e..31e0246e 100644 --- a/lib/flutter_map_tile_caching.dart +++ b/lib/flutter_map_tile_caching.dart @@ -49,6 +49,7 @@ import 'src/errors/initialisation.dart'; import 'src/errors/store_not_ready.dart'; import 'src/misc/exts.dart'; import 'src/misc/int_extremes.dart'; +import 'src/misc/obscure_query_params.dart'; import 'src/misc/typedefs.dart'; import 'src/providers/image_provider.dart'; diff --git a/lib/src/bulk_download/manager.dart b/lib/src/bulk_download/manager.dart index 84ad81e9..ee25e983 100644 --- a/lib/src/bulk_download/manager.dart +++ b/lib/src/bulk_download/manager.dart @@ -8,15 +8,28 @@ Future _downloadManager( SendPort sendPort, String rootDirectory, DownloadableRegion region, - FMTCTileProvider tileProvider, + String storeName, int parallelThreads, int maxBufferLength, bool skipExistingTiles, bool skipSeaTiles, Duration? maxReportInterval, int? rateLimit, + Iterable obscuredQueryParams, }) input, ) async { + // Precalculate shared inputs for all threads + final storeId = DatabaseTools.hash(input.storeName).toString(); + final threadBufferLength = + (input.maxBufferLength / input.parallelThreads).ceil(); + final headers = { + ...input.region.options.tileProvider.headers, + 'User-Agent': input.region.options.tileProvider.headers['User-Agent'] == + null + ? 'flutter_map_tile_caching for flutter_map (unknown)' + : 'flutter_map_tile_caching for ${input.region.options.tileProvider.headers['User-Agent']}', + }; + // Count number of tiles final maxTiles = input.region.when( rectangle: (_) => TilesCounter.rectangleTiles, @@ -30,12 +43,12 @@ Future _downloadManager( try { seaTileBytes = await http.readBytes( Uri.parse( - input.tileProvider.getTileUrl( + input.region.options.tileProvider.getTileUrl( const TileCoordinates(0, 0, 17), input.region.options, ), ), - headers: input.tileProvider.headers, + headers: headers, ); } catch (_) { seaTileBytes = null; @@ -64,22 +77,19 @@ Future _downloadManager( onExit: tileRecievePort.sendPort, debugName: '[FMTC] Tile Coords Generator Thread', ); + final rawTileStream = tileRecievePort.skip(input.region.start).take( + input.region.end == null + ? largestInt + : (input.region.end! - input.region.start), + ); final tileQueue = StreamQueue( input.rateLimit == null - ? tileRecievePort.skip(input.region.start).take( - input.region.end == null - ? largestInt - : (input.region.end! - input.region.start), - ) + ? rawTileStream : RateLimitedStream.fromSourceStream( emitEvery: Duration( microseconds: ((1 / input.rateLimit!) * 1000000).ceil(), ), - sourceStream: tileRecievePort.skip(input.region.start).take( - input.region.end == null - ? largestInt - : (input.region.end! - input.region.start), - ), + sourceStream: rawTileStream, ).stream, ); final requestTilePort = await tileQueue.next as SendPort; @@ -96,6 +106,7 @@ Future _downloadManager( int mptSlots = 50; final microsecondsPerTile = List.filled(mptSlots, null, growable: true); final tpsRates = List.filled(20, null); + final tileTimer = Stopwatch(); // This might be the wrong place to put this? // Setup cancel, pause, and resume handling final threadPausedStates = List.generate( @@ -151,9 +162,6 @@ Future _downloadManager( fallbackReportTimer = null; } - // This might be the wrong place to put this? - final tileTimer = Stopwatch(); - // Start download threads & wait for download to complete/cancelled await Future.wait( List.generate( @@ -167,16 +175,14 @@ Future _downloadManager( _singleDownloadThread, ( sendPort: downloadThreadRecievePort.sendPort, - storeId: - DatabaseTools.hash(input.tileProvider.storeDirectory.storeName) - .toString(), + storeId: storeId, rootDirectory: input.rootDirectory, - region: input.region, - tileProvider: input.tileProvider, - maxBufferLength: - (input.maxBufferLength / input.parallelThreads).ceil(), + options: input.region.options, + maxBufferLength: threadBufferLength, skipExistingTiles: input.skipExistingTiles, seaTileBytes: seaTileBytes, + obscuredQueryParams: input.obscuredQueryParams, + headers: headers, ), onExit: downloadThreadRecievePort.sendPort, debugName: '[FMTC] Bulk Download Thread #$threadNo', diff --git a/lib/src/bulk_download/thread.dart b/lib/src/bulk_download/thread.dart index 57f3f6da..1350893e 100644 --- a/lib/src/bulk_download/thread.dart +++ b/lib/src/bulk_download/thread.dart @@ -8,11 +8,12 @@ Future _singleDownloadThread( SendPort sendPort, String storeId, String rootDirectory, - DownloadableRegion region, - FMTCTileProvider tileProvider, + TileLayer options, int maxBufferLength, bool skipExistingTiles, Uint8List? seaTileBytes, + Iterable obscuredQueryParams, + Map headers, }) input, ) async { // Setup two-way communications @@ -58,18 +59,22 @@ Future _singleDownloadThread( } // Get new tile URL & any existing tile - final url = input.tileProvider.getTileUrl( + final networkUrl = input.options.tileProvider.getTileUrl( TileCoordinates(coords.$1, coords.$2, coords.$3), - input.region.options, + input.options, ); - final existingTile = await db.tiles.get(DatabaseTools.hash(url)); + final matcherUrl = obscureQueryParams( + url: networkUrl, + obscuredQueryParams: input.obscuredQueryParams, + ); + final existingTile = await db.tiles.get(DatabaseTools.hash(matcherUrl)); // Skip if tile already exists and user demands existing tile pruning if (input.skipExistingTiles && existingTile != null) { send( TileEvent._( TileEventResult.alreadyExisting, - url: url, + url: networkUrl, tileImage: Uint8List.fromList(existingTile.bytes), ), ); @@ -80,8 +85,8 @@ Future _singleDownloadThread( final http.Response response; try { response = await httpClient.get( - Uri.parse(url), - headers: input.tileProvider.headers, + Uri.parse(networkUrl), + headers: input.headers, ); } catch (e) { send( @@ -89,7 +94,7 @@ Future _singleDownloadThread( e is SocketException ? TileEventResult.noConnectionDuringFetch : TileEventResult.unknownFetchException, - url: url, + url: networkUrl, fetchError: e, ), ); @@ -100,7 +105,7 @@ Future _singleDownloadThread( send( TileEvent._( TileEventResult.negativeFetchResponse, - url: url, + url: networkUrl, fetchResponse: response, ), ); @@ -112,7 +117,7 @@ Future _singleDownloadThread( send( TileEvent._( TileEventResult.isSeaTile, - url: url, + url: networkUrl, tileImage: response.bodyBytes, fetchResponse: response, ), @@ -121,7 +126,7 @@ Future _singleDownloadThread( } // Write tile directly to database or place in buffer queue - final tile = DbTile(url: url, bytes: response.bodyBytes); + final tile = DbTile(url: matcherUrl, bytes: response.bodyBytes); if (input.maxBufferLength == 0) { db.writeTxnSync(() => db.tiles.putSync(tile)); } else { @@ -139,7 +144,7 @@ Future _singleDownloadThread( send( TileEvent._( TileEventResult.success, - url: url, + url: networkUrl, tileImage: response.bodyBytes, fetchResponse: response, wasBufferReset: wasBufferReset, diff --git a/lib/src/misc/obscure_query_params.dart b/lib/src/misc/obscure_query_params.dart new file mode 100644 index 00000000..cd35232e --- /dev/null +++ b/lib/src/misc/obscure_query_params.dart @@ -0,0 +1,16 @@ +// Copyright © Luka S (JaffaKetchup) under GPL-v3 +// A full license can be found at .\LICENSE + +String obscureQueryParams({ + required String url, + required Iterable obscuredQueryParams, +}) { + if (!url.contains('?') || obscuredQueryParams.isEmpty) return url; + + String secondPartUrl = url.split('?')[1]; + for (final matcher in obscuredQueryParams) { + secondPartUrl = secondPartUrl.replaceAll(matcher, ''); + } + + return '${url.split('?')[0]}?$secondPartUrl'; +} diff --git a/lib/src/providers/image_provider.dart b/lib/src/providers/image_provider.dart index f5078bbf..9f6300cf 100644 --- a/lib/src/providers/image_provider.dart +++ b/lib/src/providers/image_provider.dart @@ -19,6 +19,7 @@ import '../db/defs/store_descriptor.dart'; import '../db/defs/tile.dart'; import '../db/registry.dart'; import '../db/tools.dart'; +import '../misc/obscure_query_params.dart'; /// A specialised [ImageProvider] dedicated to 'flutter_map_tile_caching' class FMTCImageProvider extends ImageProvider { @@ -96,7 +97,10 @@ class FMTCImageProvider extends ImageProvider { } final networkUrl = provider.getTileUrl(coords, options); - final matcherUrl = provider.settings.obscureQueryParams(networkUrl); + final matcherUrl = obscureQueryParams( + url: networkUrl, + obscuredQueryParams: provider.settings.obscuredQueryParams, + ); final existingTile = await db.tiles.get(DatabaseTools.hash(matcherUrl)); diff --git a/lib/src/providers/tile_provider.dart b/lib/src/providers/tile_provider.dart index 1aad3d90..a165bed5 100644 --- a/lib/src/providers/tile_provider.dart +++ b/lib/src/providers/tile_provider.dart @@ -64,7 +64,10 @@ class FMTCTileProvider extends TileProvider { }) => FMTCRegistry.instance(storeDirectory.storeName).tiles.getSync( DatabaseTools.hash( - settings.obscureQueryParams(getTileUrl(coords, options)), + obscureQueryParams( + url: getTileUrl(coords, options), + obscuredQueryParams: settings.obscuredQueryParams, + ), ), ) != null; @@ -77,7 +80,10 @@ class FMTCTileProvider extends TileProvider { }) async => await FMTCRegistry.instance(storeDirectory.storeName).tiles.get( DatabaseTools.hash( - settings.obscureQueryParams(getTileUrl(coords, options)), + obscureQueryParams( + url: getTileUrl(coords, options), + obscuredQueryParams: settings.obscuredQueryParams, + ), ), ) != null; diff --git a/lib/src/regions/base_region.dart b/lib/src/regions/base_region.dart index e4af1914..ee629885 100644 --- a/lib/src/regions/base_region.dart +++ b/lib/src/regions/base_region.dart @@ -59,10 +59,10 @@ sealed class BaseRegion { /// Generate the [DownloadableRegion] ready for bulk downloading /// /// For more information see [DownloadableRegion]'s documentation. - DownloadableRegion toDownloadable( - int minZoom, - int maxZoom, - TileLayer options, { + DownloadableRegion toDownloadable({ + required int minZoom, + required int maxZoom, + required TileLayer options, int start = 0, int? end, Crs crs = const Epsg3857(), diff --git a/lib/src/regions/circle.dart b/lib/src/regions/circle.dart index 81281b2e..4b592e55 100644 --- a/lib/src/regions/circle.dart +++ b/lib/src/regions/circle.dart @@ -25,10 +25,10 @@ class CircleRegion extends BaseRegion { final double radius; @override - DownloadableRegion toDownloadable( - int minZoom, - int maxZoom, - TileLayer options, { + DownloadableRegion toDownloadable({ + required int minZoom, + required int maxZoom, + required TileLayer options, int start = 0, int? end, Crs crs = const Epsg3857(), diff --git a/lib/src/regions/line.dart b/lib/src/regions/line.dart index a5d7e7b4..19ebda0e 100644 --- a/lib/src/regions/line.dart +++ b/lib/src/regions/line.dart @@ -75,10 +75,10 @@ class LineRegion extends BaseRegion { } @override - DownloadableRegion toDownloadable( - int minZoom, - int maxZoom, - TileLayer options, { + DownloadableRegion toDownloadable({ + required int minZoom, + required int maxZoom, + required TileLayer options, int start = 0, int? end, Crs crs = const Epsg3857(), diff --git a/lib/src/regions/rectangle.dart b/lib/src/regions/rectangle.dart index 502d0754..4ab723ab 100644 --- a/lib/src/regions/rectangle.dart +++ b/lib/src/regions/rectangle.dart @@ -24,10 +24,10 @@ class RectangleRegion extends BaseRegion { final LatLngBounds bounds; @override - DownloadableRegion toDownloadable( - int minZoom, - int maxZoom, - TileLayer options, { + DownloadableRegion toDownloadable({ + required int minZoom, + required int maxZoom, + required TileLayer options, int start = 0, int? end, Crs crs = const Epsg3857(), diff --git a/lib/src/settings/tile_provider_settings.dart b/lib/src/settings/tile_provider_settings.dart index 6bbd8172..baff876d 100644 --- a/lib/src/settings/tile_provider_settings.dart +++ b/lib/src/settings/tile_provider_settings.dart @@ -76,18 +76,6 @@ class FMTCTileProviderSettings { this.errorHandler, }) : obscuredQueryParams = obscuredQueryParams.map((e) => RegExp('$e=[^&]*')); - /// Apply the [obscuredQueryParams] to the input [url] - String obscureQueryParams(String url) { - if (!url.contains('?') || obscuredQueryParams.isEmpty) return url; - - String secondPartUrl = url.split('?')[1]; - for (final r in obscuredQueryParams) { - secondPartUrl = secondPartUrl.replaceAll(r, ''); - } - - return '${url.split('?')[0]}?$secondPartUrl'; - } - @override bool operator ==(Object other) => identical(this, other) || diff --git a/lib/src/store/download.dart b/lib/src/store/download.dart index 5f07493e..8a8c6999 100644 --- a/lib/src/store/download.dart +++ b/lib/src/store/download.dart @@ -43,7 +43,7 @@ class DownloadManagement { /// /// - [parallelThreads] (defaults to 5 | 1 to disable): number of simultaneous /// download threads to run - /// - [maxBufferLength] (defaults to 100 | 0 to disable): number of tiles to + /// - [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 `true`): whether to skip downloading @@ -74,18 +74,27 @@ class DownloadManagement { /// /// --- /// + /// For information about [obscuredQueryParams], see the + /// [online documentation](https://fmtc.jaffaketchup.dev/usage/integration#obscuring-query-parameters). + /// Will default to the current value in [FMTCSettings]. + /// + /// To set additional headers, set it via [TileProvider.headers] when + /// constructing the [DownloadableRegion]. + /// + /// --- + /// /// {@macro num_instances} @useResult Stream startForeground({ required DownloadableRegion region, - FMTCTileProviderSettings? tileProviderSettings, int parallelThreads = 5, - int maxBufferLength = 100, + int maxBufferLength = 200, bool skipExistingTiles = true, bool skipSeaTiles = true, - Duration? maxReportInterval = const Duration(seconds: 1), int? rateLimit, + Duration? maxReportInterval = const Duration(seconds: 1), bool disableRecovery = false, + List? obscuredQueryParams, Object instanceId = 0, }) async* { // Check input arguments for suitability @@ -156,16 +165,17 @@ class DownloadManagement { sendPort: recievePort.sendPort, rootDirectory: FMTC.instance.rootDirectory.directory.absolute.path, region: region, - tileProvider: _storeDirectory.getTileProvider( - settings: tileProviderSettings, - headers: region.options.tileProvider.headers, - ), + storeName: _storeDirectory.storeName, parallelThreads: parallelThreads, maxBufferLength: maxBufferLength, skipExistingTiles: skipExistingTiles, skipSeaTiles: skipSeaTiles, maxReportInterval: maxReportInterval, rateLimit: rateLimit, + obscuredQueryParams: + obscuredQueryParams?.map((e) => RegExp('$e=[^&]*')) ?? + FMTC.instance.settings.defaultTileProviderSettings + .obscuredQueryParams, ), onExit: recievePort.sendPort, debugName: '[FMTC] Master Bulk Download Thread', @@ -220,7 +230,8 @@ class DownloadManagement { /// Check how many downloadable tiles are within a specified region /// - /// This does not take into account sea tile removal or redownload prevention. + /// This does not include skipped sea tiles or skipped existing tiles, as those + /// are handled during download only. /// /// Returns the number of tiles. Future check(DownloadableRegion region) => compute( diff --git a/pubspec.yaml b/pubspec.yaml index 683f81c2..13986cd1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_map_tile_caching description: Plugin for 'flutter_map' providing advanced caching functionality, with ability to download map regions for offline use. -version: 9.0.0-dev.3 +version: 9.0.0-dev.4 repository: https://github.com/JaffaKetchup/flutter_map_tile_caching issue_tracker: https://github.com/JaffaKetchup/flutter_map_tile_caching/issues documentation: https://fmtc.jaffaketchup.dev diff --git a/windowsApplicationInstallerSetup.iss b/windowsApplicationInstallerSetup.iss index 9cbb2bdf..d8fbeadb 100644 --- a/windowsApplicationInstallerSetup.iss +++ b/windowsApplicationInstallerSetup.iss @@ -69,7 +69,6 @@ Source: "example\build\windows\runner\Release\{#MyAppExeName}"; DestDir: "{app}" Source: "example\build\windows\runner\Release\flutter_windows.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "example\build\windows\runner\Release\isar_flutter_libs_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "example\build\windows\runner\Release\isar.dll"; DestDir: "{app}"; Flags: ignoreversion -Source: "example\build\windows\runner\Release\permission_handler_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "example\build\windows\runner\Release\share_plus_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "example\build\windows\runner\Release\url_launcher_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "example\build\windows\runner\Release\data\*"; DestDir: "{app}\data"; Flags: ignoreversion recursesubdirs createallsubdirs From cef1ae0acb9d7d4b2d07783383ae858baba74318 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Wed, 12 Jul 2023 17:42:39 +0200 Subject: [PATCH 029/168] Added simple location tracking to example application Changed example application's ID Former-commit-id: 37bb8b692d80dee2877aee2e97944ce0d32b8a15 [formerly 71d5682d21fac40b0a03b04ab0dac6493edafac9] Former-commit-id: 5dbff8e385c5b26946702a8deb42d153ac52ceb8 --- .github/workflows/main.yml | 4 +- .vscode/launch.json | 2 +- example/README.md | 2 +- example/android/app/build.gradle | 2 +- .../android/app/src/debug/AndroidManifest.xml | 5 +- .../android/app/src/main/AndroidManifest.xml | 7 ++- .../fmtc/example/fmtc_example/MainActivity.kt | 2 +- .../app/src/profile/AndroidManifest.xml | 5 +- example/lib/main.dart | 4 +- example/lib/screens/main/main.dart | 40 +++++++++++- .../pages/downloader/components/map_view.dart | 4 +- .../lib/screens/main/pages/map/map_view.dart | 62 +++++++++++++++---- example/lib/shared/state/map_provider.dart | 35 +++++++++++ example/pubspec.yaml | 5 ++ .../flutter/generated_plugin_registrant.cc | 3 + .../windows/flutter/generated_plugins.cmake | 1 + example/windows/runner/Runner.rc | 2 +- windowsApplicationInstallerSetup.iss | 1 + 18 files changed, 157 insertions(+), 29 deletions(-) create mode 100644 example/lib/shared/state/map_provider.dart diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7fba724b..73a011e2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -48,7 +48,7 @@ jobs: run: dart analyze --fatal-infos --fatal-warnings build-android: - name: "Build Android Example App" + name: "Build Android Demo App" runs-on: ubuntu-latest needs: analyse-code defaults: @@ -76,7 +76,7 @@ jobs: if-no-files-found: error build-windows: - name: "Build Windows Example App" + name: "Build Windows Demo App" runs-on: windows-latest needs: analyse-code defaults: diff --git a/.vscode/launch.json b/.vscode/launch.json index 2540a701..fba18866 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,7 +5,7 @@ "version": "0.2.0", "configurations": [ { - "name": "Run Example App", + "name": "Run Demo App", "request": "launch", "type": "dart", "program": "example/lib/main.dart" diff --git a/example/README.md b/example/README.md index aa3e563f..9956a8fc 100644 --- a/example/README.md +++ b/example/README.md @@ -1,4 +1,4 @@ -# Example Application For '[flutter_map_tile_caching](https://github.com/JaffaKetchup/flutter_map_tile_caching)' +# Demonstration Application For '[flutter_map_tile_caching](https://github.com/JaffaKetchup/flutter_map_tile_caching)' Showcases functionality of the library in a neat and useful format that can be used for further API references, and just to see if you want this library for your app. diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index f43b9091..8db3c5c8 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -53,7 +53,7 @@ android { } defaultConfig { - applicationId "dev.org.fmtc.example.fmtc_example" + applicationId "dev.jaffaketchup.fmtc.demo" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionCode flutterVersionCode.toInteger() diff --git a/example/android/app/src/debug/AndroidManifest.xml b/example/android/app/src/debug/AndroidManifest.xml index 4c97235c..54ee1d59 100644 --- a/example/android/app/src/debug/AndroidManifest.xml +++ b/example/android/app/src/debug/AndroidManifest.xml @@ -1,9 +1,12 @@ + package="dev.jaffaketchup.fmtc.demo"> + + + diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index c0ad88ee..f91b539a 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -1,11 +1,14 @@ + package="dev.jaffaketchup.fmtc.demo"> + + + + package="dev.jaffaketchup.fmtc.demo"> + + + diff --git a/example/lib/main.dart b/example/lib/main.dart index 39f89fb1..93e6da7e 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -10,6 +10,7 @@ import 'package:provider/provider.dart'; import 'screens/main/main.dart'; import 'shared/state/download_provider.dart'; import 'shared/state/general_provider.dart'; +import 'shared/state/map_provider.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -53,9 +54,10 @@ class AppContainer extends StatelessWidget { providers: [ ChangeNotifierProvider(create: (context) => GeneralProvider()), ChangeNotifierProvider(create: (context) => DownloadProvider()), + ChangeNotifierProvider(create: (context) => MapProvider()), ], child: MaterialApp( - title: 'FMTC Example', + title: 'FMTC Demo', theme: ThemeData( brightness: Brightness.dark, useMaterial3: true, diff --git a/example/lib/screens/main/main.dart b/example/lib/screens/main/main.dart index 02733fba..ba11c00e 100644 --- a/example/lib/screens/main/main.dart +++ b/example/lib/screens/main/main.dart @@ -4,6 +4,7 @@ import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; import 'package:provider/provider.dart'; import '../../shared/state/download_provider.dart'; +import '../../shared/state/map_provider.dart'; import 'pages/downloader/downloader.dart'; import 'pages/downloading/downloading.dart'; import 'pages/map/map_view.dart'; @@ -115,11 +116,44 @@ class _MainScreenState extends State { onDestinationSelected: _onDestinationSelected, selectedIndex: _currentPageIndex, destinations: _destinations, - labelBehavior: MediaQuery.of(context).size.width > 450 - ? null - : NavigationDestinationLabelBehavior.alwaysHide, + labelBehavior: + NavigationDestinationLabelBehavior.onlyShowSelected, height: 70, ), + floatingActionButton: _currentPageIndex != 0 + ? null + : Consumer( + builder: (context, mapProvider, _) => FloatingActionButton( + onPressed: () { + switch (mapProvider.followState) { + case UserLocationFollowState.off: + mapProvider.followState = + UserLocationFollowState.standard; + mapProvider.trackLocation(navigation: false); + mapProvider.mapController.rotate(0); + break; + case UserLocationFollowState.standard: + mapProvider.followState = + UserLocationFollowState.navigation; + mapProvider.trackLocation(navigation: true); + mapProvider.trackHeading(); + break; + case UserLocationFollowState.navigation: + mapProvider.followState = UserLocationFollowState.off; + mapProvider.mapController.rotate(0); + break; + } + setState(() {}); + }, + child: Icon( + switch (mapProvider.followState) { + UserLocationFollowState.off => Icons.gps_off, + UserLocationFollowState.standard => Icons.gps_fixed, + UserLocationFollowState.navigation => Icons.navigation, + }, + ), + ), + ), body: Row( children: [ if (MediaQuery.of(context).size.width > 950) diff --git a/example/lib/screens/main/pages/downloader/components/map_view.dart b/example/lib/screens/main/pages/downloader/components/map_view.dart index f6d9831a..773651ff 100644 --- a/example/lib/screens/main/pages/downloader/components/map_view.dart +++ b/example/lib/screens/main/pages/downloader/components/map_view.dart @@ -169,7 +169,7 @@ class _MapViewState extends State { maxZoom: 20, reset: generalProvider.resetController.stream, keepBuffer: 5, - userAgentPackageName: 'dev.org.fmtc.example.app', + userAgentPackageName: 'dev.jaffaketchup.fmtc.demo', backgroundColor: const Color(0xFFaad3df), tileBuilder: (context, widget, tile) => FutureBuilder( @@ -266,6 +266,8 @@ class _MapViewState extends State { const VerticalDivider(), IconButton( onPressed: () async { + await FilePicker.platform.clearTemporaryFiles(); + late final FilePickerResult? result; try { result = await FilePicker.platform.pickFiles( diff --git a/example/lib/screens/main/pages/map/map_view.dart b/example/lib/screens/main/pages/map/map_view.dart index 96e9ae95..0ecde54a 100644 --- a/example/lib/screens/main/pages/map/map_view.dart +++ b/example/lib/screens/main/pages/map/map_view.dart @@ -1,23 +1,26 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map_location_marker/flutter_map_location_marker.dart'; import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; import 'package:latlong2/latlong.dart'; import 'package:provider/provider.dart'; import '../../../../shared/components/loading_indicator.dart'; import '../../../../shared/state/general_provider.dart'; +import '../../../../shared/state/map_provider.dart'; import 'build_attribution.dart'; -class MapPage extends StatefulWidget { - const MapPage({ - super.key, - }); - - @override - State createState() => _MapPageState(); +enum UserLocationFollowState { + off, + standard, + navigation, } -class _MapPageState extends State { +class MapPage extends StatelessWidget { + const MapPage({super.key}); + @override Widget build(BuildContext context) => Consumer( builder: (context, provider, _) => FutureBuilder?>( @@ -30,7 +33,7 @@ class _MapPageState extends State { (provider.currentStore != null && metadata.data!.isEmpty)) { return const LoadingIndicator( message: - 'Loading Settings...\n\nSeeing this screen for a long time?\nThere may be a misconfiguration of the\nstore. Try disabling caching and deleting\n faulty stores.', + 'Loading Map...\n\nSeeing this screen for a long time?\nThere may be a misconfiguration of the\nstore. Try disabling caching and deleting\n faulty stores.', ); } @@ -40,9 +43,10 @@ class _MapPageState extends State { : 'https://tile.openstreetmap.org/{z}/{x}/{y}.png'; return FlutterMap( + mapController: Provider.of(context).mapController, options: MapOptions( initialCenter: const LatLng(51.509364, -0.128928), - initialZoom: 9.2, + initialZoom: 16, maxZoom: 22, cameraConstraint: CameraConstraint.contain( bounds: LatLngBounds.fromPoints([ @@ -53,12 +57,18 @@ class _MapPageState extends State { ]), ), interactionOptions: const InteractionOptions( - flags: InteractiveFlag.all & ~InteractiveFlag.rotate, + // flags: InteractiveFlag.all & ~InteractiveFlag.rotate, scrollWheelVelocity: 0.002, ), keepAlive: true, + onPointerDown: (_, __) => + Provider.of(context, listen: false) + .followState = UserLocationFollowState.off, + ), + nonRotatedChildren: buildStdAttribution( + urlTemplate, + alignment: AttributionAlignment.bottomLeft, ), - nonRotatedChildren: buildStdAttribution(urlTemplate), children: [ TileLayer( urlTemplate: urlTemplate, @@ -84,10 +94,36 @@ class _MapPageState extends State { ) : NetworkTileProvider(), maxZoom: 22, - userAgentPackageName: 'dev.org.fmtc.example.app', + userAgentPackageName: 'dev.jaffaketchup.fmtc.demo', panBuffer: 3, backgroundColor: const Color(0xFFaad3df), ), + Consumer( + builder: (context, mapProvider, _) => CurrentLocationLayer( + followCurrentLocationStream: + mapProvider.trackLocationStream, + turnHeadingUpLocationStream: mapProvider.trackHeadingStream, + followOnLocationUpdate: + mapProvider.followState != UserLocationFollowState.off + ? FollowOnLocationUpdate.always + : FollowOnLocationUpdate.never, + turnOnHeadingUpdate: mapProvider.followState == + UserLocationFollowState.navigation + ? TurnOnHeadingUpdate.always + : TurnOnHeadingUpdate.never, + style: const LocationMarkerStyle( + marker: DefaultLocationMarker( + child: Icon( + Icons.navigation, + color: Colors.white, + size: 18, + ), + ), + markerSize: Size(30, 30), + markerDirection: MarkerDirection.heading, + ), + ), + ), ], ); }, diff --git a/example/lib/shared/state/map_provider.dart b/example/lib/shared/state/map_provider.dart new file mode 100644 index 00000000..a13c36d0 --- /dev/null +++ b/example/lib/shared/state/map_provider.dart @@ -0,0 +1,35 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter_map/flutter_map.dart'; + +import '../../screens/main/pages/map/map_view.dart'; + +class MapProvider extends ChangeNotifier { + UserLocationFollowState _followState = UserLocationFollowState.standard; + UserLocationFollowState get followState => _followState; + set followState(UserLocationFollowState newState) { + _followState = newState; + notifyListeners(); + } + + MapController _mapController = MapController(); + MapController get mapController => _mapController; + set mapController(MapController newController) { + _mapController = newController; + notifyListeners(); + } + + // ignore: close_sinks + final _trackLocationStreamController = StreamController()..add(16); + late final trackLocationStream = + _trackLocationStreamController.stream.asBroadcastStream(); + void trackLocation({required bool navigation}) => + _trackLocationStreamController.add(navigation ? 18 : 16); + + // ignore: close_sinks + final _trackHeadingStreamController = StreamController()..add(null); + late final trackHeadingStream = + _trackHeadingStreamController.stream.asBroadcastStream(); + void trackHeading() => _trackHeadingStreamController.add(null); +} diff --git a/example/pubspec.yaml b/example/pubspec.yaml index deb9e549..17bd609f 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -17,6 +17,7 @@ dependencies: sdk: flutter flutter_foreground_task: ^6.0.0+1 flutter_map: ^5.0.0 + flutter_map_location_marker: ^7.0.1 flutter_map_tile_caching: flutter_speed_dial: ^7.0.0 fmtc_plus_sharing: ^8.0.0 @@ -36,6 +37,10 @@ dependency_overrides: flutter_map: git: url: https://github.com/fleaflet/flutter_map.git + flutter_map_location_marker: + git: + url: https://github.com/JaffaKetchup/_fmlm + ref: 'fm-v6' flutter_map_tile_caching: path: ../ http: ^1.0.0 diff --git a/example/windows/flutter/generated_plugin_registrant.cc b/example/windows/flutter/generated_plugin_registrant.cc index 6d420e2b..f84e82cf 100644 --- a/example/windows/flutter/generated_plugin_registrant.cc +++ b/example/windows/flutter/generated_plugin_registrant.cc @@ -6,11 +6,14 @@ #include "generated_plugin_registrant.h" +#include #include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + GeolocatorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("GeolocatorWindows")); IsarFlutterLibsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("IsarFlutterLibsPlugin")); SharePlusWindowsPluginCApiRegisterWithRegistrar( diff --git a/example/windows/flutter/generated_plugins.cmake b/example/windows/flutter/generated_plugins.cmake index 759d9b5d..1f658706 100644 --- a/example/windows/flutter/generated_plugins.cmake +++ b/example/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + geolocator_windows isar_flutter_libs share_plus url_launcher_windows diff --git a/example/windows/runner/Runner.rc b/example/windows/runner/Runner.rc index 15dbdb2f..13007a4b 100644 --- a/example/windows/runner/Runner.rc +++ b/example/windows/runner/Runner.rc @@ -89,7 +89,7 @@ BEGIN BEGIN BLOCK "040904e4" BEGIN - VALUE "CompanyName", "dev.org.fmtc.example" "\0" + VALUE "CompanyName", "dev.jaffaketchup.fmtc.demo" "\0" VALUE "FileDescription", "FMTC Demo" "\0" VALUE "FileVersion", VERSION_AS_STRING "\0" VALUE "InternalName", "FMTC Demo" "\0" diff --git a/windowsApplicationInstallerSetup.iss b/windowsApplicationInstallerSetup.iss index d8fbeadb..3b2284cc 100644 --- a/windowsApplicationInstallerSetup.iss +++ b/windowsApplicationInstallerSetup.iss @@ -67,6 +67,7 @@ Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{ [Files] Source: "example\build\windows\runner\Release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion Source: "example\build\windows\runner\Release\flutter_windows.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "example\build\windows\runner\Release\geolocator_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "example\build\windows\runner\Release\isar_flutter_libs_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "example\build\windows\runner\Release\isar.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "example\build\windows\runner\Release\share_plus_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion From 7e7ce0d5147f33a4d0966b7297afb43d21bdb5a1 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 20 Jul 2023 09:25:53 +0200 Subject: [PATCH 030/168] Improved documentation Renamed `DownloadProgress.lastTileEvent` to `.latestTileEvent` Minor performance and memory consumption improvements Former-commit-id: b72bd758cb9e2c29dfcfef4dd162900b7711d95a [formerly 82e7233b6a5a6dd811f5e8e5a392bb3119245a68] Former-commit-id: adb1f7b155599b424c199ff9a95e27748c7ef6e0 --- .../components/download_layout.dart | 4 +- .../main/pages/downloading/downloading.dart | 5 +- lib/src/bulk_download/download_progress.dart | 133 ++++++++++++++++-- lib/src/bulk_download/instance.dart | 9 +- lib/src/bulk_download/tile_loops/shared.dart | 13 +- lib/src/store/download.dart | 18 ++- 6 files changed, 152 insertions(+), 30 deletions(-) diff --git a/example/lib/screens/main/pages/downloading/components/download_layout.dart b/example/lib/screens/main/pages/downloading/components/download_layout.dart index a46ec4ae..2f73d9c7 100644 --- a/example/lib/screens/main/pages/downloading/components/download_layout.dart +++ b/example/lib/screens/main/pages/downloading/components/download_layout.dart @@ -30,9 +30,9 @@ class DownloadLayout extends StatelessWidget { borderRadius: BorderRadius.circular(16), child: SizedBox.square( dimension: 256, - child: download.lastTileEvent.tileImage != null + child: download.latestTileEvent.tileImage != null ? Image.memory( - download.lastTileEvent.tileImage!, + download.latestTileEvent.tileImage!, gaplessPlayback: true, ) : const Center( diff --git a/example/lib/screens/main/pages/downloading/downloading.dart b/example/lib/screens/main/pages/downloading/downloading.dart index cc194779..0b2347cf 100644 --- a/example/lib/screens/main/pages/downloading/downloading.dart +++ b/example/lib/screens/main/pages/downloading/downloading.dart @@ -72,9 +72,10 @@ class _DownloadingPageState extends State ); } - if (snapshot.data!.lastTileEvent.result.category == + if (snapshot.data!.latestTileEvent.result.category == TileEventResultCategory.failed) { - provider.addFailedTile(snapshot.data!.lastTileEvent); + provider + .addFailedTile(snapshot.data!.latestTileEvent); } return DownloadLayout( diff --git a/lib/src/bulk_download/download_progress.dart b/lib/src/bulk_download/download_progress.dart index 938c8ab3..cf30b2b1 100644 --- a/lib/src/bulk_download/download_progress.dart +++ b/lib/src/bulk_download/download_progress.dart @@ -3,39 +3,139 @@ part of flutter_map_tile_caching; +/// Statistics and information about the current progress of the download +/// +/// See the documentation on each individual property for more information. @immutable class DownloadProgress { - /// The result of the last attempted tile + /// The result of the latest attempted tile /// /// May be used for UI display, error handling, or debugging purposes. - TileEvent get lastTileEvent => _lastTileEvent!; - final TileEvent? _lastTileEvent; + /// + /// It is not recommended to construct or keep a list of all these results, as + /// that will consume memory quickly. + TileEvent get latestTileEvent => _latestTileEvent!; + final TileEvent? _latestTileEvent; - /// The number of new tiles successfully downloaded and cached, that is those - /// tiles with the result of [TileEventResult.success] - /// ([TileEventResultCategory.cached]) + /// The number of new tiles successfully downloaded and in the tile buffer or + /// cached + /// + /// [TileEvent]s with the result category of [TileEventResultCategory.cached]. + /// + /// Includes [bufferedTiles]. final int cachedTiles; + + /// The total size (in KiB) of new tiles successfully downloaded and in the + /// tile buffer or cached + /// + /// [TileEvent]s with the result category of [TileEventResultCategory.cached]. + /// + /// Includes [bufferedSize]. final double cachedSize; + + /// The number of new tiles successfully downloaded and in the tile buffer + /// waiting to be cached + /// + /// [TileEvent]s with the result category of [TileEventResultCategory.cached]. + /// + /// Part of [cachedTiles]. final int bufferedTiles; + + /// The total size (in KiB) of new tiles successfully downloaded and in the + /// tile buffer waiting to be cached + /// + /// [TileEvent]s with the result category of [TileEventResultCategory.cached]. + /// + /// Part of [cachedSize]. final double bufferedSize; + + /// The number of tiles that were skipped (not cached) because they either: + /// - already existed & `skipExistingTiles` was `true` + /// - were a sea tile & `skipSeaTiles` was `true` + /// + /// [TileEvent]s with the result category of [TileEventResultCategory.skipped]. final int skippedTiles; + + /// The total size (in KiB) of tiles that were skipped (not cached) because + /// they either: + /// - already existed & `skipExistingTiles` was `true` + /// - were a sea tile & `skipSeaTiles` was `true` + /// + /// [TileEvent]s with the result category of [TileEventResultCategory.skipped]. final double skippedSize; + + /// The number of tiles that were not successfully downloaded, potentially for + /// a variety of reasons + /// + /// [TileEvent]s with the result category of [TileEventResultCategory.failed]. + /// + /// To check why these tiles failed, use [latestTileEvent] to construct a list + /// of tiles that failed. final int failedTiles; + + /// The total number of tiles available to be potentially downloaded and + /// cached final int maxTiles; + /// The current elapsed duration of the download + /// + /// Will be accurate to within `maxReportInterval` or better. final Duration elapsedDuration; + + /// The approximate/estimated number of attempted tiles per second (TPS) + /// + /// Note that this value is not raw. It goes through multiple layers of + /// smoothing which takes into account more than just the previous second. + /// It may or may not be accurate. final double tilesPerSecond; + + /// Whether the number of [tilesPerSecond] could be higher, but is currently + /// capped by the set `rateLimit` + /// + /// This is only an approximate indicator. final bool isTPSArtificiallyCapped; + /// Whether the download is now complete + /// + /// There will be no more events after this event. + /// + /// Prefer using this over checking any other statistics for completion. final bool isComplete; + /// The number of tiles that were either cached, in buffer, or skipped + /// + /// Equal to [cachedTiles] + [skippedTiles]. int get successfulTiles => cachedTiles + skippedTiles; + + /// The total size (in KiB) of tiles that were either cached, in buffer, or + /// skipped + /// + /// Equal to [cachedSize] + [skippedSize]. double get successfulSize => cachedSize + skippedSize; + + /// The number of tiles that have been attempted, with any result + /// + /// Equal to [successfulTiles] + [failedTiles]. int get attemptedTiles => successfulTiles + failedTiles; + + /// The number of tiles that have not yet been attempted + /// + /// Equal to [maxTiles] - [attemptedTiles]. int get remainingTiles => maxTiles - attemptedTiles; + /// The number of attempted tiles over the number of available tiles as a + /// percentage + /// + /// Equal to [attemptedTiles] / [maxTiles] multiplied by 100. double get percentageProgress => (attemptedTiles / maxTiles) * 100; + /// The estimated total duration of the download + /// + /// It may or may not be accurate, except when [isComplete] is `true`, in which + /// event, this will always equal [elapsedDuration]. + /// + /// It is not recommended to display this value directly to your user. Instead, + /// prefer using language such as 'about 𝑥 minutes remaining'. Duration get estTotalDuration => isComplete ? elapsedDuration : Duration( @@ -44,13 +144,20 @@ class DownloadProgress { 10) .clamp(elapsedDuration.inSeconds, largestInt), ); + + /// The estimated remaining duration of the download. + /// + /// It may or may not be accurate. + /// + /// It is not recommended to display this value directly to your user. Instead, + /// prefer using language such as 'about 𝑥 minutes remaining'. Duration get estRemainingDuration => estTotalDuration - elapsedDuration < Duration.zero ? Duration.zero : estTotalDuration - elapsedDuration; const DownloadProgress.__({ - required TileEvent? lastTileEvent, + required TileEvent? latestTileEvent, required this.cachedTiles, required this.cachedSize, required this.bufferedTiles, @@ -63,11 +170,11 @@ class DownloadProgress { required this.tilesPerSecond, required this.isTPSArtificiallyCapped, required this.isComplete, - }) : _lastTileEvent = lastTileEvent; + }) : _latestTileEvent = latestTileEvent; factory DownloadProgress._initial({required int maxTiles}) => DownloadProgress.__( - lastTileEvent: null, + latestTileEvent: null, cachedTiles: 0, cachedSize: 0, bufferedTiles: 0, @@ -88,7 +195,7 @@ class DownloadProgress { required int? rateLimit, }) => DownloadProgress.__( - lastTileEvent: lastTileEvent, + latestTileEvent: latestTileEvent, cachedTiles: cachedTiles, cachedSize: cachedSize, bufferedTiles: bufferedTiles, @@ -114,7 +221,7 @@ class DownloadProgress { bool isComplete = false, }) => DownloadProgress.__( - lastTileEvent: newTileEvent ?? lastTileEvent, + latestTileEvent: newTileEvent ?? latestTileEvent, cachedTiles: newTileEvent == null ? cachedTiles : newTileEvent.result.category == TileEventResultCategory.cached @@ -154,7 +261,7 @@ class DownloadProgress { bool operator ==(Object other) => identical(this, other) || (other is DownloadProgress && - _lastTileEvent == other._lastTileEvent && + _latestTileEvent == other._latestTileEvent && cachedTiles == other.cachedTiles && cachedSize == other.cachedSize && bufferedTiles == other.bufferedTiles && @@ -170,7 +277,7 @@ class DownloadProgress { @override int get hashCode => Object.hashAllUnordered([ - _lastTileEvent, + _latestTileEvent, cachedTiles, cachedSize, bufferedTiles, diff --git a/lib/src/bulk_download/instance.dart b/lib/src/bulk_download/instance.dart index 760540c3..b97f9877 100644 --- a/lib/src/bulk_download/instance.dart +++ b/lib/src/bulk_download/instance.dart @@ -6,7 +6,7 @@ import 'package:meta/meta.dart'; @internal class DownloadInstance { DownloadInstance._(this.id); - static final Map _instances = {}; + static final _instances = {}; static DownloadInstance? registerIfAvailable(Object id) => _instances.containsKey(id) @@ -22,4 +22,11 @@ class DownloadInstance { bool isPaused = false; Future Function()? requestPause; void Function()? requestResume; + + @override + bool operator ==(Object other) => + identical(this, other) || (other is DownloadInstance && id == other.id); + + @override + int get hashCode => id.hashCode; } diff --git a/lib/src/bulk_download/tile_loops/shared.dart b/lib/src/bulk_download/tile_loops/shared.dart index b624934b..82189d4f 100644 --- a/lib/src/bulk_download/tile_loops/shared.dart +++ b/lib/src/bulk_download/tile_loops/shared.dart @@ -16,14 +16,15 @@ part 'count.dart'; part 'generate.dart'; class _Polygon { - _Polygon(this.nw, this.ne, this.se, this.sw) : points = [nw, ne, se, sw] { - hashCode = Object.hashAllUnordered(points); + _Polygon( + CustomPoint nw, + CustomPoint ne, + CustomPoint se, + CustomPoint sw, + ) : points = [nw, ne, se, sw] { + hashCode = Object.hashAll(points); } - final CustomPoint nw; - final CustomPoint ne; - final CustomPoint se; - final CustomPoint sw; final List> points; @override diff --git a/lib/src/store/download.dart b/lib/src/store/download.dart index 8a8c6999..ccb368ec 100644 --- a/lib/src/store/download.dart +++ b/lib/src/store/download.dart @@ -8,19 +8,24 @@ part of flutter_map_tile_caching; /// The 'fmtc_plus_background_downloading' module must be installed to add the /// background downloading functionality. /// +/// --- +/// /// {@template num_instances} /// By default, only one download per store at the same time is tolerated. /// Attempting to start more than one at the same time may cause poor /// performance or other poor behaviour. /// /// However, if necessary, multiple can be started by setting methods' -/// `instanceId` argument to a unique value on methods. Note that this unique +/// `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. /// {@endtemplate} /// +/// --- +/// /// Does not keep state. State and download instances are held internally by /// [DownloadInstance]. -@immutable class DownloadManagement { const DownloadManagement._(this._storeDirectory); final StoreDirectory _storeDirectory; @@ -63,10 +68,11 @@ class DownloadManagement { /// --- /// /// Although disabled `null` by default, [rateLimit] can be used to impose a - /// limit on the maximum number of tiles requests that can be made per second. - /// This is useful to avoid placing too much strain on tile servers and avoid - /// rate limiting. Note that the real number of requests per second may exceed - /// [rateLimit] by a very small amount. + /// 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 From 663bd7f3e2b8ef54fe145105ca469a8874554c83 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 20 Jul 2023 18:03:58 +0200 Subject: [PATCH 031/168] Added initial tests for tile generation Migrated to latest flutter_map 'master' Improved typing Former-commit-id: 47d4d06d16aef162a0d1bf3dc4174509e8c6931c [formerly 85b69d7399bbde9a9ed04baaf65a40b7afc98c38] Former-commit-id: ba10e11bb93477e86ebe231cabcb52ab43a518de --- .../pages/downloader/components/map_view.dart | 16 ++-- lib/src/bulk_download/manager.dart | 10 +-- lib/src/bulk_download/tile_loops/count.dart | 44 +++++----- .../bulk_download/tile_loops/generate.dart | 44 +++++----- lib/src/bulk_download/tile_loops/shared.dart | 15 ++-- lib/src/regions/circle.dart | 2 +- lib/src/regions/downloadable_region.dart | 16 ++-- lib/src/regions/line.dart | 2 +- lib/src/regions/rectangle.dart | 2 +- lib/src/store/download.dart | 2 +- pubspec.yaml | 6 ++ test/flutter_map_tile_caching_test.dart | 86 +++++++++++++++++++ 12 files changed, 164 insertions(+), 81 deletions(-) create mode 100644 test/flutter_map_tile_caching_test.dart diff --git a/example/lib/screens/main/pages/downloader/components/map_view.dart b/example/lib/screens/main/pages/downloader/components/map_view.dart index 773651ff..7feb9db3 100644 --- a/example/lib/screens/main/pages/downloader/components/map_view.dart +++ b/example/lib/screens/main/pages/downloader/components/map_view.dart @@ -421,12 +421,10 @@ class _MapViewState extends State { _crosshairsTop = calculatedTop - _crosshairsMovement; _crosshairsBottom = centerNormal - _crosshairsMovement; - _center = _mapController.camera - .pointToLatLng(_customPointFromPoint(centerNormal)); + _center = _mapController.camera.pointToLatLng(centerNormal); _radius = const Distance(roundResult: false).distance( _center!, - _mapController.camera - .pointToLatLng(_customPointFromPoint(calculatedTop)), + _mapController.camera.pointToLatLng(calculatedTop), ) / 1000; setState(() {}); @@ -439,10 +437,9 @@ class _MapViewState extends State { _crosshairsTop = calculatedTopLeft - _crosshairsMovement; _crosshairsBottom = calculatedBottomRight - _crosshairsMovement; - _coordsTopLeft = _mapController.camera - .pointToLatLng(_customPointFromPoint(calculatedTopLeft)); - _coordsBottomRight = _mapController.camera - .pointToLatLng(_customPointFromPoint(calculatedBottomRight)); + _coordsTopLeft = _mapController.camera.pointToLatLng(calculatedTopLeft); + _coordsBottomRight = + _mapController.camera.pointToLatLng(calculatedBottomRight); setState(() {}); } @@ -468,6 +465,3 @@ class _MapViewState extends State { } } } - -CustomPoint _customPointFromPoint(Point point) => - CustomPoint(point.x, point.y); diff --git a/lib/src/bulk_download/manager.dart b/lib/src/bulk_download/manager.dart index ee25e983..3665cfee 100644 --- a/lib/src/bulk_download/manager.dart +++ b/lib/src/bulk_download/manager.dart @@ -32,10 +32,10 @@ Future _downloadManager( // Count number of tiles final maxTiles = input.region.when( - rectangle: (_) => TilesCounter.rectangleTiles, - circle: (_) => TilesCounter.circleTiles, - line: (_) => TilesCounter.lineTiles, - )(input.region); + rectangle: TilesCounter.rectangleTiles, + circle: TilesCounter.circleTiles, + line: TilesCounter.lineTiles, + ); // Setup sea tile removal system Uint8List? seaTileBytes; @@ -72,7 +72,7 @@ Future _downloadManager( rectangle: (_) => TilesGenerator.rectangleTiles, circle: (_) => TilesGenerator.circleTiles, line: (_) => TilesGenerator.lineTiles, - ), + ) as dynamic, (sendPort: tileRecievePort.sendPort, region: input.region), onExit: tileRecievePort.sendPort, debugName: '[FMTC] Tile Coords Generator Thread', diff --git a/lib/src/bulk_download/tile_loops/count.dart b/lib/src/bulk_download/tile_loops/count.dart index 8b78bdeb..a99a4577 100644 --- a/lib/src/bulk_download/tile_loops/count.dart +++ b/lib/src/bulk_download/tile_loops/count.dart @@ -4,36 +4,34 @@ part of 'shared.dart'; class TilesCounter { - static int rectangleTiles(DownloadableRegion region) { + static int rectangleTiles(DownloadableRegion region) { final tileSize = _getTileSize(region); - final northWest = - (region.originalRegion as RectangleRegion).bounds.northWest; - final southEast = - (region.originalRegion as RectangleRegion).bounds.southEast; + final northWest = region.originalRegion.bounds.northWest; + final southEast = region.originalRegion.bounds.southEast; var numberOfTiles = 0; for (double zoomLvl = region.minZoom.toDouble(); zoomLvl <= region.maxZoom; zoomLvl++) { - final nwCustomPoint = region.crs + final nwPoint = region.crs .latLngToPoint(northWest, zoomLvl) .unscaleBy(tileSize) .floor(); - final seCustomPoint = region.crs + final sePoint = region.crs .latLngToPoint(southEast, zoomLvl) .unscaleBy(tileSize) .ceil() - - const CustomPoint(1, 1); + const Point(1, 1); - numberOfTiles += (seCustomPoint.x - nwCustomPoint.x + 1) * - (seCustomPoint.y - nwCustomPoint.y + 1); + numberOfTiles += + (sePoint.x - nwPoint.x + 1) * (sePoint.y - nwPoint.y + 1); } return numberOfTiles; } - static int circleTiles(DownloadableRegion region) { + static int circleTiles(DownloadableRegion region) { // This took some time and is fairly complicated, so this is the overall explanation: // 1. Given a `LatLng` for every x degrees on a circle's circumference, convert it into a tile number // 2. Using a `Map` per zoom level, record all the X values in it without duplicates @@ -42,7 +40,7 @@ class TilesCounter { // Theoretically, this could have been done using the same method as `lineTiles`, but `lineTiles` was built after this algorithm and this makes more sense for a circle final tileSize = _getTileSize(region); - final circleOutline = (region.originalRegion as CircleRegion).toOutline(); + final circleOutline = region.originalRegion.toOutline(); // Format: Map>> final outlineTileNums = >>{}; @@ -79,7 +77,7 @@ class TilesCounter { return numberOfTiles; } - static int lineTiles(DownloadableRegion region) { + static int lineTiles(DownloadableRegion region) { // This took some time and is fairly complicated, so this is the overall explanation: // 1. Given 4 `LatLng` points, create a 'straight' rectangle around the 'rotated' rectangle, that can be defined with just 2 `LatLng` points // 2. Convert the straight rectangle into tile numbers, and loop through the same as `rectangleTiles` @@ -97,7 +95,7 @@ class TilesCounter { final p1 = polygon.points[i1]; final p2 = polygon.points[i2]; - final normal = CustomPoint(p2.y - p1.y, p1.x - p2.x); + final normal = Point(p2.y - p1.y, p1.x - p2.x); var minA = largestInt; var maxA = smallestInt; @@ -123,7 +121,7 @@ class TilesCounter { } final tileSize = _getTileSize(region); - final lineOutline = (region.originalRegion as LineRegion).toOutlines(1); + final lineOutline = region.originalRegion.toOutlines(1); int numberOfTiles = 0; @@ -161,17 +159,17 @@ class TilesCounter { .latLngToPoint(rotatedRectangle.topRight, zoomLvl) .unscaleBy(tileSize) .ceil() - - const CustomPoint(1, 0); + const Point(1, 0); final rotatedRectangleSW = region.crs .latLngToPoint(rotatedRectangle.bottomLeft, zoomLvl) .unscaleBy(tileSize) .ceil() - - const CustomPoint(0, 1); + const Point(0, 1); final rotatedRectangleSE = region.crs .latLngToPoint(rotatedRectangle.bottomRight, zoomLvl) .unscaleBy(tileSize) .ceil() - - const CustomPoint(1, 1); + const Point(1, 1); final straightRectangleNW = region.crs .latLngToPoint( @@ -187,16 +185,16 @@ class TilesCounter { ) .unscaleBy(tileSize) .ceil() - - const CustomPoint(1, 1); + const Point(1, 1); for (int x = straightRectangleNW.x; x <= straightRectangleSE.x; x++) { bool foundOverlappingTile = false; for (int y = straightRectangleNW.y; y <= straightRectangleSE.y; y++) { final tile = _Polygon( - CustomPoint(x, y), - CustomPoint(x + 1, y), - CustomPoint(x + 1, y + 1), - CustomPoint(x, y + 1), + Point(x, y), + Point(x + 1, y), + Point(x + 1, y + 1), + Point(x, y + 1), ); if (generatedTiles.contains(tile.hashCode)) continue; if (overlap( diff --git a/lib/src/bulk_download/tile_loops/generate.dart b/lib/src/bulk_download/tile_loops/generate.dart index 3b8eac29..56209ce4 100644 --- a/lib/src/bulk_download/tile_loops/generate.dart +++ b/lib/src/bulk_download/tile_loops/generate.dart @@ -5,14 +5,12 @@ part of 'shared.dart'; class TilesGenerator { static Future rectangleTiles( - ({SendPort sendPort, DownloadableRegion region}) input, + ({SendPort sendPort, DownloadableRegion region}) input, ) async { final region = input.region; final tileSize = _getTileSize(region); - final northWest = - (region.originalRegion as RectangleRegion).bounds.northWest; - final southEast = - (region.originalRegion as RectangleRegion).bounds.southEast; + final northWest = region.originalRegion.bounds.northWest; + final southEast = region.originalRegion.bounds.southEast; final recievePort = ReceivePort(); input.sendPort.send(recievePort.sendPort); @@ -21,18 +19,18 @@ class TilesGenerator { for (double zoomLvl = region.minZoom.toDouble(); zoomLvl <= region.maxZoom; zoomLvl++) { - final nwCustomPoint = region.crs + final nwPoint = region.crs .latLngToPoint(northWest, zoomLvl) .unscaleBy(tileSize) .floor(); - final seCustomPoint = region.crs + final sePoint = region.crs .latLngToPoint(southEast, zoomLvl) .unscaleBy(tileSize) .ceil() - - const CustomPoint(1, 1); + const Point(1, 1); - for (int x = nwCustomPoint.x; x <= seCustomPoint.x; x++) { - for (int y = nwCustomPoint.y; y <= seCustomPoint.y; y++) { + for (int x = nwPoint.x; x <= sePoint.x; x++) { + for (int y = nwPoint.y; y <= sePoint.y; y++) { await requestQueue.next; input.sendPort.send((x, y, zoomLvl.toInt())); } @@ -43,7 +41,7 @@ class TilesGenerator { } static Future circleTiles( - ({SendPort sendPort, DownloadableRegion region}) input, + ({SendPort sendPort, DownloadableRegion region}) input, ) async { // This took some time and is fairly complicated, so this is the overall explanation: // 1. Given a `LatLng` for every x degrees on a circle's circumference, convert it into a tile number @@ -54,7 +52,7 @@ class TilesGenerator { final region = input.region; final tileSize = _getTileSize(region); - final circleOutline = (region.originalRegion as CircleRegion).toOutline(); + final circleOutline = region.originalRegion.toOutline(); final recievePort = ReceivePort(); input.sendPort.send(recievePort.sendPort); @@ -97,7 +95,7 @@ class TilesGenerator { } static Future lineTiles( - ({SendPort sendPort, DownloadableRegion region}) input, + ({SendPort sendPort, DownloadableRegion region}) input, ) async { // This took some time and is fairly complicated, so this is the overall explanation: // 1. Given 4 `LatLng` points, create a 'straight' rectangle around the 'rotated' rectangle, that can be defined with just 2 `LatLng` points @@ -116,7 +114,7 @@ class TilesGenerator { final p1 = polygon.points[i1]; final p2 = polygon.points[i2]; - final normal = CustomPoint(p2.y - p1.y, p1.x - p2.x); + final normal = Point(p2.y - p1.y, p1.x - p2.x); var minA = largestInt; var maxA = smallestInt; @@ -143,7 +141,7 @@ class TilesGenerator { final region = input.region; final tileSize = _getTileSize(region); - final lineOutline = (region.originalRegion as LineRegion).toOutlines(1); + final lineOutline = region.originalRegion.toOutlines(1); final recievePort = ReceivePort(); input.sendPort.send(recievePort.sendPort); @@ -183,17 +181,17 @@ class TilesGenerator { .latLngToPoint(rotatedRectangle.topRight, zoomLvl) .unscaleBy(tileSize) .ceil() - - const CustomPoint(1, 0); + const Point(1, 0); final rotatedRectangleSW = region.crs .latLngToPoint(rotatedRectangle.bottomLeft, zoomLvl) .unscaleBy(tileSize) .ceil() - - const CustomPoint(0, 1); + const Point(0, 1); final rotatedRectangleSE = region.crs .latLngToPoint(rotatedRectangle.bottomRight, zoomLvl) .unscaleBy(tileSize) .ceil() - - const CustomPoint(1, 1); + const Point(1, 1); final straightRectangleNW = region.crs .latLngToPoint( @@ -209,16 +207,16 @@ class TilesGenerator { ) .unscaleBy(tileSize) .ceil() - - const CustomPoint(1, 1); + const Point(1, 1); for (int x = straightRectangleNW.x; x <= straightRectangleSE.x; x++) { bool foundOverlappingTile = false; for (int y = straightRectangleNW.y; y <= straightRectangleSE.y; y++) { final tile = _Polygon( - CustomPoint(x, y), - CustomPoint(x + 1, y), - CustomPoint(x + 1, y + 1), - CustomPoint(x, y + 1), + Point(x, y), + Point(x + 1, y), + Point(x + 1, y + 1), + Point(x, y + 1), ); if (generatedTiles.contains(tile.hashCode)) continue; if (overlap( diff --git a/lib/src/bulk_download/tile_loops/shared.dart b/lib/src/bulk_download/tile_loops/shared.dart index 82189d4f..e9972d7a 100644 --- a/lib/src/bulk_download/tile_loops/shared.dart +++ b/lib/src/bulk_download/tile_loops/shared.dart @@ -2,6 +2,7 @@ // A full license can be found at .\LICENSE import 'dart:isolate'; +import 'dart:math'; import 'package:async/async.dart'; import 'package:collection/collection.dart'; @@ -16,16 +17,12 @@ part 'count.dart'; part 'generate.dart'; class _Polygon { - _Polygon( - CustomPoint nw, - CustomPoint ne, - CustomPoint se, - CustomPoint sw, - ) : points = [nw, ne, se, sw] { + _Polygon(Point nw, Point ne, Point se, Point sw) + : points = [nw, ne, se, sw] { hashCode = Object.hashAll(points); } - final List> points; + final List> points; @override late final int hashCode; @@ -36,5 +33,5 @@ class _Polygon { (other is _Polygon && hashCode == other.hashCode); } -CustomPoint _getTileSize(DownloadableRegion region) => - CustomPoint(region.options.tileSize, region.options.tileSize); +Point _getTileSize(DownloadableRegion region) => + Point(region.options.tileSize, region.options.tileSize); diff --git a/lib/src/regions/circle.dart b/lib/src/regions/circle.dart index 4b592e55..71d85ec7 100644 --- a/lib/src/regions/circle.dart +++ b/lib/src/regions/circle.dart @@ -25,7 +25,7 @@ class CircleRegion extends BaseRegion { final double radius; @override - DownloadableRegion toDownloadable({ + DownloadableRegion toDownloadable({ required int minZoom, required int maxZoom, required TileLayer options, diff --git a/lib/src/regions/downloadable_region.dart b/lib/src/regions/downloadable_region.dart index 907ceca7..74b52f58 100644 --- a/lib/src/regions/downloadable_region.dart +++ b/lib/src/regions/downloadable_region.dart @@ -52,14 +52,18 @@ class DownloadableRegion { } /// Output a value of type [T] dependent on [originalRegion] and its type [R] - /// - /// Shortcut for [BaseRegion.when]. T when({ - required T Function(RectangleRegion rectangle) rectangle, - required T Function(CircleRegion circle) circle, - required T Function(LineRegion line) line, + required T Function(DownloadableRegion rectangle) + rectangle, + required T Function(DownloadableRegion circle) circle, + required T Function(DownloadableRegion line) line, }) => - originalRegion.when(rectangle: rectangle, circle: circle, line: line); + switch (originalRegion) { + RectangleRegion() => + rectangle(this as DownloadableRegion), + CircleRegion() => circle(this as DownloadableRegion), + LineRegion() => line(this as DownloadableRegion), + }; @override bool operator ==(Object other) => diff --git a/lib/src/regions/line.dart b/lib/src/regions/line.dart index 19ebda0e..65610ce6 100644 --- a/lib/src/regions/line.dart +++ b/lib/src/regions/line.dart @@ -75,7 +75,7 @@ class LineRegion extends BaseRegion { } @override - DownloadableRegion toDownloadable({ + DownloadableRegion toDownloadable({ required int minZoom, required int maxZoom, required TileLayer options, diff --git a/lib/src/regions/rectangle.dart b/lib/src/regions/rectangle.dart index 4ab723ab..df7432ca 100644 --- a/lib/src/regions/rectangle.dart +++ b/lib/src/regions/rectangle.dart @@ -24,7 +24,7 @@ class RectangleRegion extends BaseRegion { final LatLngBounds bounds; @override - DownloadableRegion toDownloadable({ + DownloadableRegion toDownloadable({ required int minZoom, required int maxZoom, required TileLayer options, diff --git a/lib/src/store/download.dart b/lib/src/store/download.dart index ccb368ec..3d8cf59b 100644 --- a/lib/src/store/download.dart +++ b/lib/src/store/download.dart @@ -245,7 +245,7 @@ class DownloadManagement { rectangle: (_) => TilesCounter.rectangleTiles, circle: (_) => TilesCounter.circleTiles, line: (_) => TilesCounter.lineTiles, - ), + ) as dynamic, region, ); diff --git a/pubspec.yaml b/pubspec.yaml index 13986cd1..b8695df2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -46,5 +46,11 @@ dev_dependencies: build_runner: ^2.3.2 flutter_lints: ^2.0.1 isar_generator: ^3.1.0+1 + test: ^1.24.4 + +dependency_overrides: + flutter_map: + git: + url: https://github.com/fleaflet/flutter_map.git flutter: null diff --git a/test/flutter_map_tile_caching_test.dart b/test/flutter_map_tile_caching_test.dart new file mode 100644 index 00000000..a9103742 --- /dev/null +++ b/test/flutter_map_tile_caching_test.dart @@ -0,0 +1,86 @@ +// ignore_for_file: avoid_print + +import 'package:collection/collection.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; +import 'package:flutter_map_tile_caching/src/bulk_download/tile_loops/shared.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:test/test.dart'; + +void main() { + group('Test Region Tile Generation', () { + final rectRegion = + RectangleRegion(LatLngBounds(const LatLng(-2, -2), const LatLng(2, 2))) + .toDownloadable(minZoom: 1, maxZoom: 18, options: TileLayer()); + + test( + 'Rectangle Region Count', + () => expect(TilesCounter.rectangleTiles(rectRegion), 11329252), + ); + + test( + 'Rectangle Region Duration', + () => print( + '${List.generate( + 1000, + (index) { + final clock = Stopwatch()..start(); + TilesCounter.rectangleTiles(rectRegion); + clock.stop(); + return clock.elapsedMilliseconds; + }, + growable: false, + ).average} ms', + ), + ); + + final circleRegion = CircleRegion(const LatLng(0, 0), 1000) + .toDownloadable(minZoom: 1, maxZoom: 18, options: TileLayer()); + + test( + 'Circle Region Count', + () => expect(TilesCounter.circleTiles(circleRegion), 2994818), + ); + + test( + 'Circle Region Duration', + () => print( + '${List.generate( + 1000, + (index) { + final clock = Stopwatch()..start(); + TilesCounter.circleTiles(circleRegion); + clock.stop(); + return clock.elapsedMilliseconds; + }, + growable: false, + ).average} ms', + ), + ); + + final lineRegion = + LineRegion([const LatLng(-1, -1), const LatLng(1, 1)], 100) + .toDownloadable(minZoom: 1, maxZoom: 16, options: TileLayer()); + + test( + 'Line Region Count', + () => expect(TilesCounter.lineTiles(lineRegion), 2936), + ); + + test( + 'Line Region Duration', + () => print( + '${List.generate( + 100, + (index) { + final clock = Stopwatch()..start(); + TilesCounter.lineTiles(lineRegion); + clock.stop(); + return clock.elapsedMilliseconds; + }, + growable: false, + ).average} ms', + ), + ); + }); +} From 6b8be2234184541215f1d7052e3e13be0936c16d Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Mon, 24 Jul 2023 10:53:52 +0100 Subject: [PATCH 032/168] Fixed bugs Improved speed of `LineRegion` tile generation Former-commit-id: 58bc07d600f5611982e3c02c552aa30d6d039f85 [formerly 1b62ea41f3f278a3ab936ab3c0876718e4a3f1c4] Former-commit-id: 133b6187453c0ba02161d1461dc697955972029f --- .../pages/downloader/components/map_view.dart | 3 +- .../lib/screens/main/pages/map/map_view.dart | 20 +++++++---- lib/src/bulk_download/manager.dart | 2 +- lib/src/bulk_download/tile_loops/count.dart | 12 +++++-- .../bulk_download/tile_loops/generate.dart | 12 +++---- lib/src/regions/base_region.dart | 8 ++--- lib/src/regions/circle.dart | 32 ++++------------- lib/src/regions/line.dart | 36 +++++++++---------- lib/src/store/download.dart | 2 +- test/flutter_map_tile_caching_test.dart | 2 +- 10 files changed, 60 insertions(+), 69 deletions(-) diff --git a/example/lib/screens/main/pages/downloader/components/map_view.dart b/example/lib/screens/main/pages/downloader/components/map_view.dart index 7feb9db3..0cc2087e 100644 --- a/example/lib/screens/main/pages/downloader/components/map_view.dart +++ b/example/lib/screens/main/pages/downloader/components/map_view.dart @@ -58,7 +58,7 @@ class _MapViewState extends State { const LatLng(90, -180), const LatLng(-90, -180), ], - holePointsList: [region.toOutline()], + holePointsList: [region.toOutline().toList()], isFilled: true, borderColor: Colors.black, borderStrokeWidth: 2, @@ -203,6 +203,7 @@ class _MapViewState extends State { borderColor: Colors.black, borderStrokeWidth: 2, fillColor: Colors.green.withOpacity(2 / 3), + prettyPaint: false, ) else if (_coordsTopLeft != null && _coordsBottomRight != null && diff --git a/example/lib/screens/main/pages/map/map_view.dart b/example/lib/screens/main/pages/map/map_view.dart index 0ecde54a..3fd6caa7 100644 --- a/example/lib/screens/main/pages/map/map_view.dart +++ b/example/lib/screens/main/pages/map/map_view.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; @@ -111,15 +112,20 @@ class MapPage extends StatelessWidget { UserLocationFollowState.navigation ? TurnOnHeadingUpdate.always : TurnOnHeadingUpdate.never, - style: const LocationMarkerStyle( + headingStream: Platform.isAndroid || Platform.isIOS + ? null + : const Stream.empty(), + style: LocationMarkerStyle( marker: DefaultLocationMarker( - child: Icon( - Icons.navigation, - color: Colors.white, - size: 18, - ), + child: Platform.isAndroid || Platform.isIOS + ? const Icon( + Icons.navigation, + color: Colors.white, + size: 18, + ) + : null, ), - markerSize: Size(30, 30), + markerSize: const Size(30, 30), markerDirection: MarkerDirection.heading, ), ), diff --git a/lib/src/bulk_download/manager.dart b/lib/src/bulk_download/manager.dart index 3665cfee..f0687247 100644 --- a/lib/src/bulk_download/manager.dart +++ b/lib/src/bulk_download/manager.dart @@ -72,7 +72,7 @@ Future _downloadManager( rectangle: (_) => TilesGenerator.rectangleTiles, circle: (_) => TilesGenerator.circleTiles, line: (_) => TilesGenerator.lineTiles, - ) as dynamic, + ), (sendPort: tileRecievePort.sendPort, region: input.region), onExit: tileRecievePort.sendPort, debugName: '[FMTC] Tile Coords Generator Thread', diff --git a/lib/src/bulk_download/tile_loops/count.dart b/lib/src/bulk_download/tile_loops/count.dart index a99a4577..9234ecb3 100644 --- a/lib/src/bulk_download/tile_loops/count.dart +++ b/lib/src/bulk_download/tile_loops/count.dart @@ -4,7 +4,9 @@ part of 'shared.dart'; class TilesCounter { - static int rectangleTiles(DownloadableRegion region) { + static int rectangleTiles(DownloadableRegion region) { + region as DownloadableRegion; + final tileSize = _getTileSize(region); final northWest = region.originalRegion.bounds.northWest; final southEast = region.originalRegion.bounds.southEast; @@ -31,7 +33,9 @@ class TilesCounter { return numberOfTiles; } - static int circleTiles(DownloadableRegion region) { + static int circleTiles(DownloadableRegion region) { + region as DownloadableRegion; + // This took some time and is fairly complicated, so this is the overall explanation: // 1. Given a `LatLng` for every x degrees on a circle's circumference, convert it into a tile number // 2. Using a `Map` per zoom level, record all the X values in it without duplicates @@ -77,7 +81,9 @@ class TilesCounter { return numberOfTiles; } - static int lineTiles(DownloadableRegion region) { + static int lineTiles(DownloadableRegion region) { + region as DownloadableRegion; + // This took some time and is fairly complicated, so this is the overall explanation: // 1. Given 4 `LatLng` points, create a 'straight' rectangle around the 'rotated' rectangle, that can be defined with just 2 `LatLng` points // 2. Convert the straight rectangle into tile numbers, and loop through the same as `rectangleTiles` diff --git a/lib/src/bulk_download/tile_loops/generate.dart b/lib/src/bulk_download/tile_loops/generate.dart index 56209ce4..c1302fc9 100644 --- a/lib/src/bulk_download/tile_loops/generate.dart +++ b/lib/src/bulk_download/tile_loops/generate.dart @@ -5,9 +5,9 @@ part of 'shared.dart'; class TilesGenerator { static Future rectangleTiles( - ({SendPort sendPort, DownloadableRegion region}) input, + ({SendPort sendPort, DownloadableRegion region}) input, ) async { - final region = input.region; + final region = input.region as DownloadableRegion; final tileSize = _getTileSize(region); final northWest = region.originalRegion.bounds.northWest; final southEast = region.originalRegion.bounds.southEast; @@ -41,7 +41,7 @@ class TilesGenerator { } static Future circleTiles( - ({SendPort sendPort, DownloadableRegion region}) input, + ({SendPort sendPort, DownloadableRegion region}) input, ) async { // This took some time and is fairly complicated, so this is the overall explanation: // 1. Given a `LatLng` for every x degrees on a circle's circumference, convert it into a tile number @@ -50,7 +50,7 @@ class TilesGenerator { // 4. Loop over these XY values and add them to the list // Theoretically, this could have been done using the same method as `lineTiles`, but `lineTiles` was built after this algorithm and this makes more sense for a circle - final region = input.region; + final region = input.region as DownloadableRegion; final tileSize = _getTileSize(region); final circleOutline = region.originalRegion.toOutline(); @@ -95,7 +95,7 @@ class TilesGenerator { } static Future lineTiles( - ({SendPort sendPort, DownloadableRegion region}) input, + ({SendPort sendPort, DownloadableRegion region}) input, ) async { // This took some time and is fairly complicated, so this is the overall explanation: // 1. Given 4 `LatLng` points, create a 'straight' rectangle around the 'rotated' rectangle, that can be defined with just 2 `LatLng` points @@ -139,7 +139,7 @@ class TilesGenerator { return true; } - final region = input.region; + final region = input.region as DownloadableRegion; final tileSize = _getTileSize(region); final lineOutline = region.originalRegion.toOutlines(1); diff --git a/lib/src/regions/base_region.dart b/lib/src/regions/base_region.dart index ee629885..80ab4655 100644 --- a/lib/src/regions/base_region.dart +++ b/lib/src/regions/base_region.dart @@ -8,7 +8,7 @@ part of flutter_map_tile_caching; /// It can be converted to a: /// - [DownloadableRegion] for downloading: [toDownloadable] /// - [Widget] layer to be placed in a map: [toDrawable] -/// - list of [LatLng]s forming the outline: [toOutline]/[LineRegion.toOutlines] +/// - list of [LatLng]s forming the outline: [toOutline] /// /// Extended/implemented by: /// - [RectangleRegion] @@ -20,7 +20,7 @@ sealed class BaseRegion { /// It can be converted to a: /// - [DownloadableRegion] for downloading: [toDownloadable] /// - [Widget] layer to be placed in a map: [toDrawable] - /// - list of [LatLng]s forming the outline: [toOutline]/[LineRegion.toOutlines] + /// - list of [LatLng]s forming the outline: [toOutline] /// /// Extended/implemented by: /// - [RectangleRegion] @@ -79,8 +79,8 @@ sealed class BaseRegion { /// Generate the list of all the [LatLng]s forming the outline of this region /// - /// Returns a `List` which can be used anywhere. - List toOutline(); + /// Returns a `Iterable` which can be used anywhere. + Iterable toOutline(); @override @mustCallSuper diff --git a/lib/src/regions/circle.dart b/lib/src/regions/circle.dart index 71d85ec7..a3e12ea3 100644 --- a/lib/src/regions/circle.dart +++ b/lib/src/regions/circle.dart @@ -57,6 +57,7 @@ class CircleRegion extends BaseRegion { PolygonLayer( polygons: [ Polygon( + points: toOutline().toList(), isFilled: fillColor != null, color: fillColor ?? Colors.transparent, borderColor: borderColor, @@ -65,40 +66,19 @@ class CircleRegion extends BaseRegion { label: label, labelStyle: labelStyle, labelPlacement: labelPlacement, - points: toOutline(), ) ], ); @override - List toOutline() { - final double rad = radius / 1.852 / 3437.670013352; - final double lat = center.latitudeInRad; - final double lon = center.longitudeInRad; - final List output = []; + Iterable toOutline() sync* { + const dist = Distance(roundResult: false, calculator: Haversine()); - for (int x = 0; x <= 360; x++) { - final double brng = x * math.pi / 180; - final double latRadians = math.asin( - math.sin(lat) * math.cos(rad) + - math.cos(lat) * math.sin(rad) * math.cos(brng), - ); - final double lngRadians = lon + - math.atan2( - math.sin(brng) * math.sin(rad) * math.cos(lat), - math.cos(rad) - math.sin(lat) * math.sin(latRadians), - ); + final radius = this.radius * 1000; - output.add( - LatLng( - latRadians * 180 / math.pi, - (lngRadians * 180 / math.pi) - .clamp(-180, 180), // Clamped to fix errors with flutter_map - ), - ); + for (int angle = -180; angle <= 180; angle++) { + yield dist.offset(center, radius, angle); } - - return output; } @override diff --git a/lib/src/regions/line.dart b/lib/src/regions/line.dart index 65610ce6..23f9c268 100644 --- a/lib/src/regions/line.dart +++ b/lib/src/regions/line.dart @@ -32,46 +32,44 @@ class LineRegion extends BaseRegion { /// * -1: joined by closest corners (largest gap) /// * 0 (default): joined by centers /// * 1 (as downloaded): joined by further corners (largest overlap) - List> toOutlines([int overlap = 0]) { + Iterable> toOutlines([int overlap = 0]) sync* { if (overlap < -1 || overlap > 1) { throw ArgumentError('`overlap` must be between -1 and 1 inclusive'); } - if (line.isEmpty) return []; + if (line.isEmpty) return; const dist = Distance(); final rad = radius * math.pi / 4; - return line.map((pos) { - if ((line.indexOf(pos) + 1) >= line.length) return [const LatLng(0, 0)]; + for (int i = 0; i < line.length - 1; i++) { + final cp = line[i]; + final np = line[i + 1]; - final section = [pos, line[line.indexOf(pos) + 1]]; - - final bearing = dist.bearing(section[0], section[1]); + final bearing = dist.bearing(cp, np); final clockwiseRotation = (90 + bearing) > 360 ? 360 - (90 + bearing) : (90 + bearing); final anticlockwiseRotation = (bearing - 90) < 0 ? 360 + (bearing - 90) : (bearing - 90); - final topRight = dist.offset(section[0], rad, clockwiseRotation); - final bottomRight = dist.offset(section[1], rad, clockwiseRotation); - final bottomLeft = dist.offset(section[1], rad, anticlockwiseRotation); - final topLeft = dist.offset(section[0], rad, anticlockwiseRotation); + final topRight = dist.offset(cp, rad, clockwiseRotation); + final bottomRight = dist.offset(np, rad, clockwiseRotation); + final bottomLeft = dist.offset(np, rad, anticlockwiseRotation); + final topLeft = dist.offset(cp, rad, anticlockwiseRotation); - if (overlap == 0) return [topRight, bottomRight, bottomLeft, topLeft]; + if (overlap == 0) yield [topRight, bottomRight, bottomLeft, topLeft]; final r = overlap == -1; - final os = line.indexOf(pos) == 0; - final oe = line.indexOf(pos) == line.length - 2; + final os = i == 0; + final oe = i == line.length - 2; - return [ + yield [ os ? topRight : dist.offset(topRight, r ? rad : -rad, bearing), oe ? bottomRight : dist.offset(bottomRight, r ? -rad : rad, bearing), oe ? bottomLeft : dist.offset(bottomLeft, r ? -rad : rad, bearing), os ? topLeft : dist.offset(topLeft, r ? rad : -rad, bearing), ]; - }).toList() - ..removeLast(); + } } @override @@ -153,8 +151,8 @@ class LineRegion extends BaseRegion { /// > * 0 (default): joined by centers /// > * 1 (as downloaded): joined by further corners (most overlap) @override - List toOutline([int overlap = 1]) => - toOutlines(overlap).expand((x) => x).toList(); + Iterable toOutline([int overlap = 1]) => + toOutlines(overlap).expand((x) => x); @override bool operator ==(Object other) => diff --git a/lib/src/store/download.dart b/lib/src/store/download.dart index 3d8cf59b..ccb368ec 100644 --- a/lib/src/store/download.dart +++ b/lib/src/store/download.dart @@ -245,7 +245,7 @@ class DownloadManagement { rectangle: (_) => TilesCounter.rectangleTiles, circle: (_) => TilesCounter.circleTiles, line: (_) => TilesCounter.lineTiles, - ) as dynamic, + ), region, ); diff --git a/test/flutter_map_tile_caching_test.dart b/test/flutter_map_tile_caching_test.dart index a9103742..2a321629 100644 --- a/test/flutter_map_tile_caching_test.dart +++ b/test/flutter_map_tile_caching_test.dart @@ -39,7 +39,7 @@ void main() { test( 'Circle Region Count', - () => expect(TilesCounter.circleTiles(circleRegion), 2994818), + () => expect(TilesCounter.circleTiles(circleRegion), 2989468), ); test( From 608cc9dc1c969d3b70864e4d4efea38cfc1fca65 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Mon, 24 Jul 2023 10:59:40 +0100 Subject: [PATCH 033/168] Temporarily resolved tests Former-commit-id: fb7890a472674a6fca06e89b357b34ee13dc2b2d [formerly 67d762ea5b1c769dd692b760a5f28f1d463f480b] Former-commit-id: 77dd3618397f41567c82bd30b6d5a32d56273896 --- pubspec.yaml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index b8695df2..579ec250 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,7 +30,9 @@ dependencies: collection: ^1.16.0 flutter: sdk: flutter - flutter_map: ^5.0.0 + flutter_map: + git: + url: https://github.com/fleaflet/flutter_map.git http: ^1.1.0 isar: ^3.1.0+1 isar_flutter_libs: ^3.1.0+1 @@ -48,9 +50,4 @@ dev_dependencies: isar_generator: ^3.1.0+1 test: ^1.24.4 -dependency_overrides: - flutter_map: - git: - url: https://github.com/fleaflet/flutter_map.git - flutter: null From 1206e1ecb187476d773cb5b6e158fc4d3f78055f Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Wed, 26 Jul 2023 15:46:45 +0100 Subject: [PATCH 034/168] Minor syntactic improvements Former-commit-id: 6c234a1bedaae904f41276c987df38993357e2e4 [formerly e6e67524c575f80c9b124269577b8d188ca9c4a8] Former-commit-id: 3b735a0012ac03513a0466692001e9a5ee2f4098 --- lib/src/bulk_download/manager.dart | 69 ++++++++++++++---------------- 1 file changed, 33 insertions(+), 36 deletions(-) diff --git a/lib/src/bulk_download/manager.dart b/lib/src/bulk_download/manager.dart index f0687247..940444b3 100644 --- a/lib/src/bulk_download/manager.dart +++ b/lib/src/bulk_download/manager.dart @@ -100,22 +100,23 @@ Future _downloadManager( send(rootRecievePort.sendPort); // Start progress tracking - final downloadDuration = Stopwatch()..start(); - var lastDownloadProgress = DownloadProgress._initial(maxTiles: maxTiles); - int mptTileIndex = -1; - int mptSlots = 50; + final initialDownloadProgress = DownloadProgress._initial(maxTiles: maxTiles); + var lastDownloadProgress = initialDownloadProgress; + int tileNumber = -1; + int mptSlots = 70; final microsecondsPerTile = List.filled(mptSlots, null, growable: true); - final tpsRates = List.filled(20, null); - final tileTimer = Stopwatch(); // This might be the wrong place to put this? + final tpsRates = List.filled(40, null); + final tileTimer = Stopwatch(); // Could be a black spot for bugs + final downloadDuration = Stopwatch()..start(); // Setup cancel, pause, and resume handling final threadPausedStates = List.generate( input.parallelThreads, - (_) => Completer(), + (_) => Completer(), growable: false, ); final cancelSignal = Completer(); - Completer pauseResumeSignal = Completer()..complete(); + var pauseResumeSignal = Completer()..complete(); rootRecievePort.listen( (e) async { if (e == null) { @@ -124,9 +125,9 @@ Future _downloadManager( // ignore: avoid_catching_errors, empty_catches } on StateError {} } else if (e == 1) { - pauseResumeSignal = Completer(); + pauseResumeSignal = Completer(); for (int i = 0; i < input.parallelThreads; i++) { - threadPausedStates[i] = Completer(); + threadPausedStates[i] = Completer(); } await Future.wait(threadPausedStates.map((e) => e.future)); downloadDuration.stop(); @@ -139,28 +140,23 @@ Future _downloadManager( ); // Setup progress report fallback - final Timer? fallbackReportTimer; - if (input.maxReportInterval case final maxReportInterval?) { - fallbackReportTimer = Timer.periodic( - maxReportInterval, - (_) { - if (lastDownloadProgress != - DownloadProgress._initial(maxTiles: maxTiles) && - pauseResumeSignal.isCompleted) { - final tps = tpsRates.nonNulls.average; - send( - lastDownloadProgress = lastDownloadProgress._updateProgress( - newDuration: downloadDuration.elapsed, - tilesPerSecond: tps, - rateLimit: input.rateLimit, - ), - ); - } - }, - ); - } else { - fallbackReportTimer = null; - } + final fallbackReportTimer = input.maxReportInterval == null + ? null + : Timer.periodic( + input.maxReportInterval!, + (_) { + if (lastDownloadProgress != initialDownloadProgress && + pauseResumeSignal.isCompleted) { + send( + lastDownloadProgress = lastDownloadProgress._updateProgress( + newDuration: downloadDuration.elapsed, + tilesPerSecond: tpsRates.nonNulls.average, + rateLimit: input.rateLimit, + ), + ); + } + }, + ); // Start download threads & wait for download to complete/cancelled await Future.wait( @@ -197,7 +193,8 @@ Future _downloadManager( // kill all threads unawaited( cancelSignal.future - .then((_) async => (await sendPortCompleter.future).send(null)), + .then((_) => sendPortCompleter.future) + .then((sp) => sp.send(null)), ); downloadThreadRecievePort.listen( @@ -205,13 +202,13 @@ Future _downloadManager( // Thread is sending tile data if (evt is TileEvent) { // Handle progress estimation measurements - mptTileIndex++; - microsecondsPerTile[mptTileIndex % mptSlots] = + tileNumber++; + microsecondsPerTile[tileNumber % mptSlots] = (tileTimer..stop()).elapsedMicroseconds; final rawTPS = (1 / (microsecondsPerTile.nonNulls.average)) * 1000000; microsecondsPerTile.length = mptSlots = rawTPS.ceil() * 2; - tpsRates[mptTileIndex % 20] = rawTPS; + tpsRates[tileNumber % 40] = rawTPS; final tps = tpsRates.nonNulls.average; // If buffering is in use, send a progress update with buffer info From 316613d1c75dca24b00a9f48dbd6ea58f6e68be4 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Wed, 26 Jul 2023 18:14:09 +0100 Subject: [PATCH 035/168] Added miniature testing tile server Fixed bugs Improved performance Former-commit-id: 978bb51669d0bd86a962639bd490422f3ecd9d35 [formerly 5771867183618d4805a075554792109ae365d02d] Former-commit-id: fe3db89d43aa1a2c814e1a4c6d819bea29d047a2 --- .github/workflows/main.yml | 13 ++++ example/lib/main.dart | 1 + .../pages/downloader/components/map_view.dart | 6 +- fmtc_tile_server.bat | 2 + lib/src/regions/downloadable_region.dart | 19 ++++-- lib/src/store/manage.dart | 2 + pubspec.yaml | 1 - test/tools/tile_server/CHANGELOG.md | 3 + test/tools/tile_server/README.md | 2 + test/tools/tile_server/bin/tile_server.dart | 63 ++++++++++++++++++ .../bin/tile_server.exe.REMOVED.git-id | 1 + test/tools/tile_server/pubspec.yaml | 11 +++ .../tile_server/static/assets/fake_tile.png | Bin 0 -> 18689 bytes 13 files changed, 117 insertions(+), 7 deletions(-) create mode 100644 fmtc_tile_server.bat create mode 100644 test/tools/tile_server/CHANGELOG.md create mode 100644 test/tools/tile_server/README.md create mode 100644 test/tools/tile_server/bin/tile_server.dart create mode 100644 test/tools/tile_server/bin/tile_server.exe.REMOVED.git-id create mode 100644 test/tools/tile_server/pubspec.yaml create mode 100644 test/tools/tile_server/static/assets/fake_tile.png diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 73a011e2..efb5d469 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -46,6 +46,19 @@ jobs: run: dart format --output=none --set-exit-if-changed . - name: Check Lints run: dart analyze --fatal-infos --fatal-warnings + + run-tests: + name: "Run Tests" + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v3 + - name: Setup Flutter Environment + uses: subosito/flutter-action@v2 + with: + channel: "stable" + - name: Run Tests + run: flutter test -r expanded build-android: name: "Build Android Demo App" diff --git a/example/lib/main.dart b/example/lib/main.dart index 93e6da7e..953fd807 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -24,6 +24,7 @@ void main() async { String? damagedDatabaseDeleted; await FlutterMapTileCaching.initialise( errorHandler: (error) => damagedDatabaseDeleted = error.message, + settings: FMTCSettings(databaseMaxSize: 10000), debugMode: true, ); diff --git a/example/lib/screens/main/pages/downloader/components/map_view.dart b/example/lib/screens/main/pages/downloader/components/map_view.dart index 0cc2087e..82630534 100644 --- a/example/lib/screens/main/pages/downloader/components/map_view.dart +++ b/example/lib/screens/main/pages/downloader/components/map_view.dart @@ -151,7 +151,8 @@ class _MapViewState extends State { scrollWheelVelocity: 0.002, ), onMapReady: () { - _updatePointLatLng(); + WidgetsBinding.instance + .addPostFrameCallback((_) => _updatePointLatLng()); _countTiles(); }, onTap: (_, point) => _addLinePoint(point), @@ -353,7 +354,8 @@ class _MapViewState extends State { } void _updatePointLatLng() { - if (downloadProvider.regionMode == RegionMode.line) return; + if (downloadProvider.regionMode == RegionMode.line || + _mapKey.currentContext == null) return; final Size mapSize = _mapKey.currentContext!.size!; final bool isHeightLongestSide = mapSize.width < mapSize.height; diff --git a/fmtc_tile_server.bat b/fmtc_tile_server.bat new file mode 100644 index 00000000..e94dc98e --- /dev/null +++ b/fmtc_tile_server.bat @@ -0,0 +1,2 @@ +@echo off +start cmd /C "dart compile exe test/tools/tile_server/bin/tile_server.dart && start /b test/tools/tile_server/bin/tile_server.exe" \ No newline at end of file diff --git a/lib/src/regions/downloadable_region.dart b/lib/src/regions/downloadable_region.dart index 74b52f58..c9fd55ad 100644 --- a/lib/src/regions/downloadable_region.dart +++ b/lib/src/regions/downloadable_region.dart @@ -51,6 +51,18 @@ class DownloadableRegion { } } + /// Cast [originalRegion] from [R] to [N] + @optionalTypeArgs + DownloadableRegion _cast() => DownloadableRegion._( + originalRegion as N, + minZoom: minZoom, + maxZoom: maxZoom, + options: options, + start: start, + end: end, + crs: crs, + ); + /// Output a value of type [T] dependent on [originalRegion] and its type [R] T when({ required T Function(DownloadableRegion rectangle) @@ -59,10 +71,9 @@ class DownloadableRegion { required T Function(DownloadableRegion line) line, }) => switch (originalRegion) { - RectangleRegion() => - rectangle(this as DownloadableRegion), - CircleRegion() => circle(this as DownloadableRegion), - LineRegion() => line(this as DownloadableRegion), + RectangleRegion() => rectangle(_cast()), + CircleRegion() => circle(_cast()), + LineRegion() => line(_cast()), }; @override diff --git a/lib/src/store/manage.dart b/lib/src/store/manage.dart index 947fdeeb..b83f6259 100644 --- a/lib/src/store/manage.dart +++ b/lib/src/store/manage.dart @@ -213,6 +213,7 @@ class StoreManagement { .tiles .where(sort: Sort.desc) .anyLastModified() + .limit(1) .findFirstSync(); if (latestTile == null) return null; @@ -277,6 +278,7 @@ class StoreManagement { .tiles .where(sort: Sort.desc) .anyLastModified() + .limit(1) .findFirst(); if (latestTile == null) return null; diff --git a/pubspec.yaml b/pubspec.yaml index 579ec250..97fe7787 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -46,7 +46,6 @@ dependencies: dev_dependencies: build_runner: ^2.3.2 - flutter_lints: ^2.0.1 isar_generator: ^3.1.0+1 test: ^1.24.4 diff --git a/test/tools/tile_server/CHANGELOG.md b/test/tools/tile_server/CHANGELOG.md new file mode 100644 index 00000000..effe43c8 --- /dev/null +++ b/test/tools/tile_server/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial version. diff --git a/test/tools/tile_server/README.md b/test/tools/tile_server/README.md new file mode 100644 index 00000000..3816eca3 --- /dev/null +++ b/test/tools/tile_server/README.md @@ -0,0 +1,2 @@ +A sample command-line application with an entrypoint in `bin/`, library code +in `lib/`, and example unit test in `test/`. diff --git a/test/tools/tile_server/bin/tile_server.dart b/test/tools/tile_server/bin/tile_server.dart new file mode 100644 index 00000000..3fd41ffe --- /dev/null +++ b/test/tools/tile_server/bin/tile_server.dart @@ -0,0 +1,63 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:isolate'; + +import 'package:dart_console/dart_console.dart'; +import 'package:jaguar/jaguar.dart'; +import 'package:path/path.dart' as p; + +Future main(List _) async { + final console = Console() + ..setTextStyle(bold: true, underscore: true) + ..write('\nFMTC Testing Tile Server\n\n') + ..setTextStyle(); + + final execPath = p.split(Platform.script.toFilePath()); + final staticPath = + p.joinAll([...execPath.getRange(0, execPath.length - 2), 'static']); + + final server = Jaguar( + onRouteServed: (ctx) => console.writeLine( + '[${ctx.at}] ${ctx.method} ${ctx.path}: ${ctx.response.statusCode}', + ), + ); + + final quitHandlerRecievePort = ReceivePort(); + await Isolate.spawn( + (_) { + final console = Console(); + while (true) { + if (console.readKey().char.toLowerCase() == 'q') { + console + ..setTextStyle(bold: true) + ..write('Killed HTTP server\n') + ..setTextStyle(); + Isolate.exit(); + } + } + }, + null, + onExit: quitHandlerRecievePort.sendPort, + ); + unawaited( + quitHandlerRecievePort.first.then((_) { + server.close(); + exit(0); + }), + ); + + server + ..get('/ok', (context) => 'OK!') + ..staticFile('*', p.join(staticPath, 'assets', 'fake_tile.png')); + + console + ..setTextStyle(italic: true) + ..write('Now serving at 0.0.0.0:8080\n') + ..write("Press 'q' to kill server\n\n") + ..setTextStyle() + ..write('GET request any path to be served a 256x256 PNG map tile\n') + ..write("GET request '/ok' to be server a basic text response\n\n") + ..write('-----\n\n'); + + await server.serve(logRequests: true); +} diff --git a/test/tools/tile_server/bin/tile_server.exe.REMOVED.git-id b/test/tools/tile_server/bin/tile_server.exe.REMOVED.git-id new file mode 100644 index 00000000..e93ab320 --- /dev/null +++ b/test/tools/tile_server/bin/tile_server.exe.REMOVED.git-id @@ -0,0 +1 @@ +83e5841e826b6f8e8cd9968b5d7791952ab8dc62 \ No newline at end of file diff --git a/test/tools/tile_server/pubspec.yaml b/test/tools/tile_server/pubspec.yaml new file mode 100644 index 00000000..31e10dee --- /dev/null +++ b/test/tools/tile_server/pubspec.yaml @@ -0,0 +1,11 @@ +name: tile_server +description: A sample command-line application. +version: 1.0.0 + +environment: + sdk: ^3.0.5 + +dependencies: + dart_console: ^1.2.0 + jaguar: ^3.1.3 + path: ^1.8.3 diff --git a/test/tools/tile_server/static/assets/fake_tile.png b/test/tools/tile_server/static/assets/fake_tile.png new file mode 100644 index 0000000000000000000000000000000000000000..80f9961d825b9a82cd358d919469aac7bcd4dd25 GIT binary patch literal 18689 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 literal 0 HcmV?d00001 From f27232c6b390e35a3911977468bd657d0251888a Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Wed, 26 Jul 2023 18:20:14 +0100 Subject: [PATCH 036/168] Adjust GitHub workflow Former-commit-id: 7e7684f3d8f16aba67c6c1030ab62925b6b7cdce [formerly 5d32b4dde85ad4ce50beee31242bc452f2e8360c] Former-commit-id: 6025a16d172182078bcf82985870201b6c597777 --- .github/workflows/main.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index efb5d469..81f5e9c0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -50,6 +50,7 @@ jobs: run-tests: name: "Run Tests" runs-on: ubuntu-latest + needs: analyse-code steps: - name: Checkout Repository uses: actions/checkout@v3 @@ -63,7 +64,7 @@ jobs: build-android: name: "Build Android Demo App" runs-on: ubuntu-latest - needs: analyse-code + needs: [analyse-code, run-tests, score-package] defaults: run: working-directory: ./example @@ -91,7 +92,7 @@ jobs: build-windows: name: "Build Windows Demo App" runs-on: windows-latest - needs: analyse-code + needs: [analyse-code, run-tests, score-package] defaults: run: working-directory: ./example From 676ffbdcb219fe25e360ee003ed087542e3c720b Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Wed, 26 Jul 2023 18:27:58 +0100 Subject: [PATCH 037/168] Attempt to fix GitHub workflow Former-commit-id: af1d80148ce0920fdbff824f94668403ecf55781 [formerly 4f938c6bc7788f369cc32bc0f929575c882b096a] Former-commit-id: 9e14dd2b5a1487d80e8de55bae4b45f5b752d87e --- .github/workflows/main.yml | 4 +++- test/tools/tile_server/CHANGELOG.md | 3 --- 2 files changed, 3 insertions(+), 4 deletions(-) delete mode 100644 test/tools/tile_server/CHANGELOG.md diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 81f5e9c0..8b152e84 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -40,8 +40,10 @@ jobs: uses: subosito/flutter-action@v2 with: channel: "stable" - - name: Get All Dependencies + - name: Get Package & Example Dependencies run: flutter pub get + - name: Get Test Tile Server Dependencies + run: dart pub get -C test/tools/tile_server - name: Check Formatting run: dart format --output=none --set-exit-if-changed . - name: Check Lints diff --git a/test/tools/tile_server/CHANGELOG.md b/test/tools/tile_server/CHANGELOG.md deleted file mode 100644 index effe43c8..00000000 --- a/test/tools/tile_server/CHANGELOG.md +++ /dev/null @@ -1,3 +0,0 @@ -## 1.0.0 - -- Initial version. From 47c0240fc6cad1e6f5ba6a7917895c224635637a Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Wed, 26 Jul 2023 18:58:14 +0100 Subject: [PATCH 038/168] Adjusted GitHub workflows Former-commit-id: 44ae03a2ec5b6d7d497ee545d308b2fa9d0728e4 [formerly f2e8bf86ccac67a75646fd8b67df1729c1663d6c] Former-commit-id: 82745b25b67a730cc5d39ff63816df015f41668e --- .github/workflows/main.yml | 1 - test/tools/tile_server/.gitignore | 1 + test/tools/tile_server/README.md | 2 -- test/tools/tile_server/bin/tile_server.exe.REMOVED.git-id | 1 - test/tools/tile_server/pubspec.yaml | 4 ++-- 5 files changed, 3 insertions(+), 6 deletions(-) create mode 100644 test/tools/tile_server/.gitignore delete mode 100644 test/tools/tile_server/README.md delete mode 100644 test/tools/tile_server/bin/tile_server.exe.REMOVED.git-id diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8b152e84..418af296 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -52,7 +52,6 @@ jobs: run-tests: name: "Run Tests" runs-on: ubuntu-latest - needs: analyse-code steps: - name: Checkout Repository uses: actions/checkout@v3 diff --git a/test/tools/tile_server/.gitignore b/test/tools/tile_server/.gitignore new file mode 100644 index 00000000..ae7e7d36 --- /dev/null +++ b/test/tools/tile_server/.gitignore @@ -0,0 +1 @@ +bin/tile_server.exe \ No newline at end of file diff --git a/test/tools/tile_server/README.md b/test/tools/tile_server/README.md deleted file mode 100644 index 3816eca3..00000000 --- a/test/tools/tile_server/README.md +++ /dev/null @@ -1,2 +0,0 @@ -A sample command-line application with an entrypoint in `bin/`, library code -in `lib/`, and example unit test in `test/`. diff --git a/test/tools/tile_server/bin/tile_server.exe.REMOVED.git-id b/test/tools/tile_server/bin/tile_server.exe.REMOVED.git-id deleted file mode 100644 index e93ab320..00000000 --- a/test/tools/tile_server/bin/tile_server.exe.REMOVED.git-id +++ /dev/null @@ -1 +0,0 @@ -83e5841e826b6f8e8cd9968b5d7791952ab8dc62 \ No newline at end of file diff --git a/test/tools/tile_server/pubspec.yaml b/test/tools/tile_server/pubspec.yaml index 31e10dee..2c802e3c 100644 --- a/test/tools/tile_server/pubspec.yaml +++ b/test/tools/tile_server/pubspec.yaml @@ -1,9 +1,9 @@ name: tile_server -description: A sample command-line application. +description: Miniature tile server designed to test FMTC's throughput and download speeds. version: 1.0.0 environment: - sdk: ^3.0.5 + sdk: ">=3.0.0 <4.0.0" dependencies: dart_console: ^1.2.0 From eb7fd91a8735944620ae7c3280e1d95f7a9fac22 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Wed, 26 Jul 2023 23:14:32 +0100 Subject: [PATCH 039/168] Reimplemented bulk downloading TPS calculations Improved test tile server functionality Improved documentation Former-commit-id: 490e4fbda79ee26cb959fa48b26d4030a2a1f3fa [formerly 93fd26bb474df9d28a119cf6793c489343114e12] Former-commit-id: 78f571a7aa9468fba845a10bccddb7a13147de9b --- example/lib/main.dart | 2 +- lib/src/bulk_download/download_progress.dart | 9 +- lib/src/bulk_download/manager.dart | 52 +++++------ test/tools/tile_server/bin/tile_server.dart | 95 ++++++++++++++------ 4 files changed, 102 insertions(+), 56 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 953fd807..aaa7fbe5 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -24,7 +24,7 @@ void main() async { String? damagedDatabaseDeleted; await FlutterMapTileCaching.initialise( errorHandler: (error) => damagedDatabaseDeleted = error.message, - settings: FMTCSettings(databaseMaxSize: 10000), + settings: FMTCSettings(databaseMaxSize: 51200), debugMode: true, ); diff --git a/lib/src/bulk_download/download_progress.dart b/lib/src/bulk_download/download_progress.dart index cf30b2b1..c61659a5 100644 --- a/lib/src/bulk_download/download_progress.dart +++ b/lib/src/bulk_download/download_progress.dart @@ -97,9 +97,14 @@ class DownloadProgress { /// Whether the download is now complete /// - /// There will be no more events after this event. + /// There will be no more events after this event, regardless of other + /// statistics. /// - /// Prefer using this over checking any other statistics for completion. + /// Prefer using this over checking any other statistics for completion. If all + /// threads have unexpectedly quit due to an error (for example, the store + /// becomes full to [FMTCSettings.databaseMaxSize]), the other statistics will + /// not indicate the the download has stopped/finished/completed, but this will + /// be `true`. final bool isComplete; /// The number of tiles that were either cached, in buffer, or skipped diff --git a/lib/src/bulk_download/manager.dart b/lib/src/bulk_download/manager.dart index 940444b3..980934f0 100644 --- a/lib/src/bulk_download/manager.dart +++ b/lib/src/bulk_download/manager.dart @@ -21,7 +21,7 @@ Future _downloadManager( // Precalculate shared inputs for all threads final storeId = DatabaseTools.hash(input.storeName).toString(); final threadBufferLength = - (input.maxBufferLength / input.parallelThreads).ceil(); + (input.maxBufferLength / input.parallelThreads).floor(); final headers = { ...input.region.options.tileProvider.headers, 'User-Agent': input.region.options.tileProvider.headers['User-Agent'] == @@ -102,12 +102,27 @@ Future _downloadManager( // Start progress tracking final initialDownloadProgress = DownloadProgress._initial(maxTiles: maxTiles); var lastDownloadProgress = initialDownloadProgress; - int tileNumber = -1; - int mptSlots = 70; - final microsecondsPerTile = List.filled(mptSlots, null, growable: true); - final tpsRates = List.filled(40, null); - final tileTimer = Stopwatch(); // Could be a black spot for bugs - final downloadDuration = Stopwatch()..start(); + final downloadDuration = Stopwatch(); + final tileCompletionTimestamps = []; + const tpsSmoothingFactor = 0.5; + final tpsSmoothingStorage = List.filled( + (400 * tpsSmoothingFactor).round(), + null, + growable: true, + ); + int currentTPSSmoothingIndex = 0; + double getCurrentTPS({required bool registerNewTPS}) { + if (registerNewTPS) tileCompletionTimestamps.add(DateTime.now()); + tileCompletionTimestamps.removeWhere( + (e) => e.isBefore(DateTime.now().subtract(const Duration(seconds: 1))), + ); + currentTPSSmoothingIndex++; + tpsSmoothingStorage[currentTPSSmoothingIndex % tpsSmoothingStorage.length] = + tileCompletionTimestamps.length; + final tps = tpsSmoothingStorage.nonNulls.average; + tpsSmoothingStorage.length = (tps * tpsSmoothingFactor).ceil(); + return tps; + } // Setup cancel, pause, and resume handling final threadPausedStates = List.generate( @@ -150,7 +165,7 @@ Future _downloadManager( send( lastDownloadProgress = lastDownloadProgress._updateProgress( newDuration: downloadDuration.elapsed, - tilesPerSecond: tpsRates.nonNulls.average, + tilesPerSecond: getCurrentTPS(registerNewTPS: false), rateLimit: input.rateLimit, ), ); @@ -159,6 +174,7 @@ Future _downloadManager( ); // Start download threads & wait for download to complete/cancelled + downloadDuration.start(); await Future.wait( List.generate( input.parallelThreads, @@ -201,16 +217,6 @@ Future _downloadManager( (evt) async { // Thread is sending tile data if (evt is TileEvent) { - // Handle progress estimation measurements - tileNumber++; - microsecondsPerTile[tileNumber % mptSlots] = - (tileTimer..stop()).elapsedMicroseconds; - final rawTPS = - (1 / (microsecondsPerTile.nonNulls.average)) * 1000000; - microsecondsPerTile.length = mptSlots = rawTPS.ceil() * 2; - tpsRates[tileNumber % 40] = rawTPS; - final tps = tpsRates.nonNulls.average; - // If buffering is in use, send a progress update with buffer info if (input.maxBufferLength != 0) { if (evt.result == TileEventResult.success) { @@ -236,7 +242,7 @@ Future _downloadManager( .map((e) => e.size) .reduce((a, b) => a + b), newDuration: downloadDuration.elapsed, - tilesPerSecond: tps, + tilesPerSecond: getCurrentTPS(registerNewTPS: true), rateLimit: input.rateLimit, ), ); @@ -248,7 +254,7 @@ Future _downloadManager( newBufferedTiles: 0, newBufferedSize: 0, newDuration: downloadDuration.elapsed, - tilesPerSecond: tps, + tilesPerSecond: getCurrentTPS(registerNewTPS: true), rateLimit: input.rateLimit, ), ); @@ -263,10 +269,6 @@ Future _downloadManager( await pauseResumeSignal.future; } - tileTimer - ..reset() - ..start(); - requestTilePort.send(null); try { sendPort.send(await tileQueue.next); @@ -303,6 +305,7 @@ Future _downloadManager( growable: false, ), ); + downloadDuration.stop(); // Send final buffer cleared progress report fallbackReportTimer?.cancel(); @@ -319,7 +322,6 @@ Future _downloadManager( ); // Cleanup resources and shutdown - downloadDuration.stop(); rootRecievePort.close(); tileIsolate.kill(priority: Isolate.immediate); await tileQueue.cancel(immediate: true); diff --git a/test/tools/tile_server/bin/tile_server.dart b/test/tools/tile_server/bin/tile_server.dart index 3fd41ffe..6d59f4ba 100644 --- a/test/tools/tile_server/bin/tile_server.dart +++ b/test/tools/tile_server/bin/tile_server.dart @@ -1,3 +1,6 @@ +// Copyright © Luka S (JaffaKetchup) under GPL-v3 +// A full license can be found at .\LICENSE + import 'dart:async'; import 'dart:io'; import 'dart:isolate'; @@ -8,56 +11,92 @@ import 'package:path/path.dart' as p; Future main(List _) async { final console = Console() + ..hideCursor() ..setTextStyle(bold: true, underscore: true) - ..write('\nFMTC Testing Tile Server\n\n') - ..setTextStyle(); + ..write('\nFMTC Testing Tile Server\n') + ..setTextStyle() + ..write('© Luka S (JaffaKetchup)\n') + ..write( + "Miniature fake tile server designed to test FMTC's throughput and download speeds\n\n", + ); final execPath = p.split(Platform.script.toFilePath()); final staticPath = p.joinAll([...execPath.getRange(0, execPath.length - 2), 'static']); + final requestTimestamps = []; + var lastRate = 0; + Timer.periodic(const Duration(seconds: 1), (_) { + lastRate = requestTimestamps.length; + requestTimestamps.clear(); + }); + + const artificialDelayChangeAmount = Duration(milliseconds: 5); + Duration currentArtificialDelay = Duration.zero; + final server = Jaguar( - onRouteServed: (ctx) => console.writeLine( - '[${ctx.at}] ${ctx.method} ${ctx.path}: ${ctx.response.statusCode}', - ), + multiThread: true, + onRouteServed: (ctx) { + final requestTime = ctx.at; + requestTimestamps.add(requestTime); + console.write( + '[$requestTime] ${ctx.method} ${ctx.path}: ${ctx.response.statusCode}\t\t$lastRate tps - ${currentArtificialDelay.inMilliseconds} ms delay\n', + ); + }, ); - final quitHandlerRecievePort = ReceivePort(); + final keyboardHandlerRecievePort = ReceivePort(); await Isolate.spawn( - (_) { - final console = Console(); + (sendPort) { while (true) { - if (console.readKey().char.toLowerCase() == 'q') { - console - ..setTextStyle(bold: true) - ..write('Killed HTTP server\n') - ..setTextStyle(); - Isolate.exit(); - } + final key = Console().readKey(); + + if (key.char.toLowerCase() == 'q') Isolate.exit(); + + if (key.controlChar == ControlCharacter.arrowUp) sendPort.send(1); + if (key.controlChar == ControlCharacter.arrowDown) sendPort.send(-1); } }, - null, - onExit: quitHandlerRecievePort.sendPort, + keyboardHandlerRecievePort.sendPort, + onExit: keyboardHandlerRecievePort.sendPort, ); - unawaited( - quitHandlerRecievePort.first.then((_) { + keyboardHandlerRecievePort.listen( + (message) => + currentArtificialDelay += artificialDelayChangeAmount * message, + onDone: () { + console + ..setTextStyle(bold: true) + ..write('\n\nKilled HTTP server\n') + ..setTextStyle() + ..showCursor(); server.close(); exit(0); - }), + }, ); - server - ..get('/ok', (context) => 'OK!') - ..staticFile('*', p.join(staticPath, 'assets', 'fake_tile.png')); + final response = ByteResponse( + body: File(p.join(staticPath, 'assets', 'fake_tile.png')).readAsBytesSync(), + mimeType: MimeTypes.png, + ); + server.get( + '*', + (_) async { + if (currentArtificialDelay > Duration.zero) { + await Future.delayed(currentArtificialDelay); + } + return response; + }, + ); console ..setTextStyle(italic: true) - ..write('Now serving at 0.0.0.0:8080\n') - ..write("Press 'q' to kill server\n\n") + ..write('Now serving tiles to all requests to 0.0.0.0:8080\n\n') + ..write("Press 'q' to kill server\n") + ..write( + 'Press UP or DOWN to manipulate artificial delay by ${artificialDelayChangeAmount.inMilliseconds} ms\n\n', + ) ..setTextStyle() - ..write('GET request any path to be served a 256x256 PNG map tile\n') - ..write("GET request '/ok' to be server a basic text response\n\n") - ..write('-----\n\n'); + ..write('----------\n'); await server.serve(logRequests: true); } From 52f0b7cd18c3585ca10b2b45dc01036cad72550e Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 27 Jul 2023 13:42:39 +0100 Subject: [PATCH 040/168] Fixed bugs Improved testing tile server functionality Minor syntactic improvements Former-commit-id: cf43e39f29b602e0b6e438767a2162877a5a7176 [formerly 04c490fd5229c7b9081be0770329258ef84cd866] Former-commit-id: 2282cf5a8787041bf3025b8dce80675219a14d56 --- .../main/pages/downloading/downloading.dart | 10 +-- lib/src/bulk_download/download_progress.dart | 52 +++++++-------- lib/src/bulk_download/manager.dart | 12 ++-- lib/src/bulk_download/tile_event.dart | 33 +++++++++- test/tools/tile_server/bin/tile_server.dart | 60 ++++++++++++++---- test/tools/tile_server/static/favicon.ico | Bin 0 -> 11743 bytes .../{assets/fake_tile.png => tiles/land.png} | Bin test/tools/tile_server/static/tiles/sea.png | Bin 0 -> 103 bytes 8 files changed, 114 insertions(+), 53 deletions(-) create mode 100644 test/tools/tile_server/static/favicon.ico rename test/tools/tile_server/static/{assets/fake_tile.png => tiles/land.png} (100%) create mode 100644 test/tools/tile_server/static/tiles/sea.png diff --git a/example/lib/screens/main/pages/downloading/downloading.dart b/example/lib/screens/main/pages/downloading/downloading.dart index 0b2347cf..af5af1a9 100644 --- a/example/lib/screens/main/pages/downloading/downloading.dart +++ b/example/lib/screens/main/pages/downloading/downloading.dart @@ -72,10 +72,12 @@ class _DownloadingPageState extends State ); } - if (snapshot.data!.latestTileEvent.result.category == - TileEventResultCategory.failed) { - provider - .addFailedTile(snapshot.data!.latestTileEvent); + final latestTileEvent = snapshot.data!.latestTileEvent; + + if (latestTileEvent.result.category == + TileEventResultCategory.failed && + !latestTileEvent.isRepeat) { + provider.addFailedTile(latestTileEvent); } return DownloadLayout( diff --git a/lib/src/bulk_download/download_progress.dart b/lib/src/bulk_download/download_progress.dart index c61659a5..c4ee986c 100644 --- a/lib/src/bulk_download/download_progress.dart +++ b/lib/src/bulk_download/download_progress.dart @@ -13,7 +13,8 @@ class DownloadProgress { /// May be used for UI display, error handling, or debugging purposes. /// /// It is not recommended to construct or keep a list of all these results, as - /// that will consume memory quickly. + /// that may consume memory in large quanities. Instead, prefer counting events + /// that are of importance only. TileEvent get latestTileEvent => _latestTileEvent!; final TileEvent? _latestTileEvent; @@ -194,13 +195,13 @@ class DownloadProgress { isComplete: false, ); - DownloadProgress _updateProgress({ + DownloadProgress _fallbackReportUpdate({ required Duration newDuration, required double tilesPerSecond, required int? rateLimit, }) => DownloadProgress.__( - latestTileEvent: latestTileEvent, + latestTileEvent: latestTileEvent._repeat(), cachedTiles: cachedTiles, cachedSize: cachedSize, bufferedTiles: bufferedTiles, @@ -227,33 +228,28 @@ class DownloadProgress { }) => DownloadProgress.__( latestTileEvent: newTileEvent ?? latestTileEvent, - cachedTiles: newTileEvent == null - ? cachedTiles - : newTileEvent.result.category == TileEventResultCategory.cached - ? cachedTiles + 1 - : cachedTiles, - cachedSize: newTileEvent == null - ? cachedSize - : newTileEvent.result.category == TileEventResultCategory.cached - ? cachedSize + (newTileEvent.tileImage!.lengthInBytes / 1024) - : cachedSize, + cachedTiles: newTileEvent != null && + newTileEvent.result.category == TileEventResultCategory.cached + ? cachedTiles + 1 + : cachedTiles, + cachedSize: newTileEvent != null && + newTileEvent.result.category == TileEventResultCategory.cached + ? cachedSize + (newTileEvent.tileImage!.lengthInBytes / 1024) + : cachedSize, bufferedTiles: newBufferedTiles, bufferedSize: newBufferedSize, - skippedTiles: newTileEvent == null - ? skippedTiles - : newTileEvent.result.category == TileEventResultCategory.skipped - ? skippedTiles + 1 - : skippedTiles, - skippedSize: newTileEvent == null - ? skippedSize - : newTileEvent.result.category == TileEventResultCategory.skipped - ? skippedSize + (newTileEvent.tileImage!.lengthInBytes / 1024) - : skippedSize, - failedTiles: newTileEvent == null - ? failedTiles - : newTileEvent.result.category == TileEventResultCategory.failed - ? failedTiles + 1 - : failedTiles, + skippedTiles: newTileEvent != null && + newTileEvent.result.category == TileEventResultCategory.skipped + ? skippedTiles + 1 + : skippedTiles, + skippedSize: newTileEvent != null && + newTileEvent.result.category == TileEventResultCategory.skipped + ? skippedSize + (newTileEvent.tileImage!.lengthInBytes / 1024) + : skippedSize, + failedTiles: newTileEvent != null && + newTileEvent.result.category == TileEventResultCategory.failed + ? failedTiles + 1 + : failedTiles, maxTiles: maxTiles, elapsedDuration: newDuration, tilesPerSecond: tilesPerSecond, diff --git a/lib/src/bulk_download/manager.dart b/lib/src/bulk_download/manager.dart index 980934f0..20e0543c 100644 --- a/lib/src/bulk_download/manager.dart +++ b/lib/src/bulk_download/manager.dart @@ -105,11 +105,7 @@ Future _downloadManager( final downloadDuration = Stopwatch(); final tileCompletionTimestamps = []; const tpsSmoothingFactor = 0.5; - final tpsSmoothingStorage = List.filled( - (400 * tpsSmoothingFactor).round(), - null, - growable: true, - ); + final tpsSmoothingStorage = [null]; int currentTPSSmoothingIndex = 0; double getCurrentTPS({required bool registerNewTPS}) { if (registerNewTPS) tileCompletionTimestamps.add(DateTime.now()); @@ -120,7 +116,8 @@ Future _downloadManager( tpsSmoothingStorage[currentTPSSmoothingIndex % tpsSmoothingStorage.length] = tileCompletionTimestamps.length; final tps = tpsSmoothingStorage.nonNulls.average; - tpsSmoothingStorage.length = (tps * tpsSmoothingFactor).ceil(); + tpsSmoothingStorage.length = + (tps * tpsSmoothingFactor).ceil().clamp(1, 1000); return tps; } @@ -163,7 +160,8 @@ Future _downloadManager( if (lastDownloadProgress != initialDownloadProgress && pauseResumeSignal.isCompleted) { send( - lastDownloadProgress = lastDownloadProgress._updateProgress( + lastDownloadProgress = + lastDownloadProgress._fallbackReportUpdate( newDuration: downloadDuration.elapsed, tilesPerSecond: getCurrentTPS(registerNewTPS: false), rateLimit: input.rateLimit, diff --git a/lib/src/bulk_download/tile_event.dart b/lib/src/bulk_download/tile_event.dart index 9adc9e8a..de63a203 100644 --- a/lib/src/bulk_download/tile_event.dart +++ b/lib/src/bulk_download/tile_event.dart @@ -64,6 +64,7 @@ enum TileEventResult { /// /// Does not contain information about the download as a whole, that is /// [DownloadProgress]' responsibility. +@immutable class TileEvent { /// The status of this event, the result of attempting to cache this tile /// @@ -73,14 +74,20 @@ class TileEvent { /// - [TileEventResultCategory.cached] (tile was downloaded and cached) /// - [TileEventResultCategory.skipped] (tile was not cached, but intentionally) /// - [TileEventResultCategory.failed] (tile was not cached, due to an error) + /// + /// Remember to check [isRepeat] before keeping track of this value. final TileEventResult result; /// The URL used to request the tile + /// + /// Remember to check [isRepeat] before keeping track of this value. final String url; /// The raw bytes that were fetched from the [url], if available /// /// Not available if the result category is [TileEventResultCategory.failed]. + /// + /// Remember to check [isRepeat] before keeping track of this value. final Uint8List? tileImage; /// The raw [http.Response] from the [url], if available @@ -88,25 +95,47 @@ class TileEvent { /// Not available if [result] is [TileEventResult.noConnectionDuringFetch], /// [TileEventResult.unknownFetchException], or /// [TileEventResult.alreadyExisting]. + /// + /// Remember to check [isRepeat] before keeping track of this value. final http.Response? fetchResponse; /// The raw error thrown when fetching from the [url], if available /// /// Only available if [result] is [TileEventResult.noConnectionDuringFetch] or /// [TileEventResult.unknownFetchException]. + /// + /// Remember to check [isRepeat] before keeping track of this value. final Object? fetchError; + /// Whether this event is a repeat of the last event + /// + /// Events will occasionally be repeated due to the `maxReportInterval` + /// functionality. If using other members, such as [result], to keep count of + /// important events, do not count an event where this is `true`. + final bool isRepeat; + final bool _wasBufferReset; - TileEvent._( + const TileEvent._( this.result, { required this.url, this.tileImage, this.fetchResponse, this.fetchError, + this.isRepeat = false, bool wasBufferReset = false, }) : _wasBufferReset = wasBufferReset; + TileEvent _repeat() => TileEvent._( + result, + url: url, + tileImage: tileImage, + fetchResponse: fetchResponse, + fetchError: fetchError, + isRepeat: true, + wasBufferReset: _wasBufferReset, + ); + @override bool operator ==(Object other) => identical(this, other) || @@ -116,6 +145,7 @@ class TileEvent { tileImage == other.tileImage && fetchResponse == other.fetchResponse && fetchError == other.fetchError && + isRepeat == other.isRepeat && _wasBufferReset == other._wasBufferReset); @override @@ -125,6 +155,7 @@ class TileEvent { tileImage, fetchResponse, fetchError, + isRepeat, _wasBufferReset, ]); } diff --git a/test/tools/tile_server/bin/tile_server.dart b/test/tools/tile_server/bin/tile_server.dart index 6d59f4ba..6eaa452b 100644 --- a/test/tools/tile_server/bin/tile_server.dart +++ b/test/tools/tile_server/bin/tile_server.dart @@ -4,12 +4,14 @@ import 'dart:async'; import 'dart:io'; import 'dart:isolate'; +import 'dart:math'; import 'package:dart_console/dart_console.dart'; import 'package:jaguar/jaguar.dart'; import 'package:path/path.dart' as p; Future main(List _) async { + // Initialise console final console = Console() ..hideCursor() ..setTextStyle(bold: true, underscore: true) @@ -20,10 +22,12 @@ Future main(List _) async { "Miniature fake tile server designed to test FMTC's throughput and download speeds\n\n", ); + // Find path to '/static/' directory final execPath = p.split(Platform.script.toFilePath()); final staticPath = p.joinAll([...execPath.getRange(0, execPath.length - 2), 'static']); + // Monitor requests per second measurement (tps) final requestTimestamps = []; var lastRate = 0; Timer.periodic(const Duration(seconds: 1), (_) { @@ -31,20 +35,26 @@ Future main(List _) async { requestTimestamps.clear(); }); - const artificialDelayChangeAmount = Duration(milliseconds: 5); + // Setup artificial delay + const artificialDelayChangeAmount = Duration(milliseconds: 2); Duration currentArtificialDelay = Duration.zero; + // Track number of sea tiles served + int servedSeaTiles = 0; + + // Initialise HTTP server final server = Jaguar( multiThread: true, onRouteServed: (ctx) { final requestTime = ctx.at; requestTimestamps.add(requestTime); console.write( - '[$requestTime] ${ctx.method} ${ctx.path}: ${ctx.response.statusCode}\t\t$lastRate tps - ${currentArtificialDelay.inMilliseconds} ms delay\n', + '[$requestTime] ${ctx.method} ${ctx.path}\t\t$servedSeaTiles sea tiles\t\t\t$lastRate tps - ${currentArtificialDelay.inMilliseconds} ms delay\n', ); }, ); + // Handle keyboard events final keyboardHandlerRecievePort = ReceivePort(); await Isolate.spawn( (sendPort) { @@ -62,32 +72,55 @@ Future main(List _) async { ); keyboardHandlerRecievePort.listen( (message) => + // Control artificial delay currentArtificialDelay += artificialDelayChangeAmount * message, + // Stop server and quit onDone: () { + server.close(); console ..setTextStyle(bold: true) ..write('\n\nKilled HTTP server\n') ..setTextStyle() ..showCursor(); - server.close(); exit(0); }, ); - final response = ByteResponse( - body: File(p.join(staticPath, 'assets', 'fake_tile.png')).readAsBytesSync(), + // Preload tile responses + final landTileResponse = ByteResponse( + body: File(p.join(staticPath, 'tiles', 'land.png')).readAsBytesSync(), mimeType: MimeTypes.png, ); - server.get( - '*', - (_) async { - if (currentArtificialDelay > Duration.zero) { - await Future.delayed(currentArtificialDelay); - } - return response; - }, + final seaTileResponse = ByteResponse( + body: File(p.join(staticPath, 'tiles', 'sea.png')).readAsBytesSync(), + mimeType: MimeTypes.png, ); + // Initialise random chance for sea/land tile (1:10) + final random = Random(); + + server + // Serve 'favicon.ico' + ..staticFile('/favicon.ico', p.join(staticPath, 'favicon.ico')) + // Serve tiles to all other requests + ..get( + '*', + (ctx) async { + // Create artificial delay if applicable + if (currentArtificialDelay > Duration.zero) { + await Future.delayed(currentArtificialDelay); + } + + // Serve either sea or land tile + if (ctx.path == '/17/0/0.png' || random.nextInt(10) == 0) { + servedSeaTiles += 1; + return seaTileResponse; + } + return landTileResponse; + }, + ); + + // Output basic console instructions console ..setTextStyle(italic: true) ..write('Now serving tiles to all requests to 0.0.0.0:8080\n\n') @@ -98,5 +131,6 @@ Future main(List _) async { ..setTextStyle() ..write('----------\n'); + // Start HTTP server await server.serve(logRequests: true); } diff --git a/test/tools/tile_server/static/favicon.ico b/test/tools/tile_server/static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..4620957bc64b5d691446c7e49f0e4920fd219dbd GIT binary patch literal 11743 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 literal 0 HcmV?d00001 diff --git a/test/tools/tile_server/static/assets/fake_tile.png b/test/tools/tile_server/static/tiles/land.png similarity index 100% rename from test/tools/tile_server/static/assets/fake_tile.png rename to test/tools/tile_server/static/tiles/land.png diff --git a/test/tools/tile_server/static/tiles/sea.png b/test/tools/tile_server/static/tiles/sea.png new file mode 100644 index 0000000000000000000000000000000000000000..f607ae0a94ac5b56cfb50a8b275a4e878a391171 GIT binary patch literal 103 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K585o&?RN5XZRUpM2;1lAy>hk^bZ}xlza^*c; u978f1-yUS-1@aCp`0k&}XTAW)1cQCNia`Cd5*}U!aXnrAT-G@yGywpw9~smD literal 0 HcmV?d00001 From 8168ae42b69f1d00e286a94d4095d88142bc3079 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 27 Jul 2023 14:42:43 +0100 Subject: [PATCH 041/168] Attempt to add tile server compilation to GitHub workflow Former-commit-id: f7ed834d4859a8f57b8dc211d7eecfe69af6e781 [formerly 2aa562331c083808dfd08c0f70c5866a4259ebd9] Former-commit-id: f84cb2c5fbac1696b8f16b3e1d722b0f8ce58ac5 --- .github/workflows/main.yml | 40 ++++++++++++++------- test/tools/tile_server/bin/tile_server.dart | 2 +- 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 418af296..52652667 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -62,10 +62,10 @@ jobs: - name: Run Tests run: flutter test -r expanded - build-android: + build-demo-android: name: "Build Android Demo App" runs-on: ubuntu-latest - needs: [analyse-code, run-tests, score-package] + #needs: [analyse-code, run-tests, score-package] defaults: run: working-directory: ./example @@ -86,25 +86,20 @@ jobs: - name: Upload Artifact uses: actions/upload-artifact@v3.1.2 with: - name: apk-build + name: android-demo path: example/build/app/outputs/apk/release if-no-files-found: error - build-windows: + build-demo-windows: name: "Build Windows Demo App" runs-on: windows-latest - needs: [analyse-code, run-tests, score-package] + #needs: [analyse-code, run-tests, score-package] defaults: run: working-directory: ./example steps: - name: Checkout Repository uses: actions/checkout@v3 - - name: Setup Java 17 Environment - uses: actions/setup-java@v3 - with: - distribution: "temurin" - java-version: "17" - name: Setup Flutter Environment uses: subosito/flutter-action@v2 with: @@ -117,6 +112,27 @@ jobs: - name: Upload Artifact uses: actions/upload-artifact@v3.1.2 with: - name: exe-build + name: windows-demo path: windowsTemp/WindowsApplication.exe - if-no-files-found: error \ No newline at end of file + if-no-files-found: error + + build-tile-server-windows: + name: "Build Windows Tile Server" + runs-on: windows-latest + #needs: [analyse-code, run-tests, score-package] + defaults: + run: + working-directory: ./test/tools/tile_server + steps: + - name: Checkout Repository + uses: actions/checkout@v3 + - name: Setup Dart Environment + uses: dart-lang/setup-dart@v1.5.0 + - name: Compile + run: dart compile exe bin/tile_server.dart + - name: Upload Artifact + uses: actions/upload-artifact@v3.1.2 + with: + name: windows-ts + path: test/tools/tile_server/bin/tile_server.exe + if-no-files-found: error \ No newline at end of file diff --git a/test/tools/tile_server/bin/tile_server.dart b/test/tools/tile_server/bin/tile_server.dart index 6eaa452b..492b0ef4 100644 --- a/test/tools/tile_server/bin/tile_server.dart +++ b/test/tools/tile_server/bin/tile_server.dart @@ -123,7 +123,7 @@ Future main(List _) async { // Output basic console instructions console ..setTextStyle(italic: true) - ..write('Now serving tiles to all requests to 0.0.0.0:8080\n\n') + ..write('Now serving tiles to all requests to 127.0.0.1:8080\n\n') ..write("Press 'q' to kill server\n") ..write( 'Press UP or DOWN to manipulate artificial delay by ${artificialDelayChangeAmount.inMilliseconds} ms\n\n', From f9cb2955ae1d68cd149951d24d11955828a70cef Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 27 Jul 2023 14:44:14 +0100 Subject: [PATCH 042/168] Attempt (2) to add tile server compilation to GitHub workflow Former-commit-id: 28924e8878215d9c77a70c1a7d22d0bafcf0d448 [formerly f261c376a0cec7fc03a4427bea85d677eed8d2d5] Former-commit-id: 308d5512075f9ad9f9733e6d199cd556a49ca1f3 --- .github/workflows/main.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 52652667..f9647ec2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -128,6 +128,8 @@ jobs: uses: actions/checkout@v3 - name: Setup Dart Environment uses: dart-lang/setup-dart@v1.5.0 + - name: Get Dependencies + run: dart pub get - name: Compile run: dart compile exe bin/tile_server.dart - name: Upload Artifact From c6a54d70698310425061afec4754545956062a49 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 27 Jul 2023 15:32:51 +0100 Subject: [PATCH 043/168] Attempt (3) to add tile server compilation to GitHub workflow Former-commit-id: 1efe3e1f9cb082f379a3a35f8f6b3fe872cd330f [formerly 0594476ebd5e9a934673d41d73bd82b7e5efa6da] Former-commit-id: 7ff01bcefa347aeea0db5e7454e73450dcb5c6a8 --- .github/workflows/main.yml | 31 +++++++++++++++++++++++++++---- fmtc_tile_server.bat | 2 -- test/tools/fmtc_tile_server.bat | 2 ++ 3 files changed, 29 insertions(+), 6 deletions(-) delete mode 100644 fmtc_tile_server.bat create mode 100644 test/tools/fmtc_tile_server.bat diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f9647ec2..236b2a5c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -63,7 +63,7 @@ jobs: run: flutter test -r expanded build-demo-android: - name: "Build Android Demo App" + name: "Build Demo App (Android)" runs-on: ubuntu-latest #needs: [analyse-code, run-tests, score-package] defaults: @@ -91,7 +91,7 @@ jobs: if-no-files-found: error build-demo-windows: - name: "Build Windows Demo App" + name: "Build Demo App (Windows)" runs-on: windows-latest #needs: [analyse-code, run-tests, score-package] defaults: @@ -117,7 +117,7 @@ jobs: if-no-files-found: error build-tile-server-windows: - name: "Build Windows Tile Server" + name: "Build Tile Server (Windows)" runs-on: windows-latest #needs: [analyse-code, run-tests, score-package] defaults: @@ -137,4 +137,27 @@ jobs: with: name: windows-ts path: test/tools/tile_server/bin/tile_server.exe - if-no-files-found: error \ No newline at end of file + if-no-files-found: error + + build-tile-server-linux: + name: "Build Tile Server (Linux/Ubuntu)" + runs-on: ubuntu-latest + #needs: [analyse-code, run-tests, score-package] + defaults: + run: + working-directory: ./test/tools/tile_server + steps: + - name: Checkout Repository + uses: actions/checkout@v3 + - name: Setup Dart Environment + uses: dart-lang/setup-dart@v1.5.0 + - name: Get Dependencies + run: dart pub get + - name: Compile + run: dart compile exe bin/tile_server.dart + - name: Upload Artifact + uses: actions/upload-artifact@v3.1.2 + with: + name: linux-ts + path: test/tools/tile_server/bin/tile_server.exe + if-no-files-found: error diff --git a/fmtc_tile_server.bat b/fmtc_tile_server.bat deleted file mode 100644 index e94dc98e..00000000 --- a/fmtc_tile_server.bat +++ /dev/null @@ -1,2 +0,0 @@ -@echo off -start cmd /C "dart compile exe test/tools/tile_server/bin/tile_server.dart && start /b test/tools/tile_server/bin/tile_server.exe" \ No newline at end of file diff --git a/test/tools/fmtc_tile_server.bat b/test/tools/fmtc_tile_server.bat new file mode 100644 index 00000000..6a690c31 --- /dev/null +++ b/test/tools/fmtc_tile_server.bat @@ -0,0 +1,2 @@ +@echo off +start cmd /C "dart compile exe tile_server/bin/tile_server.dart && start /b tile_server/bin/tile_server.exe" \ No newline at end of file From 6001f1759bdf90f53d90bdcadba3851a49e3d90d Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 27 Jul 2023 22:17:13 +0100 Subject: [PATCH 044/168] Fixed bug with testing tile server Former-commit-id: b22b84cfd4333cc750133c563e0c10f0d40d408a [formerly cab2f0d20faf99430bc65ec65f85c2b87d8d3825] Former-commit-id: c4f0a5e7bd77873538a12ddea90ce86dec1b38f1 --- .github/workflows/main.yml | 42 +++++++++--------- .../tile_server/bin/generate_dart_images.dart | 39 ++++++++++++++++ test/tools/tile_server/bin/tile_server.dart | 22 ++++----- .../tile_server/static/generated/favicon.dart | 1 + .../tile_server/static/generated/land.dart | 1 + .../tile_server/static/generated/sea.dart | 1 + .../static/{ => source}/favicon.ico | Bin .../static/{tiles => source}/land.png | Bin .../static/{tiles => source}/sea.png | Bin 9 files changed, 75 insertions(+), 31 deletions(-) create mode 100644 test/tools/tile_server/bin/generate_dart_images.dart create mode 100644 test/tools/tile_server/static/generated/favicon.dart create mode 100644 test/tools/tile_server/static/generated/land.dart create mode 100644 test/tools/tile_server/static/generated/sea.dart rename test/tools/tile_server/static/{ => source}/favicon.ico (100%) rename test/tools/tile_server/static/{tiles => source}/land.png (100%) rename test/tools/tile_server/static/{tiles => source}/sea.png (100%) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 236b2a5c..02b8e39c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -117,27 +117,27 @@ jobs: if-no-files-found: error build-tile-server-windows: - name: "Build Tile Server (Windows)" - runs-on: windows-latest - #needs: [analyse-code, run-tests, score-package] - defaults: - run: - working-directory: ./test/tools/tile_server - steps: - - name: Checkout Repository - uses: actions/checkout@v3 - - name: Setup Dart Environment - uses: dart-lang/setup-dart@v1.5.0 - - name: Get Dependencies - run: dart pub get - - name: Compile - run: dart compile exe bin/tile_server.dart - - name: Upload Artifact - uses: actions/upload-artifact@v3.1.2 - with: - name: windows-ts - path: test/tools/tile_server/bin/tile_server.exe - if-no-files-found: error + name: "Build Tile Server (Windows)" + runs-on: windows-latest + #needs: [analyse-code, run-tests, score-package] + defaults: + run: + working-directory: ./test/tools/tile_server + steps: + - name: Checkout Repository + uses: actions/checkout@v3 + - name: Setup Dart Environment + uses: dart-lang/setup-dart@v1.5.0 + - name: Get Dependencies + run: dart pub get + - name: Compile + run: dart compile exe bin/tile_server.dart + - name: Upload Artifact + uses: actions/upload-artifact@v3.1.2 + with: + name: windows-ts + path: test/tools/tile_server/bin/tile_server.exe + if-no-files-found: error build-tile-server-linux: name: "Build Tile Server (Linux/Ubuntu)" diff --git a/test/tools/tile_server/bin/generate_dart_images.dart b/test/tools/tile_server/bin/generate_dart_images.dart new file mode 100644 index 00000000..37729940 --- /dev/null +++ b/test/tools/tile_server/bin/generate_dart_images.dart @@ -0,0 +1,39 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:path/path.dart' as p; + +const staticFilesInfo = [ + (name: 'sea', extension: 'png'), + (name: 'land', extension: 'png'), + (name: 'favicon', extension: 'ico'), +]; + +Future main(List _) async { + final execPath = p.split(Platform.script.toFilePath()); + final staticPath = + p.joinAll([...execPath.getRange(0, execPath.length - 2), 'static']); + + Directory(p.join(staticPath, 'generated')).createSync(); + + for (final staticFile in staticFilesInfo) { + final dartFile = File( + p.join( + staticPath, + 'generated', + '${staticFile.name}.dart', + ), + ); + final imageFile = File( + p.join( + staticPath, + 'source', + '${staticFile.name}.${staticFile.extension}', + ), + ); + + dartFile.writeAsStringSync( + 'final ${staticFile.name}TileBytes = ${imageFile.readAsBytesSync()};\n', + ); + } +} diff --git a/test/tools/tile_server/bin/tile_server.dart b/test/tools/tile_server/bin/tile_server.dart index 492b0ef4..7e693337 100644 --- a/test/tools/tile_server/bin/tile_server.dart +++ b/test/tools/tile_server/bin/tile_server.dart @@ -8,7 +8,10 @@ import 'dart:math'; import 'package:dart_console/dart_console.dart'; import 'package:jaguar/jaguar.dart'; -import 'package:path/path.dart' as p; + +import '../static/generated/favicon.dart'; +import '../static/generated/land.dart'; +import '../static/generated/sea.dart'; Future main(List _) async { // Initialise console @@ -22,11 +25,6 @@ Future main(List _) async { "Miniature fake tile server designed to test FMTC's throughput and download speeds\n\n", ); - // Find path to '/static/' directory - final execPath = p.split(Platform.script.toFilePath()); - final staticPath = - p.joinAll([...execPath.getRange(0, execPath.length - 2), 'static']); - // Monitor requests per second measurement (tps) final requestTimestamps = []; var lastRate = 0; @@ -86,13 +84,17 @@ Future main(List _) async { }, ); - // Preload tile responses + // Preload responses + final faviconReponse = ByteResponse( + body: faviconTileBytes, + mimeType: 'image/vnd.microsoft.icon', + ); final landTileResponse = ByteResponse( - body: File(p.join(staticPath, 'tiles', 'land.png')).readAsBytesSync(), + body: landTileBytes, mimeType: MimeTypes.png, ); final seaTileResponse = ByteResponse( - body: File(p.join(staticPath, 'tiles', 'sea.png')).readAsBytesSync(), + body: seaTileBytes, mimeType: MimeTypes.png, ); @@ -101,7 +103,7 @@ Future main(List _) async { server // Serve 'favicon.ico' - ..staticFile('/favicon.ico', p.join(staticPath, 'favicon.ico')) + ..get('/favicon.ico', (_) => faviconReponse) // Serve tiles to all other requests ..get( '*', diff --git a/test/tools/tile_server/static/generated/favicon.dart b/test/tools/tile_server/static/generated/favicon.dart new file mode 100644 index 00000000..259e34b4 --- /dev/null +++ b/test/tools/tile_server/static/generated/favicon.dart @@ -0,0 +1 @@ +final faviconTileBytes = [0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 32, 0, 201, 45, 0, 0, 22, 0, 0, 0, 137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 1, 0, 0, 0, 1, 0, 8, 6, 0, 0, 0, 92, 114, 168, 102, 0, 0, 45, 144, 73, 68, 65, 84, 120, 218, 237, 157, 123, 124, 27, 229, 153, 239, 127, 26, 141, 70, 23, 91, 146, 109, 249, 126, 139, 147, 216, 9, 185, 185, 9, 16, 72, 40, 183, 64, 129, 246, 208, 238, 129, 237, 210, 82, 122, 202, 161, 91, 2, 11, 45, 44, 108, 161, 13, 112, 246, 0, 165, 52, 180, 205, 46, 109, 225, 244, 190, 103, 217, 148, 101, 89, 216, 93, 232, 133, 179, 101, 11, 9, 105, 32, 9, 73, 73, 76, 156, 224, 196, 151, 92, 124, 209, 197, 182, 44, 75, 178, 46, 51, 26, 73, 231, 15, 89, 178, 36, 75, 214, 109, 164, 153, 145, 222, 239, 231, 147, 79, 28, 105, 52, 126, 53, 153, 231, 55, 207, 251, 188, 207, 243, 188, 10, 254, 163, 23, 195, 40, 0, 191, 215, 9, 243, 88, 63, 204, 163, 199, 97, 29, 59, 1, 149, 190, 25, 205, 61, 215, 160, 121, 213, 39, 80, 219, 177, 9, 52, 83, 93, 200, 233, 9, 4, 66, 17, 81, 20, 42, 0, 201, 204, 76, 157, 195, 196, 249, 62, 88, 198, 250, 225, 152, 49, 163, 174, 227, 34, 52, 245, 92, 131, 166, 238, 109, 208, 55, 174, 18, 251, 251, 18, 8, 132, 56, 4, 23, 128, 120, 120, 158, 133, 101, 52, 226, 29, 152, 199, 250, 17, 166, 52, 104, 90, 181, 13, 77, 221, 219, 208, 216, 125, 37, 241, 14, 8, 4, 145, 201, 73, 0, 38, 236, 62, 184, 61, 172, 216, 99, 134, 182, 190, 153, 76, 47, 8, 4, 1, 160, 179, 61, 112, 194, 238, 131, 66, 93, 139, 21, 203, 87, 138, 61, 102, 76, 155, 71, 224, 24, 59, 134, 134, 149, 87, 136, 61, 20, 2, 65, 214, 80, 217, 30, 232, 246, 176, 168, 111, 21, 223, 248, 1, 160, 190, 117, 37, 124, 211, 86, 177, 135, 65, 32, 200, 158, 172, 5, 128, 64, 32, 148, 31, 178, 22, 0, 235, 224, 31, 192, 115, 115, 98, 15, 131, 64, 144, 45, 89, 199, 0, 164, 200, 200, 219, 79, 227, 200, 244, 121, 212, 117, 94, 130, 166, 158, 107, 208, 176, 242, 10, 24, 155, 214, 138, 61, 44, 2, 65, 54, 228, 45, 0, 111, 255, 231, 191, 225, 143, 111, 255, 22, 0, 160, 209, 106, 209, 220, 186, 12, 87, 95, 127, 19, 150, 175, 92, 19, 59, 230, 223, 94, 252, 49, 250, 251, 222, 143, 253, 123, 109, 239, 197, 248, 252, 237, 247, 1, 0, 206, 142, 12, 224, 159, 255, 225, 239, 97, 53, 143, 97, 237, 134, 139, 241, 63, 255, 234, 155, 168, 170, 210, 3, 0, 142, 31, 61, 128, 87, 95, 252, 9, 156, 179, 51, 184, 120, 203, 85, 184, 253, 174, 111, 164, 28, 195, 117, 55, 61, 6, 158, 103, 97, 29, 63, 9, 203, 232, 126, 28, 57, 240, 99, 4, 194, 10, 52, 245, 92, 131, 198, 149, 87, 162, 177, 231, 106, 48, 154, 26, 177, 175, 49, 129, 32, 89, 242, 22, 0, 135, 125, 18, 77, 45, 29, 248, 243, 47, 109, 199, 156, 195, 137, 211, 39, 251, 240, 204, 223, 222, 139, 47, 221, 245, 16, 46, 191, 250, 70, 0, 192, 208, 233, 227, 184, 230, 147, 159, 197, 138, 158, 200, 83, 89, 171, 173, 2, 0, 112, 156, 31, 127, 247, 212, 131, 248, 226, 95, 62, 136, 222, 139, 46, 195, 203, 255, 247, 7, 120, 101, 247, 243, 248, 242, 61, 143, 192, 238, 180, 227, 255, 236, 122, 12, 247, 239, 248, 30, 58, 186, 186, 241, 179, 103, 255, 55, 222, 248, 143, 221, 184, 241, 207, 111, 79, 253, 5, 104, 53, 218, 187, 46, 68, 123, 215, 133, 0, 0, 183, 203, 6, 243, 249, 15, 49, 241, 254, 79, 113, 236, 245, 7, 161, 111, 90, 131, 166, 149, 87, 161, 185, 231, 19, 168, 237, 188, 72, 236, 235, 77, 32, 72, 138, 130, 166, 0, 90, 93, 21, 154, 27, 151, 1, 141, 64, 247, 234, 94, 116, 116, 117, 227, 185, 239, 237, 192, 37, 151, 93, 11, 134, 209, 192, 53, 235, 192, 138, 158, 181, 232, 88, 214, 157, 240, 185, 161, 83, 253, 48, 24, 106, 176, 245, 202, 27, 0, 0, 55, 221, 126, 47, 118, 220, 125, 51, 190, 248, 149, 7, 209, 119, 96, 47, 214, 125, 108, 51, 214, 245, 110, 6, 0, 252, 217, 95, 124, 25, 191, 124, 254, 219, 105, 5, 32, 25, 189, 161, 9, 171, 55, 92, 143, 213, 27, 174, 7, 207, 179, 176, 219, 206, 96, 226, 124, 31, 250, 254, 253, 85, 120, 125, 30, 52, 173, 186, 38, 226, 33, 116, 95, 5, 117, 85, 189, 216, 215, 159, 64, 16, 21, 65, 99, 0, 189, 23, 94, 6, 138, 82, 98, 232, 84, 63, 214, 245, 110, 134, 219, 237, 68, 223, 159, 246, 227, 248, 7, 7, 208, 209, 213, 141, 222, 11, 47, 3, 0, 88, 39, 206, 163, 61, 78, 20, 76, 70, 19, 0, 192, 53, 235, 128, 213, 60, 138, 214, 182, 174, 216, 123, 237, 93, 61, 152, 180, 78, 228, 247, 229, 104, 53, 154, 218, 214, 160, 169, 109, 13, 112, 217, 23, 224, 247, 58, 49, 113, 190, 15, 230, 99, 187, 113, 252, 119, 223, 132, 198, 216, 129, 166, 85, 215, 162, 105, 229, 85, 36, 177, 136, 80, 145, 8, 30, 4, 172, 51, 53, 194, 237, 114, 0, 0, 62, 245, 103, 183, 197, 94, 255, 247, 151, 126, 134, 55, 127, 251, 47, 120, 248, 241, 231, 224, 247, 121, 161, 102, 212, 9, 159, 211, 234, 244, 112, 187, 103, 193, 113, 44, 106, 106, 23, 158, 204, 85, 85, 122, 4, 2, 28, 60, 30, 119, 44, 70, 144, 47, 26, 157, 17, 43, 215, 92, 133, 149, 107, 174, 2, 0, 76, 217, 134, 97, 29, 59, 129, 83, 255, 185, 3, 179, 179, 54, 152, 150, 109, 65, 211, 170, 107, 208, 188, 234, 90, 84, 213, 46, 43, 222, 85, 39, 16, 36, 130, 224, 2, 48, 61, 101, 129, 222, 80, 11, 0, 9, 110, 251, 117, 159, 254, 28, 190, 122, 251, 245, 24, 59, 63, 140, 106, 67, 13, 216, 179, 131, 9, 159, 243, 121, 221, 208, 84, 49, 208, 235, 141, 240, 121, 23, 150, 246, 60, 30, 55, 0, 20, 108, 252, 169, 104, 104, 234, 70, 67, 83, 55, 54, 92, 124, 19, 252, 94, 39, 108, 230, 83, 176, 12, 255, 30, 251, 247, 124, 15, 148, 218, 128, 166, 85, 159, 64, 211, 170, 109, 168, 239, 218, 74, 188, 3, 66, 89, 66, 191, 251, 135, 31, 163, 181, 179, 23, 173, 29, 27, 160, 209, 25, 11, 58, 217, 161, 119, 255, 11, 20, 165, 68, 207, 5, 27, 22, 189, 199, 48, 26, 104, 117, 122, 240, 124, 0, 109, 29, 93, 120, 243, 55, 47, 197, 222, 179, 59, 237, 224, 88, 22, 166, 186, 54, 52, 183, 47, 195, 209, 247, 247, 197, 222, 27, 63, 55, 132, 150, 214, 206, 162, 95, 8, 141, 206, 136, 101, 221, 151, 98, 89, 247, 165, 0, 0, 215, 172, 5, 227, 231, 142, 225, 204, 158, 157, 56, 50, 117, 22, 198, 214, 141, 104, 238, 185, 6, 77, 171, 175, 37, 75, 141, 132, 178, 129, 174, 93, 115, 51, 206, 13, 239, 197, 7, 7, 94, 65, 85, 149, 30, 45, 29, 189, 104, 237, 236, 141, 204, 155, 51, 224, 243, 122, 96, 157, 60, 15, 231, 148, 29, 253, 199, 14, 225, 205, 223, 189, 140, 237, 247, 63, 30, 9, 0, 186, 28, 24, 57, 221, 143, 85, 107, 55, 1, 0, 222, 121, 243, 53, 168, 213, 106, 180, 117, 44, 7, 195, 104, 16, 8, 112, 120, 247, 157, 55, 176, 105, 243, 149, 120, 125, 247, 143, 113, 233, 229, 215, 129, 97, 52, 216, 180, 249, 74, 188, 244, 15, 207, 226, 228, 241, 35, 232, 232, 234, 198, 111, 254, 237, 31, 99, 193, 194, 82, 98, 168, 105, 193, 218, 141, 45, 88, 187, 241, 191, 197, 150, 26, 173, 227, 135, 113, 228, 240, 47, 17, 8, 43, 208, 184, 242, 42, 52, 245, 108, 67, 195, 138, 203, 73, 48, 145, 32, 91, 20, 46, 135, 45, 86, 13, 232, 24, 253, 0, 214, 161, 183, 96, 27, 217, 7, 183, 109, 0, 77, 173, 23, 160, 181, 243, 99, 104, 237, 236, 197, 248, 172, 10, 43, 214, 127, 60, 246, 193, 228, 60, 128, 182, 142, 21, 216, 118, 195, 159, 199, 34, 254, 30, 143, 27, 255, 240, 252, 83, 56, 63, 114, 26, 148, 82, 137, 229, 221, 107, 241, 185, 47, 125, 21, 245, 141, 45, 0, 128, 177, 243, 195, 248, 167, 159, 125, 23, 147, 86, 51, 46, 88, 183, 41, 33, 15, 224, 228, 241, 35, 120, 249, 133, 31, 97, 110, 206, 137, 141, 23, 95, 142, 47, 220, 113, 63, 24, 70, 147, 48, 240, 51, 39, 222, 195, 5, 157, 53, 162, 92, 180, 57, 215, 84, 164, 196, 121, 244, 67, 216, 204, 167, 200, 82, 35, 65, 182, 36, 8, 64, 60, 172, 103, 26, 83, 103, 222, 133, 109, 104, 47, 172, 131, 111, 99, 195, 182, 199, 19, 4, 64, 108, 196, 20, 128, 100, 108, 19, 3, 176, 140, 245, 195, 60, 250, 33, 60, 30, 55, 76, 203, 183, 160, 169, 123, 27, 154, 87, 93, 11, 173, 177, 77, 236, 225, 17, 8, 105, 73, 43, 0, 201, 140, 190, 255, 42, 17, 128, 44, 32, 45, 210, 8, 114, 66, 214, 181, 0, 82, 68, 163, 51, 98, 197, 234, 203, 177, 98, 245, 229, 0, 22, 90, 164, 157, 126, 243, 49, 56, 102, 204, 48, 45, 219, 130, 198, 149, 87, 144, 22, 105, 4, 73, 64, 4, 160, 200, 212, 53, 116, 161, 174, 161, 11, 27, 46, 190, 105, 161, 69, 218, 249, 119, 112, 224, 221, 231, 72, 139, 52, 130, 232, 100, 61, 5, 152, 26, 217, 15, 85, 8, 146, 104, 10, 50, 109, 30, 65, 152, 117, 160, 205, 164, 21, 123, 40, 5, 17, 93, 106, 180, 140, 245, 99, 218, 54, 2, 99, 75, 47, 154, 87, 125, 2, 77, 221, 87, 195, 216, 186, 161, 240, 95, 64, 32, 100, 32, 107, 1, 224, 185, 57, 56, 198, 142, 73, 162, 19, 143, 190, 74, 141, 38, 35, 5, 154, 86, 23, 126, 50, 137, 192, 243, 44, 166, 204, 167, 99, 241, 3, 63, 199, 161, 169, 251, 42, 82, 183, 64, 40, 42, 89, 11, 128, 80, 36, 47, 53, 54, 182, 172, 70, 107, 231, 6, 180, 116, 108, 128, 161, 166, 69, 236, 235, 33, 25, 230, 92, 83, 176, 77, 124, 20, 105, 177, 62, 126, 18, 213, 166, 110, 52, 175, 254, 4, 26, 86, 92, 137, 250, 229, 91, 197, 30, 30, 161, 76, 40, 185, 0, 196, 195, 249, 103, 49, 125, 230, 0, 172, 131, 111, 193, 54, 180, 7, 84, 152, 71, 107, 199, 6, 180, 117, 109, 68, 115, 251, 186, 178, 122, 194, 23, 202, 148, 109, 24, 230, 115, 125, 48, 143, 126, 136, 57, 247, 52, 76, 43, 34, 129, 196, 166, 158, 109, 208, 213, 116, 136, 61, 60, 130, 76, 17, 85, 0, 146, 113, 218, 62, 194, 212, 200, 126, 216, 134, 246, 96, 102, 244, 48, 234, 234, 151, 161, 165, 115, 3, 90, 59, 122, 81, 215, 208, 37, 246, 240, 36, 67, 116, 169, 209, 58, 118, 2, 214, 241, 147, 80, 86, 53, 160, 113, 197, 21, 164, 110, 129, 144, 51, 146, 18, 128, 120, 120, 110, 14, 211, 231, 14, 98, 106, 228, 93, 88, 135, 246, 32, 224, 182, 162, 181, 179, 23, 205, 29, 235, 5, 169, 91, 40, 39, 102, 166, 206, 193, 60, 118, 28, 150, 209, 126, 204, 144, 22, 105, 132, 28, 144, 172, 0, 36, 227, 157, 29, 131, 109, 104, 47, 108, 195, 123, 97, 63, 179, 31, 213, 250, 250, 72, 154, 114, 215, 70, 52, 52, 117, 23, 254, 11, 202, 132, 133, 22, 105, 253, 176, 140, 245, 147, 22, 105, 132, 37, 145, 141, 0, 36, 51, 125, 246, 32, 166, 206, 252, 17, 214, 211, 111, 97, 206, 62, 140, 150, 246, 117, 104, 110, 95, 143, 214, 206, 94, 84, 27, 26, 196, 30, 158, 100, 136, 182, 72, 51, 143, 246, 99, 210, 114, 154, 212, 45, 16, 18, 144, 173, 0, 196, 195, 122, 166, 49, 57, 188, 15, 182, 161, 61, 176, 13, 239, 131, 134, 97, 98, 37, 206, 13, 173, 171, 73, 48, 113, 158, 248, 22, 105, 150, 209, 227, 164, 69, 26, 161, 60, 4, 32, 25, 167, 185, 31, 182, 225, 119, 96, 29, 124, 11, 78, 203, 113, 212, 55, 173, 68, 75, 199, 6, 180, 119, 109, 34, 75, 141, 113, 196, 90, 164, 141, 246, 195, 58, 113, 146, 180, 72, 171, 64, 202, 82, 0, 226, 225, 185, 57, 76, 14, 255, 17, 182, 225, 189, 176, 13, 238, 133, 34, 228, 71, 107, 199, 6, 180, 118, 246, 162, 165, 115, 3, 241, 14, 226, 136, 182, 72, 51, 159, 239, 35, 45, 210, 42, 132, 178, 23, 128, 100, 220, 147, 131, 17, 49, 152, 95, 106, 172, 53, 117, 160, 165, 99, 3, 218, 150, 109, 36, 75, 141, 113, 68, 91, 164, 153, 199, 142, 195, 114, 254, 120, 66, 139, 52, 125, 195, 42, 208, 76, 21, 153, 50, 148, 1, 21, 39, 0, 241, 68, 211, 155, 173, 131, 111, 193, 58, 180, 7, 156, 219, 130, 150, 121, 239, 128, 44, 53, 38, 50, 51, 117, 14, 214, 137, 143, 96, 62, 255, 33, 188, 115, 118, 176, 172, 7, 1, 206, 15, 5, 165, 4, 173, 214, 67, 165, 174, 134, 74, 173, 135, 82, 93, 13, 70, 87, 3, 90, 165, 131, 74, 99, 4, 163, 53, 66, 169, 210, 65, 165, 53, 130, 102, 170, 160, 210, 26, 64, 171, 170, 161, 82, 87, 131, 214, 26, 136, 144, 136, 76, 69, 11, 64, 50, 62, 231, 4, 172, 131, 111, 71, 150, 26, 207, 30, 202, 185, 69, 90, 37, 194, 243, 44, 120, 206, 143, 0, 239, 71, 128, 245, 33, 192, 249, 16, 8, 248, 192, 177, 94, 4, 56, 47, 2, 129, 200, 235, 28, 235, 137, 252, 204, 249, 16, 224, 188, 224, 3, 126, 176, 108, 228, 239, 69, 66, 162, 49, 66, 165, 49, 128, 214, 84, 167, 20, 18, 149, 198, 0, 90, 93, 5, 149, 218, 0, 90, 163, 135, 74, 173, 7, 173, 209, 147, 37, 206, 60, 32, 2, 176, 4, 75, 181, 72, 35, 75, 141, 194, 146, 74, 72, 22, 68, 195, 11, 142, 245, 130, 15, 176, 139, 132, 36, 192, 122, 193, 5, 252, 224, 3, 126, 112, 172, 7, 180, 74, 3, 74, 85, 181, 72, 72, 84, 234, 136, 183, 65, 132, 36, 17, 34, 0, 89, 18, 223, 34, 109, 114, 100, 31, 84, 138, 48, 90, 58, 54, 160, 165, 115, 3, 169, 91, 144, 16, 81, 33, 225, 184, 136, 96, 68, 133, 132, 99, 61, 224, 121, 118, 222, 51, 137, 122, 42, 139, 133, 132, 99, 61, 224, 3, 126, 208, 42, 13, 104, 141, 17, 180, 90, 63, 239, 133, 44, 8, 9, 163, 173, 1, 205, 232, 202, 66, 72, 136, 0, 228, 137, 211, 246, 17, 108, 167, 223, 134, 117, 104, 15, 156, 230, 62, 152, 26, 150, 163, 117, 217, 199, 208, 220, 182, 150, 4, 19, 203, 128, 120, 33, 97, 89, 15, 120, 214, 11, 158, 231, 82, 10, 9, 199, 121, 192, 7, 184, 140, 66, 162, 154, 23, 7, 37, 163, 75, 16, 18, 149, 182, 6, 74, 149, 54, 38, 36, 106, 77, 29, 40, 181, 22, 140, 198, 56, 31, 59, 41, 222, 114, 44, 17, 0, 1, 136, 214, 45, 216, 6, 247, 194, 54, 248, 22, 66, 172, 11, 45, 203, 122, 209, 218, 209, 139, 166, 214, 11, 72, 48, 177, 130, 89, 74, 72, 88, 214, 131, 32, 207, 45, 8, 9, 235, 141, 136, 9, 231, 3, 199, 249, 22, 4, 39, 224, 7, 205, 84, 129, 86, 87, 47, 18, 18, 70, 91, 27, 241, 56, 210, 8, 9, 173, 209, 71, 188, 147, 52, 66, 66, 4, 160, 8, 120, 28, 231, 35, 193, 196, 193, 61, 176, 159, 63, 132, 154, 154, 38, 180, 46, 219, 136, 230, 142, 245, 164, 110, 129, 144, 23, 169, 132, 132, 227, 124, 243, 193, 212, 136, 144, 112, 172, 7, 1, 214, 139, 0, 239, 143, 196, 76, 230, 133, 36, 242, 26, 155, 82, 72, 136, 0, 20, 153, 232, 82, 163, 109, 100, 31, 108, 131, 111, 195, 239, 28, 67, 115, 219, 58, 180, 118, 70, 114, 15, 136, 119, 64, 40, 37, 60, 207, 194, 239, 117, 33, 20, 226, 193, 178, 30, 34, 0, 165, 198, 231, 156, 192, 244, 185, 67, 145, 186, 133, 193, 61, 208, 105, 171, 208, 210, 217, 139, 150, 246, 117, 164, 110, 129, 80, 114, 136, 0, 136, 12, 105, 145, 70, 16, 19, 34, 0, 18, 130, 243, 207, 98, 114, 232, 29, 76, 142, 252, 17, 182, 161, 61, 100, 169, 145, 80, 116, 136, 0, 72, 24, 210, 34, 141, 80, 108, 136, 0, 200, 132, 132, 165, 198, 161, 61, 8, 249, 103, 209, 220, 190, 142, 180, 72, 35, 20, 4, 17, 0, 153, 226, 113, 156, 143, 52, 65, 33, 45, 210, 8, 5, 64, 4, 160, 76, 152, 62, 123, 16, 214, 161, 183, 49, 53, 188, 47, 214, 34, 173, 109, 217, 70, 52, 181, 173, 37, 117, 11, 132, 180, 16, 1, 40, 67, 72, 139, 52, 66, 182, 16, 1, 168, 0, 72, 139, 52, 66, 58, 136, 0, 84, 24, 164, 69, 26, 33, 30, 34, 0, 21, 78, 66, 139, 180, 177, 15, 80, 91, 215, 74, 90, 164, 85, 16, 68, 0, 8, 49, 146, 91, 164, 5, 220, 86, 52, 119, 172, 71, 93, 67, 23, 12, 53, 205, 208, 85, 213, 129, 86, 169, 193, 48, 58, 208, 140, 134, 120, 11, 101, 0, 17, 0, 66, 90, 162, 117, 11, 179, 230, 227, 112, 79, 143, 128, 157, 155, 66, 192, 239, 68, 128, 157, 67, 40, 176, 80, 239, 206, 168, 171, 192, 168, 52, 80, 169, 117, 80, 49, 58, 168, 24, 45, 84, 42, 77, 228, 111, 70, 11, 38, 250, 250, 252, 177, 42, 70, 75, 132, 68, 34, 16, 1, 32, 20, 4, 231, 159, 5, 239, 119, 35, 192, 186, 231, 255, 118, 129, 103, 61, 8, 248, 93, 8, 248, 156, 8, 6, 188, 224, 124, 78, 240, 156, 39, 242, 158, 127, 46, 242, 222, 188, 144, 240, 172, 27, 225, 80, 16, 42, 70, 19, 17, 147, 20, 66, 194, 168, 171, 34, 130, 145, 66, 72, 84, 106, 45, 84, 180, 134, 8, 73, 158, 16, 1, 32, 72, 2, 214, 51, 13, 158, 243, 68, 254, 204, 11, 73, 192, 231, 138, 8, 71, 156, 144, 4, 252, 78, 240, 1, 47, 56, 239, 44, 130, 156, 55, 173, 144, 168, 213, 58, 208, 42, 77, 76, 72, 212, 234, 170, 200, 191, 213, 81, 239, 68, 23, 17, 20, 149, 182, 162, 133, 132, 8, 0, 161, 172, 136, 9, 137, 207, 21, 17, 134, 192, 92, 130, 144, 112, 126, 103, 76, 56, 162, 158, 73, 144, 157, 139, 8, 9, 231, 77, 16, 18, 245, 188, 231, 17, 21, 17, 38, 234, 149, 48, 90, 208, 140, 166, 44, 132, 132, 8, 0, 129, 144, 4, 207, 205, 33, 24, 240, 39, 8, 73, 128, 117, 130, 103, 61, 17, 1, 137, 254, 157, 78, 72, 230, 167, 68, 0, 192, 196, 98, 32, 218, 121, 143, 100, 177, 144, 48, 76, 220, 20, 71, 21, 141, 155, 84, 129, 166, 153, 162, 215, 120, 16, 1, 32, 16, 138, 68, 182, 66, 18, 152, 23, 19, 206, 231, 4, 207, 186, 17, 240, 187, 16, 228, 60, 105, 133, 68, 197, 232, 98, 65, 212, 168, 88, 40, 85, 76, 94, 66, 66, 4, 128, 64, 144, 56, 201, 66, 194, 249, 102, 17, 96, 221, 8, 6, 124, 25, 133, 132, 103, 61, 145, 159, 211, 8, 9, 17, 0, 2, 161, 66, 136, 10, 9, 231, 153, 65, 136, 103, 193, 249, 102, 137, 0, 16, 8, 149, 12, 37, 246, 0, 8, 4, 130, 120, 16, 1, 32, 16, 42, 24, 34, 0, 4, 66, 5, 67, 4, 128, 64, 168, 96, 136, 0, 16, 8, 21, 12, 17, 0, 2, 161, 130, 161, 197, 30, 0, 129, 16, 101, 192, 113, 2, 78, 191, 3, 0, 64, 81, 74, 208, 97, 26, 148, 82, 1, 53, 173, 5, 173, 160, 161, 85, 234, 208, 174, 239, 20, 123, 152, 101, 5, 201, 3, 32, 136, 206, 128, 227, 4, 166, 125, 147, 240, 243, 190, 172, 142, 167, 20, 20, 40, 5, 5, 5, 148, 80, 82, 20, 40, 80, 80, 42, 148, 160, 40, 37, 40, 80, 160, 41, 37, 104, 74, 5, 37, 69, 67, 163, 212, 194, 200, 212, 160, 78, 99, 18, 251, 107, 74, 18, 226, 1, 16, 68, 35, 87, 195, 143, 18, 10, 135, 16, 10, 135, 0, 240, 8, 132, 178, 255, 28, 77, 69, 110, 119, 74, 161, 140, 136, 72, 156, 112, 68, 189, 13, 154, 82, 65, 173, 212, 84, 140, 183, 65, 60, 0, 66, 201, 201, 215, 240, 197, 32, 87, 111, 163, 90, 85, 141, 6, 109, 147, 216, 195, 206, 26, 226, 1, 16, 74, 134, 156, 12, 63, 74, 185, 123, 27, 196, 3, 32, 20, 29, 57, 26, 190, 24, 228, 228, 109, 64, 141, 106, 141, 161, 96, 111, 131, 120, 0, 132, 162, 81, 206, 134, 207, 7, 120, 76, 78, 78, 161, 177, 177, 1, 180, 138, 70, 40, 20, 134, 195, 49, 3, 214, 207, 65, 167, 211, 193, 104, 212, 67, 65, 229, 182, 202, 158, 179, 183, 17, 169, 240, 205, 217, 219, 48, 168, 141, 48, 48, 145, 254, 0, 196, 3, 32, 8, 78, 57, 27, 126, 148, 57, 183, 27, 44, 199, 65, 205, 48, 168, 214, 235, 49, 235, 112, 2, 0, 12, 70, 3, 92, 78, 23, 0, 160, 166, 86, 186, 59, 54, 71, 189, 13, 146, 8, 68, 16, 140, 1, 199, 9, 236, 55, 239, 193, 184, 251, 124, 89, 27, 63, 0, 120, 61, 126, 24, 13, 70, 120, 61, 254, 200, 191, 189, 94, 24, 140, 6, 80, 148, 2, 6, 163, 1, 94, 175, 87, 236, 33, 46, 73, 40, 28, 2, 31, 226, 201, 20, 128, 80, 56, 149, 240, 196, 143, 135, 15, 240, 80, 170, 40, 208, 42, 26, 74, 21, 5, 62, 192, 47, 58, 134, 202, 209, 253, 23, 11, 34, 0, 132, 188, 17, 218, 240, 139, 49, 175, 46, 6, 126, 191, 15, 106, 134, 1, 0, 168, 25, 6, 126, 191, 15, 58, 157, 14, 46, 167, 43, 54, 5, 208, 104, 52, 98, 15, 51, 43, 196, 191, 154, 4, 217, 81, 44, 87, 223, 239, 247, 65, 173, 137, 24, 20, 0, 184, 156, 46, 40, 41, 26, 205, 45, 205, 0, 0, 167, 211, 45, 246, 87, 7, 16, 113, 255, 53, 26, 45, 0, 64, 163, 209, 194, 235, 241, 195, 96, 52, 32, 24, 226, 97, 181, 88, 17, 12, 241, 48, 24, 13, 98, 15, 51, 43, 136, 7, 64, 200, 154, 98, 187, 250, 94, 143, 31, 117, 166, 90, 204, 216, 29, 168, 214, 235, 225, 245, 122, 209, 220, 210, 28, 155, 87, 91, 45, 86, 73, 4, 214, 248, 96, 196, 83, 137, 135, 162, 20, 48, 153, 76, 48, 79, 88, 96, 50, 201, 39, 237, 152, 8, 0, 33, 35, 165, 152, 227, 203, 105, 94, 221, 218, 214, 2, 0, 48, 79, 88, 98, 63, 203, 21, 34, 0, 132, 180, 148, 50, 184, 87, 78, 243, 106, 57, 65, 4, 128, 176, 8, 49, 162, 250, 81, 247, 31, 136, 204, 171, 103, 236, 14, 212, 55, 214, 195, 225, 152, 129, 213, 98, 133, 90, 195, 160, 182, 182, 78, 236, 75, 83, 118, 144, 68, 32, 66, 12, 49, 151, 243, 204, 19, 150, 69, 175, 149, 147, 171, 45, 4, 233, 86, 69, 146, 87, 79, 114, 129, 120, 0, 4, 73, 172, 227, 19, 99, 207, 204, 194, 170, 72, 29, 92, 78, 23, 156, 78, 55, 106, 106, 141, 9, 171, 39, 213, 42, 125, 78, 231, 148, 70, 84, 133, 32, 10, 149, 148, 185, 87, 14, 164, 203, 54, 76, 206, 74, 204, 5, 226, 1, 84, 32, 82, 120, 226, 19, 10, 135, 154, 119, 255, 147, 87, 79, 114, 153, 6, 144, 24, 64, 5, 65, 12, 95, 222, 164, 42, 56, 162, 233, 136, 19, 95, 173, 215, 99, 206, 237, 142, 253, 156, 45, 68, 0, 42, 0, 98, 248, 229, 65, 124, 16, 48, 186, 42, 50, 61, 57, 141, 58, 83, 45, 104, 21, 13, 62, 192, 99, 198, 238, 64, 99, 115, 67, 194, 231, 194, 161, 16, 154, 221, 141, 9, 175, 241, 170, 32, 0, 50, 5, 40, 107, 136, 225, 151, 23, 169, 178, 13, 83, 101, 37, 46, 194, 27, 68, 107, 221, 10, 212, 183, 45, 8, 3, 207, 205, 97, 206, 21, 34, 2, 80, 142, 16, 195, 175, 28, 178, 90, 61, 209, 41, 17, 240, 37, 222, 11, 52, 83, 141, 154, 122, 226, 1, 148, 21, 196, 240, 9, 169, 80, 80, 20, 248, 32, 159, 242, 61, 34, 0, 101, 0, 49, 124, 66, 190, 16, 1, 144, 57, 135, 44, 251, 225, 14, 184, 196, 30, 6, 161, 132, 164, 114, 245, 51, 37, 79, 133, 66, 169, 61, 0, 146, 8, 36, 115, 124, 65, 105, 183, 158, 34, 72, 27, 34, 0, 50, 102, 220, 61, 10, 62, 141, 178, 19, 8, 217, 64, 4, 64, 198, 204, 114, 51, 98, 15, 129, 32, 115, 136, 0, 200, 24, 63, 159, 123, 238, 55, 161, 50, 225, 130, 108, 202, 215, 137, 0, 200, 24, 54, 72, 4, 32, 19, 161, 80, 24, 118, 187, 29, 230, 9, 11, 102, 29, 78, 132, 67, 145, 29, 55, 248, 0, 15, 243, 132, 37, 101, 231, 161, 74, 130, 8, 128, 140, 153, 117, 146, 27, 58, 19, 233, 26, 139, 38, 55, 32, 173, 84, 136, 0, 200, 24, 46, 228, 35, 55, 116, 6, 138, 81, 66, 43, 71, 248, 112, 32, 229, 235, 68, 0, 100, 202, 89, 215, 8, 116, 85, 213, 21, 123, 67, 231, 75, 186, 18, 218, 114, 39, 0, 34, 0, 101, 133, 39, 16, 121, 242, 87, 234, 13, 157, 45, 209, 198, 162, 161, 80, 56, 214, 88, 52, 85, 3, 210, 74, 133, 8, 128, 76, 241, 242, 94, 114, 67, 103, 65, 170, 13, 59, 82, 109, 236, 81, 238, 240, 170, 32, 102, 167, 23, 103, 140, 18, 1, 144, 41, 211, 147, 230, 138, 190, 161, 179, 37, 90, 66, 11, 0, 38, 147, 9, 20, 165, 136, 149, 208, 154, 39, 44, 152, 156, 156, 74, 91, 40, 83, 78, 76, 135, 38, 83, 190, 78, 106, 1, 100, 138, 39, 196, 229, 86, 19, 94, 36, 138, 209, 169, 182, 216, 84, 100, 3, 82, 154, 2, 207, 46, 206, 5, 32, 30, 128, 76, 241, 251, 23, 158, 242, 173, 109, 45, 177, 27, 57, 254, 231, 82, 64, 150, 217, 100, 2, 163, 128, 223, 51, 183, 232, 101, 34, 0, 50, 100, 200, 121, 26, 161, 249, 245, 127, 177, 33, 203, 108, 242, 64, 65, 81, 8, 135, 23, 119, 255, 35, 2, 32, 67, 60, 156, 116, 203, 127, 165, 186, 42, 145, 79, 9, 109, 33, 72, 49, 3, 49, 85, 73, 48, 17, 0, 25, 194, 133, 2, 37, 191, 161, 211, 65, 150, 217, 82, 35, 151, 169, 17, 17, 0, 25, 34, 165, 26, 0, 178, 204, 150, 26, 185, 76, 141, 164, 21, 158, 37, 100, 5, 31, 10, 20, 126, 18, 129, 200, 187, 83, 109, 133, 33, 196, 38, 30, 197, 128, 8, 128, 204, 152, 241, 219, 37, 223, 4, 164, 34, 151, 217, 146, 72, 181, 181, 121, 170, 169, 81, 174, 123, 249, 21, 66, 170, 146, 96, 50, 5, 144, 25, 54, 159, 165, 240, 147, 16, 138, 142, 92, 166, 70, 196, 3, 144, 25, 44, 233, 252, 43, 11, 228, 50, 53, 34, 2, 32, 51, 164, 218, 5, 72, 42, 171, 18, 82, 70, 236, 169, 81, 170, 146, 96, 50, 5, 144, 25, 129, 176, 116, 2, 128, 4, 121, 145, 170, 36, 152, 8, 128, 204, 144, 210, 10, 0, 65, 254, 16, 1, 144, 17, 22, 239, 132, 228, 87, 0, 8, 137, 72, 105, 106, 196, 171, 130, 224, 185, 196, 122, 0, 34, 0, 50, 194, 230, 49, 139, 61, 4, 130, 140, 9, 168, 188, 152, 115, 37, 214, 144, 144, 32, 160, 76, 56, 106, 61, 12, 59, 39, 173, 8, 50, 65, 94, 56, 217, 185, 69, 37, 193, 68, 0, 100, 192, 97, 219, 1, 56, 57, 135, 216, 195, 32, 72, 156, 76, 189, 25, 26, 76, 245, 139, 183, 9, 23, 123, 208, 132, 165, 169, 228, 205, 63, 127, 254, 244, 175, 48, 126, 102, 233, 105, 79, 199, 202, 54, 108, 127, 244, 127, 136, 61, 84, 73, 176, 80, 128, 84, 7, 151, 211, 5, 167, 211, 141, 154, 90, 99, 172, 0, 137, 13, 248, 193, 211, 137, 49, 36, 34, 0, 18, 197, 197, 57, 241, 145, 253, 120, 197, 26, 127, 40, 20, 198, 96, 255, 8, 206, 14, 140, 46, 121, 92, 128, 35, 65, 209, 40, 94, 175, 23, 205, 45, 205, 177, 2, 36, 171, 197, 138, 154, 218, 72, 225, 81, 157, 169, 22, 51, 118, 7, 66, 122, 34, 0, 146, 103, 198, 111, 199, 192, 76, 63, 188, 188, 71, 236, 161, 136, 134, 203, 233, 130, 66, 161, 0, 0, 236, 220, 185, 19, 91, 182, 108, 73, 120, 255, 224, 193, 131, 120, 244, 209, 71, 197, 30, 166, 164, 73, 85, 128, 148, 12, 17, 0, 137, 97, 241, 78, 96, 120, 246, 52, 252, 21, 158, 242, 235, 245, 122, 161, 84, 42, 1, 0, 87, 95, 125, 245, 34, 1, 80, 171, 213, 98, 15, 81, 114, 100, 83, 128, 148, 92, 16, 68, 4, 64, 66, 140, 187, 71, 49, 226, 26, 76, 187, 145, 35, 129, 176, 20, 6, 163, 1, 14, 199, 12, 172, 22, 43, 212, 26, 6, 181, 181, 117, 152, 158, 156, 70, 157, 169, 22, 64, 164, 0, 105, 118, 102, 22, 227, 131, 103, 65, 205, 183, 7, 19, 77, 0, 44, 222, 9, 76, 184, 199, 176, 194, 216, 131, 58, 141, 73, 236, 107, 39, 58, 103, 93, 35, 56, 231, 26, 38, 137, 62, 243, 232, 116, 58, 4, 131, 65, 177, 135, 33, 43, 178, 41, 64, 82, 53, 48, 216, 220, 189, 60, 246, 111, 81, 4, 96, 198, 111, 143, 185, 185, 31, 78, 59, 209, 92, 213, 134, 53, 181, 235, 69, 190, 124, 226, 49, 228, 60, 141, 113, 247, 57, 98, 252, 113, 24, 140, 134, 148, 77, 44, 9, 185, 145, 92, 128, 100, 80, 27, 19, 222, 23, 69, 0, 6, 102, 250, 99, 115, 92, 62, 196, 99, 220, 125, 30, 78, 191, 3, 203, 140, 43, 208, 162, 107, 19, 249, 146, 149, 248, 90, 56, 78, 192, 234, 33, 41, 190, 201, 80, 148, 2, 52, 77, 102, 168, 197, 166, 228, 169, 192, 135, 44, 251, 83, 70, 183, 221, 1, 23, 78, 205, 156, 192, 9, 123, 159, 216, 215, 164, 100, 12, 56, 78, 192, 60, 55, 86, 144, 241, 75, 177, 251, 44, 65, 62, 148, 84, 98, 15, 219, 14, 44, 185, 174, 205, 135, 120, 88, 60, 19, 112, 176, 51, 104, 215, 118, 96, 121, 109, 143, 216, 215, 167, 104, 156, 176, 247, 193, 230, 181, 32, 20, 46, 172, 191, 127, 166, 228, 143, 82, 183, 157, 34, 136, 207, 82, 5, 72, 20, 40, 88, 92, 67, 177, 215, 75, 230, 1, 252, 201, 118, 8, 78, 54, 187, 116, 86, 235, 148, 21, 125, 230, 15, 208, 55, 117, 164, 84, 195, 43, 41, 125, 83, 71, 96, 241, 76, 20, 108, 252, 128, 124, 186, 207, 18, 164, 129, 46, 204, 193, 225, 181, 196, 254, 148, 68, 0, 250, 166, 142, 192, 193, 218, 179, 62, 222, 235, 245, 66, 87, 85, 141, 41, 223, 36, 222, 25, 127, 11, 3, 142, 19, 162, 93, 48, 161, 57, 106, 61, 140, 41, 223, 100, 225, 39, 74, 131, 84, 55, 230, 32, 72, 147, 162, 11, 192, 9, 123, 95, 65, 55, 60, 203, 251, 113, 116, 232, 79, 56, 100, 217, 143, 25, 127, 246, 34, 34, 69, 14, 219, 14, 8, 94, 209, 71, 54, 230, 32, 20, 66, 81, 5, 96, 192, 113, 2, 22, 207, 68, 206, 159, 75, 190, 169, 25, 53, 3, 203, 204, 4, 14, 141, 238, 151, 173, 55, 112, 200, 178, 63, 235, 41, 80, 46, 200, 165, 251, 44, 65, 154, 20, 45, 8, 24, 141, 112, 231, 67, 186, 140, 38, 141, 73, 139, 113, 247, 121, 204, 248, 166, 177, 162, 166, 71, 22, 75, 134, 197, 46, 234, 145, 75, 247, 89, 130, 52, 41, 138, 0, 12, 57, 79, 195, 60, 55, 150, 119, 144, 43, 155, 155, 122, 204, 54, 138, 245, 157, 189, 88, 111, 218, 88, 250, 171, 150, 37, 98, 21, 245, 136, 221, 125, 182, 156, 200, 84, 99, 223, 216, 216, 32, 250, 238, 62, 133, 32, 248, 20, 224, 172, 107, 4, 227, 238, 115, 130, 68, 184, 227, 137, 223, 247, 190, 181, 173, 5, 38, 147, 9, 22, 207, 4, 222, 51, 191, 131, 113, 247, 104, 129, 103, 23, 30, 139, 119, 2, 39, 103, 62, 172, 232, 138, 190, 114, 64, 46, 155, 124, 230, 139, 160, 2, 48, 238, 30, 45, 121, 62, 187, 151, 247, 224, 244, 236, 73, 73, 45, 25, 142, 187, 71, 49, 232, 24, 168, 248, 138, 190, 114, 160, 220, 151, 89, 5, 19, 128, 41, 159, 13, 35, 174, 65, 81, 82, 90, 67, 225, 16, 166, 124, 147, 216, 111, 222, 131, 179, 174, 145, 146, 255, 254, 120, 206, 186, 70, 48, 228, 28, 40, 121, 69, 159, 148, 186, 207, 150, 51, 229, 182, 204, 42, 136, 0, 204, 248, 237, 56, 229, 56, 41, 248, 77, 159, 235, 77, 237, 231, 125, 56, 227, 28, 196, 159, 108, 135, 4, 190, 76, 217, 49, 228, 60, 77, 42, 250, 202, 140, 114, 95, 102, 21, 68, 0, 226, 139, 123, 196, 38, 20, 14, 193, 193, 218, 177, 111, 226, 45, 12, 205, 158, 42, 217, 239, 29, 112, 156, 192, 168, 235, 12, 49, 254, 50, 163, 220, 151, 89, 11, 14, 95, 166, 43, 238, 17, 27, 46, 200, 226, 156, 107, 4, 124, 152, 47, 122, 169, 113, 223, 212, 17, 156, 60, 123, 18, 140, 154, 41, 187, 40, 113, 165, 83, 238, 203, 172, 5, 121, 0, 153, 138, 123, 42, 129, 163, 214, 195, 56, 59, 57, 130, 250, 198, 122, 0, 229, 23, 37, 38, 44, 38, 121, 69, 74, 206, 177, 150, 188, 5, 32, 151, 226, 30, 49, 169, 215, 52, 20, 237, 220, 125, 83, 71, 112, 242, 252, 9, 232, 170, 170, 203, 54, 74, 76, 40, 111, 242, 18, 128, 92, 139, 123, 196, 130, 166, 104, 52, 104, 155, 138, 114, 238, 104, 81, 79, 40, 148, 152, 239, 80, 110, 81, 98, 66, 121, 147, 151, 0, 84, 49, 6, 168, 40, 233, 119, 101, 165, 20, 202, 162, 156, 55, 190, 168, 167, 220, 163, 196, 132, 8, 197, 94, 102, 21, 171, 177, 75, 94, 2, 208, 99, 92, 141, 222, 250, 77, 208, 209, 85, 146, 238, 58, 67, 43, 132, 15, 190, 37, 23, 245, 148, 123, 148, 56, 27, 146, 111, 210, 116, 55, 51, 33, 61, 98, 101, 28, 230, 29, 3, 168, 211, 152, 240, 241, 214, 171, 177, 97, 121, 47, 66, 156, 8, 87, 44, 11, 84, 74, 70, 176, 115, 185, 56, 103, 202, 109, 186, 162, 81, 98, 0, 48, 153, 76, 160, 40, 69, 44, 74, 108, 158, 176, 96, 114, 114, 10, 124, 80, 154, 2, 41, 20, 201, 55, 105, 186, 155, 153, 144, 30, 177, 50, 14, 11, 206, 3, 184, 176, 249, 18, 172, 109, 91, 15, 29, 93, 85, 218, 43, 150, 5, 12, 165, 18, 228, 60, 51, 126, 59, 250, 167, 143, 101, 189, 226, 81, 78, 81, 226, 108, 72, 190, 73, 211, 221, 204, 132, 236, 41, 85, 44, 73, 144, 68, 160, 229, 134, 149, 248, 120, 235, 213, 104, 208, 54, 130, 82, 148, 188, 207, 104, 90, 170, 4, 232, 133, 71, 138, 122, 150, 38, 155, 155, 148, 162, 164, 115, 79, 72, 21, 177, 98, 73, 130, 254, 207, 108, 108, 216, 140, 78, 195, 10, 201, 4, 8, 77, 2, 44, 1, 218, 125, 83, 146, 201, 114, 148, 34, 169, 110, 210, 84, 55, 51, 97, 105, 196, 138, 37, 9, 46, 205, 209, 0, 161, 94, 101, 40, 254, 85, 91, 2, 154, 162, 5, 217, 113, 200, 207, 103, 119, 209, 43, 181, 24, 39, 213, 77, 154, 234, 102, 38, 44, 141, 88, 177, 164, 162, 228, 168, 214, 105, 76, 216, 210, 114, 197, 124, 63, 64, 91, 73, 243, 227, 163, 13, 28, 148, 20, 13, 180, 23, 126, 62, 62, 20, 40, 217, 216, 229, 72, 170, 180, 216, 84, 233, 179, 132, 220, 41, 69, 99, 151, 162, 38, 169, 175, 55, 109, 196, 184, 123, 20, 231, 221, 103, 74, 54, 135, 142, 70, 160, 91, 27, 90, 5, 57, 95, 32, 76, 4, 96, 41, 72, 247, 33, 121, 83, 244, 232, 76, 187, 190, 179, 164, 1, 194, 104, 4, 90, 168, 37, 192, 32, 89, 195, 38, 148, 49, 37, 11, 207, 150, 58, 64, 168, 163, 117, 130, 156, 39, 72, 60, 0, 66, 9, 41, 117, 44, 169, 164, 235, 51, 165, 8, 16, 70, 35, 208, 106, 101, 225, 145, 103, 139, 87, 152, 221, 123, 42, 1, 177, 130, 160, 28, 199, 145, 61, 17, 11, 160, 228, 11, 180, 209, 0, 97, 75, 85, 27, 104, 74, 248, 16, 68, 52, 2, 45, 196, 18, 224, 172, 12, 170, 29, 43, 29, 134, 137, 36, 123, 145, 50, 236, 252, 16, 45, 67, 99, 189, 105, 35, 122, 140, 107, 4, 207, 32, 164, 40, 5, 154, 26, 154, 4, 89, 2, 100, 201, 250, 191, 12, 32, 101, 216, 133, 32, 106, 138, 86, 177, 2, 132, 180, 64, 41, 192, 28, 89, 2, 148, 13, 164, 12, 59, 63, 36, 145, 163, 41, 116, 128, 80, 165, 16, 70, 0, 66, 161, 160, 152, 151, 37, 143, 241, 138, 83, 82, 42, 46, 164, 12, 187, 16, 36, 33, 0, 128, 176, 1, 66, 161, 60, 0, 185, 229, 0, 148, 251, 38, 22, 169, 224, 184, 64, 197, 150, 97, 11, 129, 100, 4, 0, 16, 46, 64, 168, 161, 133, 201, 61, 151, 91, 22, 160, 216, 155, 88, 240, 1, 30, 167, 250, 134, 75, 254, 189, 133, 74, 157, 61, 117, 108, 168, 76, 189, 164, 244, 72, 74, 0, 162, 20, 26, 32, 84, 211, 90, 65, 198, 33, 247, 22, 223, 165, 156, 23, 7, 184, 0, 190, 115, 223, 15, 241, 248, 157, 223, 195, 177, 247, 250, 69, 249, 190, 133, 148, 97, 31, 123, 175, 31, 143, 111, 255, 62, 190, 115, 223, 15, 43, 74, 4, 36, 41, 0, 64, 97, 1, 194, 38, 109, 115, 193, 191, 95, 236, 29, 134, 242, 65, 172, 146, 210, 0, 23, 192, 83, 247, 62, 139, 15, 15, 158, 4, 31, 224, 241, 204, 95, 63, 39, 154, 8, 228, 195, 177, 247, 250, 241, 204, 95, 63, 7, 62, 192, 227, 195, 131, 39, 241, 157, 251, 126, 136, 0, 39, 47, 239, 47, 95, 36, 43, 0, 81, 114, 13, 16, 210, 20, 13, 3, 99, 44, 248, 247, 122, 2, 242, 235, 98, 35, 70, 73, 41, 235, 231, 240, 212, 189, 207, 226, 228, 145, 83, 88, 182, 108, 25, 190, 254, 245, 175, 203, 74, 4, 226, 141, 223, 100, 50, 129, 97, 24, 124, 120, 240, 36, 158, 186, 247, 217, 138, 16, 1, 201, 11, 0, 144, 91, 128, 80, 168, 0, 160, 220, 230, 255, 64, 233, 75, 74, 89, 63, 135, 39, 239, 222, 133, 147, 71, 78, 161, 189, 189, 29, 239, 190, 251, 46, 118, 237, 218, 133, 103, 159, 125, 54, 38, 2, 253, 135, 7, 138, 250, 157, 25, 102, 113, 205, 71, 182, 174, 127, 188, 241, 223, 124, 243, 205, 176, 217, 108, 216, 183, 111, 31, 170, 170, 170, 112, 242, 200, 41, 60, 117, 239, 179, 96, 253, 18, 237, 119, 39, 16, 178, 16, 0, 32, 251, 0, 161, 80, 75, 128, 229, 146, 3, 80, 172, 246, 100, 62, 175, 31, 79, 222, 189, 11, 167, 251, 134, 209, 222, 222, 142, 253, 251, 247, 163, 189, 61, 82, 127, 253, 192, 3, 15, 196, 68, 224, 233, 175, 254, 160, 232, 34, 144, 15, 241, 198, 127, 235, 173, 183, 226, 213, 87, 95, 133, 82, 169, 196, 150, 45, 91, 176, 111, 223, 62, 24, 12, 6, 156, 60, 114, 10, 79, 222, 189, 171, 172, 69, 64, 54, 2, 16, 37, 83, 128, 80, 176, 37, 192, 96, 249, 254, 167, 23, 138, 207, 235, 199, 19, 219, 191, 159, 96, 252, 93, 93, 93, 9, 199, 68, 69, 32, 192, 5, 36, 39, 2, 253, 135, 7, 18, 140, 255, 165, 151, 94, 130, 82, 185, 208, 66, 254, 162, 139, 46, 194, 254, 253, 251, 81, 83, 83, 131, 211, 125, 195, 120, 242, 238, 93, 240, 121, 203, 115, 73, 81, 118, 2, 0, 44, 29, 32, 20, 108, 9, 48, 92, 57, 145, 224, 92, 57, 221, 55, 140, 225, 19, 103, 1, 0, 59, 118, 236, 88, 100, 252, 81, 164, 40, 2, 253, 135, 7, 240, 244, 87, 127, 144, 96, 252, 10, 133, 98, 209, 113, 189, 189, 189, 120, 244, 209, 71, 99, 223, 119, 232, 248, 25, 177, 135, 94, 20, 100, 41, 0, 81, 162, 1, 66, 70, 185, 16, 32, 20, 162, 17, 40, 0, 132, 194, 242, 202, 2, 140, 167, 216, 149, 121, 27, 47, 91, 143, 135, 118, 221, 11, 74, 73, 225, 129, 7, 30, 192, 27, 111, 188, 145, 246, 88, 41, 137, 64, 212, 248, 3, 92, 96, 73, 227, 7, 128, 215, 94, 123, 13, 143, 62, 250, 40, 40, 37, 133, 135, 118, 221, 139, 222, 45, 107, 83, 30, 39, 247, 61, 17, 100, 45, 0, 64, 36, 64, 184, 193, 20, 9, 16, 82, 10, 10, 203, 13, 43, 11, 62, 167, 139, 115, 22, 156, 3, 32, 247, 27, 35, 19, 91, 175, 187, 24, 127, 243, 221, 191, 66, 40, 28, 194, 77, 55, 221, 36, 121, 17, 200, 213, 248, 111, 185, 229, 22, 132, 194, 33, 252, 205, 119, 255, 10, 91, 175, 187, 56, 237, 121, 229, 190, 39, 130, 236, 5, 0, 88, 8, 16, 118, 234, 151, 11, 114, 62, 155, 207, 154, 242, 117, 69, 128, 135, 225, 131, 97, 180, 253, 234, 109, 172, 248, 238, 43, 184, 224, 161, 95, 96, 245, 55, 127, 137, 21, 207, 188, 130, 182, 95, 189, 13, 195, 7, 195, 80, 204, 27, 188, 220, 111, 140, 108, 136, 138, 64, 48, 24, 196, 77, 55, 221, 132, 215, 94, 123, 45, 237, 177, 15, 60, 240, 0, 118, 238, 220, 41, 138, 8, 196, 27, 255, 237, 183, 223, 158, 149, 241, 135, 17, 206, 104, 252, 128, 252, 247, 68, 40, 11, 1, 136, 210, 83, 115, 129, 32, 231, 73, 85, 6, 92, 253, 209, 40, 86, 62, 253, 47, 104, 255, 167, 63, 192, 120, 100, 16, 154, 9, 59, 40, 142, 135, 210, 23, 128, 198, 108, 135, 241, 200, 32, 218, 255, 233, 15, 88, 249, 244, 191, 160, 250, 163, 81, 217, 223, 24, 217, 178, 245, 186, 139, 113, 255, 211, 119, 34, 24, 12, 226, 150, 91, 110, 89, 82, 4, 118, 236, 216, 145, 32, 2, 3, 199, 6, 139, 62, 190, 100, 227, 127, 225, 133, 23, 4, 51, 254, 114, 216, 19, 65, 218, 163, 19, 137, 228, 86, 224, 245, 111, 126, 128, 206, 159, 190, 1, 102, 102, 46, 227, 103, 153, 153, 57, 116, 254, 244, 13, 44, 63, 120, 74, 214, 55, 70, 46, 92, 121, 227, 86, 220, 255, 244, 157, 8, 133, 66, 57, 137, 192, 83, 247, 60, 91, 84, 17, 200, 197, 248, 95, 126, 249, 229, 156, 140, 31, 40, 143, 61, 17, 202, 231, 46, 20, 144, 248, 36, 160, 250, 55, 63, 64, 227, 27, 135, 115, 62, 199, 178, 189, 39, 80, 255, 230, 7, 178, 189, 49, 114, 37, 31, 17, 96, 125, 108, 209, 68, 96, 224, 216, 96, 78, 198, 127, 219, 109, 183, 229, 100, 252, 64, 121, 236, 137, 64, 191, 49, 240, 107, 180, 212, 182, 160, 86, 103, 18, 36, 128, 38, 22, 238, 57, 15, 94, 248, 231, 215, 240, 238, 193, 163, 176, 216, 166, 10, 63, 33, 128, 13, 138, 0, 30, 81, 185, 129, 52, 55, 78, 38, 26, 223, 56, 140, 185, 118, 19, 70, 77, 85, 168, 111, 172, 135, 195, 49, 3, 171, 197, 10, 181, 134, 65, 109, 109, 157, 216, 151, 76, 112, 174, 188, 113, 43, 0, 224, 71, 143, 253, 18, 183, 220, 114, 11, 94, 124, 241, 69, 220, 122, 235, 173, 41, 143, 221, 177, 99, 7, 0, 224, 145, 71, 30, 193, 83, 247, 60, 139, 191, 253, 201, 131, 88, 179, 105, 149, 32, 227, 24, 56, 54, 136, 167, 238, 137, 4, 29, 183, 111, 223, 142, 159, 253, 236, 103, 25, 141, 95, 65, 41, 114, 50, 126, 64, 62, 123, 34, 68, 247, 202, 96, 253, 28, 140, 29, 106, 84, 211, 11, 217, 147, 148, 219, 239, 196, 121, 251, 25, 12, 207, 158, 194, 222, 241, 55, 241, 158, 249, 29, 28, 181, 30, 198, 144, 243, 180, 216, 227, 206, 137, 127, 124, 241, 63, 240, 234, 235, 111, 46, 105, 252, 52, 157, 125, 137, 49, 131, 48, 238, 86, 121, 210, 222, 56, 217, 210, 250, 210, 59, 8, 249, 217, 148, 105, 186, 229, 72, 188, 39, 112, 219, 109, 183, 225, 229, 151, 95, 78, 123, 108, 49, 60, 129, 168, 241, 179, 62, 22, 219, 183, 111, 199, 207, 127, 254, 243, 140, 198, 15, 32, 103, 227, 7, 228, 179, 9, 108, 114, 0, 58, 30, 218, 104, 48, 98, 198, 238, 64, 181, 94, 15, 62, 196, 131, 15, 241, 240, 194, 3, 59, 55, 133, 113, 247, 57, 208, 148, 10, 90, 165, 14, 85, 76, 53, 214, 212, 174, 23, 251, 187, 164, 229, 205, 183, 222, 77, 255, 166, 174, 26, 45, 219, 191, 129, 150, 143, 93, 136, 41, 127, 16, 142, 131, 123, 225, 126, 233, 121, 40, 150, 200, 139, 191, 68, 193, 193, 164, 8, 167, 125, 127, 142, 15, 194, 50, 95, 44, 210, 194, 168, 80, 77, 43, 83, 30, 167, 113, 251, 176, 122, 210, 11, 103, 151, 216, 87, 168, 116, 196, 123, 2, 81, 3, 91, 202, 19, 96, 89, 22, 79, 60, 241, 68, 193, 158, 64, 42, 227, 79, 199, 238, 221, 187, 113, 199, 29, 119, 0, 0, 238, 127, 250, 206, 156, 141, 95, 78, 120, 189, 94, 52, 183, 52, 131, 162, 20, 9, 79, 127, 0, 160, 227, 3, 85, 180, 42, 241, 9, 25, 21, 4, 63, 239, 131, 131, 181, 195, 60, 55, 6, 70, 169, 134, 90, 169, 129, 158, 49, 160, 73, 219, 34, 72, 243, 77, 33, 112, 123, 210, 68, 213, 181, 85, 104, 186, 243, 27, 168, 89, 191, 9, 238, 64, 16, 246, 183, 126, 13, 239, 235, 47, 64, 145, 97, 29, 126, 147, 50, 125, 45, 192, 144, 215, 143, 253, 206, 196, 157, 142, 174, 48, 86, 161, 71, 151, 122, 94, 95, 125, 242, 28, 156, 151, 8, 227, 222, 202, 133, 92, 68, 224, 241, 199, 31, 135, 223, 239, 199, 51, 207, 60, 147, 183, 8, 20, 98, 252, 209, 177, 86, 34, 52, 176, 16, 193, 172, 206, 144, 69, 23, 10, 135, 224, 231, 125, 240, 243, 62, 56, 89, 7, 198, 221, 231, 35, 130, 64, 169, 81, 205, 232, 97, 210, 54, 160, 69, 215, 38, 246, 119, 138, 161, 168, 50, 96, 217, 87, 31, 5, 213, 189, 1, 254, 96, 16, 83, 7, 246, 194, 243, 250, 238, 140, 198, 15, 0, 203, 21, 169, 51, 1, 231, 248, 224, 34, 227, 7, 128, 253, 78, 79, 90, 79, 64, 59, 110, 23, 251, 82, 136, 66, 178, 8, 112, 28, 135, 219, 111, 191, 61, 229, 177, 59, 119, 238, 4, 128, 152, 8, 60, 241, 139, 135, 179, 254, 61, 241, 198, 255, 181, 175, 125, 13, 207, 61, 247, 92, 218, 99, 139, 97, 252, 82, 223, 24, 54, 26, 128, 54, 24, 13, 152, 227, 185, 196, 24, 0, 80, 88, 157, 56, 23, 100, 225, 14, 184, 96, 241, 76, 224, 196, 116, 31, 222, 25, 127, 11, 135, 44, 251, 209, 55, 117, 4, 227, 238, 81, 209, 190, 180, 66, 163, 67, 199, 87, 30, 132, 113, 213, 90, 240, 60, 15, 219, 127, 189, 14, 247, 139, 207, 67, 145, 101, 134, 95, 157, 34, 181, 72, 156, 91, 162, 50, 204, 146, 166, 126, 156, 158, 93, 72, 250, 145, 210, 141, 33, 20, 75, 101, 61, 246, 94, 182, 22, 247, 125, 251, 43, 0, 128, 59, 238, 184, 3, 187, 119, 239, 78, 123, 158, 157, 59, 119, 70, 166, 4, 62, 22, 223, 186, 107, 23, 134, 250, 51, 231, 223, 15, 30, 31, 17, 213, 248, 229, 64, 252, 202, 68, 50, 148, 208, 117, 226, 129, 80, 68, 16, 166, 124, 147, 24, 112, 244, 199, 2, 139, 125, 83, 71, 74, 215, 101, 167, 74, 143, 186, 175, 125, 11, 117, 31, 219, 12, 132, 21, 152, 57, 184, 23, 222, 223, 254, 10, 10, 74, 1, 208, 42, 132, 149, 153, 43, 6, 133, 44, 5, 10, 169, 138, 186, 7, 171, 232, 100, 202, 122, 252, 216, 199, 215, 225, 158, 199, 239, 0, 144, 189, 8, 228, 82, 125, 151, 141, 241, 255, 226, 23, 191, 168, 72, 227, 7, 18, 251, 68, 44, 138, 1, 0, 197, 125, 42, 197, 2, 139, 188, 7, 83, 190, 73, 156, 115, 13, 131, 161, 34, 113, 4, 163, 186, 70, 176, 236, 189, 120, 212, 23, 94, 14, 69, 215, 42, 216, 217, 0, 102, 222, 250, 29, 60, 191, 255, 87, 104, 175, 255, 28, 148, 109, 93, 64, 56, 4, 238, 232, 1, 176, 71, 247, 67, 177, 196, 182, 95, 147, 97, 10, 203, 83, 120, 1, 93, 26, 6, 135, 221, 139, 227, 13, 12, 20, 104, 97, 82, 11, 11, 215, 80, 120, 135, 34, 41, 227, 245, 248, 81, 103, 170, 141, 5, 147, 227, 131, 78, 6, 163, 1, 86, 139, 21, 215, 222, 124, 5, 0, 224, 39, 79, 190, 16, 51, 196, 108, 166, 3, 217, 144, 141, 241, 223, 117, 215, 93, 80, 40, 20, 21, 103, 252, 153, 40, 249, 163, 41, 94, 16, 28, 172, 29, 163, 238, 179, 9, 129, 197, 182, 170, 142, 130, 91, 122, 113, 71, 246, 33, 112, 225, 229, 240, 217, 38, 48, 247, 155, 221, 80, 223, 248, 37, 168, 55, 109, 5, 20, 0, 123, 244, 61, 176, 199, 150, 54, 126, 0, 24, 11, 41, 177, 156, 90, 124, 76, 53, 173, 196, 21, 198, 170, 69, 113, 128, 75, 141, 186, 180, 43, 1, 190, 174, 194, 123, 20, 74, 149, 92, 210, 97, 139, 33, 2, 196, 248, 11, 67, 116, 223, 52, 57, 176, 104, 158, 27, 139, 44, 61, 210, 58, 232, 104, 29, 154, 116, 45, 104, 208, 54, 229, 116, 206, 176, 223, 11, 231, 79, 191, 5, 168, 212, 96, 62, 243, 151, 160, 47, 222, 6, 62, 200, 130, 63, 250, 14, 216, 255, 122, 37, 171, 32, 224, 31, 130, 106, 92, 73, 167, 158, 211, 247, 232, 52, 104, 97, 84, 89, 45, 3, 2, 128, 227, 202, 117, 98, 95, 230, 162, 177, 84, 58, 172, 193, 104, 88, 148, 245, 152, 44, 2, 44, 27, 137, 218, 167, 34, 42, 2, 233, 120, 232, 161, 135, 240, 253, 239, 127, 63, 237, 251, 196, 248, 51, 67, 75, 45, 40, 21, 10, 135, 192, 5, 89, 112, 65, 22, 78, 214, 1, 139, 103, 34, 97, 165, 161, 134, 169, 67, 187, 190, 51, 243, 137, 88, 22, 225, 48, 64, 85, 233, 17, 6, 48, 113, 98, 20, 179, 239, 157, 70, 135, 207, 151, 85, 254, 243, 48, 84, 24, 8, 41, 177, 134, 74, 189, 26, 80, 77, 43, 209, 179, 132, 209, 71, 241, 244, 180, 130, 107, 168, 17, 251, 178, 22, 141, 168, 251, 15, 68, 130, 201, 51, 118, 71, 198, 172, 199, 120, 17, 184, 235, 174, 187, 0, 96, 73, 17, 240, 249, 22, 23, 103, 109, 218, 180, 9, 91, 183, 166, 55, 232, 231, 159, 127, 30, 247, 221, 119, 31, 49, 254, 56, 82, 217, 186, 232, 30, 64, 54, 68, 5, 33, 186, 218, 48, 236, 28, 132, 70, 169, 134, 134, 214, 192, 168, 174, 75, 155, 194, 172, 224, 88, 176, 175, 60, 15, 254, 234, 219, 48, 49, 228, 65, 64, 213, 12, 218, 176, 1, 45, 174, 126, 80, 8, 103, 252, 189, 154, 155, 84, 192, 27, 65, 32, 207, 222, 32, 97, 37, 5, 243, 231, 175, 18, 251, 242, 21, 149, 124, 211, 97, 115, 17, 1, 173, 118, 241, 62, 15, 75, 213, 82, 16, 227, 207, 30, 89, 8, 64, 50, 129, 16, 155, 176, 218, 112, 206, 181, 196, 110, 52, 156, 31, 138, 63, 236, 134, 174, 230, 34, 184, 116, 29, 176, 87, 119, 131, 87, 170, 209, 225, 248, 0, 20, 210, 79, 5, 158, 252, 10, 135, 79, 93, 14, 216, 41, 26, 83, 175, 231, 183, 38, 48, 245, 169, 139, 17, 104, 172, 17, 251, 114, 21, 149, 232, 83, 197, 60, 97, 201, 57, 152, 156, 139, 8, 100, 75, 188, 241, 255, 229, 35, 183, 226, 178, 235, 55, 139, 125, 137, 36, 141, 44, 5, 32, 153, 76, 221, 123, 148, 97, 30, 93, 142, 247, 1, 199, 251, 89, 157, 239, 201, 175, 112, 248, 179, 203, 35, 143, 125, 211, 13, 74, 120, 6, 130, 240, 158, 206, 236, 49, 196, 227, 233, 105, 197, 244, 117, 23, 138, 125, 105, 36, 79, 178, 8, 176, 108, 100, 73, 47, 31, 226, 141, 255, 158, 199, 239, 192, 165, 159, 216, 152, 85, 130, 91, 37, 67, 202, 129, 147, 136, 55, 126, 0, 128, 66, 129, 182, 237, 12, 168, 26, 38, 235, 115, 132, 141, 70, 140, 127, 249, 134, 188, 171, 8, 43, 141, 107, 111, 190, 34, 150, 39, 112, 223, 125, 247, 225, 249, 231, 159, 207, 249, 28, 187, 118, 237, 138, 25, 255, 221, 255, 235, 118, 92, 123, 243, 21, 100, 131, 208, 44, 32, 2, 16, 199, 34, 227, 159, 199, 161, 217, 12, 207, 23, 238, 201, 250, 60, 190, 47, 126, 25, 193, 234, 242, 170, 247, 207, 68, 161, 233, 176, 215, 222, 124, 5, 238, 122, 236, 75, 0, 114, 23, 129, 93, 187, 118, 225, 225, 135, 31, 134, 66, 161, 192, 23, 254, 250, 102, 172, 219, 186, 170, 40, 27, 161, 148, 35, 68, 0, 230, 73, 103, 252, 211, 252, 197, 56, 199, 222, 138, 96, 119, 15, 184, 109, 215, 102, 60, 15, 119, 245, 53, 8, 173, 236, 46, 201, 152, 211, 53, 26, 77, 78, 205, 149, 11, 55, 124, 110, 91, 206, 34, 16, 111, 252, 247, 60, 126, 7, 62, 251, 229, 79, 203, 162, 68, 87, 42, 16, 1, 64, 102, 227, 143, 194, 221, 240, 105, 132, 140, 53, 105, 207, 19, 170, 51, 129, 251, 228, 103, 74, 54, 238, 116, 141, 70, 147, 83, 115, 229, 68, 178, 8, 236, 218, 181, 43, 237, 177, 201, 198, 31, 141, 39, 16, 178, 167, 226, 5, 32, 157, 241, 115, 218, 107, 224, 53, 62, 144, 248, 162, 74, 5, 246, 150, 91, 211, 158, 139, 253, 252, 109, 128, 74, 152, 157, 137, 178, 33, 93, 163, 209, 228, 134, 164, 114, 35, 94, 4, 30, 126, 248, 225, 148, 34, 240, 228, 147, 79, 38, 184, 253, 196, 248, 243, 163, 162, 5, 32, 147, 241, 27, 244, 6, 52, 54, 38, 166, 241, 6, 215, 172, 71, 112, 213, 234, 69, 159, 9, 174, 90, 141, 96, 247, 106, 136, 9, 69, 81, 89, 165, 230, 202, 129, 100, 17, 248, 193, 15, 126, 16, 123, 239, 153, 103, 158, 193, 19, 79, 60, 145, 224, 246, 39, 67, 92, 255, 236, 40, 139, 101, 192, 124, 200, 246, 201, 111, 208, 71, 154, 58, 78, 78, 46, 148, 82, 114, 87, 108, 131, 118, 48, 177, 101, 26, 119, 229, 182, 146, 127, 135, 84, 41, 183, 169, 82, 115, 229, 186, 12, 118, 195, 231, 34, 215, 244, 23, 223, 121, 17, 15, 62, 248, 32, 212, 106, 53, 88, 150, 197, 35, 143, 60, 146, 16, 237, 39, 100, 134, 166, 104, 52, 209, 139, 19, 170, 42, 82, 0, 210, 25, 255, 107, 254, 110, 120, 212, 159, 69, 242, 243, 36, 89, 4, 130, 107, 214, 33, 100, 170, 7, 101, 159, 6, 16, 153, 251, 7, 47, 40, 125, 190, 191, 193, 104, 88, 148, 114, 59, 61, 57, 189, 40, 53, 183, 90, 47, 79, 1, 0, 34, 34, 160, 173, 210, 224, 71, 143, 253, 18, 247, 222, 123, 47, 128, 72, 166, 225, 125, 223, 38, 25, 126, 217, 98, 164, 212, 139, 202, 128, 163, 84, 156, 0, 44, 101, 252, 223, 116, 95, 5, 184, 143, 2, 0, 62, 93, 219, 145, 240, 126, 130, 8, 40, 20, 8, 108, 190, 20, 234, 223, 71, 182, 195, 10, 92, 178, 69, 148, 53, 255, 84, 41, 183, 169, 82, 115, 229, 206, 149, 55, 110, 5, 173, 162, 241, 119, 15, 255, 4, 0, 240, 240, 223, 127, 13, 151, 108, 219, 36, 246, 176, 100, 65, 27, 179, 180, 248, 87, 148, 0, 100, 52, 254, 121, 190, 53, 158, 89, 4, 66, 221, 61, 177, 215, 67, 43, 165, 211, 239, 175, 144, 212, 92, 41, 115, 217, 245, 155, 161, 98, 104, 168, 181, 106, 244, 94, 186, 182, 240, 19, 150, 57, 26, 138, 134, 41, 133, 203, 159, 76, 197, 8, 64, 182, 198, 31, 37, 163, 8, 4, 131, 192, 124, 155, 241, 224, 178, 46, 177, 191, 94, 69, 176, 249, 106, 242, 212, 207, 134, 165, 92, 254, 100, 42, 66, 0, 114, 53, 254, 40, 153, 68, 192, 181, 124, 190, 10, 81, 153, 185, 44, 152, 64, 200, 23, 62, 16, 153, 214, 53, 54, 54, 128, 86, 209, 9, 27, 125, 232, 116, 58, 24, 141, 122, 40, 230, 155, 174, 100, 114, 249, 147, 41, 251, 101, 192, 124, 141, 63, 202, 183, 198, 143, 226, 119, 142, 177, 69, 175, 27, 244, 6, 232, 46, 189, 12, 225, 246, 44, 122, 19, 20, 25, 169, 119, 165, 37, 20, 70, 54, 59, 77, 27, 41, 117, 206, 198, 15, 228, 233, 1, 164, 83, 160, 100, 165, 18, 155, 66, 141, 63, 74, 58, 79, 160, 238, 47, 190, 0, 218, 237, 74, 88, 34, 44, 71, 254, 245, 39, 191, 22, 123, 8, 146, 228, 243, 247, 252, 247, 146, 252, 158, 76, 61, 23, 89, 199, 92, 214, 46, 127, 50, 121, 89, 233, 130, 2, 213, 193, 229, 116, 193, 233, 116, 163, 166, 214, 152, 160, 84, 98, 175, 61, 11, 101, 252, 81, 178, 9, 12, 150, 43, 175, 252, 148, 8, 64, 42, 62, 123, 231, 141, 69, 127, 224, 101, 74, 236, 234, 208, 24, 128, 150, 252, 55, 32, 205, 107, 212, 169, 186, 190, 214, 212, 26, 23, 41, 149, 88, 8, 109, 252, 81, 42, 89, 4, 128, 200, 14, 62, 132, 72, 26, 50, 128, 146, 60, 240, 210, 245, 92, 12, 186, 253, 232, 104, 104, 44, 248, 252, 130, 200, 86, 186, 20, 84, 49, 166, 1, 197, 50, 254, 40, 149, 44, 2, 79, 60, 241, 132, 216, 67, 144, 4, 81, 1, 40, 197, 3, 47, 85, 207, 197, 158, 214, 246, 188, 93, 254, 100, 242, 178, 80, 169, 166, 160, 22, 219, 248, 163, 84, 178, 8, 16, 22, 40, 197, 3, 47, 62, 177, 139, 86, 40, 209, 94, 215, 32, 152, 241, 3, 121, 174, 2, 196, 111, 53, 20, 12, 241, 145, 74, 52, 143, 31, 26, 77, 36, 241, 64, 140, 78, 44, 165, 50, 254, 40, 75, 173, 14, 24, 141, 181, 37, 253, 238, 4, 113, 72, 126, 224, 21, 131, 104, 79, 3, 131, 90, 139, 77, 93, 43, 209, 96, 200, 127, 190, 159, 138, 188, 36, 75, 138, 41, 168, 215, 94, 180, 184, 193, 103, 177, 140, 63, 74, 58, 79, 32, 28, 38, 173, 192, 42, 129, 248, 7, 94, 49, 167, 1, 109, 140, 30, 104, 45, 206, 185, 5, 243, 89, 196, 78, 65, 221, 127, 174, 25, 159, 92, 99, 137, 253, 187, 216, 198, 31, 37, 149, 8, 124, 56, 212, 135, 112, 147, 56, 49, 16, 66, 233, 40, 246, 3, 47, 219, 116, 222, 66, 40, 155, 59, 212, 187, 242, 147, 216, 51, 240, 91, 172, 107, 158, 193, 1, 170, 19, 223, 228, 74, 215, 143, 255, 91, 227, 71, 209, 74, 169, 209, 30, 160, 240, 209, 217, 147, 240, 25, 205, 128, 31, 162, 47, 133, 10, 141, 130, 52, 57, 77, 160, 181, 173, 165, 104, 15, 188, 92, 210, 121, 11, 161, 108, 4, 0, 0, 102, 87, 126, 6, 239, 205, 255, 252, 140, 54, 243, 246, 95, 249, 18, 159, 8, 21, 45, 195, 29, 60, 253, 58, 166, 77, 181, 160, 155, 104, 104, 2, 242, 47, 195, 37, 136, 71, 62, 25, 125, 249, 82, 144, 0, 84, 106, 10, 170, 20, 99, 32, 197, 228, 185, 255, 247, 116, 44, 239, 35, 20, 10, 99, 210, 54, 137, 250, 122, 19, 156, 46, 39, 76, 38, 19, 236, 118, 59, 140, 6, 99, 217, 79, 121, 138, 61, 189, 213, 64, 13, 19, 83, 252, 167, 126, 60, 229, 253, 63, 86, 66, 196, 142, 129, 20, 19, 169, 46, 251, 150, 154, 98, 62, 240, 244, 42, 3, 12, 138, 220, 54, 159, 17, 130, 178, 47, 6, 34, 20, 142, 20, 151, 125, 203, 137, 54, 70, 47, 138, 241, 3, 50, 244, 0, 114, 41, 141, 36, 8, 67, 165, 77, 121, 74, 69, 169, 2, 125, 75, 33, 59, 75, 201, 166, 52, 178, 84, 84, 106, 12, 36, 250, 61, 201, 6, 28, 249, 19, 53, 254, 169, 201, 105, 81, 199, 33, 59, 1, 72, 238, 121, 159, 174, 55, 62, 129, 32, 69, 52, 20, 13, 35, 165, 198, 220, 212, 12, 92, 46, 23, 212, 26, 226, 1, 100, 77, 54, 61, 239, 41, 226, 254, 151, 13, 229, 182, 245, 153, 145, 82, 195, 68, 107, 225, 155, 113, 1, 0, 88, 63, 7, 131, 192, 169, 189, 185, 34, 43, 107, 73, 87, 26, 233, 114, 186, 16, 10, 133, 99, 17, 106, 66, 113, 40, 245, 148, 167, 156, 182, 62, 107, 99, 244, 240, 205, 184, 34, 129, 212, 249, 13, 75, 131, 193, 72, 96, 213, 106, 17, 174, 120, 44, 215, 115, 201, 42, 8, 152, 170, 52, 178, 190, 177, 126, 81, 111, 124, 66, 121, 32, 245, 190, 19, 217, 16, 159, 206, 219, 208, 88, 47, 246, 112, 22, 33, 43, 15, 32, 26, 121, 142, 223, 250, 57, 26, 161, 6, 0, 147, 201, 4, 138, 34, 233, 170, 229, 138, 220, 182, 62, 139, 186, 252, 165, 32, 57, 152, 152, 109, 112, 81, 86, 30, 64, 57, 39, 219, 16, 22, 35, 231, 4, 164, 82, 166, 243, 2, 128, 90, 195, 192, 229, 138, 196, 22, 114, 9, 46, 202, 202, 3, 32, 84, 22, 114, 76, 64, 202, 183, 59, 111, 193, 215, 202, 96, 0, 235, 231, 0, 228, 22, 92, 148, 149, 7, 64, 168, 44, 228, 150, 128, 36, 86, 98, 79, 114, 224, 47, 26, 92, 4, 16, 11, 160, 166, 67, 150, 2, 80, 201, 9, 56, 149, 142, 20, 167, 129, 233, 118, 222, 45, 21, 153, 140, 124, 201, 177, 139, 54, 106, 2, 161, 12, 144, 66, 58, 111, 33, 16, 1, 32, 16, 242, 68, 140, 185, 190, 208, 16, 1, 32, 72, 30, 169, 77, 249, 74, 209, 170, 171, 84, 144, 85, 0, 153, 144, 156, 254, 154, 46, 77, 150, 80, 92, 74, 185, 182, 95, 10, 136, 0, 200, 4, 41, 85, 65, 86, 42, 26, 138, 150, 245, 124, 63, 21, 68, 0, 100, 2, 169, 130, 20, 31, 127, 136, 199, 4, 231, 134, 157, 151, 79, 13, 66, 38, 72, 12, 64, 6, 144, 42, 72, 105, 17, 21, 2, 64, 254, 241, 0, 34, 0, 50, 96, 169, 42, 200, 248, 52, 89, 66, 233, 137, 23, 3, 154, 162, 81, 5, 165, 172, 166, 9, 228, 177, 33, 3, 82, 165, 191, 166, 74, 147, 37, 136, 11, 31, 226, 225, 12, 177, 152, 224, 220, 152, 224, 220, 152, 227, 57, 184, 61, 156, 216, 195, 90, 18, 193, 61, 128, 116, 61, 250, 146, 123, 249, 17, 178, 39, 85, 250, 107, 170, 52, 89, 130, 180, 112, 134, 88, 152, 39, 45, 104, 110, 105, 70, 45, 173, 65, 53, 205, 96, 142, 231, 36, 229, 33, 8, 238, 1, 148, 83, 19, 7, 169, 64, 250, 239, 201, 31, 103, 136, 197, 152, 223, 133, 211, 19, 163, 176, 115, 28, 166, 230, 43, 247, 196, 70, 112, 1, 72, 23, 157, 78, 142, 98, 151, 3, 229, 214, 178, 138, 32, 60, 169, 58, 86, 77, 187, 167, 49, 19, 152, 195, 4, 231, 198, 148, 203, 37, 234, 52, 161, 232, 49, 0, 185, 53, 113, 200, 5, 226, 237, 16, 50, 145, 169, 164, 217, 171, 12, 98, 216, 62, 30, 139, 25, 148, 26, 193, 5, 32, 149, 226, 165, 138, 98, 151, 3, 165, 246, 118, 164, 150, 18, 75, 200, 76, 170, 142, 85, 169, 58, 91, 1, 88, 20, 64, 44, 201, 248, 132, 62, 161, 28, 155, 56, 8, 69, 57, 123, 59, 4, 225, 200, 38, 166, 147, 73, 12, 132, 106, 36, 42, 120, 56, 94, 110, 77, 28, 10, 65, 206, 45, 171, 42, 25, 185, 237, 46, 229, 12, 177, 112, 114, 44, 128, 133, 196, 35, 93, 149, 78, 144, 115, 151, 228, 91, 150, 107, 20, 187, 146, 189, 29, 57, 35, 231, 186, 138, 104, 226, 145, 91, 163, 128, 157, 247, 21, 60, 85, 144, 142, 204, 201, 144, 92, 230, 119, 149, 140, 212, 42, 25, 197, 168, 171, 40, 70, 252, 198, 31, 151, 120, 100, 203, 83, 12, 72, 70, 142, 192, 72, 177, 101, 149, 216, 196, 63, 113, 171, 85, 250, 184, 39, 110, 29, 92, 78, 23, 156, 78, 55, 106, 106, 141, 37, 25, 75, 185, 214, 85, 240, 33, 30, 78, 240, 177, 169, 66, 192, 233, 67, 87, 67, 99, 198, 207, 21, 237, 155, 146, 136, 53, 33, 138, 148, 42, 25, 43, 101, 119, 169, 169, 185, 89, 28, 57, 59, 136, 35, 103, 7, 49, 108, 49, 167, 77, 60, 146, 159, 212, 17, 100, 133, 212, 158, 184, 149, 82, 87, 17, 31, 107, 211, 154, 244, 224, 52, 10, 216, 82, 148, 49, 147, 41, 128, 0, 16, 111, 39, 61, 82, 171, 100, 172, 228, 186, 10, 62, 196, 199, 226, 4, 209, 122, 4, 226, 1, 16, 138, 138, 212, 158, 184, 229, 186, 34, 149, 45, 206, 16, 11, 235, 212, 194, 182, 97, 196, 3, 32, 20, 149, 74, 126, 226, 74, 129, 84, 2, 167, 53, 45, 228, 165, 16, 1, 32, 20, 21, 178, 42, 34, 77, 38, 56, 55, 218, 24, 61, 254, 63, 91, 213, 67, 145, 233, 13, 169, 197, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130]; diff --git a/test/tools/tile_server/static/generated/land.dart b/test/tools/tile_server/static/generated/land.dart new file mode 100644 index 00000000..b271f0eb --- /dev/null +++ b/test/tools/tile_server/static/generated/land.dart @@ -0,0 +1 @@ +final landTileBytes = [137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 1, 0, 0, 0, 1, 0, 8, 3, 0, 0, 0, 107, 172, 88, 84, 0, 0, 3, 0, 80, 76, 84, 69, 51, 52, 49, 72, 52, 55, 60, 65, 58, 76, 85, 24, 157, 17, 17, 83, 80, 48, 162, 28, 28, 73, 74, 72, 82, 75, 68, 78, 83, 73, 92, 100, 43, 83, 86, 71, 86, 87, 84, 169, 46, 46, 108, 79, 76, 124, 90, 55, 104, 112, 57, 89, 109, 86, 129, 96, 61, 102, 108, 85, 113, 94, 97, 131, 89, 89, 134, 105, 70, 104, 105, 102, 94, 131, 93, 104, 115, 103, 121, 128, 77, 139, 112, 78, 141, 89, 99, 141, 101, 88, 113, 117, 107, 182, 78, 78, 142, 116, 84, 117, 117, 116, 149, 107, 95, 146, 121, 90, 151, 150, 59, 117, 141, 112, 147, 111, 113, 128, 134, 111, 138, 144, 96, 148, 150, 83, 130, 132, 125, 168, 127, 94, 154, 135, 104, 177, 114, 110, 129, 155, 119, 177, 153, 75, 136, 137, 133, 141, 168, 98, 147, 153, 107, 150, 171, 87, 156, 139, 114, 211, 118, 88, 153, 166, 102, 157, 149, 115, 165, 165, 91, 135, 165, 124, 125, 185, 115, 202, 111, 113, 152, 137, 138, 141, 150, 137, 134, 167, 129, 218, 89, 126, 224, 83, 126, 206, 146, 84, 142, 189, 107, 166, 171, 101, 172, 150, 116, 150, 150, 139, 153, 171, 115, 156, 180, 104, 132, 189, 120, 153, 139, 150, 221, 92, 129, 182, 122, 144, 224, 93, 131, 161, 165, 123, 210, 110, 130, 168, 179, 105, 140, 184, 131, 149, 169, 137, 180, 173, 102, 206, 141, 108, 206, 164, 86, 152, 187, 120, 155, 155, 151, 142, 195, 125, 147, 182, 133, 149, 178, 137, 142, 194, 129, 168, 154, 145, 149, 184, 135, 182, 183, 104, 157, 182, 131, 183, 166, 121, 172, 165, 134, 168, 184, 120, 151, 185, 137, 154, 179, 141, 157, 165, 153, 147, 197, 133, 157, 187, 134, 228, 110, 142, 148, 205, 128, 154, 189, 139, 150, 197, 137, 155, 196, 133, 155, 184, 147, 172, 146, 168, 181, 168, 139, 184, 186, 118, 228, 142, 118, 170, 189, 130, 204, 173, 112, 165, 169, 156, 157, 192, 142, 153, 206, 133, 208, 143, 141, 186, 195, 115, 155, 201, 141, 156, 209, 135, 158, 198, 145, 168, 168, 165, 161, 196, 145, 182, 172, 148, 169, 196, 138, 182, 186, 135, 168, 184, 157, 163, 204, 149, 167, 193, 156, 172, 199, 145, 182, 183, 151, 163, 213, 141, 171, 182, 166, 177, 172, 170, 195, 202, 122, 188, 197, 136, 231, 141, 149, 172, 203, 148, 203, 154, 166, 167, 205, 152, 171, 196, 157, 204, 179, 141, 184, 167, 179, 195, 200, 136, 171, 205, 156, 180, 199, 154, 198, 173, 163, 228, 186, 120, 185, 183, 167, 172, 202, 163, 173, 209, 158, 232, 146, 162, 175, 210, 161, 201, 210, 136, 181, 214, 155, 197, 186, 167, 212, 199, 139, 182, 202, 168, 218, 168, 166, 178, 212, 163, 198, 201, 155, 205, 169, 180, 187, 186, 183, 182, 216, 164, 213, 184, 165, 182, 213, 169, 187, 212, 165, 196, 216, 153, 198, 198, 169, 210, 215, 140, 186, 199, 181, 214, 200, 152, 184, 225, 158, 194, 189, 187, 186, 219, 166, 187, 214, 171, 209, 172, 191, 188, 219, 171, 248, 177, 155, 190, 225, 166, 189, 216, 178, 197, 201, 185, 215, 220, 148, 199, 215, 171, 214, 202, 169, 215, 187, 183, 191, 206, 193, 241, 203, 146, 198, 197, 196, 235, 181, 178, 215, 199, 182, 216, 212, 170, 226, 202, 170, 198, 228, 173, 199, 218, 183, 221, 226, 155, 225, 215, 164, 224, 229, 159, 210, 204, 200, 205, 234, 176, 214, 216, 185, 248, 193, 174, 204, 216, 197, 232, 201, 184, 236, 188, 197, 245, 197, 183, 204, 226, 196, 210, 230, 187, 229, 233, 166, 213, 217, 200, 252, 214, 164, 233, 200, 199, 228, 218, 187, 215, 228, 201, 244, 200, 202, 231, 218, 198, 232, 231, 184, 217, 217, 215, 245, 220, 185, 217, 240, 195, 237, 241, 178, 244, 202, 210, 249, 230, 178, 226, 221, 219, 229, 233, 204, 228, 227, 212, 220, 234, 214, 252, 215, 204, 228, 226, 220, 229, 234, 211, 232, 229, 213, 245, 229, 201, 244, 216, 219, 227, 236, 219, 235, 238, 211, 237, 228, 219, 247, 250, 191, 234, 234, 222, 247, 249, 194, 238, 240, 213, 245, 233, 214, 232, 231, 230, 248, 219, 226, 241, 242, 214, 234, 243, 229, 243, 238, 233, 243, 243, 242, 246, 247, 235, 251, 238, 241, 244, 249, 241, 251, 243, 244, 251, 251, 245, 253, 246, 248, 254, 254, 254, 65, 248, 164, 27, 0, 0, 69, 188, 73, 68, 65, 84, 120, 156, 205, 125, 15, 96, 84, 213, 153, 239, 172, 221, 87, 197, 133, 85, 233, 43, 20, 125, 75, 228, 165, 246, 45, 44, 42, 187, 90, 153, 74, 89, 183, 27, 144, 170, 200, 172, 65, 134, 171, 109, 22, 162, 99, 145, 170, 132, 56, 188, 68, 95, 32, 140, 153, 104, 128, 148, 65, 111, 90, 184, 25, 192, 100, 220, 164, 3, 17, 155, 228, 133, 204, 52, 74, 131, 118, 174, 175, 51, 105, 74, 39, 206, 194, 213, 55, 187, 242, 38, 25, 34, 75, 130, 60, 54, 144, 73, 128, 48, 239, 59, 231, 220, 255, 127, 102, 238, 4, 218, 183, 159, 114, 51, 127, 238, 189, 115, 207, 239, 124, 255, 207, 119, 206, 177, 48, 255, 81, 201, 251, 222, 219, 231, 3, 44, 165, 250, 180, 189, 203, 221, 37, 80, 40, 210, 204, 48, 161, 72, 22, 234, 234, 98, 51, 125, 109, 249, 255, 209, 54, 83, 228, 27, 126, 121, 248, 124, 31, 165, 250, 244, 0, 0, 80, 39, 32, 16, 241, 121, 59, 100, 109, 209, 197, 130, 237, 202, 12, 209, 127, 92, 0, 152, 254, 143, 62, 58, 207, 246, 177, 170, 79, 235, 220, 117, 117, 18, 15, 48, 2, 0, 29, 29, 161, 102, 166, 57, 11, 51, 132, 116, 152, 225, 15, 11, 192, 214, 173, 215, 112, 113, 223, 240, 203, 231, 207, 159, 239, 87, 125, 10, 0, 144, 198, 151, 188, 3, 44, 32, 244, 110, 136, 124, 153, 165, 249, 136, 105, 254, 184, 0, 60, 249, 164, 222, 167, 59, 169, 157, 12, 189, 158, 218, 68, 103, 185, 186, 239, 237, 207, 0, 1, 37, 11, 248, 170, 221, 64, 34, 0, 2, 117, 162, 239, 218, 12, 153, 29, 244, 64, 40, 210, 245, 199, 7, 160, 228, 123, 223, 211, 99, 129, 205, 235, 55, 51, 155, 54, 209, 155, 54, 101, 187, 190, 107, 23, 0, 192, 41, 63, 34, 4, 175, 182, 111, 197, 210, 221, 73, 90, 29, 242, 42, 212, 129, 170, 249, 2, 233, 168, 195, 220, 1, 48, 219, 127, 12, 67, 127, 15, 72, 231, 180, 103, 118, 62, 195, 80, 52, 67, 83, 89, 239, 128, 100, 192, 175, 248, 4, 183, 227, 0, 126, 89, 72, 90, 192, 243, 125, 200, 64, 5, 242, 172, 207, 155, 141, 235, 1, 128, 233, 254, 99, 94, 70, 0, 172, 209, 124, 188, 115, 61, 179, 126, 39, 2, 224, 135, 89, 239, 240, 214, 137, 243, 195, 202, 79, 14, 64, 227, 125, 228, 101, 17, 105, 65, 155, 172, 53, 29, 62, 21, 27, 32, 166, 103, 69, 0, 16, 78, 254, 16, 2, 171, 243, 26, 0, 48, 221, 127, 117, 223, 195, 84, 167, 254, 124, 243, 102, 248, 223, 28, 132, 125, 39, 52, 74, 80, 162, 183, 72, 135, 50, 62, 190, 41, 240, 182, 67, 173, 6, 112, 163, 101, 237, 239, 96, 188, 33, 172, 50, 67, 147, 6, 192, 124, 255, 189, 76, 0, 120, 89, 253, 249, 51, 59, 25, 192, 112, 61, 181, 62, 187, 16, 185, 183, 114, 237, 134, 95, 242, 109, 16, 94, 132, 186, 180, 82, 64, 90, 205, 226, 63, 232, 75, 220, 114, 130, 192, 164, 1, 200, 161, 255, 12, 136, 66, 4, 127, 76, 156, 234, 182, 101, 254, 190, 77, 232, 205, 16, 81, 242, 42, 4, 26, 186, 176, 18, 136, 72, 186, 31, 97, 212, 6, 151, 25, 0, 96, 70, 191, 229, 208, 127, 134, 68, 137, 135, 108, 100, 205, 248, 109, 41, 121, 134, 230, 136, 92, 202, 229, 12, 32, 189, 148, 65, 131, 125, 6, 125, 17, 48, 163, 223, 114, 232, 63, 227, 123, 136, 135, 108, 100, 205, 248, 109, 41, 207, 32, 146, 157, 211, 218, 121, 177, 209, 242, 246, 203, 92, 70, 5, 0, 230, 244, 155, 249, 199, 191, 246, 59, 88, 51, 243, 89, 129, 213, 90, 80, 169, 0, 64, 31, 1, 212, 104, 252, 167, 185, 141, 92, 168, 15, 128, 73, 253, 102, 254, 241, 175, 157, 10, 178, 10, 90, 45, 252, 123, 167, 48, 147, 169, 15, 225, 70, 135, 34, 157, 240, 183, 51, 4, 212, 44, 247, 25, 45, 178, 95, 48, 169, 223, 40, 241, 240, 135, 39, 171, 153, 147, 192, 45, 222, 110, 196, 0, 157, 200, 76, 32, 10, 33, 46, 208, 129, 199, 82, 108, 173, 21, 110, 116, 61, 244, 219, 117, 38, 107, 246, 83, 218, 161, 217, 219, 183, 122, 15, 160, 238, 15, 225, 22, 203, 136, 102, 58, 59, 165, 83, 51, 122, 130, 182, 210, 235, 162, 223, 152, 118, 150, 5, 211, 237, 13, 4, 192, 99, 99, 217, 128, 247, 90, 238, 85, 106, 45, 206, 126, 18, 106, 121, 73, 29, 227, 37, 221, 175, 108, 101, 243, 51, 242, 51, 59, 117, 218, 47, 1, 64, 151, 50, 215, 131, 187, 163, 189, 107, 31, 239, 101, 253, 201, 234, 96, 192, 159, 220, 178, 176, 58, 57, 105, 4, 104, 155, 181, 88, 135, 21, 137, 153, 70, 6, 91, 32, 64, 160, 208, 203, 248, 48, 7, 224, 238, 150, 26, 186, 158, 146, 95, 170, 27, 45, 40, 29, 33, 138, 161, 233, 201, 3, 0, 79, 229, 227, 166, 239, 222, 61, 125, 40, 57, 127, 250, 150, 64, 244, 254, 37, 193, 252, 106, 150, 241, 70, 147, 28, 23, 141, 114, 81, 127, 246, 91, 200, 168, 184, 82, 247, 99, 162, 163, 144, 193, 22, 9, 68, 192, 139, 25, 1, 71, 137, 216, 61, 2, 213, 215, 129, 253, 35, 249, 165, 122, 237, 215, 0, 224, 182, 150, 82, 57, 61, 167, 140, 224, 169, 252, 189, 51, 146, 220, 244, 36, 87, 189, 4, 0, 200, 111, 73, 174, 93, 203, 50, 129, 150, 45, 243, 243, 91, 214, 222, 185, 112, 18, 220, 80, 170, 249, 132, 152, 105, 100, 176, 5, 106, 199, 1, 162, 20, 38, 162, 216, 32, 180, 30, 196, 48, 18, 17, 52, 32, 33, 125, 0, 100, 249, 6, 110, 120, 184, 159, 251, 66, 157, 131, 50, 79, 232, 169, 184, 181, 119, 222, 185, 133, 11, 36, 1, 128, 0, 59, 99, 126, 126, 210, 207, 4, 170, 111, 225, 122, 111, 169, 78, 230, 239, 15, 152, 189, 147, 104, 253, 172, 154, 175, 176, 153, 198, 6, 155, 188, 63, 32, 185, 0, 40, 108, 240, 117, 18, 86, 223, 236, 37, 127, 101, 87, 234, 39, 204, 44, 77, 193, 6, 254, 215, 56, 8, 190, 207, 15, 247, 247, 225, 12, 68, 45, 147, 51, 161, 167, 98, 98, 249, 107, 231, 207, 79, 122, 89, 196, 1, 213, 249, 107, 103, 176, 1, 0, 96, 126, 52, 121, 75, 18, 125, 100, 124, 177, 66, 180, 109, 98, 199, 23, 184, 213, 39, 98, 17, 192, 6, 91, 209, 126, 146, 34, 32, 226, 143, 101, 61, 68, 108, 189, 236, 74, 221, 246, 71, 44, 233, 177, 193, 112, 119, 211, 27, 78, 87, 67, 79, 63, 70, 0, 254, 231, 250, 251, 75, 144, 4, 114, 95, 244, 247, 153, 7, 0, 61, 149, 127, 203, 66, 142, 203, 111, 105, 71, 173, 77, 78, 79, 70, 91, 242, 163, 0, 192, 253, 4, 128, 13, 122, 0, 180, 17, 63, 69, 71, 180, 17, 213, 22, 104, 46, 64, 102, 154, 24, 108, 68, 72, 247, 117, 109, 23, 66, 238, 16, 31, 0, 71, 36, 231, 223, 199, 72, 223, 105, 41, 196, 88, 210, 60, 141, 141, 196, 187, 91, 155, 90, 99, 95, 16, 20, 254, 237, 215, 61, 93, 125, 232, 149, 121, 0, 208, 83, 121, 246, 223, 25, 139, 205, 232, 37, 0, 204, 8, 38, 55, 220, 159, 17, 128, 102, 146, 209, 234, 208, 136, 182, 91, 96, 95, 29, 95, 152, 18, 3, 18, 76, 7, 186, 14, 128, 190, 236, 192, 74, 47, 34, 37, 62, 24, 62, 81, 210, 145, 9, 0, 144, 23, 17, 0, 158, 70, 194, 173, 77, 65, 238, 95, 48, 253, 27, 206, 200, 177, 156, 73, 237, 141, 30, 234, 135, 220, 134, 59, 243, 119, 195, 69, 213, 45, 1, 182, 119, 126, 254, 66, 136, 231, 3, 193, 45, 209, 228, 253, 73, 244, 145, 230, 26, 145, 83, 149, 162, 29, 217, 94, 216, 73, 60, 248, 74, 45, 11, 80, 226, 65, 160, 210, 82, 194, 223, 66, 242, 11, 249, 67, 56, 72, 6, 125, 216, 44, 156, 228, 211, 34, 128, 140, 166, 165, 169, 169, 181, 59, 62, 56, 38, 7, 97, 44, 222, 221, 180, 255, 67, 64, 96, 24, 84, 2, 128, 96, 90, 12, 224, 169, 188, 44, 27, 101, 193, 42, 71, 163, 94, 38, 64, 76, 159, 55, 26, 245, 7, 162, 1, 252, 145, 154, 80, 122, 2, 39, 40, 68, 209, 166, 193, 105, 7, 117, 21, 42, 122, 139, 176, 111, 177, 198, 23, 210, 2, 80, 89, 204, 200, 219, 143, 88, 0, 161, 8, 183, 13, 69, 100, 134, 48, 18, 217, 91, 94, 4, 196, 39, 80, 246, 21, 35, 85, 99, 65, 28, 223, 31, 11, 54, 53, 53, 117, 135, 19, 50, 28, 6, 187, 155, 130, 88, 41, 156, 83, 229, 101, 51, 3, 144, 163, 31, 229, 139, 132, 182, 17, 65, 229, 69, 27, 245, 19, 60, 35, 124, 246, 86, 97, 209, 65, 116, 74, 129, 218, 31, 208, 254, 74, 109, 1, 86, 126, 56, 249, 215, 215, 39, 1, 0, 93, 28, 145, 123, 194, 161, 131, 251, 246, 49, 109, 40, 32, 58, 88, 96, 45, 176, 149, 214, 10, 0, 240, 244, 5, 224, 208, 26, 30, 145, 56, 33, 188, 27, 84, 226, 224, 224, 192, 31, 14, 0, 232, 161, 78, 94, 99, 53, 135, 58, 55, 131, 96, 163, 134, 176, 7, 139, 10, 139, 138, 238, 141, 188, 5, 158, 76, 136, 214, 250, 2, 26, 178, 34, 65, 98, 187, 160, 237, 125, 8, 6, 22, 0, 232, 68, 247, 109, 150, 78, 169, 180, 226, 63, 26, 71, 82, 14, 0, 143, 66, 107, 83, 119, 156, 103, 133, 177, 166, 216, 96, 34, 145, 200, 165, 73, 57, 145, 32, 140, 44, 159, 176, 15, 81, 50, 99, 181, 119, 27, 249, 107, 226, 62, 52, 186, 21, 106, 124, 159, 144, 7, 12, 225, 192, 72, 58, 195, 205, 107, 83, 141, 181, 209, 0, 128, 137, 3, 86, 136, 99, 8, 226, 77, 241, 68, 162, 183, 183, 247, 122, 55, 157, 144, 198, 42, 237, 227, 95, 224, 70, 188, 69, 94, 155, 138, 77, 155, 241, 37, 125, 17, 1, 0, 232, 124, 159, 252, 251, 2, 222, 177, 209, 56, 146, 250, 0, 96, 16, 118, 199, 137, 28, 116, 55, 133, 19, 137, 201, 251, 135, 153, 40, 20, 209, 27, 211, 69, 236, 12, 13, 225, 185, 162, 211, 106, 230, 78, 116, 9, 18, 127, 116, 241, 129, 3, 237, 10, 213, 71, 168, 152, 23, 36, 181, 35, 153, 1, 0, 208, 141, 77, 9, 34, 9, 97, 64, 32, 102, 156, 159, 158, 52, 225, 182, 31, 44, 58, 168, 104, 126, 51, 207, 24, 88, 44, 74, 66, 33, 171, 57, 167, 180, 212, 250, 14, 200, 128, 156, 235, 49, 85, 90, 201, 245, 2, 0, 42, 71, 18, 0, 248, 232, 68, 191, 49, 4, 177, 38, 162, 12, 154, 64, 17, 196, 175, 83, 171, 101, 132, 1, 104, 59, 136, 212, 85, 73, 209, 54, 192, 1, 84, 116, 179, 194, 99, 57, 88, 104, 178, 253, 64, 37, 96, 54, 66, 29, 170, 15, 75, 233, 90, 171, 27, 132, 136, 22, 4, 73, 233, 72, 34, 0, 222, 219, 245, 182, 49, 2, 195, 193, 166, 110, 48, 11, 35, 131, 35, 32, 5, 62, 36, 85, 62, 63, 24, 116, 54, 10, 1, 174, 233, 208, 198, 144, 58, 121, 111, 132, 112, 194, 182, 194, 18, 162, 181, 197, 140, 93, 91, 199, 190, 44, 89, 81, 37, 161, 203, 107, 53, 81, 180, 91, 25, 80, 80, 74, 71, 82, 16, 129, 247, 79, 160, 227, 197, 241, 139, 106, 8, 6, 19, 77, 233, 244, 68, 247, 216, 96, 34, 57, 62, 26, 101, 216, 36, 208, 145, 36, 215, 59, 122, 36, 153, 91, 124, 175, 247, 192, 168, 149, 168, 207, 218, 58, 145, 75, 28, 58, 88, 200, 251, 125, 68, 248, 153, 230, 182, 156, 218, 143, 137, 182, 89, 69, 187, 169, 137, 164, 16, 81, 226, 1, 147, 229, 226, 197, 243, 23, 225, 95, 255, 123, 39, 224, 207, 120, 10, 35, 112, 17, 17, 249, 11, 0, 0, 11, 196, 111, 28, 27, 25, 28, 111, 170, 72, 122, 147, 111, 44, 29, 29, 154, 51, 111, 74, 172, 247, 198, 100, 192, 239, 103, 188, 237, 237, 192, 23, 126, 95, 123, 187, 215, 215, 158, 43, 34, 161, 136, 74, 91, 249, 112, 123, 139, 109, 194, 128, 103, 200, 154, 227, 29, 17, 209, 197, 0, 65, 177, 21, 72, 215, 131, 160, 196, 3, 38, 203, 248, 229, 203, 227, 64, 163, 240, 239, 242, 68, 236, 107, 227, 227, 169, 243, 227, 132, 46, 226, 227, 185, 145, 49, 0, 96, 10, 2, 192, 62, 111, 212, 155, 180, 207, 74, 14, 37, 23, 28, 26, 5, 0, 4, 138, 114, 226, 203, 220, 52, 101, 155, 86, 91, 99, 170, 180, 150, 16, 41, 216, 167, 141, 4, 76, 81, 113, 109, 169, 33, 235, 80, 226, 1, 147, 101, 106, 124, 100, 206, 212, 121, 169, 137, 214, 169, 83, 231, 164, 166, 90, 166, 110, 249, 172, 127, 188, 162, 102, 230, 84, 251, 248, 229, 209, 5, 83, 103, 198, 198, 198, 154, 102, 206, 116, 76, 77, 97, 0, 146, 8, 128, 209, 161, 197, 83, 166, 54, 133, 111, 76, 114, 179, 122, 47, 191, 49, 117, 202, 210, 161, 209, 197, 111, 220, 54, 245, 13, 199, 212, 219, 142, 36, 115, 123, 208, 78, 159, 209, 55, 5, 133, 160, 19, 67, 238, 220, 243, 211, 149, 54, 51, 153, 84, 137, 44, 137, 137, 169, 21, 227, 11, 230, 77, 88, 6, 199, 195, 19, 77, 51, 71, 199, 83, 195, 227, 11, 110, 236, 233, 185, 113, 247, 196, 236, 5, 163, 77, 55, 164, 6, 45, 241, 145, 153, 83, 199, 70, 18, 99, 0, 192, 102, 4, 128, 125, 74, 111, 111, 18, 56, 96, 170, 99, 60, 120, 195, 145, 222, 169, 246, 203, 51, 167, 198, 14, 89, 230, 113, 139, 103, 94, 187, 94, 16, 136, 182, 21, 148, 228, 156, 149, 1, 5, 96, 211, 79, 37, 26, 146, 229, 242, 136, 165, 166, 102, 233, 141, 19, 51, 231, 196, 198, 1, 128, 241, 241, 243, 231, 199, 23, 44, 24, 31, 119, 206, 185, 106, 73, 141, 95, 158, 218, 84, 51, 123, 34, 213, 52, 21, 148, 32, 0, 48, 190, 153, 179, 207, 26, 155, 234, 76, 70, 135, 130, 83, 110, 91, 156, 28, 157, 183, 32, 153, 124, 99, 234, 229, 153, 246, 100, 210, 210, 155, 60, 2, 122, 33, 215, 103, 206, 64, 149, 57, 117, 37, 34, 58, 199, 214, 51, 72, 7, 196, 45, 118, 187, 189, 98, 226, 114, 197, 148, 217, 8, 128, 139, 8, 128, 165, 227, 227, 53, 51, 199, 44, 169, 212, 216, 204, 154, 138, 121, 169, 241, 238, 169, 19, 9, 12, 128, 47, 186, 116, 246, 229, 41, 111, 112, 126, 238, 144, 101, 234, 204, 161, 209, 217, 75, 147, 209, 67, 83, 46, 207, 116, 2, 0, 156, 22, 0, 109, 149, 152, 54, 171, 109, 76, 181, 86, 116, 212, 198, 195, 122, 167, 150, 22, 23, 152, 8, 154, 116, 200, 50, 62, 110, 73, 140, 143, 79, 76, 128, 42, 188, 49, 222, 10, 28, 48, 14, 0, 192, 159, 121, 11, 38, 110, 104, 253, 108, 228, 134, 120, 247, 148, 241, 241, 138, 169, 99, 111, 15, 142, 119, 223, 56, 62, 196, 205, 92, 124, 121, 22, 116, 59, 82, 130, 192, 251, 75, 103, 37, 147, 139, 103, 27, 2, 160, 173, 18, 51, 72, 125, 233, 19, 180, 189, 22, 73, 116, 150, 42, 1, 56, 169, 160, 184, 116, 18, 89, 76, 68, 150, 209, 241, 138, 27, 230, 205, 89, 60, 49, 117, 193, 188, 41, 227, 35, 55, 204, 11, 34, 0, 166, 204, 158, 115, 195, 224, 68, 211, 141, 243, 166, 46, 72, 143, 205, 156, 57, 111, 230, 212, 212, 137, 183, 71, 199, 231, 220, 56, 107, 202, 84, 110, 232, 200, 13, 179, 102, 85, 128, 18, 236, 189, 225, 200, 232, 212, 153, 179, 111, 236, 29, 55, 0, 64, 167, 74, 76, 155, 213, 206, 68, 5, 196, 145, 213, 166, 70, 175, 31, 89, 46, 142, 142, 159, 107, 13, 143, 79, 12, 182, 118, 131, 69, 60, 215, 122, 14, 1, 96, 79, 4, 83, 99, 35, 169, 17, 112, 1, 70, 82, 151, 195, 221, 169, 196, 216, 197, 247, 222, 27, 226, 130, 111, 28, 226, 184, 246, 100, 236, 141, 221, 67, 224, 8, 37, 123, 123, 199, 135, 14, 29, 74, 14, 13, 245, 130, 33, 12, 98, 15, 73, 174, 4, 245, 170, 196, 52, 193, 72, 6, 18, 235, 53, 42, 179, 177, 192, 53, 144, 5, 185, 127, 96, 252, 241, 1, 191, 188, 136, 0, 24, 3, 7, 32, 145, 72, 167, 6, 39, 38, 226, 9, 64, 34, 61, 242, 197, 249, 247, 134, 3, 1, 46, 186, 231, 135, 212, 250, 6, 52, 210, 19, 13, 180, 195, 63, 22, 94, 250, 2, 209, 168, 159, 141, 178, 62, 86, 233, 30, 235, 85, 137, 105, 130, 145, 12, 36, 213, 107, 88, 179, 156, 233, 214, 40, 191, 206, 72, 200, 199, 168, 3, 3, 61, 210, 139, 6, 199, 237, 21, 99, 241, 88, 60, 145, 30, 139, 39, 194, 40, 26, 2, 0, 18, 195, 40, 66, 166, 208, 37, 148, 137, 219, 34, 210, 175, 18, 83, 7, 35, 25, 72, 170, 215, 144, 123, 196, 109, 40, 213, 161, 106, 26, 109, 85, 9, 9, 142, 179, 155, 15, 32, 125, 187, 41, 196, 71, 24, 250, 169, 21, 21, 0, 195, 3, 177, 88, 124, 100, 12, 8, 103, 136, 33, 8, 12, 163, 236, 208, 216, 96, 12, 127, 75, 161, 75, 168, 236, 143, 142, 201, 160, 74, 140, 82, 6, 35, 198, 36, 171, 215, 40, 149, 201, 0, 9, 33, 85, 231, 170, 198, 15, 58, 248, 188, 194, 230, 77, 128, 215, 62, 226, 109, 25, 228, 150, 100, 0, 160, 182, 139, 233, 225, 177, 68, 188, 59, 28, 135, 246, 67, 44, 24, 15, 115, 95, 144, 83, 230, 210, 140, 33, 0, 102, 43, 72, 41, 241, 160, 71, 224, 202, 88, 173, 164, 185, 242, 122, 13, 137, 5, 240, 136, 159, 214, 131, 182, 202, 223, 240, 209, 100, 200, 231, 163, 124, 161, 200, 62, 156, 36, 208, 178, 13, 33, 9, 128, 47, 112, 246, 99, 108, 48, 30, 14, 3, 223, 119, 135, 19, 19, 233, 120, 119, 247, 88, 122, 36, 54, 44, 242, 71, 101, 134, 232, 204, 108, 5, 41, 37, 30, 244, 8, 141, 9, 251, 136, 212, 200, 235, 53, 100, 67, 197, 161, 236, 0, 136, 169, 37, 250, 135, 62, 146, 115, 13, 133, 12, 24, 128, 177, 12, 15, 15, 247, 247, 115, 125, 129, 190, 225, 112, 28, 218, 30, 71, 169, 143, 65, 194, 4, 152, 27, 226, 3, 50, 1, 97, 50, 32, 96, 182, 130, 148, 18, 15, 70, 228, 109, 14, 133, 188, 110, 69, 189, 134, 77, 248, 89, 60, 144, 20, 210, 14, 48, 40, 68, 64, 224, 128, 72, 27, 10, 43, 153, 205, 252, 128, 185, 238, 143, 137, 195, 227, 253, 56, 246, 199, 20, 31, 155, 136, 143, 196, 195, 241, 48, 202, 134, 13, 159, 151, 3, 192, 184, 173, 6, 222, 166, 32, 177, 126, 20, 26, 70, 51, 181, 207, 152, 64, 147, 241, 18, 208, 220, 108, 179, 233, 98, 213, 28, 49, 96, 101, 57, 181, 225, 124, 120, 243, 250, 245, 235, 105, 186, 13, 177, 81, 7, 202, 51, 234, 159, 43, 2, 208, 215, 47, 2, 144, 136, 167, 227, 152, 19, 128, 255, 101, 253, 127, 30, 143, 17, 25, 165, 233, 5, 137, 221, 19, 204, 223, 176, 144, 31, 74, 241, 27, 6, 123, 186, 84, 202, 119, 99, 41, 86, 233, 168, 102, 7, 253, 165, 148, 39, 241, 93, 25, 242, 9, 7, 61, 34, 3, 174, 166, 244, 173, 8, 192, 230, 205, 125, 220, 224, 0, 199, 245, 245, 69, 184, 240, 68, 10, 1, 16, 158, 72, 135, 85, 70, 130, 156, 75, 235, 120, 102, 162, 196, 38, 91, 150, 36, 73, 14, 153, 141, 113, 185, 32, 192, 15, 93, 136, 196, 151, 106, 80, 224, 233, 202, 126, 15, 51, 245, 206, 77, 40, 93, 78, 135, 20, 242, 168, 141, 3, 41, 241, 96, 76, 34, 0, 168, 1, 175, 17, 217, 225, 226, 35, 19, 97, 204, 8, 131, 95, 168, 92, 4, 50, 76, 72, 23, 104, 93, 51, 65, 98, 217, 96, 254, 140, 249, 100, 60, 53, 58, 191, 37, 135, 4, 137, 219, 170, 254, 68, 8, 111, 132, 246, 99, 123, 222, 12, 38, 206, 183, 121, 61, 176, 244, 129, 144, 79, 161, 112, 105, 205, 13, 40, 241, 96, 76, 34, 0, 120, 108, 151, 7, 0, 154, 62, 50, 152, 136, 79, 140, 197, 212, 62, 210, 48, 63, 62, 80, 172, 147, 172, 37, 63, 151, 188, 37, 8, 94, 49, 210, 4, 81, 0, 0, 142, 237, 160, 19, 134, 134, 224, 144, 121, 148, 185, 214, 88, 189, 242, 13, 11, 97, 41, 14, 161, 164, 255, 51, 184, 175, 66, 234, 17, 66, 253, 39, 202, 244, 171, 50, 0, 248, 51, 67, 24, 128, 240, 200, 4, 30, 15, 208, 122, 137, 2, 2, 110, 171, 134, 9, 200, 207, 37, 111, 137, 69, 3, 254, 228, 252, 59, 102, 180, 36, 231, 231, 231, 79, 223, 157, 92, 187, 118, 198, 218, 253, 243, 17, 95, 100, 122, 144, 90, 195, 246, 243, 61, 27, 34, 238, 79, 40, 226, 197, 10, 23, 213, 253, 11, 37, 173, 228, 82, 243, 9, 116, 57, 169, 1, 112, 183, 33, 0, 18, 200, 25, 8, 111, 24, 214, 2, 32, 77, 96, 208, 252, 28, 190, 158, 138, 238, 190, 101, 97, 140, 171, 158, 159, 228, 166, 143, 206, 95, 146, 228, 110, 25, 93, 114, 71, 50, 89, 13, 255, 102, 196, 114, 211, 137, 152, 108, 162, 4, 68, 136, 37, 7, 39, 159, 40, 92, 250, 96, 225, 220, 185, 214, 202, 218, 82, 190, 233, 98, 37, 65, 101, 78, 64, 88, 92, 34, 242, 168, 1, 205, 155, 65, 198, 56, 222, 24, 236, 210, 105, 191, 178, 100, 68, 39, 255, 228, 141, 38, 55, 76, 231, 150, 204, 200, 207, 191, 101, 116, 254, 238, 0, 55, 125, 96, 201, 90, 54, 176, 101, 97, 52, 122, 103, 48, 163, 12, 232, 231, 61, 10, 220, 124, 193, 16, 46, 122, 11, 225, 170, 239, 16, 86, 184, 197, 214, 31, 130, 149, 43, 64, 141, 47, 69, 215, 86, 210, 116, 37, 122, 28, 91, 110, 73, 49, 139, 147, 97, 60, 226, 59, 226, 62, 32, 0, 226, 177, 24, 167, 11, 128, 162, 92, 162, 22, 204, 182, 77, 105, 18, 88, 54, 192, 45, 172, 94, 187, 22, 73, 254, 252, 106, 54, 121, 203, 208, 146, 13, 8, 0, 54, 27, 0, 250, 58, 160, 212, 202, 119, 44, 180, 189, 19, 121, 116, 200, 10, 212, 34, 133, 101, 149, 73, 55, 186, 182, 214, 106, 53, 40, 45, 204, 72, 32, 2, 180, 221, 197, 191, 241, 18, 37, 216, 151, 24, 140, 66, 67, 250, 244, 1, 80, 87, 13, 41, 51, 119, 94, 110, 73, 117, 245, 244, 88, 108, 250, 150, 253, 91, 134, 230, 223, 209, 242, 120, 190, 25, 0, 74, 11, 138, 245, 147, 248, 12, 115, 47, 223, 40, 48, 237, 72, 237, 225, 74, 160, 72, 39, 133, 6, 251, 40, 233, 25, 172, 181, 149, 242, 172, 73, 14, 201, 100, 164, 3, 92, 194, 21, 194, 148, 171, 1, 232, 102, 150, 59, 175, 15, 192, 121, 189, 129, 226, 98, 107, 1, 159, 137, 79, 110, 89, 178, 182, 55, 202, 246, 174, 93, 219, 146, 108, 137, 173, 221, 144, 140, 6, 131, 129, 64, 111, 11, 27, 221, 173, 235, 21, 16, 231, 218, 56, 143, 79, 23, 20, 243, 6, 0, 123, 245, 33, 60, 4, 30, 161, 144, 105, 144, 0, 0, 86, 84, 92, 100, 37, 127, 124, 38, 148, 14, 175, 4, 157, 78, 90, 4, 224, 95, 65, 247, 5, 128, 151, 207, 27, 140, 154, 234, 143, 148, 211, 197, 86, 220, 20, 95, 128, 101, 161, 167, 253, 32, 10, 190, 0, 188, 246, 225, 186, 233, 64, 192, 235, 99, 181, 78, 1, 92, 100, 51, 211, 91, 66, 201, 139, 72, 20, 18, 123, 202, 240, 124, 94, 13, 24, 184, 255, 10, 226, 1, 240, 56, 237, 72, 19, 52, 119, 132, 250, 134, 249, 70, 6, 244, 155, 127, 254, 11, 19, 15, 108, 146, 74, 141, 248, 94, 77, 32, 252, 111, 21, 225, 130, 145, 189, 219, 138, 138, 138, 172, 5, 214, 204, 51, 41, 104, 98, 18, 155, 141, 34, 32, 25, 137, 102, 208, 37, 104, 2, 110, 24, 3, 224, 215, 179, 129, 106, 37, 168, 165, 82, 177, 75, 233, 220, 71, 117, 12, 233, 96, 81, 209, 246, 142, 131, 200, 16, 188, 133, 142, 69, 217, 206, 239, 8, 21, 226, 191, 128, 88, 182, 250, 100, 201, 15, 16, 17, 64, 16, 244, 51, 237, 231, 13, 68, 32, 75, 205, 88, 32, 122, 226, 125, 224, 117, 182, 103, 123, 193, 214, 247, 63, 226, 184, 235, 50, 82, 130, 84, 191, 172, 212, 183, 48, 27, 182, 145, 200, 65, 220, 245, 29, 57, 112, 0, 136, 129, 136, 0, 211, 119, 190, 143, 61, 111, 160, 4, 51, 3, 208, 30, 203, 207, 207, 255, 48, 192, 237, 191, 51, 201, 36, 239, 156, 49, 227, 126, 14, 12, 89, 169, 27, 180, 145, 87, 209, 23, 149, 5, 57, 57, 110, 252, 20, 0, 190, 152, 42, 107, 213, 128, 24, 254, 119, 228, 192, 1, 50, 227, 65, 244, 128, 1, 0, 153, 171, 133, 162, 247, 87, 39, 183, 44, 76, 110, 152, 127, 11, 0, 48, 29, 156, 1, 56, 157, 229, 184, 30, 136, 13, 184, 46, 155, 213, 134, 67, 93, 52, 132, 151, 163, 223, 42, 47, 27, 9, 133, 42, 51, 15, 128, 242, 229, 21, 248, 101, 54, 22, 80, 205, 28, 117, 210, 126, 182, 79, 96, 253, 93, 122, 50, 32, 115, 3, 154, 201, 132, 24, 31, 142, 210, 72, 145, 57, 195, 174, 93, 152, 92, 184, 129, 139, 37, 111, 73, 122, 185, 25, 16, 11, 65, 223, 115, 119, 220, 159, 63, 125, 109, 254, 140, 37, 81, 218, 93, 140, 236, 85, 229, 36, 28, 22, 9, 1, 220, 164, 226, 76, 76, 192, 231, 131, 200, 67, 102, 185, 175, 26, 0, 215, 105, 169, 173, 253, 47, 235, 32, 32, 1, 208, 62, 124, 126, 216, 139, 196, 5, 7, 72, 240, 6, 65, 224, 79, 222, 113, 75, 126, 210, 239, 7, 0, 124, 220, 140, 59, 167, 175, 133, 15, 185, 25, 213, 220, 134, 124, 46, 118, 75, 142, 131, 231, 10, 34, 89, 45, 169, 254, 75, 27, 61, 75, 132, 192, 58, 88, 110, 166, 192, 80, 3, 0, 195, 244, 15, 100, 70, 64, 2, 0, 151, 17, 251, 241, 52, 3, 150, 33, 39, 14, 247, 143, 174, 93, 24, 155, 191, 54, 202, 0, 0, 136, 245, 147, 211, 123, 125, 0, 64, 48, 80, 189, 48, 144, 188, 38, 0, 208, 120, 0, 244, 166, 84, 5, 102, 204, 69, 216, 161, 221, 251, 86, 54, 0, 72, 102, 77, 3, 64, 207, 155, 221, 138, 52, 224, 176, 218, 28, 138, 103, 178, 253, 253, 220, 48, 231, 133, 174, 239, 243, 242, 90, 227, 124, 63, 154, 36, 192, 205, 224, 16, 0, 222, 40, 132, 5, 119, 244, 2, 68, 51, 122, 27, 170, 23, 66, 88, 96, 164, 63, 115, 25, 50, 54, 65, 33, 62, 35, 154, 241, 36, 95, 136, 232, 71, 237, 236, 241, 214, 55, 79, 139, 98, 128, 114, 24, 15, 23, 190, 125, 66, 14, 128, 148, 146, 100, 81, 171, 155, 217, 62, 78, 194, 136, 139, 222, 127, 255, 254, 249, 75, 48, 7, 4, 130, 75, 170, 23, 130, 49, 64, 0, 248, 51, 2, 144, 211, 144, 113, 118, 34, 234, 34, 179, 240, 55, 11, 90, 82, 111, 250, 60, 119, 250, 159, 248, 84, 8, 122, 226, 103, 118, 86, 22, 204, 45, 234, 98, 57, 196, 11, 195, 195, 178, 5, 13, 200, 28, 155, 128, 95, 18, 25, 0, 54, 217, 178, 161, 37, 233, 103, 146, 224, 249, 39, 119, 111, 216, 143, 170, 134, 216, 253, 92, 67, 111, 208, 207, 85, 27, 37, 139, 115, 27, 50, 22, 200, 176, 126, 40, 100, 156, 4, 23, 169, 67, 128, 72, 119, 253, 128, 96, 119, 236, 252, 105, 80, 106, 88, 224, 197, 244, 188, 187, 0, 165, 172, 101, 202, 183, 143, 8, 137, 79, 76, 28, 34, 116, 188, 1, 22, 249, 62, 190, 128, 15, 191, 68, 108, 182, 190, 221, 191, 179, 170, 125, 253, 250, 61, 70, 54, 57, 151, 33, 99, 137, 172, 70, 95, 248, 68, 35, 152, 129, 58, 248, 41, 117, 6, 11, 40, 112, 167, 91, 187, 161, 235, 136, 219, 75, 137, 7, 80, 61, 50, 239, 157, 229, 251, 189, 79, 18, 1, 93, 63, 57, 235, 140, 212, 92, 134, 140, 37, 42, 214, 9, 36, 248, 241, 57, 38, 100, 34, 12, 34, 100, 184, 130, 68, 211, 35, 118, 23, 159, 41, 161, 196, 3, 33, 136, 80, 73, 236, 205, 145, 86, 251, 3, 253, 106, 0, 252, 28, 199, 245, 247, 127, 209, 223, 207, 234, 222, 65, 77, 57, 12, 25, 139, 20, 10, 233, 132, 82, 230, 87, 120, 17, 200, 120, 9, 13, 151, 253, 17, 187, 19, 191, 162, 196, 3, 79, 116, 169, 13, 143, 223, 4, 192, 5, 24, 198, 173, 230, 161, 16, 220, 68, 150, 235, 240, 183, 131, 21, 236, 63, 175, 229, 33, 93, 162, 204, 14, 25, 75, 4, 46, 65, 137, 56, 144, 42, 80, 198, 241, 57, 52, 240, 170, 113, 159, 50, 173, 33, 226, 178, 219, 237, 116, 198, 168, 238, 4, 71, 251, 250, 112, 47, 19, 38, 16, 156, 4, 145, 35, 120, 149, 73, 137, 7, 3, 202, 122, 130, 14, 137, 195, 157, 210, 3, 102, 94, 1, 193, 86, 170, 51, 7, 43, 227, 34, 42, 85, 118, 167, 171, 202, 105, 215, 249, 134, 70, 41, 4, 15, 179, 189, 176, 128, 239, 3, 150, 101, 251, 165, 64, 137, 7, 96, 56, 151, 233, 86, 198, 0, 232, 15, 188, 251, 68, 0, 10, 196, 192, 32, 251, 10, 8, 54, 117, 16, 145, 17, 0, 218, 97, 183, 195, 255, 46, 218, 165, 230, 2, 135, 139, 241, 56, 156, 4, 124, 228, 146, 129, 91, 124, 94, 90, 241, 166, 131, 55, 162, 230, 211, 224, 148, 120, 208, 33, 125, 193, 110, 147, 6, 188, 197, 121, 85, 25, 86, 64, 16, 180, 183, 188, 154, 14, 76, 85, 123, 102, 14, 112, 217, 29, 85, 140, 7, 180, 193, 35, 252, 39, 180, 203, 233, 116, 216, 113, 26, 17, 181, 222, 99, 231, 245, 100, 241, 71, 138, 30, 239, 151, 233, 3, 66, 154, 66, 254, 28, 72, 38, 216, 78, 89, 203, 36, 0, 104, 65, 180, 51, 216, 27, 81, 250, 173, 232, 80, 91, 90, 9, 239, 185, 112, 50, 110, 201, 144, 91, 162, 171, 60, 118, 210, 247, 46, 135, 131, 113, 226, 196, 153, 221, 233, 116, 73, 105, 116, 4, 132, 11, 62, 160, 25, 91, 73, 215, 38, 182, 63, 0, 224, 163, 128, 23, 20, 131, 114, 210, 109, 91, 164, 99, 18, 163, 34, 132, 228, 130, 45, 101, 109, 48, 0, 194, 61, 37, 209, 166, 196, 131, 146, 164, 114, 75, 212, 160, 130, 130, 98, 80, 136, 5, 201, 151, 94, 106, 181, 84, 102, 200, 203, 209, 50, 222, 119, 61, 2, 93, 255, 8, 121, 75, 187, 28, 146, 98, 112, 193, 231, 118, 167, 167, 242, 129, 185, 15, 0, 163, 218, 230, 62, 92, 160, 45, 33, 240, 226, 169, 252, 205, 147, 227, 2, 133, 96, 67, 31, 56, 121, 12, 34, 178, 162, 15, 113, 186, 53, 37, 30, 20, 68, 138, 78, 121, 226, 155, 236, 143, 142, 191, 244, 82, 210, 66, 230, 232, 103, 207, 223, 185, 92, 85, 30, 116, 150, 3, 169, 69, 80, 13, 242, 239, 64, 48, 236, 246, 103, 118, 46, 117, 18, 70, 165, 53, 217, 10, 92, 214, 210, 38, 250, 167, 190, 156, 22, 18, 80, 9, 54, 237, 178, 139, 55, 149, 252, 125, 33, 191, 68, 137, 7, 5, 41, 170, 200, 106, 105, 52, 31, 56, 16, 127, 179, 123, 244, 205, 55, 121, 29, 80, 108, 186, 198, 220, 227, 4, 5, 8, 34, 81, 165, 248, 212, 97, 119, 56, 156, 207, 58, 159, 162, 157, 46, 222, 2, 217, 172, 197, 98, 210, 167, 141, 207, 80, 8, 61, 102, 106, 46, 160, 72, 162, 96, 163, 62, 128, 247, 85, 60, 248, 222, 144, 44, 222, 81, 116, 177, 134, 72, 165, 165, 24, 116, 250, 184, 112, 107, 52, 249, 82, 56, 201, 13, 188, 36, 42, 193, 98, 235, 3, 38, 98, 82, 33, 227, 235, 178, 171, 121, 230, 197, 23, 65, 46, 128, 190, 251, 44, 67, 148, 4, 93, 74, 146, 215, 224, 71, 72, 217, 28, 212, 245, 62, 148, 214, 200, 137, 7, 40, 114, 0, 97, 3, 240, 153, 42, 221, 115, 50, 86, 211, 18, 13, 40, 6, 157, 209, 238, 71, 90, 185, 240, 75, 225, 214, 55, 99, 49, 201, 10, 108, 90, 85, 96, 62, 38, 213, 122, 71, 192, 168, 47, 218, 237, 207, 62, 245, 172, 3, 112, 96, 170, 156, 88, 53, 98, 43, 105, 181, 150, 202, 150, 254, 196, 229, 58, 29, 217, 211, 213, 114, 162, 196, 131, 199, 94, 149, 203, 184, 23, 79, 188, 134, 16, 131, 206, 246, 100, 252, 145, 216, 80, 211, 155, 173, 221, 47, 113, 24, 0, 214, 39, 255, 58, 231, 251, 147, 187, 83, 212, 19, 118, 39, 50, 85, 200, 86, 128, 190, 4, 226, 109, 8, 26, 56, 171, 109, 70, 35, 155, 178, 245, 14, 39, 9, 128, 195, 174, 225, 61, 129, 140, 103, 201, 200, 102, 141, 242, 65, 39, 219, 253, 82, 50, 154, 76, 134, 223, 228, 44, 43, 26, 144, 27, 7, 18, 69, 190, 158, 235, 206, 45, 38, 149, 63, 38, 237, 240, 144, 199, 196, 252, 129, 28, 233, 71, 156, 252, 119, 238, 90, 52, 126, 87, 42, 46, 112, 147, 45, 88, 53, 32, 164, 109, 29, 78, 131, 47, 221, 217, 102, 139, 200, 131, 78, 238, 205, 55, 147, 67, 47, 189, 153, 244, 91, 234, 151, 255, 52, 192, 165, 216, 100, 212, 31, 245, 195, 215, 155, 10, 172, 255, 56, 185, 135, 83, 43, 224, 42, 16, 88, 165, 164, 84, 22, 131, 237, 125, 43, 210, 214, 102, 106, 14, 132, 30, 121, 104, 87, 21, 227, 208, 87, 2, 12, 154, 33, 168, 87, 86, 94, 41, 62, 132, 44, 232, 244, 37, 95, 106, 141, 15, 113, 126, 198, 82, 95, 191, 110, 93, 11, 155, 74, 182, 251, 55, 145, 175, 221, 15, 232, 196, 76, 38, 168, 189, 23, 185, 126, 20, 122, 117, 121, 2, 94, 54, 36, 122, 145, 50, 180, 123, 180, 167, 210, 197, 215, 82, 255, 238, 114, 56, 13, 33, 208, 44, 54, 160, 172, 157, 162, 164, 160, 51, 218, 250, 18, 158, 220, 0, 0, 212, 215, 47, 95, 209, 224, 147, 127, 93, 105, 84, 13, 153, 137, 70, 203, 174, 240, 142, 25, 87, 86, 6, 81, 209, 232, 221, 103, 208, 15, 184, 92, 122, 39, 235, 60, 104, 14, 4, 16, 72, 93, 164, 140, 83, 180, 227, 77, 242, 159, 162, 196, 3, 19, 141, 145, 57, 126, 24, 0, 128, 160, 140, 150, 127, 77, 235, 35, 144, 105, 188, 221, 155, 254, 106, 26, 180, 201, 232, 232, 196, 208, 104, 222, 177, 116, 210, 151, 62, 126, 117, 52, 125, 121, 212, 96, 48, 221, 236, 184, 176, 1, 185, 28, 2, 99, 129, 86, 144, 67, 172, 241, 7, 220, 242, 0, 152, 18, 15, 16, 192, 99, 53, 236, 226, 1, 168, 223, 177, 124, 197, 22, 217, 215, 180, 36, 5, 236, 144, 144, 203, 100, 175, 92, 65, 3, 93, 151, 117, 211, 251, 129, 147, 121, 163, 12, 123, 41, 47, 239, 104, 99, 122, 90, 99, 94, 217, 232, 165, 69, 233, 178, 198, 188, 188, 84, 187, 75, 87, 107, 93, 27, 0, 160, 98, 69, 49, 240, 56, 228, 8, 168, 74, 164, 104, 133, 56, 83, 226, 129, 191, 212, 105, 23, 0, 0, 90, 247, 80, 89, 157, 142, 27, 57, 177, 40, 205, 191, 74, 46, 106, 4, 44, 46, 231, 165, 244, 74, 93, 162, 63, 41, 75, 194, 119, 141, 87, 167, 53, 94, 250, 211, 198, 171, 95, 189, 122, 116, 209, 68, 94, 222, 165, 188, 198, 40, 10, 96, 244, 149, 138, 45, 163, 247, 146, 153, 170, 36, 4, 104, 99, 181, 200, 100, 20, 102, 15, 132, 250, 150, 29, 18, 2, 192, 6, 143, 150, 85, 139, 95, 147, 146, 35, 127, 106, 218, 101, 16, 235, 116, 58, 125, 249, 242, 180, 11, 233, 81, 127, 250, 66, 26, 21, 17, 170, 83, 220, 67, 139, 142, 70, 129, 11, 224, 164, 51, 71, 243, 46, 79, 124, 245, 106, 89, 99, 250, 79, 83, 233, 188, 15, 88, 102, 207, 30, 239, 27, 246, 61, 94, 45, 213, 22, 88, 221, 58, 31, 155, 163, 42, 135, 240, 106, 143, 195, 94, 37, 191, 43, 45, 189, 46, 40, 149, 125, 193, 168, 124, 15, 143, 29, 4, 201, 178, 98, 121, 189, 130, 214, 61, 186, 98, 3, 169, 215, 39, 234, 51, 122, 116, 209, 16, 195, 157, 156, 150, 7, 13, 154, 182, 104, 90, 217, 232, 201, 69, 19, 139, 202, 166, 77, 75, 73, 5, 47, 232, 222, 204, 229, 105, 151, 134, 70, 27, 23, 141, 94, 248, 106, 186, 172, 108, 40, 53, 45, 157, 119, 242, 204, 180, 203, 233, 175, 94, 194, 122, 195, 227, 226, 115, 8, 42, 162, 117, 66, 71, 179, 36, 56, 4, 40, 54, 149, 235, 1, 89, 205, 169, 94, 226, 88, 246, 227, 78, 15, 202, 8, 85, 175, 88, 167, 132, 160, 126, 7, 128, 80, 182, 133, 38, 213, 219, 136, 239, 125, 87, 167, 157, 73, 127, 245, 248, 201, 63, 61, 121, 21, 53, 47, 253, 213, 21, 233, 188, 163, 81, 54, 26, 229, 184, 100, 138, 16, 119, 229, 171, 103, 206, 164, 26, 243, 174, 228, 229, 93, 205, 91, 4, 156, 15, 58, 17, 193, 129, 216, 135, 255, 57, 187, 174, 46, 152, 196, 100, 79, 190, 217, 224, 111, 226, 168, 3, 94, 57, 156, 114, 61, 224, 54, 87, 120, 4, 228, 36, 41, 49, 45, 4, 8, 132, 229, 143, 174, 184, 251, 175, 182, 84, 143, 230, 157, 9, 68, 161, 41, 105, 96, 234, 69, 67, 208, 172, 69, 199, 82, 127, 122, 53, 13, 140, 190, 238, 239, 214, 237, 216, 177, 142, 167, 250, 11, 121, 160, 255, 174, 46, 202, 59, 222, 8, 204, 159, 87, 118, 233, 210, 138, 75, 141, 199, 134, 78, 150, 13, 73, 143, 172, 143, 192, 36, 9, 57, 197, 14, 232, 123, 23, 142, 212, 161, 45, 242, 0, 189, 18, 141, 225, 100, 159, 121, 142, 180, 19, 9, 134, 170, 213, 130, 32, 192, 176, 98, 29, 52, 185, 254, 88, 89, 217, 165, 99, 211, 174, 46, 106, 60, 115, 60, 47, 61, 237, 210, 209, 188, 11, 87, 224, 83, 37, 157, 60, 115, 230, 204, 201, 163, 103, 46, 92, 58, 115, 236, 194, 133, 11, 39, 235, 63, 61, 115, 236, 204, 201, 117, 199, 142, 183, 72, 191, 71, 219, 117, 125, 2, 147, 179, 99, 85, 84, 5, 17, 184, 139, 217, 249, 148, 144, 153, 164, 245, 114, 183, 217, 0, 112, 73, 73, 209, 186, 199, 30, 213, 178, 1, 208, 209, 51, 211, 26, 27, 47, 52, 230, 29, 207, 91, 4, 42, 224, 248, 180, 227, 87, 166, 93, 93, 81, 118, 225, 88, 222, 5, 93, 196, 180, 180, 238, 209, 50, 113, 226, 156, 107, 233, 19, 250, 17, 183, 45, 119, 207, 112, 211, 122, 231, 179, 235, 81, 240, 42, 116, 189, 97, 144, 100, 68, 248, 124, 135, 44, 41, 186, 97, 197, 242, 29, 154, 231, 255, 201, 153, 178, 178, 178, 51, 87, 225, 223, 241, 116, 227, 177, 69, 199, 47, 164, 26, 47, 53, 158, 57, 115, 252, 216, 201, 178, 178, 250, 250, 111, 153, 129, 0, 92, 12, 222, 180, 108, 122, 214, 190, 84, 63, 226, 206, 88, 240, 161, 75, 154, 224, 213, 233, 200, 173, 46, 13, 171, 64, 218, 169, 200, 10, 215, 149, 173, 88, 174, 226, 131, 187, 227, 159, 77, 212, 28, 171, 63, 121, 242, 228, 177, 163, 232, 80, 127, 236, 211, 163, 159, 162, 99, 253, 183, 30, 171, 175, 255, 115, 125, 166, 65, 210, 160, 248, 104, 249, 138, 58, 254, 161, 171, 40, 230, 9, 189, 136, 59, 211, 124, 52, 67, 0, 148, 3, 170, 46, 172, 21, 76, 223, 192, 133, 163, 20, 90, 147, 22, 223, 178, 226, 81, 5, 8, 83, 207, 110, 40, 211, 237, 216, 187, 239, 54, 0, 224, 234, 34, 160, 99, 199, 148, 31, 18, 95, 27, 61, 180, 103, 169, 83, 47, 226, 54, 158, 47, 160, 79, 250, 3, 170, 85, 134, 193, 178, 150, 136, 51, 173, 55, 46, 80, 183, 165, 12, 161, 176, 14, 11, 196, 99, 250, 205, 175, 175, 47, 3, 14, 184, 91, 246, 30, 89, 132, 229, 143, 62, 186, 98, 197, 165, 227, 199, 143, 79, 59, 3, 65, 166, 226, 244, 29, 15, 109, 152, 228, 40, 176, 1, 233, 15, 168, 58, 120, 219, 152, 141, 196, 115, 140, 7, 70, 0, 134, 199, 86, 172, 120, 244, 209, 229, 0, 5, 216, 187, 250, 250, 79, 47, 32, 82, 162, 128, 48, 90, 183, 28, 44, 230, 138, 178, 178, 45, 213, 68, 219, 249, 254, 21, 28, 162, 241, 80, 221, 6, 64, 81, 174, 84, 214, 173, 240, 240, 15, 237, 89, 170, 219, 221, 40, 112, 15, 105, 214, 130, 50, 36, 74, 103, 64, 181, 202, 1, 134, 209, 238, 204, 10, 1, 54, 72, 120, 121, 7, 19, 27, 44, 212, 85, 111, 217, 176, 1, 84, 33, 248, 56, 121, 121, 199, 234, 31, 125, 244, 161, 229, 162, 144, 192, 223, 29, 101, 138, 138, 148, 206, 72, 164, 239, 202, 180, 75, 125, 56, 241, 91, 87, 166, 176, 45, 15, 109, 225, 31, 122, 169, 46, 163, 86, 90, 113, 225, 198, 129, 205, 230, 234, 133, 40, 241, 32, 181, 203, 133, 211, 166, 250, 225, 151, 130, 16, 68, 104, 121, 7, 173, 14, 48, 36, 239, 196, 149, 43, 169, 105, 87, 112, 181, 122, 228, 231, 101, 82, 231, 150, 241, 9, 79, 146, 230, 2, 0, 70, 23, 149, 141, 70, 58, 219, 72, 87, 42, 92, 140, 117, 119, 147, 135, 246, 232, 11, 124, 165, 21, 79, 113, 237, 52, 151, 155, 165, 196, 131, 140, 240, 80, 14, 173, 151, 133, 145, 19, 254, 26, 47, 239, 224, 201, 97, 139, 141, 182, 200, 248, 162, 198, 30, 33, 173, 23, 18, 33, 216, 177, 66, 172, 224, 12, 161, 165, 77, 255, 245, 204, 180, 116, 36, 132, 75, 123, 241, 202, 96, 93, 50, 8, 118, 172, 16, 242, 14, 154, 241, 86, 68, 165, 214, 16, 73, 152, 154, 201, 205, 82, 226, 65, 67, 46, 35, 151, 75, 248, 154, 17, 150, 119, 200, 5, 0, 134, 133, 150, 29, 180, 30, 20, 19, 187, 101, 127, 183, 67, 80, 241, 242, 101, 237, 39, 242, 142, 125, 17, 233, 144, 198, 2, 154, 229, 190, 246, 142, 135, 234, 240, 67, 123, 244, 31, 177, 210, 90, 27, 138, 116, 208, 116, 174, 245, 66, 106, 170, 178, 27, 184, 92, 136, 136, 1, 36, 203, 59, 56, 115, 1, 224, 114, 222, 113, 46, 210, 89, 41, 78, 77, 6, 73, 224, 69, 28, 124, 157, 247, 133, 246, 254, 107, 170, 108, 34, 18, 121, 38, 212, 209, 214, 214, 129, 102, 57, 226, 241, 176, 13, 143, 74, 138, 128, 120, 69, 180, 193, 120, 220, 166, 77, 161, 200, 206, 107, 182, 20, 155, 94, 180, 191, 104, 36, 72, 132, 61, 248, 229, 29, 126, 144, 3, 0, 28, 196, 249, 160, 10, 124, 205, 251, 14, 10, 0, 68, 34, 239, 61, 196, 115, 193, 186, 117, 63, 231, 17, 248, 226, 124, 95, 164, 153, 230, 81, 234, 104, 38, 195, 96, 117, 18, 19, 60, 42, 164, 28, 244, 37, 117, 61, 101, 45, 121, 184, 54, 183, 122, 33, 13, 81, 160, 14, 13, 6, 57, 248, 136, 68, 88, 222, 193, 242, 138, 250, 123, 163, 170, 77, 255, 213, 105, 23, 80, 150, 179, 35, 18, 178, 21, 32, 8, 154, 9, 4, 63, 23, 60, 232, 117, 203, 5, 8, 34, 145, 205, 146, 68, 8, 245, 90, 27, 150, 171, 17, 112, 234, 35, 64, 209, 149, 15, 204, 157, 59, 151, 154, 100, 219, 249, 123, 48, 206, 39, 116, 11, 239, 104, 133, 171, 228, 97, 44, 223, 156, 171, 12, 200, 13, 171, 54, 163, 23, 142, 146, 208, 22, 77, 220, 40, 182, 22, 20, 90, 75, 249, 149, 255, 68, 125, 184, 110, 249, 46, 190, 213, 12, 209, 137, 29, 29, 178, 201, 238, 213, 15, 169, 17, 208, 215, 85, 148, 120, 40, 181, 78, 118, 129, 32, 104, 197, 179, 246, 23, 245, 4, 201, 229, 144, 73, 30, 68, 232, 150, 183, 223, 126, 242, 222, 2, 217, 111, 24, 143, 144, 177, 56, 11, 230, 245, 122, 65, 182, 81, 114, 137, 70, 249, 150, 226, 130, 18, 100, 186, 118, 241, 158, 193, 142, 71, 177, 46, 8, 53, 11, 166, 17, 113, 64, 39, 63, 14, 84, 39, 34, 240, 80, 93, 6, 4, 36, 0, 24, 188, 68, 212, 164, 102, 196, 174, 127, 2, 2, 3, 221, 194, 59, 185, 230, 241, 184, 92, 150, 109, 111, 191, 253, 171, 95, 61, 121, 175, 152, 67, 209, 6, 25, 237, 100, 103, 4, 31, 203, 178, 94, 175, 118, 72, 207, 109, 181, 97, 125, 200, 11, 57, 146, 131, 66, 90, 102, 20, 58, 219, 196, 145, 176, 58, 65, 97, 212, 63, 196, 255, 156, 75, 71, 10, 40, 241, 32, 251, 141, 130, 202, 218, 210, 92, 114, 71, 52, 42, 110, 210, 22, 222, 105, 221, 100, 75, 9, 66, 224, 159, 255, 249, 103, 79, 222, 75, 82, 245, 90, 127, 221, 159, 74, 250, 0, 130, 246, 148, 209, 34, 154, 219, 201, 250, 183, 239, 19, 8, 150, 119, 69, 10, 133, 198, 35, 99, 136, 87, 56, 230, 135, 242, 233, 21, 2, 2, 43, 132, 39, 114, 232, 223, 83, 77, 181, 197, 5, 136, 21, 244, 86, 46, 208, 167, 167, 170, 60, 78, 23, 165, 250, 80, 101, 124, 209, 91, 75, 9, 143, 192, 63, 255, 243, 175, 0, 132, 123, 239, 181, 130, 254, 177, 206, 181, 22, 88, 231, 22, 20, 216, 138, 139, 75, 43, 221, 76, 52, 149, 74, 5, 252, 108, 74, 59, 239, 15, 147, 173, 80, 176, 249, 4, 130, 21, 37, 123, 101, 12, 32, 238, 128, 70, 60, 69, 1, 129, 29, 101, 252, 213, 180, 126, 182, 212, 128, 244, 86, 46, 208, 39, 10, 233, 59, 135, 218, 210, 42, 223, 227, 148, 216, 127, 187, 111, 251, 91, 60, 2, 152, 126, 37, 167, 159, 253, 236, 103, 47, 63, 249, 228, 247, 158, 124, 242, 229, 183, 127, 253, 153, 1, 0, 160, 9, 36, 167, 167, 12, 218, 183, 188, 48, 34, 7, 64, 246, 186, 77, 134, 192, 58, 49, 255, 238, 145, 137, 1, 237, 113, 58, 51, 135, 50, 165, 38, 39, 91, 81, 232, 240, 132, 98, 196, 128, 118, 33, 43, 142, 94, 145, 35, 152, 33, 154, 177, 212, 204, 190, 235, 225, 174, 247, 100, 8, 232, 211, 175, 0, 137, 239, 221, 139, 56, 196, 86, 92, 169, 245, 96, 196, 110, 70, 14, 207, 202, 136, 17, 161, 167, 120, 72, 37, 4, 152, 120, 217, 164, 237, 118, 39, 170, 55, 114, 17, 119, 157, 214, 131, 162, 54, 151, 68, 122, 149, 92, 194, 156, 14, 154, 77, 198, 146, 81, 95, 20, 142, 1, 198, 159, 140, 181, 162, 21, 37, 43, 102, 125, 227, 59, 30, 174, 231, 95, 178, 32, 32, 113, 8, 98, 10, 12, 69, 129, 13, 100, 164, 184, 20, 164, 164, 150, 70, 83, 187, 145, 180, 255, 29, 210, 112, 134, 0, 160, 248, 72, 176, 5, 59, 228, 230, 8, 185, 4, 40, 134, 35, 17, 130, 199, 131, 197, 211, 99, 183, 163, 89, 76, 154, 86, 229, 50, 170, 38, 79, 149, 186, 60, 129, 224, 140, 249, 51, 90, 146, 91, 238, 152, 63, 131, 99, 185, 252, 252, 94, 0, 96, 222, 156, 217, 179, 110, 251, 198, 139, 135, 122, 199, 39, 254, 253, 223, 204, 162, 32, 8, 203, 207, 48, 189, 12, 136, 0, 97, 6, 89, 4, 77, 91, 116, 80, 119, 75, 35, 158, 5, 252, 213, 200, 35, 58, 124, 184, 190, 76, 254, 160, 30, 52, 127, 89, 245, 240, 30, 0, 130, 70, 176, 120, 104, 218, 131, 7, 65, 61, 57, 168, 11, 66, 226, 32, 42, 22, 127, 52, 169, 175, 101, 254, 232, 116, 46, 185, 176, 122, 40, 127, 3, 6, 96, 164, 6, 35, 240, 157, 245, 47, 30, 138, 197, 7, 207, 141, 79, 32, 74, 161, 127, 255, 254, 217, 175, 223, 219, 245, 50, 208, 207, 178, 201, 135, 146, 254, 207, 39, 31, 191, 187, 119, 219, 182, 242, 162, 114, 157, 101, 195, 25, 95, 42, 85, 191, 3, 218, 127, 248, 240, 215, 205, 38, 193, 60, 168, 248, 202, 238, 244, 160, 202, 84, 89, 107, 76, 145, 200, 64, 56, 68, 102, 247, 223, 201, 230, 239, 78, 222, 191, 176, 229, 142, 24, 203, 5, 49, 0, 13, 225, 193, 165, 128, 192, 109, 119, 253, 195, 143, 105, 102, 79, 67, 67, 67, 48, 60, 146, 78, 28, 58, 4, 175, 248, 25, 30, 181, 165, 54, 43, 244, 45, 104, 194, 159, 253, 202, 20, 20, 255, 231, 75, 158, 182, 109, 215, 97, 1, 46, 149, 90, 142, 1, 40, 187, 223, 108, 43, 104, 90, 150, 240, 197, 58, 210, 180, 30, 16, 149, 0, 210, 128, 32, 246, 119, 78, 191, 147, 99, 217, 233, 183, 44, 73, 110, 174, 106, 193, 0, 48, 76, 67, 60, 60, 239, 182, 219, 238, 153, 117, 207, 143, 241, 175, 248, 82, 137, 145, 244, 72, 131, 206, 99, 84, 22, 99, 36, 238, 253, 30, 64, 145, 137, 43, 68, 0, 190, 252, 252, 118, 45, 0, 126, 54, 186, 127, 29, 2, 224, 112, 189, 249, 128, 79, 138, 80, 60, 78, 60, 0, 96, 122, 226, 173, 67, 174, 72, 145, 8, 84, 231, 143, 206, 232, 77, 206, 223, 210, 224, 219, 207, 3, 192, 120, 15, 37, 106, 110, 123, 202, 191, 120, 214, 223, 98, 4, 184, 212, 161, 65, 93, 4, 164, 199, 169, 219, 94, 92, 128, 177, 192, 112, 240, 36, 50, 200, 151, 18, 125, 100, 83, 139, 0, 166, 175, 175, 196, 8, 60, 102, 26, 0, 121, 132, 226, 65, 186, 210, 244, 34, 147, 46, 60, 241, 133, 215, 47, 220, 140, 88, 52, 57, 125, 104, 122, 50, 90, 125, 127, 192, 191, 63, 63, 42, 228, 4, 247, 28, 9, 223, 117, 215, 158, 67, 119, 221, 133, 204, 102, 52, 197, 246, 198, 211, 169, 67, 134, 247, 244, 129, 216, 112, 237, 126, 84, 240, 138, 198, 156, 3, 169, 20, 56, 73, 180, 187, 180, 216, 134, 80, 121, 178, 168, 188, 124, 219, 182, 189, 239, 126, 66, 120, 192, 166, 211, 126, 134, 190, 9, 3, 240, 219, 181, 102, 1, 80, 70, 40, 200, 68, 88, 77, 93, 71, 147, 146, 255, 71, 30, 33, 254, 0, 91, 125, 199, 194, 59, 182, 12, 205, 207, 191, 127, 70, 208, 239, 111, 149, 0, 64, 16, 60, 241, 141, 123, 162, 111, 204, 94, 76, 179, 104, 176, 55, 14, 8, 24, 241, 128, 255, 80, 119, 119, 107, 107, 119, 124, 36, 133, 198, 134, 3, 62, 64, 108, 72, 83, 59, 67, 187, 87, 21, 124, 132, 16, 248, 205, 178, 182, 80, 72, 213, 126, 134, 249, 206, 127, 197, 8, 28, 203, 5, 0, 89, 132, 2, 156, 106, 53, 147, 252, 102, 240, 24, 162, 253, 17, 167, 16, 117, 69, 193, 250, 69, 253, 209, 222, 96, 178, 97, 167, 55, 22, 102, 229, 89, 225, 61, 47, 222, 243, 141, 55, 162, 115, 230, 188, 49, 10, 237, 241, 122, 227, 6, 82, 224, 13, 180, 118, 99, 106, 69, 32, 32, 176, 252, 16, 37, 232, 254, 246, 173, 159, 35, 4, 62, 46, 29, 74, 13, 69, 3, 29, 138, 109, 31, 188, 83, 49, 0, 167, 170, 117, 47, 212, 146, 54, 66, 113, 98, 179, 144, 157, 170, 236, 143, 32, 253, 15, 110, 49, 118, 182, 188, 126, 60, 145, 207, 239, 69, 11, 103, 33, 126, 146, 101, 132, 118, 254, 195, 223, 126, 227, 174, 89, 179, 102, 47, 29, 65, 237, 105, 72, 164, 227, 58, 213, 156, 62, 150, 111, 191, 0, 66, 120, 144, 245, 233, 112, 0, 166, 251, 8, 2, 239, 224, 10, 2, 86, 113, 183, 134, 175, 99, 4, 206, 152, 45, 24, 213, 14, 131, 160, 2, 129, 236, 3, 0, 224, 10, 241, 21, 101, 85, 184, 176, 138, 44, 245, 76, 225, 3, 46, 96, 83, 108, 186, 186, 122, 211, 61, 179, 110, 155, 53, 123, 78, 77, 131, 151, 137, 38, 82, 105, 173, 26, 240, 31, 145, 183, 31, 83, 83, 138, 77, 166, 244, 235, 199, 252, 54, 5, 2, 228, 67, 190, 201, 127, 70, 88, 224, 131, 236, 45, 32, 68, 105, 135, 65, 220, 224, 28, 101, 25, 1, 240, 200, 87, 73, 66, 8, 144, 165, 158, 241, 93, 40, 7, 50, 145, 202, 141, 151, 191, 255, 224, 139, 119, 205, 154, 61, 123, 78, 69, 60, 120, 238, 220, 17, 173, 16, 176, 9, 77, 251, 187, 91, 195, 208, 56, 221, 173, 4, 123, 19, 9, 27, 214, 132, 159, 148, 156, 195, 178, 130, 56, 48, 48, 228, 15, 32, 131, 70, 253, 5, 86, 131, 39, 133, 177, 115, 111, 150, 197, 7, 41, 241, 32, 146, 149, 81, 76, 219, 209, 33, 85, 186, 193, 229, 88, 67, 150, 122, 38, 55, 195, 206, 133, 50, 41, 250, 218, 131, 223, 127, 238, 111, 231, 204, 158, 61, 123, 241, 96, 60, 234, 77, 164, 195, 252, 231, 188, 25, 222, 156, 210, 52, 31, 33, 144, 74, 13, 233, 4, 138, 94, 188, 38, 163, 149, 119, 137, 74, 222, 63, 151, 66, 219, 144, 98, 94, 240, 33, 131, 70, 88, 160, 190, 49, 64, 26, 238, 141, 114, 129, 76, 43, 77, 105, 1, 16, 202, 42, 144, 135, 168, 59, 30, 168, 232, 126, 130, 128, 118, 169, 103, 85, 86, 248, 251, 15, 62, 253, 220, 230, 121, 179, 43, 106, 230, 116, 179, 204, 161, 9, 193, 18, 240, 102, 152, 77, 169, 24, 32, 30, 71, 199, 115, 81, 159, 78, 166, 136, 65, 0, 36, 214, 20, 9, 62, 209, 222, 242, 173, 149, 8, 128, 161, 88, 180, 151, 221, 180, 243, 153, 123, 238, 38, 8, 156, 33, 203, 236, 249, 192, 67, 204, 180, 224, 138, 6, 0, 249, 20, 64, 23, 216, 121, 117, 232, 200, 123, 76, 10, 90, 163, 88, 234, 25, 79, 8, 84, 0, 112, 224, 192, 211, 15, 174, 126, 238, 53, 102, 233, 188, 193, 248, 188, 154, 6, 102, 80, 208, 2, 188, 25, 78, 170, 36, 32, 49, 50, 18, 70, 40, 36, 89, 157, 125, 212, 24, 63, 94, 144, 109, 176, 228, 19, 209, 45, 250, 120, 91, 121, 121, 73, 121, 73, 145, 109, 251, 1, 239, 250, 157, 127, 78, 0, 56, 154, 66, 211, 235, 252, 208, 254, 220, 182, 167, 80, 22, 20, 208, 218, 161, 22, 173, 134, 116, 43, 151, 122, 198, 89, 105, 37, 0, 117, 79, 3, 7, 236, 100, 14, 184, 230, 180, 142, 204, 171, 104, 141, 11, 50, 192, 155, 97, 182, 91, 213, 254, 4, 250, 19, 14, 27, 116, 29, 139, 0, 24, 73, 189, 245, 238, 151, 26, 250, 228, 221, 251, 30, 190, 13, 3, 112, 244, 210, 25, 224, 10, 110, 104, 100, 132, 211, 91, 165, 209, 144, 180, 155, 15, 32, 15, 81, 104, 52, 173, 171, 28, 85, 75, 61, 187, 52, 0, 48, 93, 79, 63, 248, 220, 115, 128, 108, 87, 195, 188, 154, 177, 5, 142, 116, 154, 223, 92, 136, 55, 195, 190, 67, 10, 254, 31, 73, 12, 146, 87, 156, 129, 49, 139, 99, 0, 82, 49, 219, 222, 207, 181, 24, 124, 254, 192, 95, 97, 59, 144, 190, 128, 245, 194, 96, 98, 164, 206, 93, 224, 187, 182, 29, 26, 80, 50, 5, 99, 224, 49, 83, 39, 129, 189, 35, 37, 0, 59, 87, 175, 126, 238, 199, 76, 59, 218, 199, 117, 129, 99, 204, 190, 120, 66, 176, 3, 188, 25, 222, 163, 96, 128, 193, 238, 48, 136, 64, 120, 176, 219, 143, 182, 31, 210, 249, 5, 16, 130, 93, 184, 117, 167, 223, 41, 41, 255, 141, 6, 2, 44, 3, 191, 77, 167, 143, 195, 25, 35, 137, 196, 135, 41, 111, 97, 52, 165, 95, 92, 157, 19, 6, 102, 235, 100, 116, 0, 120, 229, 251, 207, 61, 87, 203, 111, 90, 184, 120, 105, 124, 206, 130, 177, 176, 223, 239, 71, 218, 153, 194, 102, 120, 147, 66, 4, 64, 254, 1, 129, 145, 110, 84, 65, 173, 255, 224, 241, 216, 254, 148, 64, 61, 219, 139, 182, 253, 242, 147, 207, 101, 188, 240, 53, 44, 3, 87, 211, 169, 146, 237, 239, 39, 18, 225, 53, 189, 76, 97, 42, 167, 117, 104, 229, 36, 197, 139, 180, 126, 133, 190, 150, 60, 2, 0, 18, 3, 255, 248, 193, 23, 94, 243, 10, 219, 54, 46, 5, 123, 56, 47, 158, 72, 97, 237, 68, 161, 175, 41, 175, 198, 6, 198, 71, 226, 221, 73, 136, 135, 88, 93, 41, 0, 79, 128, 248, 64, 108, 42, 137, 146, 234, 149, 171, 138, 139, 139, 151, 253, 181, 173, 8, 43, 198, 199, 203, 16, 0, 23, 210, 233, 79, 191, 220, 118, 111, 225, 46, 206, 231, 99, 77, 182, 95, 39, 51, 42, 139, 23, 205, 166, 204, 240, 40, 161, 165, 161, 97, 143, 143, 223, 177, 153, 97, 158, 126, 240, 5, 183, 180, 145, 175, 3, 28, 130, 57, 77, 131, 216, 60, 241, 0, 168, 253, 160, 48, 232, 193, 115, 41, 63, 50, 237, 122, 20, 72, 12, 162, 246, 71, 253, 76, 59, 27, 197, 167, 28, 168, 235, 234, 218, 220, 213, 181, 111, 153, 109, 219, 151, 159, 96, 95, 232, 211, 244, 213, 127, 67, 42, 161, 40, 6, 254, 148, 185, 77, 106, 212, 43, 16, 227, 103, 147, 226, 69, 179, 201, 18, 194, 1, 19, 19, 169, 48, 200, 60, 233, 192, 239, 63, 184, 89, 6, 64, 56, 12, 60, 48, 167, 102, 80, 2, 192, 175, 105, 127, 28, 28, 65, 206, 103, 40, 185, 137, 4, 135, 0, 16, 223, 251, 16, 0, 64, 104, 109, 177, 226, 34, 162, 4, 254, 253, 255, 18, 137, 120, 247, 45, 179, 155, 244, 20, 232, 88, 139, 73, 172, 67, 66, 116, 192, 200, 200, 68, 58, 33, 52, 249, 233, 213, 110, 183, 180, 147, 115, 48, 221, 61, 15, 152, 160, 34, 193, 91, 121, 47, 27, 212, 180, 31, 249, 129, 108, 192, 144, 115, 227, 137, 56, 10, 175, 69, 213, 206, 183, 31, 16, 240, 118, 165, 122, 176, 25, 56, 44, 0, 240, 229, 111, 204, 86, 204, 90, 117, 62, 155, 68, 5, 26, 118, 162, 45, 94, 239, 161, 145, 244, 32, 191, 125, 243, 234, 213, 110, 65, 7, 118, 245, 196, 209, 10, 219, 243, 102, 207, 89, 80, 17, 39, 29, 220, 158, 138, 183, 106, 218, 15, 17, 49, 103, 44, 185, 108, 47, 27, 72, 113, 81, 86, 58, 161, 78, 64, 184, 231, 92, 215, 174, 29, 68, 13, 78, 92, 36, 8, 236, 53, 201, 188, 186, 217, 32, 89, 188, 104, 114, 0, 141, 248, 1, 208, 185, 13, 131, 233, 56, 126, 166, 125, 171, 95, 144, 0, 136, 37, 194, 233, 177, 56, 59, 207, 14, 14, 65, 28, 59, 105, 224, 199, 42, 21, 96, 28, 7, 2, 172, 34, 140, 81, 214, 23, 192, 221, 105, 159, 98, 243, 203, 174, 174, 246, 3, 7, 152, 230, 174, 46, 142, 235, 122, 103, 35, 6, 224, 247, 233, 244, 196, 255, 194, 8, 44, 51, 7, 128, 62, 81, 98, 188, 168, 93, 100, 91, 151, 60, 40, 80, 182, 112, 104, 134, 219, 216, 68, 16, 181, 185, 22, 0, 216, 39, 0, 16, 143, 143, 164, 209, 94, 155, 11, 42, 198, 22, 47, 238, 198, 87, 36, 83, 234, 96, 160, 53, 161, 246, 2, 149, 245, 5, 94, 13, 191, 182, 31, 96, 200, 253, 135, 216, 174, 151, 15, 19, 2, 59, 80, 95, 136, 0, 40, 191, 70, 0, 132, 104, 33, 243, 130, 155, 152, 132, 108, 177, 5, 55, 0, 34, 95, 132, 0, 243, 227, 87, 36, 29, 40, 44, 178, 207, 46, 176, 143, 217, 23, 28, 97, 188, 129, 246, 33, 112, 88, 148, 8, 180, 158, 83, 235, 127, 101, 246, 78, 11, 0, 64, 64, 238, 255, 69, 87, 215, 74, 30, 0, 84, 86, 219, 184, 23, 5, 11, 215, 50, 155, 142, 18, 15, 102, 200, 78, 123, 240, 152, 129, 5, 139, 183, 55, 158, 158, 24, 73, 4, 247, 84, 29, 58, 18, 227, 98, 74, 0, 18, 236, 226, 197, 99, 21, 11, 186, 219, 177, 69, 87, 133, 67, 173, 131, 41, 149, 3, 160, 212, 198, 94, 198, 173, 39, 176, 93, 0, 115, 127, 87, 215, 46, 34, 2, 135, 27, 209, 80, 89, 137, 121, 25, 208, 46, 80, 32, 182, 157, 50, 217, 126, 218, 193, 71, 79, 22, 144, 224, 64, 128, 105, 143, 163, 253, 150, 247, 188, 216, 112, 228, 195, 120, 92, 0, 96, 132, 32, 48, 200, 62, 177, 32, 213, 52, 47, 30, 30, 68, 74, 79, 37, 2, 41, 53, 0, 74, 109, 236, 101, 106, 117, 21, 18, 0, 192, 193, 111, 124, 27, 183, 127, 199, 113, 4, 192, 239, 76, 3, 96, 48, 171, 209, 240, 116, 189, 170, 39, 23, 41, 174, 175, 114, 90, 2, 169, 40, 120, 122, 192, 150, 193, 238, 68, 32, 16, 109, 216, 192, 183, 191, 7, 156, 211, 20, 217, 105, 39, 222, 229, 156, 87, 49, 123, 206, 200, 200, 238, 45, 27, 246, 139, 109, 111, 34, 0, 12, 169, 127, 79, 145, 189, 51, 76, 249, 117, 29, 64, 0, 244, 16, 4, 26, 63, 172, 63, 153, 158, 64, 0, 216, 204, 236, 86, 99, 98, 231, 45, 57, 139, 168, 171, 158, 112, 192, 232, 176, 187, 104, 218, 131, 0, 72, 14, 121, 163, 126, 210, 230, 186, 127, 168, 138, 14, 165, 71, 186, 121, 35, 0, 8, 12, 166, 241, 78, 11, 108, 151, 11, 185, 68, 225, 234, 221, 187, 215, 108, 17, 250, 126, 13, 254, 147, 210, 46, 145, 71, 201, 178, 119, 198, 57, 79, 47, 6, 0, 59, 195, 135, 191, 205, 29, 7, 254, 67, 238, 192, 54, 218, 196, 106, 88, 214, 172, 237, 87, 156, 162, 170, 122, 2, 247, 167, 10, 229, 202, 29, 40, 161, 232, 114, 90, 192, 255, 108, 247, 226, 109, 203, 145, 17, 168, 2, 167, 32, 157, 10, 131, 70, 196, 226, 31, 15, 98, 4, 64, 43, 84, 35, 4, 154, 100, 204, 191, 101, 119, 55, 206, 7, 106, 125, 64, 74, 60, 100, 2, 128, 65, 155, 164, 119, 253, 142, 104, 129, 141, 31, 94, 74, 167, 145, 51, 240, 249, 125, 217, 119, 171, 49, 99, 228, 212, 0, 200, 125, 68, 26, 45, 68, 225, 176, 67, 228, 236, 241, 31, 233, 181, 36, 185, 84, 187, 143, 99, 32, 30, 232, 170, 92, 141, 34, 129, 96, 98, 34, 61, 22, 38, 0, 36, 214, 132, 211, 35, 240, 10, 177, 199, 156, 217, 118, 94, 1, 236, 70, 109, 95, 211, 74, 116, 160, 214, 123, 85, 1, 96, 196, 175, 24, 116, 142, 200, 192, 198, 207, 128, 5, 176, 12, 252, 230, 71, 25, 87, 195, 194, 100, 66, 3, 200, 1, 208, 245, 17, 209, 74, 48, 78, 127, 114, 97, 190, 133, 1, 9, 224, 192, 89, 111, 111, 239, 122, 97, 117, 37, 182, 130, 193, 248, 4, 8, 63, 66, 32, 182, 43, 158, 72, 199, 145, 12, 116, 117, 53, 60, 193, 246, 242, 8, 44, 225, 255, 33, 55, 64, 235, 4, 170, 0, 208, 115, 220, 17, 249, 251, 36, 37, 112, 120, 99, 234, 2, 145, 129, 47, 109, 89, 87, 195, 50, 67, 10, 53, 161, 55, 181, 194, 3, 44, 64, 247, 206, 95, 27, 179, 4, 162, 120, 222, 163, 175, 93, 30, 9, 4, 7, 201, 102, 91, 107, 208, 86, 51, 35, 251, 19, 61, 130, 251, 74, 16, 216, 45, 168, 128, 214, 120, 150, 4, 142, 151, 223, 253, 64, 143, 2, 92, 31, 247, 197, 239, 136, 55, 188, 177, 231, 195, 244, 5, 108, 8, 191, 188, 207, 112, 53, 172, 28, 168, 22, 135, 196, 194, 174, 35, 148, 102, 76, 193, 133, 203, 112, 184, 233, 225, 55, 44, 124, 186, 226, 28, 116, 178, 60, 18, 8, 38, 208, 62, 83, 137, 48, 50, 134, 187, 18, 49, 49, 64, 80, 217, 193, 96, 150, 7, 241, 234, 237, 125, 34, 144, 47, 0, 63, 204, 179, 192, 202, 174, 227, 233, 48, 14, 9, 126, 243, 215, 70, 171, 97, 137, 100, 26, 27, 158, 17, 40, 241, 192, 19, 180, 31, 217, 130, 238, 59, 119, 199, 5, 0, 82, 44, 68, 2, 207, 73, 0, 0, 4, 168, 82, 34, 157, 128, 144, 232, 74, 88, 66, 0, 152, 64, 6, 65, 171, 241, 16, 50, 33, 164, 4, 141, 141, 22, 2, 128, 247, 133, 54, 190, 255, 254, 213, 120, 250, 99, 60, 134, 80, 201, 100, 1, 192, 154, 229, 87, 17, 225, 81, 3, 126, 238, 48, 37, 30, 48, 33, 13, 136, 49, 108, 8, 231, 231, 139, 0, 112, 202, 72, 128, 80, 119, 10, 52, 0, 138, 137, 18, 113, 65, 10, 186, 88, 25, 23, 180, 102, 11, 224, 51, 15, 253, 33, 233, 251, 140, 71, 160, 172, 235, 248, 96, 122, 43, 78, 152, 217, 104, 198, 71, 101, 186, 206, 154, 229, 87, 17, 97, 205, 99, 37, 107, 88, 83, 226, 1, 147, 84, 160, 203, 38, 147, 34, 0, 169, 158, 85, 90, 0, 186, 194, 0, 0, 222, 112, 176, 91, 206, 4, 92, 24, 143, 13, 119, 183, 102, 147, 128, 204, 0, 120, 147, 169, 145, 84, 138, 15, 8, 190, 221, 213, 146, 26, 27, 33, 195, 40, 247, 249, 82, 209, 118, 227, 75, 77, 5, 123, 232, 156, 74, 155, 77, 171, 130, 93, 146, 8, 249, 185, 222, 106, 9, 128, 212, 166, 213, 242, 124, 24, 239, 15, 167, 227, 131, 97, 180, 239, 220, 88, 56, 28, 150, 62, 102, 123, 82, 231, 226, 225, 238, 112, 214, 12, 78, 102, 14, 104, 239, 29, 140, 167, 142, 19, 22, 216, 177, 43, 152, 30, 57, 182, 163, 28, 143, 26, 44, 227, 82, 169, 33, 86, 39, 203, 192, 111, 65, 106, 98, 74, 149, 85, 247, 83, 112, 254, 236, 210, 120, 97, 32, 56, 255, 113, 25, 0, 254, 23, 86, 85, 170, 218, 31, 156, 72, 133, 227, 169, 116, 56, 30, 79, 143, 77, 140, 176, 50, 4, 128, 123, 163, 108, 32, 107, 246, 5, 1, 144, 193, 108, 31, 73, 199, 69, 53, 248, 109, 224, 182, 250, 250, 50, 60, 138, 242, 203, 82, 36, 148, 58, 23, 32, 251, 248, 35, 235, 143, 76, 76, 169, 210, 55, 190, 16, 0, 202, 135, 17, 217, 40, 43, 1, 112, 206, 231, 251, 145, 91, 5, 64, 24, 111, 60, 136, 66, 34, 120, 21, 23, 210, 89, 93, 61, 236, 122, 159, 239, 135, 102, 22, 65, 69, 0, 24, 57, 2, 64, 135, 32, 12, 15, 127, 192, 123, 131, 35, 233, 32, 42, 51, 197, 9, 227, 109, 117, 40, 149, 24, 208, 48, 16, 118, 17, 107, 205, 76, 169, 178, 89, 149, 203, 135, 96, 114, 105, 135, 211, 37, 0, 146, 62, 127, 109, 173, 10, 128, 4, 248, 129, 241, 48, 7, 93, 95, 23, 12, 174, 17, 0, 240, 250, 252, 248, 1, 168, 172, 79, 65, 56, 192, 184, 158, 105, 207, 8, 90, 142, 227, 42, 41, 24, 74, 39, 186, 126, 94, 127, 244, 18, 241, 6, 182, 85, 202, 74, 10, 36, 2, 0, 106, 77, 166, 62, 43, 181, 155, 129, 121, 116, 214, 225, 146, 0, 136, 85, 85, 53, 28, 137, 199, 130, 114, 9, 72, 143, 12, 6, 24, 175, 15, 13, 21, 212, 185, 249, 246, 31, 192, 15, 34, 30, 50, 19, 238, 66, 171, 234, 67, 89, 124, 218, 16, 31, 73, 196, 70, 82, 120, 140, 44, 190, 129, 97, 142, 130, 71, 76, 20, 225, 187, 184, 170, 66, 229, 103, 209, 15, 204, 181, 62, 12, 110, 45, 157, 53, 245, 169, 55, 147, 156, 118, 232, 204, 207, 32, 0, 64, 63, 199, 143, 4, 162, 201, 81, 188, 215, 172, 136, 65, 56, 29, 110, 199, 131, 215, 93, 7, 188, 13, 110, 55, 242, 19, 249, 1, 144, 204, 0, 176, 177, 211, 3, 177, 24, 43, 0, 160, 126, 22, 121, 124, 138, 166, 96, 52, 124, 248, 251, 223, 66, 76, 124, 234, 4, 195, 180, 92, 73, 167, 199, 203, 73, 142, 184, 4, 35, 192, 75, 129, 251, 222, 90, 212, 169, 149, 235, 169, 127, 180, 206, 181, 90, 43, 179, 201, 128, 222, 234, 33, 78, 189, 249, 41, 150, 64, 87, 207, 57, 188, 201, 104, 116, 207, 158, 61, 135, 130, 225, 4, 216, 188, 17, 164, 225, 99, 225, 32, 0, 192, 111, 13, 226, 99, 200, 128, 17, 107, 98, 236, 146, 59, 43, 80, 148, 145, 206, 151, 213, 243, 104, 103, 229, 108, 40, 43, 251, 244, 212, 169, 83, 63, 223, 202, 124, 8, 79, 114, 154, 32, 240, 121, 81, 15, 146, 76, 22, 155, 154, 82, 193, 157, 162, 144, 91, 251, 240, 3, 84, 150, 135, 208, 42, 30, 143, 110, 251, 25, 11, 3, 156, 62, 6, 162, 30, 254, 225, 63, 8, 161, 80, 42, 45, 236, 189, 155, 78, 200, 212, 144, 159, 5, 202, 58, 134, 31, 19, 155, 127, 118, 0, 129, 17, 31, 32, 218, 156, 150, 126, 93, 103, 12, 99, 37, 106, 255, 169, 83, 27, 183, 50, 39, 225, 119, 143, 23, 144, 52, 249, 47, 183, 158, 59, 183, 189, 188, 92, 33, 203, 184, 229, 84, 214, 113, 116, 13, 7, 160, 74, 124, 189, 19, 1, 128, 224, 68, 26, 153, 248, 213, 210, 160, 72, 176, 59, 1, 50, 1, 129, 96, 122, 196, 228, 88, 141, 64, 1, 212, 254, 56, 252, 119, 54, 134, 218, 207, 68, 79, 35, 36, 122, 149, 39, 233, 196, 167, 181, 239, 96, 0, 78, 173, 116, 215, 165, 78, 28, 171, 175, 183, 146, 49, 212, 207, 203, 17, 51, 124, 110, 180, 26, 86, 6, 162, 213, 17, 136, 126, 255, 99, 0, 70, 112, 251, 15, 172, 126, 90, 30, 9, 160, 138, 149, 20, 88, 0, 214, 159, 203, 202, 135, 66, 247, 15, 240, 12, 192, 120, 79, 35, 4, 98, 140, 194, 1, 211, 141, 79, 223, 39, 8, 84, 51, 107, 208, 130, 5, 141, 203, 126, 41, 27, 71, 255, 101, 165, 116, 49, 37, 30, 50, 147, 10, 0, 163, 217, 196, 180, 101, 79, 55, 25, 23, 66, 145, 64, 157, 4, 192, 1, 47, 14, 147, 123, 15, 29, 49, 207, 3, 129, 129, 179, 137, 211, 146, 4, 32, 37, 200, 13, 182, 54, 4, 17, 0, 180, 195, 197, 216, 93, 178, 86, 168, 107, 222, 214, 236, 32, 8, 108, 45, 248, 41, 74, 145, 126, 80, 90, 36, 13, 164, 127, 190, 74, 93, 17, 146, 107, 250, 156, 54, 154, 152, 84, 101, 129, 136, 15, 107, 125, 247, 234, 23, 54, 211, 50, 19, 216, 14, 161, 26, 23, 237, 61, 114, 196, 116, 229, 78, 44, 142, 26, 61, 16, 71, 93, 94, 211, 20, 59, 132, 74, 144, 26, 236, 118, 135, 131, 127, 4, 84, 181, 42, 172, 0, 45, 30, 36, 90, 115, 24, 3, 176, 235, 175, 152, 70, 240, 6, 210, 31, 210, 18, 19, 124, 190, 76, 29, 0, 100, 94, 34, 74, 67, 198, 115, 211, 156, 150, 9, 52, 36, 2, 202, 239, 149, 7, 159, 126, 238, 53, 121, 40, 224, 227, 2, 94, 63, 123, 164, 215, 100, 205, 74, 59, 102, 123, 6, 47, 179, 222, 78, 87, 57, 92, 168, 229, 180, 211, 30, 166, 101, 206, 135, 176, 6, 28, 37, 30, 100, 180, 242, 247, 208, 254, 79, 127, 187, 113, 141, 247, 232, 85, 240, 142, 90, 192, 131, 218, 203, 215, 83, 124, 188, 140, 86, 114, 128, 113, 146, 65, 143, 104, 131, 73, 186, 232, 43, 75, 16, 187, 54, 93, 7, 190, 79, 202, 163, 160, 235, 5, 0, 114, 210, 127, 188, 244, 15, 28, 114, 224, 181, 199, 241, 242, 175, 85, 46, 123, 69, 171, 130, 129, 92, 120, 230, 190, 195, 249, 4, 122, 67, 169, 239, 113, 224, 212, 169, 223, 131, 71, 116, 247, 93, 117, 187, 222, 79, 167, 175, 180, 48, 237, 7, 86, 45, 187, 143, 48, 65, 193, 92, 101, 81, 152, 53, 203, 243, 200, 57, 68, 91, 47, 40, 35, 75, 23, 121, 66, 250, 193, 7, 159, 123, 142, 127, 14, 30, 129, 44, 63, 33, 167, 118, 196, 248, 136, 5, 154, 28, 221, 241, 24, 230, 25, 36, 115, 180, 189, 53, 22, 67, 85, 70, 78, 178, 16, 115, 21, 89, 146, 27, 254, 232, 175, 127, 183, 235, 20, 118, 138, 255, 226, 174, 234, 75, 96, 12, 47, 181, 128, 137, 102, 126, 244, 49, 65, 224, 147, 162, 101, 114, 8, 172, 70, 143, 66, 134, 21, 30, 120, 88, 58, 217, 227, 200, 180, 160, 132, 80, 35, 132, 106, 68, 127, 204, 191, 230, 57, 32, 91, 171, 37, 130, 238, 7, 229, 151, 56, 27, 174, 177, 215, 156, 29, 136, 113, 14, 180, 208, 151, 179, 102, 61, 21, 59, 27, 179, 87, 52, 245, 186, 28, 104, 137, 89, 135, 11, 55, 27, 61, 153, 83, 71, 38, 225, 247, 220, 124, 110, 224, 235, 247, 128, 71, 56, 241, 105, 253, 110, 232, 11, 102, 153, 80, 101, 247, 121, 145, 172, 91, 75, 141, 100, 128, 12, 43, 216, 100, 57, 85, 35, 3, 72, 72, 0, 224, 21, 0, 224, 21, 254, 245, 1, 148, 140, 48, 191, 249, 17, 145, 254, 112, 83, 141, 179, 2, 248, 31, 58, 60, 214, 228, 168, 169, 168, 176, 55, 57, 192, 44, 0, 48, 77, 200, 9, 116, 56, 232, 42, 162, 138, 112, 17, 23, 216, 3, 81, 49, 181, 15, 12, 4, 24, 118, 0, 213, 166, 59, 151, 16, 4, 254, 252, 59, 45, 87, 143, 173, 92, 185, 178, 112, 59, 200, 103, 229, 125, 66, 129, 217, 54, 19, 202, 95, 51, 172, 224, 201, 92, 80, 45, 0, 240, 28, 0, 240, 63, 228, 95, 152, 222, 252, 168, 247, 244, 217, 193, 214, 38, 123, 205, 238, 134, 216, 233, 32, 64, 16, 27, 136, 67, 147, 155, 226, 206, 138, 110, 2, 13, 118, 8, 92, 18, 211, 123, 156, 104, 114, 164, 244, 137, 255, 44, 98, 160, 1, 228, 49, 58, 239, 249, 58, 143, 192, 83, 251, 241, 228, 194, 93, 93, 72, 68, 87, 149, 243, 38, 81, 25, 223, 233, 5, 60, 218, 77, 54, 140, 21, 160, 2, 128, 213, 200, 8, 200, 191, 48, 187, 208, 52, 210, 126, 173, 53, 173, 113, 104, 193, 64, 47, 93, 21, 24, 136, 215, 216, 29, 85, 235, 99, 3, 53, 208, 114, 248, 50, 129, 151, 109, 166, 21, 15, 235, 66, 171, 29, 161, 79, 208, 204, 64, 28, 59, 116, 55, 57, 223, 232, 6, 165, 233, 185, 231, 47, 4, 4, 156, 208, 254, 75, 151, 90, 176, 142, 166, 151, 217, 176, 42, 216, 171, 112, 129, 221, 58, 131, 164, 224, 93, 254, 200, 42, 31, 86, 112, 100, 169, 168, 23, 0, 32, 70, 64, 5, 128, 137, 200, 155, 3, 237, 23, 182, 199, 207, 246, 114, 16, 247, 208, 85, 206, 67, 65, 59, 252, 228, 166, 205, 76, 140, 183, 11, 113, 189, 171, 104, 59, 222, 187, 3, 44, 165, 195, 225, 26, 168, 169, 176, 87, 212, 160, 119, 40, 87, 123, 23, 153, 76, 115, 248, 166, 197, 246, 122, 80, 133, 87, 63, 228, 47, 41, 221, 198, 123, 68, 242, 219, 104, 215, 166, 5, 7, 211, 74, 201, 135, 21, 208, 100, 220, 76, 105, 116, 11, 17, 114, 153, 17, 224, 201, 76, 209, 81, 128, 67, 142, 127, 83, 69, 24, 115, 57, 131, 54, 31, 112, 56, 60, 85, 78, 215, 179, 224, 231, 6, 101, 14, 161, 154, 208, 98, 128, 100, 233, 117, 52, 70, 229, 168, 105, 170, 113, 160, 5, 250, 113, 137, 231, 44, 1, 129, 77, 40, 54, 78, 167, 5, 4, 108, 122, 195, 231, 154, 122, 65, 136, 20, 209, 226, 27, 148, 248, 1, 174, 158, 213, 123, 120, 94, 201, 89, 214, 111, 238, 5, 187, 37, 55, 2, 60, 101, 217, 252, 168, 61, 24, 27, 72, 160, 6, 182, 58, 226, 103, 99, 138, 86, 122, 92, 216, 207, 133, 72, 48, 62, 32, 64, 163, 36, 143, 67, 62, 37, 216, 131, 108, 132, 211, 89, 133, 192, 112, 185, 28, 51, 239, 230, 17, 216, 220, 130, 172, 97, 250, 40, 169, 238, 94, 166, 85, 2, 186, 219, 10, 96, 135, 129, 146, 127, 162, 207, 2, 188, 146, 179, 4, 27, 80, 43, 218, 101, 70, 64, 32, 74, 103, 173, 46, 145, 216, 10, 187, 189, 6, 91, 126, 187, 189, 233, 172, 238, 24, 57, 195, 190, 17, 12, 170, 35, 65, 66, 180, 203, 35, 215, 76, 85, 14, 59, 63, 5, 202, 227, 116, 84, 85, 221, 38, 32, 240, 90, 203, 133, 116, 250, 100, 125, 61, 70, 64, 23, 0, 157, 220, 231, 92, 225, 231, 179, 16, 175, 228, 44, 103, 131, 3, 241, 193, 179, 175, 169, 141, 0, 147, 57, 238, 10, 84, 216, 91, 227, 21, 224, 243, 87, 84, 56, 237, 78, 157, 225, 1, 116, 29, 253, 132, 179, 166, 187, 23, 47, 93, 154, 153, 104, 112, 20, 248, 147, 122, 89, 127, 59, 115, 27, 239, 15, 220, 180, 179, 225, 12, 94, 189, 109, 63, 124, 243, 194, 39, 230, 170, 168, 180, 143, 237, 210, 76, 203, 198, 167, 16, 37, 103, 57, 27, 235, 61, 205, 177, 26, 35, 160, 123, 39, 145, 2, 187, 155, 90, 43, 104, 187, 19, 148, 57, 27, 220, 173, 103, 103, 208, 117, 244, 19, 246, 138, 112, 76, 59, 43, 92, 75, 158, 42, 212, 253, 222, 134, 134, 166, 166, 26, 59, 136, 211, 109, 164, 112, 98, 199, 77, 180, 23, 215, 15, 93, 58, 238, 101, 106, 145, 67, 180, 45, 235, 173, 244, 238, 174, 107, 8, 121, 37, 103, 65, 66, 238, 213, 26, 1, 38, 35, 0, 193, 138, 65, 224, 237, 42, 82, 150, 109, 188, 146, 37, 112, 118, 171, 62, 252, 122, 228, 173, 169, 104, 106, 109, 114, 217, 145, 47, 59, 133, 32, 80, 182, 146, 102, 32, 58, 6, 65, 56, 211, 194, 32, 51, 240, 75, 237, 64, 99, 109, 182, 73, 180, 180, 193, 96, 42, 81, 114, 22, 16, 114, 118, 192, 167, 49, 2, 25, 137, 181, 135, 73, 150, 195, 248, 71, 105, 228, 242, 33, 59, 103, 188, 20, 190, 154, 2, 103, 207, 114, 3, 12, 253, 162, 29, 124, 217, 245, 127, 86, 198, 15, 151, 212, 50, 251, 143, 33, 85, 120, 229, 67, 148, 28, 250, 132, 216, 65, 239, 59, 89, 72, 118, 91, 227, 133, 213, 40, 164, 228, 44, 20, 218, 27, 253, 53, 0, 224, 199, 6, 167, 105, 137, 109, 106, 61, 155, 25, 0, 180, 207, 136, 211, 229, 82, 237, 198, 147, 141, 218, 113, 252, 249, 140, 235, 153, 222, 96, 128, 190, 137, 47, 28, 248, 182, 155, 105, 56, 137, 51, 148, 133, 146, 29, 124, 167, 231, 252, 197, 12, 116, 190, 71, 66, 128, 54, 14, 133, 40, 116, 176, 80, 49, 191, 50, 18, 200, 78, 193, 10, 100, 56, 178, 229, 9, 240, 6, 76, 224, 220, 184, 170, 114, 42, 117, 0, 223, 171, 97, 224, 244, 0, 237, 184, 137, 175, 162, 252, 246, 90, 134, 249, 16, 60, 130, 147, 24, 128, 251, 150, 189, 0, 183, 123, 39, 99, 251, 1, 1, 9, 128, 12, 43, 235, 81, 232, 96, 193, 71, 77, 36, 144, 137, 2, 224, 248, 36, 194, 167, 201, 27, 65, 1, 232, 55, 146, 246, 120, 80, 8, 12, 114, 128, 215, 62, 174, 50, 49, 211, 21, 28, 47, 79, 239, 217, 196, 233, 88, 195, 15, 26, 249, 210, 129, 53, 12, 211, 114, 230, 82, 253, 18, 62, 46, 222, 182, 140, 126, 39, 115, 251, 47, 94, 124, 199, 45, 60, 144, 195, 120, 189, 97, 220, 116, 226, 10, 63, 173, 99, 4, 228, 212, 219, 59, 112, 182, 162, 162, 34, 140, 123, 189, 117, 119, 156, 56, 120, 52, 74, 181, 224, 77, 79, 156, 153, 153, 157, 44, 0, 2, 241, 95, 70, 167, 148, 16, 242, 189, 60, 103, 7, 226, 30, 186, 106, 169, 80, 60, 1, 170, 144, 249, 121, 125, 153, 56, 235, 104, 89, 118, 0, 74, 121, 39, 57, 203, 210, 138, 2, 0, 122, 70, 64, 70, 120, 168, 35, 30, 107, 173, 104, 5, 171, 30, 176, 199, 79, 227, 8, 15, 140, 128, 48, 95, 17, 26, 150, 69, 215, 129, 135, 227, 180, 103, 123, 24, 68, 216, 247, 98, 79, 163, 210, 19, 215, 221, 59, 4, 49, 0, 229, 95, 215, 88, 40, 0, 240, 46, 15, 64, 79, 79, 207, 137, 139, 23, 251, 91, 250, 209, 155, 225, 174, 139, 253, 240, 65, 79, 63, 225, 128, 130, 101, 182, 242, 109, 69, 203, 150, 102, 91, 112, 26, 3, 160, 141, 4, 20, 196, 138, 35, 29, 142, 214, 1, 174, 169, 105, 16, 15, 250, 32, 35, 151, 109, 237, 78, 57, 33, 119, 207, 148, 69, 160, 196, 131, 107, 165, 192, 3, 72, 12, 152, 159, 22, 9, 217, 33, 2, 192, 240, 87, 110, 191, 253, 241, 139, 61, 55, 255, 229, 205, 61, 240, 238, 111, 190, 114, 177, 238, 246, 219, 111, 255, 74, 53, 6, 192, 182, 23, 159, 249, 193, 134, 108, 187, 153, 96, 0, 50, 27, 1, 60, 214, 49, 120, 54, 145, 56, 59, 208, 10, 109, 174, 24, 224, 67, 252, 172, 145, 166, 154, 170, 28, 14, 51, 128, 81, 226, 129, 97, 214, 240, 67, 231, 135, 113, 64, 224, 221, 255, 206, 55, 113, 142, 148, 0, 112, 226, 86, 116, 252, 203, 234, 139, 239, 220, 126, 241, 98, 203, 237, 55, 163, 119, 253, 55, 15, 99, 0, 8, 82, 61, 63, 253, 224, 203, 119, 51, 215, 31, 99, 0, 50, 27, 1, 212, 96, 28, 246, 12, 180, 54, 57, 80, 206, 11, 236, 53, 142, 165, 60, 246, 170, 44, 9, 35, 245, 160, 138, 203, 97, 98, 198, 63, 37, 30, 128, 182, 226, 242, 137, 141, 63, 61, 117, 170, 5, 49, 65, 3, 25, 58, 38, 0, 116, 221, 186, 21, 204, 1, 116, 255, 240, 205, 23, 251, 111, 237, 255, 10, 250, 236, 241, 133, 23, 37, 0, 78, 252, 116, 255, 255, 70, 213, 167, 25, 195, 97, 116, 200, 108, 4, 184, 179, 97, 62, 180, 63, 221, 234, 116, 116, 3, 16, 113, 228, 72, 2, 75, 59, 179, 37, 140, 180, 163, 74, 154, 125, 234, 50, 16, 126, 110, 247, 183, 55, 238, 216, 88, 246, 193, 169, 83, 167, 14, 175, 92, 195, 7, 197, 60, 0, 253, 143, 175, 189, 253, 214, 139, 55, 131, 208, 127, 229, 226, 237, 45, 23, 17, 0, 231, 111, 238, 151, 0, 248, 205, 150, 159, 158, 192, 167, 91, 51, 252, 8, 6, 32, 139, 17, 128, 46, 63, 61, 128, 178, 59, 103, 195, 221, 21, 177, 248, 217, 112, 47, 10, 37, 160, 251, 179, 38, 140, 180, 0, 208, 235, 209, 142, 100, 56, 117, 235, 201, 166, 56, 121, 90, 178, 114, 139, 189, 6, 15, 154, 28, 61, 192, 44, 251, 92, 2, 0, 209, 87, 250, 111, 237, 1, 190, 127, 231, 230, 199, 31, 255, 202, 218, 139, 23, 215, 254, 37, 111, 5, 190, 252, 188, 124, 217, 11, 149, 15, 148, 139, 3, 43, 25, 1, 200, 98, 4, 8, 129, 167, 26, 70, 201, 187, 4, 86, 0, 79, 129, 73, 203, 158, 48, 210, 2, 128, 66, 144, 103, 237, 142, 23, 55, 87, 57, 157, 230, 182, 156, 64, 171, 101, 219, 143, 147, 113, 179, 83, 191, 91, 137, 151, 229, 224, 149, 32, 48, 193, 87, 206, 255, 205, 227, 23, 171, 111, 239, 217, 186, 117, 43, 82, 127, 88, 29, 98, 37, 184, 138, 70, 243, 135, 233, 101, 216, 118, 22, 105, 238, 138, 22, 181, 5, 238, 253, 206, 83, 24, 128, 7, 201, 148, 233, 44, 20, 59, 11, 234, 159, 29, 136, 159, 109, 71, 254, 165, 195, 204, 44, 53, 45, 0, 56, 10, 135, 48, 17, 173, 1, 110, 206, 67, 68, 190, 134, 235, 196, 41, 129, 14, 223, 107, 43, 47, 34, 0, 84, 223, 124, 235, 205, 213, 32, 253, 183, 146, 102, 131, 8, 84, 223, 46, 248, 1, 140, 144, 13, 197, 44, 163, 137, 162, 239, 249, 90, 93, 221, 254, 27, 143, 31, 255, 238, 119, 17, 0, 166, 35, 129, 40, 10, 0, 118, 7, 161, 83, 92, 230, 118, 203, 214, 7, 0, 56, 135, 118, 152, 114, 10, 48, 85, 65, 75, 182, 226, 113, 51, 66, 159, 117, 173, 228, 69, 224, 124, 191, 192, 9, 26, 71, 8, 93, 136, 245, 45, 206, 163, 148, 51, 175, 241, 104, 215, 181, 180, 124, 120, 252, 248, 153, 178, 63, 135, 24, 243, 198, 116, 186, 123, 158, 133, 9, 133, 94, 121, 240, 191, 239, 219, 103, 106, 183, 242, 129, 179, 3, 177, 128, 131, 232, 49, 42, 83, 194, 200, 16, 0, 49, 213, 248, 162, 9, 109, 200, 243, 8, 90, 15, 140, 94, 179, 81, 130, 224, 84, 118, 79, 16, 93, 140, 183, 123, 124, 247, 203, 47, 255, 247, 159, 89, 44, 55, 237, 255, 240, 248, 201, 163, 101, 101, 95, 175, 72, 143, 204, 156, 233, 56, 250, 39, 245, 245, 103, 0, 128, 196, 76, 75, 36, 18, 217, 247, 252, 235, 251, 126, 33, 174, 251, 153, 137, 144, 75, 24, 115, 100, 24, 224, 204, 10, 128, 140, 115, 64, 184, 179, 4, 203, 188, 201, 228, 211, 14, 91, 187, 62, 21, 1, 200, 26, 12, 157, 57, 246, 210, 99, 141, 199, 206, 156, 193, 102, 115, 239, 183, 230, 164, 107, 194, 233, 121, 63, 249, 250, 159, 60, 118, 227, 200, 188, 238, 176, 229, 83, 0, 224, 228, 148, 137, 244, 200, 84, 4, 192, 235, 0, 0, 90, 1, 48, 251, 46, 136, 200, 18, 8, 249, 191, 73, 2, 32, 231, 28, 136, 148, 204, 56, 135, 98, 2, 85, 228, 130, 143, 178, 133, 195, 61, 233, 227, 143, 253, 164, 254, 100, 186, 7, 87, 29, 46, 121, 236, 63, 167, 29, 21, 233, 165, 63, 248, 47, 255, 165, 254, 91, 77, 179, 103, 87, 180, 158, 252, 147, 250, 198, 15, 230, 125, 109, 233, 148, 187, 16, 0, 175, 2, 0, 230, 54, 54, 64, 67, 61, 66, 150, 243, 26, 0, 144, 93, 87, 133, 85, 129, 65, 82, 137, 111, 184, 228, 206, 211, 75, 86, 254, 142, 32, 144, 57, 31, 210, 51, 145, 254, 159, 24, 0, 82, 121, 107, 253, 171, 155, 38, 154, 22, 164, 43, 190, 123, 247, 215, 235, 191, 53, 239, 7, 55, 253, 96, 118, 221, 13, 232, 158, 247, 124, 237, 59, 12, 2, 224, 249, 231, 247, 237, 51, 185, 19, 238, 64, 123, 76, 55, 205, 107, 64, 217, 1, 32, 123, 197, 85, 233, 234, 3, 126, 80, 75, 53, 146, 122, 96, 199, 41, 57, 253, 254, 112, 99, 215, 174, 149, 43, 215, 108, 5, 203, 86, 215, 178, 255, 167, 141, 141, 199, 46, 92, 186, 116, 229, 202, 79, 30, 171, 175, 255, 244, 106, 57, 63, 174, 250, 159, 246, 206, 189, 97, 203, 77, 95, 123, 118, 10, 243, 212, 93, 204, 119, 190, 118, 143, 20, 190, 3, 0, 161, 231, 65, 7, 202, 23, 250, 98, 154, 59, 141, 164, 33, 199, 21, 62, 76, 0, 128, 138, 103, 92, 14, 93, 73, 112, 160, 202, 86, 90, 179, 109, 138, 123, 229, 198, 223, 158, 82, 211, 239, 127, 119, 226, 231, 27, 87, 174, 220, 234, 93, 179, 102, 205, 214, 58, 111, 93, 245, 15, 182, 212, 1, 217, 248, 248, 241, 107, 95, 126, 121, 235, 159, 81, 79, 209, 207, 104, 166, 227, 1, 0, 191, 120, 254, 213, 125, 191, 16, 90, 220, 44, 44, 158, 79, 86, 13, 15, 25, 66, 97, 138, 76, 21, 88, 225, 5, 239, 116, 85, 1, 10, 230, 117, 22, 85, 5, 125, 120, 224, 215, 26, 8, 68, 40, 142, 30, 254, 117, 207, 174, 149, 119, 175, 172, 6, 48, 10, 164, 21, 108, 222, 165, 116, 167, 227, 89, 4, 35, 128, 85, 64, 115, 91, 40, 212, 214, 204, 239, 140, 237, 107, 238, 232, 8, 73, 203, 130, 162, 253, 35, 180, 235, 226, 93, 7, 0, 112, 164, 236, 241, 232, 68, 73, 168, 200, 198, 32, 158, 119, 175, 92, 121, 216, 16, 131, 83, 167, 62, 253, 201, 63, 17, 48, 94, 46, 218, 198, 231, 81, 62, 182, 46, 169, 213, 153, 142, 103, 17, 140, 64, 51, 191, 67, 54, 179, 89, 177, 51, 184, 79, 185, 52, 50, 15, 133, 217, 157, 96, 76, 151, 216, 185, 132, 90, 42, 5, 217, 237, 85, 25, 246, 75, 217, 186, 181, 235, 119, 191, 55, 0, 224, 131, 159, 124, 32, 190, 254, 245, 46, 108, 11, 203, 127, 255, 219, 223, 125, 216, 184, 113, 227, 198, 158, 30, 56, 108, 252, 136, 63, 90, 120, 35, 16, 226, 55, 8, 71, 132, 23, 196, 247, 117, 192, 31, 111, 91, 73, 200, 108, 111, 235, 81, 46, 53, 134, 58, 188, 142, 246, 140, 201, 104, 38, 107, 215, 172, 92, 89, 214, 115, 226, 183, 159, 170, 1, 248, 167, 159, 200, 63, 194, 170, 112, 111, 201, 191, 232, 65, 101, 33, 70, 32, 36, 91, 30, 91, 100, 246, 80, 91, 40, 178, 175, 252, 143, 5, 192, 100, 182, 139, 227, 175, 92, 179, 100, 229, 202, 246, 234, 141, 59, 14, 11, 202, 241, 184, 140, 1, 128, 248, 245, 204, 182, 149, 232, 2, 128, 141, 0, 209, 125, 161, 230, 54, 53, 179, 231, 248, 40, 170, 186, 154, 140, 0, 52, 135, 58, 178, 251, 158, 230, 137, 162, 107, 183, 62, 190, 243, 31, 87, 2, 117, 237, 127, 172, 241, 176, 76, 58, 118, 9, 203, 153, 149, 107, 33, 176, 96, 35, 176, 19, 115, 62, 218, 222, 69, 209, 126, 153, 168, 155, 91, 149, 67, 93, 87, 147, 17, 0, 159, 108, 247, 137, 235, 64, 138, 130, 134, 218, 173, 91, 215, 48, 117, 24, 140, 247, 55, 30, 62, 92, 184, 87, 40, 180, 42, 223, 165, 1, 96, 223, 243, 175, 50, 191, 0, 77, 23, 242, 169, 181, 157, 212, 126, 183, 201, 109, 130, 213, 117, 53, 153, 69, 32, 148, 211, 14, 236, 89, 73, 138, 50, 92, 138, 196, 27, 237, 222, 186, 213, 109, 21, 107, 205, 202, 75, 176, 250, 67, 170, 112, 199, 142, 30, 80, 130, 175, 3, 7, 116, 134, 152, 72, 71, 179, 74, 6, 38, 241, 12, 234, 186, 154, 108, 0, 248, 24, 166, 35, 196, 215, 227, 153, 47, 75, 51, 252, 117, 33, 202, 208, 171, 139, 170, 20, 43, 111, 63, 47, 95, 134, 75, 150, 200, 122, 213, 150, 200, 171, 171, 193, 10, 242, 58, 48, 212, 38, 110, 15, 147, 195, 174, 103, 18, 169, 235, 106, 178, 139, 64, 165, 184, 216, 172, 233, 178, 52, 67, 162, 196, 131, 254, 94, 110, 54, 17, 130, 34, 41, 71, 102, 193, 70, 128, 102, 120, 37, 8, 215, 19, 125, 216, 1, 93, 51, 137, 103, 80, 165, 73, 50, 2, 128, 182, 168, 177, 189, 37, 178, 154, 217, 178, 52, 99, 162, 248, 131, 203, 104, 189, 221, 23, 108, 31, 139, 16, 8, 229, 86, 150, 16, 0, 240, 58, 136, 64, 167, 248, 36, 33, 193, 22, 78, 242, 33, 100, 105, 146, 44, 50, 30, 10, 213, 122, 69, 99, 51, 137, 181, 176, 180, 191, 141, 15, 25, 134, 3, 37, 8, 126, 115, 31, 249, 29, 203, 62, 20, 9, 244, 97, 119, 143, 180, 184, 243, 26, 116, 128, 58, 212, 201, 2, 0, 90, 140, 215, 139, 119, 37, 107, 158, 212, 90, 88, 250, 148, 185, 52, 244, 71, 54, 97, 136, 177, 8, 143, 25, 89, 94, 121, 254, 213, 215, 127, 1, 26, 95, 210, 250, 161, 63, 26, 0, 60, 193, 111, 33, 69, 104, 42, 203, 104, 130, 178, 148, 134, 74, 16, 148, 35, 4, 44, 96, 4, 94, 111, 131, 30, 144, 56, 158, 168, 131, 206, 146, 131, 154, 75, 179, 47, 240, 50, 41, 0, 218, 120, 222, 163, 76, 100, 25, 179, 147, 137, 100, 251, 42, 178, 224, 51, 154, 166, 206, 88, 94, 125, 254, 199, 208, 100, 89, 62, 144, 55, 132, 109, 109, 90, 0, 178, 47, 240, 162, 34, 147, 118, 190, 141, 88, 66, 74, 60, 92, 11, 101, 29, 16, 71, 180, 108, 175, 48, 96, 98, 121, 254, 249, 231, 94, 87, 4, 184, 136, 1, 246, 234, 43, 64, 177, 18, 219, 108, 205, 71, 70, 0, 52, 27, 132, 83, 226, 225, 90, 72, 103, 122, 172, 154, 94, 176, 149, 111, 251, 146, 31, 48, 65, 0, 236, 67, 42, 144, 79, 124, 144, 238, 47, 210, 245, 1, 118, 174, 167, 231, 22, 96, 101, 237, 182, 154, 91, 250, 47, 19, 0, 252, 196, 54, 90, 194, 129, 18, 15, 58, 103, 155, 117, 147, 76, 48, 128, 52, 25, 105, 155, 8, 0, 67, 218, 223, 204, 155, 128, 131, 50, 14, 160, 221, 5, 86, 235, 189, 232, 105, 55, 231, 188, 192, 139, 9, 17, 40, 54, 55, 1, 204, 180, 155, 148, 189, 46, 107, 149, 180, 210, 51, 230, 128, 213, 178, 145, 113, 201, 17, 38, 44, 64, 163, 77, 220, 11, 138, 209, 42, 166, 52, 169, 94, 201, 109, 129, 151, 235, 231, 235, 155, 117, 147, 244, 118, 111, 83, 81, 185, 216, 254, 79, 176, 14, 80, 140, 140, 119, 10, 94, 128, 94, 42, 144, 87, 209, 148, 249, 199, 54, 6, 192, 6, 200, 154, 191, 143, 105, 55, 41, 203, 244, 16, 68, 149, 216, 21, 122, 247, 221, 143, 127, 89, 142, 28, 98, 196, 1, 138, 145, 241, 80, 38, 39, 128, 18, 15, 230, 232, 250, 113, 128, 89, 55, 41, 251, 86, 76, 171, 144, 9, 44, 170, 173, 37, 235, 171, 0, 0, 170, 145, 113, 25, 0, 248, 148, 74, 177, 30, 187, 178, 88, 9, 128, 137, 133, 192, 175, 99, 184, 107, 202, 77, 210, 203, 172, 50, 170, 217, 95, 88, 5, 72, 227, 229, 8, 0, 61, 14, 232, 68, 73, 128, 218, 226, 2, 171, 108, 117, 74, 155, 141, 66, 127, 40, 242, 206, 204, 220, 69, 93, 0, 116, 106, 252, 205, 16, 101, 194, 77, 210, 115, 2, 155, 85, 226, 236, 70, 54, 96, 175, 216, 233, 42, 29, 192, 111, 149, 25, 41, 57, 88, 104, 45, 40, 45, 80, 237, 247, 89, 44, 111, 179, 219, 132, 37, 212, 5, 32, 151, 53, 228, 101, 68, 137, 7, 67, 210, 157, 33, 220, 172, 206, 59, 225, 9, 233, 226, 220, 27, 4, 192, 43, 242, 179, 121, 59, 248, 205, 125, 122, 191, 80, 169, 93, 151, 36, 35, 233, 1, 96, 184, 182, 88, 22, 162, 196, 131, 33, 121, 244, 18, 1, 94, 245, 168, 183, 114, 238, 141, 229, 121, 101, 137, 32, 223, 254, 66, 131, 124, 72, 165, 233, 45, 47, 201, 143, 235, 124, 54, 217, 69, 210, 40, 241, 96, 68, 166, 156, 96, 177, 100, 130, 39, 203, 171, 223, 87, 104, 65, 180, 77, 108, 251, 222, 162, 156, 243, 193, 250, 116, 93, 115, 126, 89, 41, 67, 30, 64, 78, 181, 123, 229, 50, 96, 121, 125, 245, 106, 69, 137, 28, 234, 255, 162, 76, 99, 95, 250, 171, 164, 234, 147, 2, 0, 19, 177, 228, 53, 145, 73, 6, 224, 101, 64, 176, 3, 150, 125, 207, 43, 230, 139, 33, 37, 176, 173, 232, 96, 196, 56, 99, 15, 158, 241, 228, 60, 65, 228, 199, 254, 227, 195, 215, 184, 82, 162, 49, 209, 38, 162, 32, 66, 216, 16, 10, 165, 115, 150, 78, 0, 64, 46, 3, 33, 99, 79, 80, 32, 243, 170, 80, 1, 0, 242, 99, 31, 40, 205, 176, 88, 232, 181, 145, 105, 6, 224, 89, 0, 47, 100, 238, 101, 45, 12, 146, 1, 153, 39, 112, 240, 160, 106, 72, 68, 135, 240, 162, 62, 102, 72, 14, 0, 246, 99, 231, 22, 23, 103, 88, 44, 244, 90, 40, 235, 166, 83, 50, 90, 69, 138, 7, 107, 233, 202, 150, 207, 44, 204, 62, 185, 33, 116, 91, 75, 66, 38, 210, 97, 86, 164, 7, 10, 138, 179, 106, 3, 57, 0, 216, 143, 253, 230, 3, 59, 65, 4, 220, 185, 45, 128, 97, 134, 52, 237, 207, 24, 63, 147, 85, 25, 10, 42, 183, 191, 191, 213, 194, 180, 65, 52, 32, 148, 73, 66, 208, 151, 93, 2, 152, 98, 50, 105, 153, 174, 204, 26, 209, 200, 1, 192, 126, 172, 21, 199, 146, 116, 49, 127, 93, 78, 91, 232, 102, 34, 151, 176, 165, 154, 72, 25, 227, 231, 157, 100, 184, 180, 232, 179, 0, 42, 149, 125, 85, 244, 134, 43, 11, 219, 120, 71, 96, 115, 46, 131, 52, 165, 214, 98, 3, 165, 32, 0, 80, 90, 92, 74, 252, 88, 171, 232, 72, 163, 67, 78, 27, 11, 101, 34, 157, 85, 226, 50, 199, 207, 165, 100, 164, 108, 219, 42, 4, 192, 235, 0, 192, 43, 248, 227, 131, 17, 33, 35, 16, 50, 64, 79, 127, 89, 115, 128, 64, 127, 205, 60, 2, 192, 214, 194, 173, 216, 129, 122, 192, 10, 29, 78, 201, 239, 150, 161, 77, 185, 145, 182, 236, 54, 75, 252, 188, 138, 100, 133, 62, 94, 6, 0, 52, 63, 255, 60, 177, 3, 72, 246, 249, 124, 64, 200, 0, 61, 155, 17, 203, 10, 161, 145, 102, 127, 5, 166, 13, 109, 69, 206, 63, 19, 125, 29, 114, 126, 90, 210, 157, 154, 153, 45, 126, 94, 69, 6, 140, 63, 71, 181, 194, 175, 255, 253, 243, 207, 253, 143, 182, 237, 88, 249, 9, 99, 2, 6, 232, 217, 178, 233, 61, 205, 254, 10, 29, 82, 122, 129, 18, 15, 215, 151, 156, 106, 249, 199, 148, 45, 126, 126, 129, 232, 1, 0, 32, 100, 253, 230, 125, 223, 188, 175, 112, 47, 30, 28, 108, 70, 142, 0, 99, 136, 94, 105, 54, 165, 165, 218, 95, 193, 237, 38, 128, 254, 1, 125, 98, 151, 145, 40, 81, 153, 226, 103, 224, 209, 90, 188, 96, 153, 5, 60, 223, 123, 161, 253, 184, 86, 54, 20, 226, 213, 255, 228, 6, 105, 80, 180, 172, 20, 61, 183, 149, 38, 28, 48, 217, 253, 163, 178, 147, 177, 253, 167, 196, 131, 14, 225, 33, 14, 180, 58, 11, 0, 240, 86, 209, 235, 247, 61, 143, 170, 165, 101, 37, 49, 212, 164, 6, 105, 74, 173, 74, 209, 195, 46, 99, 232, 15, 200, 1, 59, 41, 104, 191, 81, 120, 65, 137, 7, 29, 34, 67, 28, 171, 182, 33, 0, 58, 75, 66, 115, 81, 157, 20, 174, 143, 48, 115, 177, 49, 65, 139, 101, 204, 195, 111, 8, 229, 109, 235, 184, 166, 106, 203, 12, 180, 249, 217, 165, 78, 195, 60, 61, 37, 30, 180, 196, 47, 54, 211, 118, 208, 6, 0, 20, 118, 70, 150, 253, 61, 174, 23, 55, 119, 113, 38, 170, 180, 74, 204, 67, 91, 105, 157, 142, 247, 50, 62, 115, 165, 81, 102, 130, 199, 103, 170, 190, 107, 207, 184, 23, 129, 17, 145, 33, 142, 205, 145, 182, 77, 150, 208, 189, 224, 252, 239, 251, 251, 87, 65, 6, 66, 146, 164, 82, 226, 65, 73, 181, 217, 226, 160, 90, 197, 165, 106, 0, 124, 29, 109, 145, 80, 134, 72, 83, 241, 140, 217, 7, 34, 161, 31, 159, 181, 47, 205, 184, 23, 129, 1, 97, 30, 245, 118, 118, 108, 166, 45, 86, 84, 163, 17, 34, 181, 130, 38, 200, 109, 205, 118, 6, 37, 30, 20, 0, 248, 80, 69, 16, 95, 124, 98, 242, 25, 179, 236, 52, 65, 250, 241, 217, 165, 246, 73, 68, 216, 152, 71, 193, 221, 67, 21, 34, 88, 75, 163, 106, 81, 115, 53, 65, 217, 13, 97, 173, 21, 252, 94, 178, 176, 189, 0, 0, 95, 130, 141, 142, 222, 144, 57, 0, 228, 75, 66, 129, 163, 137, 126, 149, 86, 255, 52, 238, 71, 231, 210, 73, 237, 69, 64, 161, 26, 45, 10, 215, 10, 35, 66, 117, 34, 160, 5, 38, 81, 23, 165, 67, 16, 235, 240, 65, 142, 8, 64, 68, 70, 230, 76, 162, 114, 32, 146, 164, 160, 11, 84, 142, 56, 238, 71, 143, 157, 154, 204, 67, 82, 72, 20, 41, 17, 128, 208, 125, 196, 14, 92, 31, 4, 68, 82, 3, 224, 99, 154, 59, 76, 90, 68, 221, 129, 200, 98, 117, 240, 73, 161, 209, 176, 39, 38, 243, 104, 181, 208, 126, 247, 125, 229, 229, 60, 0, 145, 85, 127, 253, 250, 206, 95, 252, 193, 0, 16, 38, 33, 228, 112, 169, 56, 16, 153, 49, 235, 64, 161, 69, 26, 168, 73, 60, 89, 40, 178, 189, 152, 46, 32, 158, 32, 166, 95, 204, 253, 239, 175, 239, 51, 249, 132, 58, 27, 94, 26, 144, 0, 64, 155, 207, 155, 43, 0, 162, 54, 205, 104, 119, 40, 241, 144, 27, 117, 130, 203, 91, 252, 77, 228, 10, 255, 63, 107, 43, 43, 245, 20, 183, 135, 247, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130]; diff --git a/test/tools/tile_server/static/generated/sea.dart b/test/tools/tile_server/static/generated/sea.dart new file mode 100644 index 00000000..f3707c9e --- /dev/null +++ b/test/tools/tile_server/static/generated/sea.dart @@ -0,0 +1 @@ +final seaTileBytes = [137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 1, 0, 0, 0, 1, 0, 1, 3, 0, 0, 0, 102, 188, 58, 37, 0, 0, 0, 3, 80, 76, 84, 69, 170, 211, 223, 207, 236, 188, 245, 0, 0, 0, 31, 73, 68, 65, 84, 104, 129, 237, 193, 1, 13, 0, 0, 0, 194, 160, 247, 79, 109, 14, 55, 160, 0, 0, 0, 0, 0, 0, 0, 0, 190, 13, 33, 0, 0, 1, 154, 96, 225, 213, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130]; diff --git a/test/tools/tile_server/static/favicon.ico b/test/tools/tile_server/static/source/favicon.ico similarity index 100% rename from test/tools/tile_server/static/favicon.ico rename to test/tools/tile_server/static/source/favicon.ico diff --git a/test/tools/tile_server/static/tiles/land.png b/test/tools/tile_server/static/source/land.png similarity index 100% rename from test/tools/tile_server/static/tiles/land.png rename to test/tools/tile_server/static/source/land.png diff --git a/test/tools/tile_server/static/tiles/sea.png b/test/tools/tile_server/static/source/sea.png similarity index 100% rename from test/tools/tile_server/static/tiles/sea.png rename to test/tools/tile_server/static/source/sea.png From b7a80370f66b5130aa416aba33c74e03f17d634c Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 27 Jul 2023 22:24:04 +0100 Subject: [PATCH 045/168] Fixed lints Former-commit-id: 83842f0fcb4a894b234977c64c233980dc40e5ad [formerly fbaba685420092bbae23c66739b04a8b826cdcf2] Former-commit-id: f5e8e44d6f8f2bf7bc51d4cb4a1b3fb724b9c43a --- .../tile_server/static/generated/favicon.dart | 11746 +++++++++- .../tile_server/static/generated/land.dart | 18692 +++++++++++++++- .../tile_server/static/generated/sea.dart | 106 +- 3 files changed, 30541 insertions(+), 3 deletions(-) diff --git a/test/tools/tile_server/static/generated/favicon.dart b/test/tools/tile_server/static/generated/favicon.dart index 259e34b4..537d61a9 100644 --- a/test/tools/tile_server/static/generated/favicon.dart +++ b/test/tools/tile_server/static/generated/favicon.dart @@ -1 +1,11745 @@ -final faviconTileBytes = [0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 32, 0, 201, 45, 0, 0, 22, 0, 0, 0, 137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 1, 0, 0, 0, 1, 0, 8, 6, 0, 0, 0, 92, 114, 168, 102, 0, 0, 45, 144, 73, 68, 65, 84, 120, 218, 237, 157, 123, 124, 27, 229, 153, 239, 127, 26, 141, 70, 23, 91, 146, 109, 249, 126, 139, 147, 216, 9, 185, 185, 9, 16, 72, 40, 183, 64, 129, 246, 208, 238, 129, 237, 210, 82, 122, 202, 161, 91, 2, 11, 45, 44, 108, 161, 13, 112, 246, 0, 165, 52, 180, 205, 46, 109, 225, 244, 190, 103, 217, 148, 101, 89, 216, 93, 232, 133, 179, 101, 11, 9, 105, 32, 9, 73, 73, 76, 156, 224, 196, 151, 92, 124, 209, 197, 182, 44, 75, 178, 46, 51, 26, 73, 231, 15, 89, 178, 36, 75, 214, 109, 164, 153, 145, 222, 239, 231, 147, 79, 28, 105, 52, 126, 53, 153, 231, 55, 207, 251, 188, 207, 243, 188, 10, 254, 163, 23, 195, 40, 0, 191, 215, 9, 243, 88, 63, 204, 163, 199, 97, 29, 59, 1, 149, 190, 25, 205, 61, 215, 160, 121, 213, 39, 80, 219, 177, 9, 52, 83, 93, 200, 233, 9, 4, 66, 17, 81, 20, 42, 0, 201, 204, 76, 157, 195, 196, 249, 62, 88, 198, 250, 225, 152, 49, 163, 174, 227, 34, 52, 245, 92, 131, 166, 238, 109, 208, 55, 174, 18, 251, 251, 18, 8, 132, 56, 4, 23, 128, 120, 120, 158, 133, 101, 52, 226, 29, 152, 199, 250, 17, 166, 52, 104, 90, 181, 13, 77, 221, 219, 208, 216, 125, 37, 241, 14, 8, 4, 145, 201, 73, 0, 38, 236, 62, 184, 61, 172, 216, 99, 134, 182, 190, 153, 76, 47, 8, 4, 1, 160, 179, 61, 112, 194, 238, 131, 66, 93, 139, 21, 203, 87, 138, 61, 102, 76, 155, 71, 224, 24, 59, 134, 134, 149, 87, 136, 61, 20, 2, 65, 214, 80, 217, 30, 232, 246, 176, 168, 111, 21, 223, 248, 1, 160, 190, 117, 37, 124, 211, 86, 177, 135, 65, 32, 200, 158, 172, 5, 128, 64, 32, 148, 31, 178, 22, 0, 235, 224, 31, 192, 115, 115, 98, 15, 131, 64, 144, 45, 89, 199, 0, 164, 200, 200, 219, 79, 227, 200, 244, 121, 212, 117, 94, 130, 166, 158, 107, 208, 176, 242, 10, 24, 155, 214, 138, 61, 44, 2, 65, 54, 228, 45, 0, 111, 255, 231, 191, 225, 143, 111, 255, 22, 0, 160, 209, 106, 209, 220, 186, 12, 87, 95, 127, 19, 150, 175, 92, 19, 59, 230, 223, 94, 252, 49, 250, 251, 222, 143, 253, 123, 109, 239, 197, 248, 252, 237, 247, 1, 0, 206, 142, 12, 224, 159, 255, 225, 239, 97, 53, 143, 97, 237, 134, 139, 241, 63, 255, 234, 155, 168, 170, 210, 3, 0, 142, 31, 61, 128, 87, 95, 252, 9, 156, 179, 51, 184, 120, 203, 85, 184, 253, 174, 111, 164, 28, 195, 117, 55, 61, 6, 158, 103, 97, 29, 63, 9, 203, 232, 126, 28, 57, 240, 99, 4, 194, 10, 52, 245, 92, 131, 198, 149, 87, 162, 177, 231, 106, 48, 154, 26, 177, 175, 49, 129, 32, 89, 242, 22, 0, 135, 125, 18, 77, 45, 29, 248, 243, 47, 109, 199, 156, 195, 137, 211, 39, 251, 240, 204, 223, 222, 139, 47, 221, 245, 16, 46, 191, 250, 70, 0, 192, 208, 233, 227, 184, 230, 147, 159, 197, 138, 158, 200, 83, 89, 171, 173, 2, 0, 112, 156, 31, 127, 247, 212, 131, 248, 226, 95, 62, 136, 222, 139, 46, 195, 203, 255, 247, 7, 120, 101, 247, 243, 248, 242, 61, 143, 192, 238, 180, 227, 255, 236, 122, 12, 247, 239, 248, 30, 58, 186, 186, 241, 179, 103, 255, 55, 222, 248, 143, 221, 184, 241, 207, 111, 79, 253, 5, 104, 53, 218, 187, 46, 68, 123, 215, 133, 0, 0, 183, 203, 6, 243, 249, 15, 49, 241, 254, 79, 113, 236, 245, 7, 161, 111, 90, 131, 166, 149, 87, 161, 185, 231, 19, 168, 237, 188, 72, 236, 235, 77, 32, 72, 138, 130, 166, 0, 90, 93, 21, 154, 27, 151, 1, 141, 64, 247, 234, 94, 116, 116, 117, 227, 185, 239, 237, 192, 37, 151, 93, 11, 134, 209, 192, 53, 235, 192, 138, 158, 181, 232, 88, 214, 157, 240, 185, 161, 83, 253, 48, 24, 106, 176, 245, 202, 27, 0, 0, 55, 221, 126, 47, 118, 220, 125, 51, 190, 248, 149, 7, 209, 119, 96, 47, 214, 125, 108, 51, 214, 245, 110, 6, 0, 252, 217, 95, 124, 25, 191, 124, 254, 219, 105, 5, 32, 25, 189, 161, 9, 171, 55, 92, 143, 213, 27, 174, 7, 207, 179, 176, 219, 206, 96, 226, 124, 31, 250, 254, 253, 85, 120, 125, 30, 52, 173, 186, 38, 226, 33, 116, 95, 5, 117, 85, 189, 216, 215, 159, 64, 16, 21, 65, 99, 0, 189, 23, 94, 6, 138, 82, 98, 232, 84, 63, 214, 245, 110, 134, 219, 237, 68, 223, 159, 246, 227, 248, 7, 7, 208, 209, 213, 141, 222, 11, 47, 3, 0, 88, 39, 206, 163, 61, 78, 20, 76, 70, 19, 0, 192, 53, 235, 128, 213, 60, 138, 214, 182, 174, 216, 123, 237, 93, 61, 152, 180, 78, 228, 247, 229, 104, 53, 154, 218, 214, 160, 169, 109, 13, 112, 217, 23, 224, 247, 58, 49, 113, 190, 15, 230, 99, 187, 113, 252, 119, 223, 132, 198, 216, 129, 166, 85, 215, 162, 105, 229, 85, 36, 177, 136, 80, 145, 8, 30, 4, 172, 51, 53, 194, 237, 114, 0, 0, 62, 245, 103, 183, 197, 94, 255, 247, 151, 126, 134, 55, 127, 251, 47, 120, 248, 241, 231, 224, 247, 121, 161, 102, 212, 9, 159, 211, 234, 244, 112, 187, 103, 193, 113, 44, 106, 106, 23, 158, 204, 85, 85, 122, 4, 2, 28, 60, 30, 119, 44, 70, 144, 47, 26, 157, 17, 43, 215, 92, 133, 149, 107, 174, 2, 0, 76, 217, 134, 97, 29, 59, 129, 83, 255, 185, 3, 179, 179, 54, 152, 150, 109, 65, 211, 170, 107, 208, 188, 234, 90, 84, 213, 46, 43, 222, 85, 39, 16, 36, 130, 224, 2, 48, 61, 101, 129, 222, 80, 11, 0, 9, 110, 251, 117, 159, 254, 28, 190, 122, 251, 245, 24, 59, 63, 140, 106, 67, 13, 216, 179, 131, 9, 159, 243, 121, 221, 208, 84, 49, 208, 235, 141, 240, 121, 23, 150, 246, 60, 30, 55, 0, 20, 108, 252, 169, 104, 104, 234, 70, 67, 83, 55, 54, 92, 124, 19, 252, 94, 39, 108, 230, 83, 176, 12, 255, 30, 251, 247, 124, 15, 148, 218, 128, 166, 85, 159, 64, 211, 170, 109, 168, 239, 218, 74, 188, 3, 66, 89, 66, 191, 251, 135, 31, 163, 181, 179, 23, 173, 29, 27, 160, 209, 25, 11, 58, 217, 161, 119, 255, 11, 20, 165, 68, 207, 5, 27, 22, 189, 199, 48, 26, 104, 117, 122, 240, 124, 0, 109, 29, 93, 120, 243, 55, 47, 197, 222, 179, 59, 237, 224, 88, 22, 166, 186, 54, 52, 183, 47, 195, 209, 247, 247, 197, 222, 27, 63, 55, 132, 150, 214, 206, 162, 95, 8, 141, 206, 136, 101, 221, 151, 98, 89, 247, 165, 0, 0, 215, 172, 5, 227, 231, 142, 225, 204, 158, 157, 56, 50, 117, 22, 198, 214, 141, 104, 238, 185, 6, 77, 171, 175, 37, 75, 141, 132, 178, 129, 174, 93, 115, 51, 206, 13, 239, 197, 7, 7, 94, 65, 85, 149, 30, 45, 29, 189, 104, 237, 236, 141, 204, 155, 51, 224, 243, 122, 96, 157, 60, 15, 231, 148, 29, 253, 199, 14, 225, 205, 223, 189, 140, 237, 247, 63, 30, 9, 0, 186, 28, 24, 57, 221, 143, 85, 107, 55, 1, 0, 222, 121, 243, 53, 168, 213, 106, 180, 117, 44, 7, 195, 104, 16, 8, 112, 120, 247, 157, 55, 176, 105, 243, 149, 120, 125, 247, 143, 113, 233, 229, 215, 129, 97, 52, 216, 180, 249, 74, 188, 244, 15, 207, 226, 228, 241, 35, 232, 232, 234, 198, 111, 254, 237, 31, 99, 193, 194, 82, 98, 168, 105, 193, 218, 141, 45, 88, 187, 241, 191, 197, 150, 26, 173, 227, 135, 113, 228, 240, 47, 17, 8, 43, 208, 184, 242, 42, 52, 245, 108, 67, 195, 138, 203, 73, 48, 145, 32, 91, 20, 46, 135, 45, 86, 13, 232, 24, 253, 0, 214, 161, 183, 96, 27, 217, 7, 183, 109, 0, 77, 173, 23, 160, 181, 243, 99, 104, 237, 236, 197, 248, 172, 10, 43, 214, 127, 60, 246, 193, 228, 60, 128, 182, 142, 21, 216, 118, 195, 159, 199, 34, 254, 30, 143, 27, 255, 240, 252, 83, 56, 63, 114, 26, 148, 82, 137, 229, 221, 107, 241, 185, 47, 125, 21, 245, 141, 45, 0, 128, 177, 243, 195, 248, 167, 159, 125, 23, 147, 86, 51, 46, 88, 183, 41, 33, 15, 224, 228, 241, 35, 120, 249, 133, 31, 97, 110, 206, 137, 141, 23, 95, 142, 47, 220, 113, 63, 24, 70, 147, 48, 240, 51, 39, 222, 195, 5, 157, 53, 162, 92, 180, 57, 215, 84, 164, 196, 121, 244, 67, 216, 204, 167, 200, 82, 35, 65, 182, 36, 8, 64, 60, 172, 103, 26, 83, 103, 222, 133, 109, 104, 47, 172, 131, 111, 99, 195, 182, 199, 19, 4, 64, 108, 196, 20, 128, 100, 108, 19, 3, 176, 140, 245, 195, 60, 250, 33, 60, 30, 55, 76, 203, 183, 160, 169, 123, 27, 154, 87, 93, 11, 173, 177, 77, 236, 225, 17, 8, 105, 73, 43, 0, 201, 140, 190, 255, 42, 17, 128, 44, 32, 45, 210, 8, 114, 66, 214, 181, 0, 82, 68, 163, 51, 98, 197, 234, 203, 177, 98, 245, 229, 0, 22, 90, 164, 157, 126, 243, 49, 56, 102, 204, 48, 45, 219, 130, 198, 149, 87, 144, 22, 105, 4, 73, 64, 4, 160, 200, 212, 53, 116, 161, 174, 161, 11, 27, 46, 190, 105, 161, 69, 218, 249, 119, 112, 224, 221, 231, 72, 139, 52, 130, 232, 100, 61, 5, 152, 26, 217, 15, 85, 8, 146, 104, 10, 50, 109, 30, 65, 152, 117, 160, 205, 164, 21, 123, 40, 5, 17, 93, 106, 180, 140, 245, 99, 218, 54, 2, 99, 75, 47, 154, 87, 125, 2, 77, 221, 87, 195, 216, 186, 161, 240, 95, 64, 32, 100, 32, 107, 1, 224, 185, 57, 56, 198, 142, 73, 162, 19, 143, 190, 74, 141, 38, 35, 5, 154, 86, 23, 126, 50, 137, 192, 243, 44, 166, 204, 167, 99, 241, 3, 63, 199, 161, 169, 251, 42, 82, 183, 64, 40, 42, 89, 11, 128, 80, 36, 47, 53, 54, 182, 172, 70, 107, 231, 6, 180, 116, 108, 128, 161, 166, 69, 236, 235, 33, 25, 230, 92, 83, 176, 77, 124, 20, 105, 177, 62, 126, 18, 213, 166, 110, 52, 175, 254, 4, 26, 86, 92, 137, 250, 229, 91, 197, 30, 30, 161, 76, 40, 185, 0, 196, 195, 249, 103, 49, 125, 230, 0, 172, 131, 111, 193, 54, 180, 7, 84, 152, 71, 107, 199, 6, 180, 117, 109, 68, 115, 251, 186, 178, 122, 194, 23, 202, 148, 109, 24, 230, 115, 125, 48, 143, 126, 136, 57, 247, 52, 76, 43, 34, 129, 196, 166, 158, 109, 208, 213, 116, 136, 61, 60, 130, 76, 17, 85, 0, 146, 113, 218, 62, 194, 212, 200, 126, 216, 134, 246, 96, 102, 244, 48, 234, 234, 151, 161, 165, 115, 3, 90, 59, 122, 81, 215, 208, 37, 246, 240, 36, 67, 116, 169, 209, 58, 118, 2, 214, 241, 147, 80, 86, 53, 160, 113, 197, 21, 164, 110, 129, 144, 51, 146, 18, 128, 120, 120, 110, 14, 211, 231, 14, 98, 106, 228, 93, 88, 135, 246, 32, 224, 182, 162, 181, 179, 23, 205, 29, 235, 5, 169, 91, 40, 39, 102, 166, 206, 193, 60, 118, 28, 150, 209, 126, 204, 144, 22, 105, 132, 28, 144, 172, 0, 36, 227, 157, 29, 131, 109, 104, 47, 108, 195, 123, 97, 63, 179, 31, 213, 250, 250, 72, 154, 114, 215, 70, 52, 52, 117, 23, 254, 11, 202, 132, 133, 22, 105, 253, 176, 140, 245, 147, 22, 105, 132, 37, 145, 141, 0, 36, 51, 125, 246, 32, 166, 206, 252, 17, 214, 211, 111, 97, 206, 62, 140, 150, 246, 117, 104, 110, 95, 143, 214, 206, 94, 84, 27, 26, 196, 30, 158, 100, 136, 182, 72, 51, 143, 246, 99, 210, 114, 154, 212, 45, 16, 18, 144, 173, 0, 196, 195, 122, 166, 49, 57, 188, 15, 182, 161, 61, 176, 13, 239, 131, 134, 97, 98, 37, 206, 13, 173, 171, 73, 48, 113, 158, 248, 22, 105, 150, 209, 227, 164, 69, 26, 161, 60, 4, 32, 25, 167, 185, 31, 182, 225, 119, 96, 29, 124, 11, 78, 203, 113, 212, 55, 173, 68, 75, 199, 6, 180, 119, 109, 34, 75, 141, 113, 196, 90, 164, 141, 246, 195, 58, 113, 146, 180, 72, 171, 64, 202, 82, 0, 226, 225, 185, 57, 76, 14, 255, 17, 182, 225, 189, 176, 13, 238, 133, 34, 228, 71, 107, 199, 6, 180, 118, 246, 162, 165, 115, 3, 241, 14, 226, 136, 182, 72, 51, 159, 239, 35, 45, 210, 42, 132, 178, 23, 128, 100, 220, 147, 131, 17, 49, 152, 95, 106, 172, 53, 117, 160, 165, 99, 3, 218, 150, 109, 36, 75, 141, 113, 68, 91, 164, 153, 199, 142, 195, 114, 254, 120, 66, 139, 52, 125, 195, 42, 208, 76, 21, 153, 50, 148, 1, 21, 39, 0, 241, 68, 211, 155, 173, 131, 111, 193, 58, 180, 7, 156, 219, 130, 150, 121, 239, 128, 44, 53, 38, 50, 51, 117, 14, 214, 137, 143, 96, 62, 255, 33, 188, 115, 118, 176, 172, 7, 1, 206, 15, 5, 165, 4, 173, 214, 67, 165, 174, 134, 74, 173, 135, 82, 93, 13, 70, 87, 3, 90, 165, 131, 74, 99, 4, 163, 53, 66, 169, 210, 65, 165, 53, 130, 102, 170, 160, 210, 26, 64, 171, 170, 161, 82, 87, 131, 214, 26, 136, 144, 136, 76, 69, 11, 64, 50, 62, 231, 4, 172, 131, 111, 71, 150, 26, 207, 30, 202, 185, 69, 90, 37, 194, 243, 44, 120, 206, 143, 0, 239, 71, 128, 245, 33, 192, 249, 16, 8, 248, 192, 177, 94, 4, 56, 47, 2, 129, 200, 235, 28, 235, 137, 252, 204, 249, 16, 224, 188, 224, 3, 126, 176, 108, 228, 239, 69, 66, 162, 49, 66, 165, 49, 128, 214, 84, 167, 20, 18, 149, 198, 0, 90, 93, 5, 149, 218, 0, 90, 163, 135, 74, 173, 7, 173, 209, 147, 37, 206, 60, 32, 2, 176, 4, 75, 181, 72, 35, 75, 141, 194, 146, 74, 72, 22, 68, 195, 11, 142, 245, 130, 15, 176, 139, 132, 36, 192, 122, 193, 5, 252, 224, 3, 126, 112, 172, 7, 180, 74, 3, 74, 85, 181, 72, 72, 84, 234, 136, 183, 65, 132, 36, 17, 34, 0, 89, 18, 223, 34, 109, 114, 100, 31, 84, 138, 48, 90, 58, 54, 160, 165, 115, 3, 169, 91, 144, 16, 81, 33, 225, 184, 136, 96, 68, 133, 132, 99, 61, 224, 121, 118, 222, 51, 137, 122, 42, 139, 133, 132, 99, 61, 224, 3, 126, 208, 42, 13, 104, 141, 17, 180, 90, 63, 239, 133, 44, 8, 9, 163, 173, 1, 205, 232, 202, 66, 72, 136, 0, 228, 137, 211, 246, 17, 108, 167, 223, 134, 117, 104, 15, 156, 230, 62, 152, 26, 150, 163, 117, 217, 199, 208, 220, 182, 150, 4, 19, 203, 128, 120, 33, 97, 89, 15, 120, 214, 11, 158, 231, 82, 10, 9, 199, 121, 192, 7, 184, 140, 66, 162, 154, 23, 7, 37, 163, 75, 16, 18, 149, 182, 6, 74, 149, 54, 38, 36, 106, 77, 29, 40, 181, 22, 140, 198, 56, 31, 59, 41, 222, 114, 44, 17, 0, 1, 136, 214, 45, 216, 6, 247, 194, 54, 248, 22, 66, 172, 11, 45, 203, 122, 209, 218, 209, 139, 166, 214, 11, 72, 48, 177, 130, 89, 74, 72, 88, 214, 131, 32, 207, 45, 8, 9, 235, 141, 136, 9, 231, 3, 199, 249, 22, 4, 39, 224, 7, 205, 84, 129, 86, 87, 47, 18, 18, 70, 91, 27, 241, 56, 210, 8, 9, 173, 209, 71, 188, 147, 52, 66, 66, 4, 160, 8, 120, 28, 231, 35, 193, 196, 193, 61, 176, 159, 63, 132, 154, 154, 38, 180, 46, 219, 136, 230, 142, 245, 164, 110, 129, 144, 23, 169, 132, 132, 227, 124, 243, 193, 212, 136, 144, 112, 172, 7, 1, 214, 139, 0, 239, 143, 196, 76, 230, 133, 36, 242, 26, 155, 82, 72, 136, 0, 20, 153, 232, 82, 163, 109, 100, 31, 108, 131, 111, 195, 239, 28, 67, 115, 219, 58, 180, 118, 70, 114, 15, 136, 119, 64, 40, 37, 60, 207, 194, 239, 117, 33, 20, 226, 193, 178, 30, 34, 0, 165, 198, 231, 156, 192, 244, 185, 67, 145, 186, 133, 193, 61, 208, 105, 171, 208, 210, 217, 139, 150, 246, 117, 164, 110, 129, 80, 114, 136, 0, 136, 12, 105, 145, 70, 16, 19, 34, 0, 18, 130, 243, 207, 98, 114, 232, 29, 76, 142, 252, 17, 182, 161, 61, 100, 169, 145, 80, 116, 136, 0, 72, 24, 210, 34, 141, 80, 108, 136, 0, 200, 132, 132, 165, 198, 161, 61, 8, 249, 103, 209, 220, 190, 142, 180, 72, 35, 20, 4, 17, 0, 153, 226, 113, 156, 143, 52, 65, 33, 45, 210, 8, 5, 64, 4, 160, 76, 152, 62, 123, 16, 214, 161, 183, 49, 53, 188, 47, 214, 34, 173, 109, 217, 70, 52, 181, 173, 37, 117, 11, 132, 180, 16, 1, 40, 67, 72, 139, 52, 66, 182, 16, 1, 168, 0, 72, 139, 52, 66, 58, 136, 0, 84, 24, 164, 69, 26, 33, 30, 34, 0, 21, 78, 66, 139, 180, 177, 15, 80, 91, 215, 74, 90, 164, 85, 16, 68, 0, 8, 49, 146, 91, 164, 5, 220, 86, 52, 119, 172, 71, 93, 67, 23, 12, 53, 205, 208, 85, 213, 129, 86, 169, 193, 48, 58, 208, 140, 134, 120, 11, 101, 0, 17, 0, 66, 90, 162, 117, 11, 179, 230, 227, 112, 79, 143, 128, 157, 155, 66, 192, 239, 68, 128, 157, 67, 40, 176, 80, 239, 206, 168, 171, 192, 168, 52, 80, 169, 117, 80, 49, 58, 168, 24, 45, 84, 42, 77, 228, 111, 70, 11, 38, 250, 250, 252, 177, 42, 70, 75, 132, 68, 34, 16, 1, 32, 20, 4, 231, 159, 5, 239, 119, 35, 192, 186, 231, 255, 118, 129, 103, 61, 8, 248, 93, 8, 248, 156, 8, 6, 188, 224, 124, 78, 240, 156, 39, 242, 158, 127, 46, 242, 222, 188, 144, 240, 172, 27, 225, 80, 16, 42, 70, 19, 17, 147, 20, 66, 194, 168, 171, 34, 130, 145, 66, 72, 84, 106, 45, 84, 180, 134, 8, 73, 158, 16, 1, 32, 72, 2, 214, 51, 13, 158, 243, 68, 254, 204, 11, 73, 192, 231, 138, 8, 71, 156, 144, 4, 252, 78, 240, 1, 47, 56, 239, 44, 130, 156, 55, 173, 144, 168, 213, 58, 208, 42, 77, 76, 72, 212, 234, 170, 200, 191, 213, 81, 239, 68, 23, 17, 20, 149, 182, 162, 133, 132, 8, 0, 161, 172, 136, 9, 137, 207, 21, 17, 134, 192, 92, 130, 144, 112, 126, 103, 76, 56, 162, 158, 73, 144, 157, 139, 8, 9, 231, 77, 16, 18, 245, 188, 231, 17, 21, 17, 38, 234, 149, 48, 90, 208, 140, 166, 44, 132, 132, 8, 0, 129, 144, 4, 207, 205, 33, 24, 240, 39, 8, 73, 128, 117, 130, 103, 61, 17, 1, 137, 254, 157, 78, 72, 230, 167, 68, 0, 192, 196, 98, 32, 218, 121, 143, 100, 177, 144, 48, 76, 220, 20, 71, 21, 141, 155, 84, 129, 166, 153, 162, 215, 120, 16, 1, 32, 16, 138, 68, 182, 66, 18, 152, 23, 19, 206, 231, 4, 207, 186, 17, 240, 187, 16, 228, 60, 105, 133, 68, 197, 232, 98, 65, 212, 168, 88, 40, 85, 76, 94, 66, 66, 4, 128, 64, 144, 56, 201, 66, 194, 249, 102, 17, 96, 221, 8, 6, 124, 25, 133, 132, 103, 61, 145, 159, 211, 8, 9, 17, 0, 2, 161, 66, 136, 10, 9, 231, 153, 65, 136, 103, 193, 249, 102, 137, 0, 16, 8, 149, 12, 37, 246, 0, 8, 4, 130, 120, 16, 1, 32, 16, 42, 24, 34, 0, 4, 66, 5, 67, 4, 128, 64, 168, 96, 136, 0, 16, 8, 21, 12, 17, 0, 2, 161, 130, 161, 197, 30, 0, 129, 16, 101, 192, 113, 2, 78, 191, 3, 0, 64, 81, 74, 208, 97, 26, 148, 82, 1, 53, 173, 5, 173, 160, 161, 85, 234, 208, 174, 239, 20, 123, 152, 101, 5, 201, 3, 32, 136, 206, 128, 227, 4, 166, 125, 147, 240, 243, 190, 172, 142, 167, 20, 20, 40, 5, 5, 5, 148, 80, 82, 20, 40, 80, 80, 42, 148, 160, 40, 37, 40, 80, 160, 41, 37, 104, 74, 5, 37, 69, 67, 163, 212, 194, 200, 212, 160, 78, 99, 18, 251, 107, 74, 18, 226, 1, 16, 68, 35, 87, 195, 143, 18, 10, 135, 16, 10, 135, 0, 240, 8, 132, 178, 255, 28, 77, 69, 110, 119, 74, 161, 140, 136, 72, 156, 112, 68, 189, 13, 154, 82, 65, 173, 212, 84, 140, 183, 65, 60, 0, 66, 201, 201, 215, 240, 197, 32, 87, 111, 163, 90, 85, 141, 6, 109, 147, 216, 195, 206, 26, 226, 1, 16, 74, 134, 156, 12, 63, 74, 185, 123, 27, 196, 3, 32, 20, 29, 57, 26, 190, 24, 228, 228, 109, 64, 141, 106, 141, 161, 96, 111, 131, 120, 0, 132, 162, 81, 206, 134, 207, 7, 120, 76, 78, 78, 161, 177, 177, 1, 180, 138, 70, 40, 20, 134, 195, 49, 3, 214, 207, 65, 167, 211, 193, 104, 212, 67, 65, 229, 182, 202, 158, 179, 183, 17, 169, 240, 205, 217, 219, 48, 168, 141, 48, 48, 145, 254, 0, 196, 3, 32, 8, 78, 57, 27, 126, 148, 57, 183, 27, 44, 199, 65, 205, 48, 168, 214, 235, 49, 235, 112, 2, 0, 12, 70, 3, 92, 78, 23, 0, 160, 166, 86, 186, 59, 54, 71, 189, 13, 146, 8, 68, 16, 140, 1, 199, 9, 236, 55, 239, 193, 184, 251, 124, 89, 27, 63, 0, 120, 61, 126, 24, 13, 70, 120, 61, 254, 200, 191, 189, 94, 24, 140, 6, 80, 148, 2, 6, 163, 1, 94, 175, 87, 236, 33, 46, 73, 40, 28, 2, 31, 226, 201, 20, 128, 80, 56, 149, 240, 196, 143, 135, 15, 240, 80, 170, 40, 208, 42, 26, 74, 21, 5, 62, 192, 47, 58, 134, 202, 209, 253, 23, 11, 34, 0, 132, 188, 17, 218, 240, 139, 49, 175, 46, 6, 126, 191, 15, 106, 134, 1, 0, 168, 25, 6, 126, 191, 15, 58, 157, 14, 46, 167, 43, 54, 5, 208, 104, 52, 98, 15, 51, 43, 196, 191, 154, 4, 217, 81, 44, 87, 223, 239, 247, 65, 173, 137, 24, 20, 0, 184, 156, 46, 40, 41, 26, 205, 45, 205, 0, 0, 167, 211, 45, 246, 87, 7, 16, 113, 255, 53, 26, 45, 0, 64, 163, 209, 194, 235, 241, 195, 96, 52, 32, 24, 226, 97, 181, 88, 17, 12, 241, 48, 24, 13, 98, 15, 51, 43, 136, 7, 64, 200, 154, 98, 187, 250, 94, 143, 31, 117, 166, 90, 204, 216, 29, 168, 214, 235, 225, 245, 122, 209, 220, 210, 28, 155, 87, 91, 45, 86, 73, 4, 214, 248, 96, 196, 83, 137, 135, 162, 20, 48, 153, 76, 48, 79, 88, 96, 50, 201, 39, 237, 152, 8, 0, 33, 35, 165, 152, 227, 203, 105, 94, 221, 218, 214, 2, 0, 48, 79, 88, 98, 63, 203, 21, 34, 0, 132, 180, 148, 50, 184, 87, 78, 243, 106, 57, 65, 4, 128, 176, 8, 49, 162, 250, 81, 247, 31, 136, 204, 171, 103, 236, 14, 212, 55, 214, 195, 225, 152, 129, 213, 98, 133, 90, 195, 160, 182, 182, 78, 236, 75, 83, 118, 144, 68, 32, 66, 12, 49, 151, 243, 204, 19, 150, 69, 175, 149, 147, 171, 45, 4, 233, 86, 69, 146, 87, 79, 114, 129, 120, 0, 4, 73, 172, 227, 19, 99, 207, 204, 194, 170, 72, 29, 92, 78, 23, 156, 78, 55, 106, 106, 141, 9, 171, 39, 213, 42, 125, 78, 231, 148, 70, 84, 133, 32, 10, 149, 148, 185, 87, 14, 164, 203, 54, 76, 206, 74, 204, 5, 226, 1, 84, 32, 82, 120, 226, 19, 10, 135, 154, 119, 255, 147, 87, 79, 114, 153, 6, 144, 24, 64, 5, 65, 12, 95, 222, 164, 42, 56, 162, 233, 136, 19, 95, 173, 215, 99, 206, 237, 142, 253, 156, 45, 68, 0, 42, 0, 98, 248, 229, 65, 124, 16, 48, 186, 42, 50, 61, 57, 141, 58, 83, 45, 104, 21, 13, 62, 192, 99, 198, 238, 64, 99, 115, 67, 194, 231, 194, 161, 16, 154, 221, 141, 9, 175, 241, 170, 32, 0, 50, 5, 40, 107, 136, 225, 151, 23, 169, 178, 13, 83, 101, 37, 46, 194, 27, 68, 107, 221, 10, 212, 183, 45, 8, 3, 207, 205, 97, 206, 21, 34, 2, 80, 142, 16, 195, 175, 28, 178, 90, 61, 209, 41, 17, 240, 37, 222, 11, 52, 83, 141, 154, 122, 226, 1, 148, 21, 196, 240, 9, 169, 80, 80, 20, 248, 32, 159, 242, 61, 34, 0, 101, 0, 49, 124, 66, 190, 16, 1, 144, 57, 135, 44, 251, 225, 14, 184, 196, 30, 6, 161, 132, 164, 114, 245, 51, 37, 79, 133, 66, 169, 61, 0, 146, 8, 36, 115, 124, 65, 105, 183, 158, 34, 72, 27, 34, 0, 50, 102, 220, 61, 10, 62, 141, 178, 19, 8, 217, 64, 4, 64, 198, 204, 114, 51, 98, 15, 129, 32, 115, 136, 0, 200, 24, 63, 159, 123, 238, 55, 161, 50, 225, 130, 108, 202, 215, 137, 0, 200, 24, 54, 72, 4, 32, 19, 161, 80, 24, 118, 187, 29, 230, 9, 11, 102, 29, 78, 132, 67, 145, 29, 55, 248, 0, 15, 243, 132, 37, 101, 231, 161, 74, 130, 8, 128, 140, 153, 117, 146, 27, 58, 19, 233, 26, 139, 38, 55, 32, 173, 84, 136, 0, 200, 24, 46, 228, 35, 55, 116, 6, 138, 81, 66, 43, 71, 248, 112, 32, 229, 235, 68, 0, 100, 202, 89, 215, 8, 116, 85, 213, 21, 123, 67, 231, 75, 186, 18, 218, 114, 39, 0, 34, 0, 101, 133, 39, 16, 121, 242, 87, 234, 13, 157, 45, 209, 198, 162, 161, 80, 56, 214, 88, 52, 85, 3, 210, 74, 133, 8, 128, 76, 241, 242, 94, 114, 67, 103, 65, 170, 13, 59, 82, 109, 236, 81, 238, 240, 170, 32, 102, 167, 23, 103, 140, 18, 1, 144, 41, 211, 147, 230, 138, 190, 161, 179, 37, 90, 66, 11, 0, 38, 147, 9, 20, 165, 136, 149, 208, 154, 39, 44, 152, 156, 156, 74, 91, 40, 83, 78, 76, 135, 38, 83, 190, 78, 106, 1, 100, 138, 39, 196, 229, 86, 19, 94, 36, 138, 209, 169, 182, 216, 84, 100, 3, 82, 154, 2, 207, 46, 206, 5, 32, 30, 128, 76, 241, 251, 23, 158, 242, 173, 109, 45, 177, 27, 57, 254, 231, 82, 64, 150, 217, 100, 2, 163, 128, 223, 51, 183, 232, 101, 34, 0, 50, 100, 200, 121, 26, 161, 249, 245, 127, 177, 33, 203, 108, 242, 64, 65, 81, 8, 135, 23, 119, 255, 35, 2, 32, 67, 60, 156, 116, 203, 127, 165, 186, 42, 145, 79, 9, 109, 33, 72, 49, 3, 49, 85, 73, 48, 17, 0, 25, 194, 133, 2, 37, 191, 161, 211, 65, 150, 217, 82, 35, 151, 169, 17, 17, 0, 25, 34, 165, 26, 0, 178, 204, 150, 26, 185, 76, 141, 164, 21, 158, 37, 100, 5, 31, 10, 20, 126, 18, 129, 200, 187, 83, 109, 133, 33, 196, 38, 30, 197, 128, 8, 128, 204, 152, 241, 219, 37, 223, 4, 164, 34, 151, 217, 146, 72, 181, 181, 121, 170, 169, 81, 174, 123, 249, 21, 66, 170, 146, 96, 50, 5, 144, 25, 54, 159, 165, 240, 147, 16, 138, 142, 92, 166, 70, 196, 3, 144, 25, 44, 233, 252, 43, 11, 228, 50, 53, 34, 2, 32, 51, 164, 218, 5, 72, 42, 171, 18, 82, 70, 236, 169, 81, 170, 146, 96, 50, 5, 144, 25, 129, 176, 116, 2, 128, 4, 121, 145, 170, 36, 152, 8, 128, 204, 144, 210, 10, 0, 65, 254, 16, 1, 144, 17, 22, 239, 132, 228, 87, 0, 8, 137, 72, 105, 106, 196, 171, 130, 224, 185, 196, 122, 0, 34, 0, 50, 194, 230, 49, 139, 61, 4, 130, 140, 9, 168, 188, 152, 115, 37, 214, 144, 144, 32, 160, 76, 56, 106, 61, 12, 59, 39, 173, 8, 50, 65, 94, 56, 217, 185, 69, 37, 193, 68, 0, 100, 192, 97, 219, 1, 56, 57, 135, 216, 195, 32, 72, 156, 76, 189, 25, 26, 76, 245, 139, 183, 9, 23, 123, 208, 132, 165, 169, 228, 205, 63, 127, 254, 244, 175, 48, 126, 102, 233, 105, 79, 199, 202, 54, 108, 127, 244, 127, 136, 61, 84, 73, 176, 80, 128, 84, 7, 151, 211, 5, 167, 211, 141, 154, 90, 99, 172, 0, 137, 13, 248, 193, 211, 137, 49, 36, 34, 0, 18, 197, 197, 57, 241, 145, 253, 120, 197, 26, 127, 40, 20, 198, 96, 255, 8, 206, 14, 140, 46, 121, 92, 128, 35, 65, 209, 40, 94, 175, 23, 205, 45, 205, 177, 2, 36, 171, 197, 138, 154, 218, 72, 225, 81, 157, 169, 22, 51, 118, 7, 66, 122, 34, 0, 146, 103, 198, 111, 199, 192, 76, 63, 188, 188, 71, 236, 161, 136, 134, 203, 233, 130, 66, 161, 0, 0, 236, 220, 185, 19, 91, 182, 108, 73, 120, 255, 224, 193, 131, 120, 244, 209, 71, 197, 30, 166, 164, 73, 85, 128, 148, 12, 17, 0, 137, 97, 241, 78, 96, 120, 246, 52, 252, 21, 158, 242, 235, 245, 122, 161, 84, 42, 1, 0, 87, 95, 125, 245, 34, 1, 80, 171, 213, 98, 15, 81, 114, 100, 83, 128, 148, 92, 16, 68, 4, 64, 66, 140, 187, 71, 49, 226, 26, 76, 187, 145, 35, 129, 176, 20, 6, 163, 1, 14, 199, 12, 172, 22, 43, 212, 26, 6, 181, 181, 117, 152, 158, 156, 70, 157, 169, 22, 64, 164, 0, 105, 118, 102, 22, 227, 131, 103, 65, 205, 183, 7, 19, 77, 0, 44, 222, 9, 76, 184, 199, 176, 194, 216, 131, 58, 141, 73, 236, 107, 39, 58, 103, 93, 35, 56, 231, 26, 38, 137, 62, 243, 232, 116, 58, 4, 131, 65, 177, 135, 33, 43, 178, 41, 64, 82, 53, 48, 216, 220, 189, 60, 246, 111, 81, 4, 96, 198, 111, 143, 185, 185, 31, 78, 59, 209, 92, 213, 134, 53, 181, 235, 69, 190, 124, 226, 49, 228, 60, 141, 113, 247, 57, 98, 252, 113, 24, 140, 134, 148, 77, 44, 9, 185, 145, 92, 128, 100, 80, 27, 19, 222, 23, 69, 0, 6, 102, 250, 99, 115, 92, 62, 196, 99, 220, 125, 30, 78, 191, 3, 203, 140, 43, 208, 162, 107, 19, 249, 146, 149, 248, 90, 56, 78, 192, 234, 33, 41, 190, 201, 80, 148, 2, 52, 77, 102, 168, 197, 166, 228, 169, 192, 135, 44, 251, 83, 70, 183, 221, 1, 23, 78, 205, 156, 192, 9, 123, 159, 216, 215, 164, 100, 12, 56, 78, 192, 60, 55, 86, 144, 241, 75, 177, 251, 44, 65, 62, 148, 84, 98, 15, 219, 14, 44, 185, 174, 205, 135, 120, 88, 60, 19, 112, 176, 51, 104, 215, 118, 96, 121, 109, 143, 216, 215, 167, 104, 156, 176, 247, 193, 230, 181, 32, 20, 46, 172, 191, 127, 166, 228, 143, 82, 183, 157, 34, 136, 207, 82, 5, 72, 20, 40, 88, 92, 67, 177, 215, 75, 230, 1, 252, 201, 118, 8, 78, 54, 187, 116, 86, 235, 148, 21, 125, 230, 15, 208, 55, 117, 164, 84, 195, 43, 41, 125, 83, 71, 96, 241, 76, 20, 108, 252, 128, 124, 186, 207, 18, 164, 129, 46, 204, 193, 225, 181, 196, 254, 148, 68, 0, 250, 166, 142, 192, 193, 218, 179, 62, 222, 235, 245, 66, 87, 85, 141, 41, 223, 36, 222, 25, 127, 11, 3, 142, 19, 162, 93, 48, 161, 57, 106, 61, 140, 41, 223, 100, 225, 39, 74, 131, 84, 55, 230, 32, 72, 147, 162, 11, 192, 9, 123, 95, 65, 55, 60, 203, 251, 113, 116, 232, 79, 56, 100, 217, 143, 25, 127, 246, 34, 34, 69, 14, 219, 14, 8, 94, 209, 71, 54, 230, 32, 20, 66, 81, 5, 96, 192, 113, 2, 22, 207, 68, 206, 159, 75, 190, 169, 25, 53, 3, 203, 204, 4, 14, 141, 238, 151, 173, 55, 112, 200, 178, 63, 235, 41, 80, 46, 200, 165, 251, 44, 65, 154, 20, 45, 8, 24, 141, 112, 231, 67, 186, 140, 38, 141, 73, 139, 113, 247, 121, 204, 248, 166, 177, 162, 166, 71, 22, 75, 134, 197, 46, 234, 145, 75, 247, 89, 130, 52, 41, 138, 0, 12, 57, 79, 195, 60, 55, 150, 119, 144, 43, 155, 155, 122, 204, 54, 138, 245, 157, 189, 88, 111, 218, 88, 250, 171, 150, 37, 98, 21, 245, 136, 221, 125, 182, 156, 200, 84, 99, 223, 216, 216, 32, 250, 238, 62, 133, 32, 248, 20, 224, 172, 107, 4, 227, 238, 115, 130, 68, 184, 227, 137, 223, 247, 190, 181, 173, 5, 38, 147, 9, 22, 207, 4, 222, 51, 191, 131, 113, 247, 104, 129, 103, 23, 30, 139, 119, 2, 39, 103, 62, 172, 232, 138, 190, 114, 64, 46, 155, 124, 230, 139, 160, 2, 48, 238, 30, 45, 121, 62, 187, 151, 247, 224, 244, 236, 73, 73, 45, 25, 142, 187, 71, 49, 232, 24, 168, 248, 138, 190, 114, 160, 220, 151, 89, 5, 19, 128, 41, 159, 13, 35, 174, 65, 81, 82, 90, 67, 225, 16, 166, 124, 147, 216, 111, 222, 131, 179, 174, 145, 146, 255, 254, 120, 206, 186, 70, 48, 228, 28, 40, 121, 69, 159, 148, 186, 207, 150, 51, 229, 182, 204, 42, 136, 0, 204, 248, 237, 56, 229, 56, 41, 248, 77, 159, 235, 77, 237, 231, 125, 56, 227, 28, 196, 159, 108, 135, 4, 190, 76, 217, 49, 228, 60, 77, 42, 250, 202, 140, 114, 95, 102, 21, 68, 0, 226, 139, 123, 196, 38, 20, 14, 193, 193, 218, 177, 111, 226, 45, 12, 205, 158, 42, 217, 239, 29, 112, 156, 192, 168, 235, 12, 49, 254, 50, 163, 220, 151, 89, 11, 14, 95, 166, 43, 238, 17, 27, 46, 200, 226, 156, 107, 4, 124, 152, 47, 122, 169, 113, 223, 212, 17, 156, 60, 123, 18, 140, 154, 41, 187, 40, 113, 165, 83, 238, 203, 172, 5, 121, 0, 153, 138, 123, 42, 129, 163, 214, 195, 56, 59, 57, 130, 250, 198, 122, 0, 229, 23, 37, 38, 44, 38, 121, 69, 74, 206, 177, 150, 188, 5, 32, 151, 226, 30, 49, 169, 215, 52, 20, 237, 220, 125, 83, 71, 112, 242, 252, 9, 232, 170, 170, 203, 54, 74, 76, 40, 111, 242, 18, 128, 92, 139, 123, 196, 130, 166, 104, 52, 104, 155, 138, 114, 238, 104, 81, 79, 40, 148, 152, 239, 80, 110, 81, 98, 66, 121, 147, 151, 0, 84, 49, 6, 168, 40, 233, 119, 101, 165, 20, 202, 162, 156, 55, 190, 168, 167, 220, 163, 196, 132, 8, 197, 94, 102, 21, 171, 177, 75, 94, 2, 208, 99, 92, 141, 222, 250, 77, 208, 209, 85, 146, 238, 58, 67, 43, 132, 15, 190, 37, 23, 245, 148, 123, 148, 56, 27, 146, 111, 210, 116, 55, 51, 33, 61, 98, 101, 28, 230, 29, 3, 168, 211, 152, 240, 241, 214, 171, 177, 97, 121, 47, 66, 156, 8, 87, 44, 11, 84, 74, 70, 176, 115, 185, 56, 103, 202, 109, 186, 162, 81, 98, 0, 48, 153, 76, 160, 40, 69, 44, 74, 108, 158, 176, 96, 114, 114, 10, 124, 80, 154, 2, 41, 20, 201, 55, 105, 186, 155, 153, 144, 30, 177, 50, 14, 11, 206, 3, 184, 176, 249, 18, 172, 109, 91, 15, 29, 93, 85, 218, 43, 150, 5, 12, 165, 18, 228, 60, 51, 126, 59, 250, 167, 143, 101, 189, 226, 81, 78, 81, 226, 108, 72, 190, 73, 211, 221, 204, 132, 236, 41, 85, 44, 73, 144, 68, 160, 229, 134, 149, 248, 120, 235, 213, 104, 208, 54, 130, 82, 148, 188, 207, 104, 90, 170, 4, 232, 133, 71, 138, 122, 150, 38, 155, 155, 148, 162, 164, 115, 79, 72, 21, 177, 98, 73, 130, 254, 207, 108, 108, 216, 140, 78, 195, 10, 201, 4, 8, 77, 2, 44, 1, 218, 125, 83, 146, 201, 114, 148, 34, 169, 110, 210, 84, 55, 51, 97, 105, 196, 138, 37, 9, 46, 205, 209, 0, 161, 94, 101, 40, 254, 85, 91, 2, 154, 162, 5, 217, 113, 200, 207, 103, 119, 209, 43, 181, 24, 39, 213, 77, 154, 234, 102, 38, 44, 141, 88, 177, 164, 162, 228, 168, 214, 105, 76, 216, 210, 114, 197, 124, 63, 64, 91, 73, 243, 227, 163, 13, 28, 148, 20, 13, 180, 23, 126, 62, 62, 20, 40, 217, 216, 229, 72, 170, 180, 216, 84, 233, 179, 132, 220, 41, 69, 99, 151, 162, 38, 169, 175, 55, 109, 196, 184, 123, 20, 231, 221, 103, 74, 54, 135, 142, 70, 160, 91, 27, 90, 5, 57, 95, 32, 76, 4, 96, 41, 72, 247, 33, 121, 83, 244, 232, 76, 187, 190, 179, 164, 1, 194, 104, 4, 90, 168, 37, 192, 32, 89, 195, 38, 148, 49, 37, 11, 207, 150, 58, 64, 168, 163, 117, 130, 156, 39, 72, 60, 0, 66, 9, 41, 117, 44, 169, 164, 235, 51, 165, 8, 16, 70, 35, 208, 106, 101, 225, 145, 103, 139, 87, 152, 221, 123, 42, 1, 177, 130, 160, 28, 199, 145, 61, 17, 11, 160, 228, 11, 180, 209, 0, 97, 75, 85, 27, 104, 74, 248, 16, 68, 52, 2, 45, 196, 18, 224, 172, 12, 170, 29, 43, 29, 134, 137, 36, 123, 145, 50, 236, 252, 16, 45, 67, 99, 189, 105, 35, 122, 140, 107, 4, 207, 32, 164, 40, 5, 154, 26, 154, 4, 89, 2, 100, 201, 250, 191, 12, 32, 101, 216, 133, 32, 106, 138, 86, 177, 2, 132, 180, 64, 41, 192, 28, 89, 2, 148, 13, 164, 12, 59, 63, 36, 145, 163, 41, 116, 128, 80, 165, 16, 70, 0, 66, 161, 160, 152, 151, 37, 143, 241, 138, 83, 82, 42, 46, 164, 12, 187, 16, 36, 33, 0, 128, 176, 1, 66, 161, 60, 0, 185, 229, 0, 148, 251, 38, 22, 169, 224, 184, 64, 197, 150, 97, 11, 129, 100, 4, 0, 16, 46, 64, 168, 161, 133, 201, 61, 151, 91, 22, 160, 216, 155, 88, 240, 1, 30, 167, 250, 134, 75, 254, 189, 133, 74, 157, 61, 117, 108, 168, 76, 189, 164, 244, 72, 74, 0, 162, 20, 26, 32, 84, 211, 90, 65, 198, 33, 247, 22, 223, 165, 156, 23, 7, 184, 0, 190, 115, 223, 15, 241, 248, 157, 223, 195, 177, 247, 250, 69, 249, 190, 133, 148, 97, 31, 123, 175, 31, 143, 111, 255, 62, 190, 115, 223, 15, 43, 74, 4, 36, 41, 0, 64, 97, 1, 194, 38, 109, 115, 193, 191, 95, 236, 29, 134, 242, 65, 172, 146, 210, 0, 23, 192, 83, 247, 62, 139, 15, 15, 158, 4, 31, 224, 241, 204, 95, 63, 39, 154, 8, 228, 195, 177, 247, 250, 241, 204, 95, 63, 7, 62, 192, 227, 195, 131, 39, 241, 157, 251, 126, 136, 0, 39, 47, 239, 47, 95, 36, 43, 0, 81, 114, 13, 16, 210, 20, 13, 3, 99, 44, 248, 247, 122, 2, 242, 235, 98, 35, 70, 73, 41, 235, 231, 240, 212, 189, 207, 226, 228, 145, 83, 88, 182, 108, 25, 190, 254, 245, 175, 203, 74, 4, 226, 141, 223, 100, 50, 129, 97, 24, 124, 120, 240, 36, 158, 186, 247, 217, 138, 16, 1, 201, 11, 0, 144, 91, 128, 80, 168, 0, 160, 220, 230, 255, 64, 233, 75, 74, 89, 63, 135, 39, 239, 222, 133, 147, 71, 78, 161, 189, 189, 29, 239, 190, 251, 46, 118, 237, 218, 133, 103, 159, 125, 54, 38, 2, 253, 135, 7, 138, 250, 157, 25, 102, 113, 205, 71, 182, 174, 127, 188, 241, 223, 124, 243, 205, 176, 217, 108, 216, 183, 111, 31, 170, 170, 170, 112, 242, 200, 41, 60, 117, 239, 179, 96, 253, 18, 237, 119, 39, 16, 178, 16, 0, 32, 251, 0, 161, 80, 75, 128, 229, 146, 3, 80, 172, 246, 100, 62, 175, 31, 79, 222, 189, 11, 167, 251, 134, 209, 222, 222, 142, 253, 251, 247, 163, 189, 61, 82, 127, 253, 192, 3, 15, 196, 68, 224, 233, 175, 254, 160, 232, 34, 144, 15, 241, 198, 127, 235, 173, 183, 226, 213, 87, 95, 133, 82, 169, 196, 150, 45, 91, 176, 111, 223, 62, 24, 12, 6, 156, 60, 114, 10, 79, 222, 189, 171, 172, 69, 64, 54, 2, 16, 37, 83, 128, 80, 176, 37, 192, 96, 249, 254, 167, 23, 138, 207, 235, 199, 19, 219, 191, 159, 96, 252, 93, 93, 93, 9, 199, 68, 69, 32, 192, 5, 36, 39, 2, 253, 135, 7, 18, 140, 255, 165, 151, 94, 130, 82, 185, 208, 66, 254, 162, 139, 46, 194, 254, 253, 251, 81, 83, 83, 131, 211, 125, 195, 120, 242, 238, 93, 240, 121, 203, 115, 73, 81, 118, 2, 0, 44, 29, 32, 20, 108, 9, 48, 92, 57, 145, 224, 92, 57, 221, 55, 140, 225, 19, 103, 1, 0, 59, 118, 236, 88, 100, 252, 81, 164, 40, 2, 253, 135, 7, 240, 244, 87, 127, 144, 96, 252, 10, 133, 98, 209, 113, 189, 189, 189, 120, 244, 209, 71, 99, 223, 119, 232, 248, 25, 177, 135, 94, 20, 100, 41, 0, 81, 162, 1, 66, 70, 185, 16, 32, 20, 162, 17, 40, 0, 132, 194, 242, 202, 2, 140, 167, 216, 149, 121, 27, 47, 91, 143, 135, 118, 221, 11, 74, 73, 225, 129, 7, 30, 192, 27, 111, 188, 145, 246, 88, 41, 137, 64, 212, 248, 3, 92, 96, 73, 227, 7, 128, 215, 94, 123, 13, 143, 62, 250, 40, 40, 37, 133, 135, 118, 221, 139, 222, 45, 107, 83, 30, 39, 247, 61, 17, 100, 45, 0, 64, 36, 64, 184, 193, 20, 9, 16, 82, 10, 10, 203, 13, 43, 11, 62, 167, 139, 115, 22, 156, 3, 32, 247, 27, 35, 19, 91, 175, 187, 24, 127, 243, 221, 191, 66, 40, 28, 194, 77, 55, 221, 36, 121, 17, 200, 213, 248, 111, 185, 229, 22, 132, 194, 33, 252, 205, 119, 255, 10, 91, 175, 187, 56, 237, 121, 229, 190, 39, 130, 236, 5, 0, 88, 8, 16, 118, 234, 151, 11, 114, 62, 155, 207, 154, 242, 117, 69, 128, 135, 225, 131, 97, 180, 253, 234, 109, 172, 248, 238, 43, 184, 224, 161, 95, 96, 245, 55, 127, 137, 21, 207, 188, 130, 182, 95, 189, 13, 195, 7, 195, 80, 204, 27, 188, 220, 111, 140, 108, 136, 138, 64, 48, 24, 196, 77, 55, 221, 132, 215, 94, 123, 45, 237, 177, 15, 60, 240, 0, 118, 238, 220, 41, 138, 8, 196, 27, 255, 237, 183, 223, 158, 149, 241, 135, 17, 206, 104, 252, 128, 252, 247, 68, 40, 11, 1, 136, 210, 83, 115, 129, 32, 231, 73, 85, 6, 92, 253, 209, 40, 86, 62, 253, 47, 104, 255, 167, 63, 192, 120, 100, 16, 154, 9, 59, 40, 142, 135, 210, 23, 128, 198, 108, 135, 241, 200, 32, 218, 255, 233, 15, 88, 249, 244, 191, 160, 250, 163, 81, 217, 223, 24, 217, 178, 245, 186, 139, 113, 255, 211, 119, 34, 24, 12, 226, 150, 91, 110, 89, 82, 4, 118, 236, 216, 145, 32, 2, 3, 199, 6, 139, 62, 190, 100, 227, 127, 225, 133, 23, 4, 51, 254, 114, 216, 19, 65, 218, 163, 19, 137, 228, 86, 224, 245, 111, 126, 128, 206, 159, 190, 1, 102, 102, 46, 227, 103, 153, 153, 57, 116, 254, 244, 13, 44, 63, 120, 74, 214, 55, 70, 46, 92, 121, 227, 86, 220, 255, 244, 157, 8, 133, 66, 57, 137, 192, 83, 247, 60, 91, 84, 17, 200, 197, 248, 95, 126, 249, 229, 156, 140, 31, 40, 143, 61, 17, 202, 231, 46, 20, 144, 248, 36, 160, 250, 55, 63, 64, 227, 27, 135, 115, 62, 199, 178, 189, 39, 80, 255, 230, 7, 178, 189, 49, 114, 37, 31, 17, 96, 125, 108, 209, 68, 96, 224, 216, 96, 78, 198, 127, 219, 109, 183, 229, 100, 252, 64, 121, 236, 137, 64, 191, 49, 240, 107, 180, 212, 182, 160, 86, 103, 18, 36, 128, 38, 22, 238, 57, 15, 94, 248, 231, 215, 240, 238, 193, 163, 176, 216, 166, 10, 63, 33, 128, 13, 138, 0, 30, 81, 185, 129, 52, 55, 78, 38, 26, 223, 56, 140, 185, 118, 19, 70, 77, 85, 168, 111, 172, 135, 195, 49, 3, 171, 197, 10, 181, 134, 65, 109, 109, 157, 216, 151, 76, 112, 174, 188, 113, 43, 0, 224, 71, 143, 253, 18, 183, 220, 114, 11, 94, 124, 241, 69, 220, 122, 235, 173, 41, 143, 221, 177, 99, 7, 0, 224, 145, 71, 30, 193, 83, 247, 60, 139, 191, 253, 201, 131, 88, 179, 105, 149, 32, 227, 24, 56, 54, 136, 167, 238, 137, 4, 29, 183, 111, 223, 142, 159, 253, 236, 103, 25, 141, 95, 65, 41, 114, 50, 126, 64, 62, 123, 34, 68, 247, 202, 96, 253, 28, 140, 29, 106, 84, 211, 11, 217, 147, 148, 219, 239, 196, 121, 251, 25, 12, 207, 158, 194, 222, 241, 55, 241, 158, 249, 29, 28, 181, 30, 198, 144, 243, 180, 216, 227, 206, 137, 127, 124, 241, 63, 240, 234, 235, 111, 46, 105, 252, 52, 157, 125, 137, 49, 131, 48, 238, 86, 121, 210, 222, 56, 217, 210, 250, 210, 59, 8, 249, 217, 148, 105, 186, 229, 72, 188, 39, 112, 219, 109, 183, 225, 229, 151, 95, 78, 123, 108, 49, 60, 129, 168, 241, 179, 62, 22, 219, 183, 111, 199, 207, 127, 254, 243, 140, 198, 15, 32, 103, 227, 7, 228, 179, 9, 108, 114, 0, 58, 30, 218, 104, 48, 98, 198, 238, 64, 181, 94, 15, 62, 196, 131, 15, 241, 240, 194, 3, 59, 55, 133, 113, 247, 57, 208, 148, 10, 90, 165, 14, 85, 76, 53, 214, 212, 174, 23, 251, 187, 164, 229, 205, 183, 222, 77, 255, 166, 174, 26, 45, 219, 191, 129, 150, 143, 93, 136, 41, 127, 16, 142, 131, 123, 225, 126, 233, 121, 40, 150, 200, 139, 191, 68, 193, 193, 164, 8, 167, 125, 127, 142, 15, 194, 50, 95, 44, 210, 194, 168, 80, 77, 43, 83, 30, 167, 113, 251, 176, 122, 210, 11, 103, 151, 216, 87, 168, 116, 196, 123, 2, 81, 3, 91, 202, 19, 96, 89, 22, 79, 60, 241, 68, 193, 158, 64, 42, 227, 79, 199, 238, 221, 187, 113, 199, 29, 119, 0, 0, 238, 127, 250, 206, 156, 141, 95, 78, 120, 189, 94, 52, 183, 52, 131, 162, 20, 9, 79, 127, 0, 160, 227, 3, 85, 180, 42, 241, 9, 25, 21, 4, 63, 239, 131, 131, 181, 195, 60, 55, 6, 70, 169, 134, 90, 169, 129, 158, 49, 160, 73, 219, 34, 72, 243, 77, 33, 112, 123, 210, 68, 213, 181, 85, 104, 186, 243, 27, 168, 89, 191, 9, 238, 64, 16, 246, 183, 126, 13, 239, 235, 47, 64, 145, 97, 29, 126, 147, 50, 125, 45, 192, 144, 215, 143, 253, 206, 196, 157, 142, 174, 48, 86, 161, 71, 151, 122, 94, 95, 125, 242, 28, 156, 151, 8, 227, 222, 202, 133, 92, 68, 224, 241, 199, 31, 135, 223, 239, 199, 51, 207, 60, 147, 183, 8, 20, 98, 252, 209, 177, 86, 34, 52, 176, 16, 193, 172, 206, 144, 69, 23, 10, 135, 224, 231, 125, 240, 243, 62, 56, 89, 7, 198, 221, 231, 35, 130, 64, 169, 81, 205, 232, 97, 210, 54, 160, 69, 215, 38, 246, 119, 138, 161, 168, 50, 96, 217, 87, 31, 5, 213, 189, 1, 254, 96, 16, 83, 7, 246, 194, 243, 250, 238, 140, 198, 15, 0, 203, 21, 169, 51, 1, 231, 248, 224, 34, 227, 7, 128, 253, 78, 79, 90, 79, 64, 59, 110, 23, 251, 82, 136, 66, 178, 8, 112, 28, 135, 219, 111, 191, 61, 229, 177, 59, 119, 238, 4, 128, 152, 8, 60, 241, 139, 135, 179, 254, 61, 241, 198, 255, 181, 175, 125, 13, 207, 61, 247, 92, 218, 99, 139, 97, 252, 82, 223, 24, 54, 26, 128, 54, 24, 13, 152, 227, 185, 196, 24, 0, 80, 88, 157, 56, 23, 100, 225, 14, 184, 96, 241, 76, 224, 196, 116, 31, 222, 25, 127, 11, 135, 44, 251, 209, 55, 117, 4, 227, 238, 81, 209, 190, 180, 66, 163, 67, 199, 87, 30, 132, 113, 213, 90, 240, 60, 15, 219, 127, 189, 14, 247, 139, 207, 67, 145, 101, 134, 95, 157, 34, 181, 72, 156, 91, 162, 50, 204, 146, 166, 126, 156, 158, 93, 72, 250, 145, 210, 141, 33, 20, 75, 101, 61, 246, 94, 182, 22, 247, 125, 251, 43, 0, 128, 59, 238, 184, 3, 187, 119, 239, 78, 123, 158, 157, 59, 119, 70, 166, 4, 62, 22, 223, 186, 107, 23, 134, 250, 51, 231, 223, 15, 30, 31, 17, 213, 248, 229, 64, 252, 202, 68, 50, 148, 208, 117, 226, 129, 80, 68, 16, 166, 124, 147, 24, 112, 244, 199, 2, 139, 125, 83, 71, 74, 215, 101, 167, 74, 143, 186, 175, 125, 11, 117, 31, 219, 12, 132, 21, 152, 57, 184, 23, 222, 223, 254, 10, 10, 74, 1, 208, 42, 132, 149, 153, 43, 6, 133, 44, 5, 10, 169, 138, 186, 7, 171, 232, 100, 202, 122, 252, 216, 199, 215, 225, 158, 199, 239, 0, 144, 189, 8, 228, 82, 125, 151, 141, 241, 255, 226, 23, 191, 168, 72, 227, 7, 18, 251, 68, 44, 138, 1, 0, 197, 125, 42, 197, 2, 139, 188, 7, 83, 190, 73, 156, 115, 13, 131, 161, 34, 113, 4, 163, 186, 70, 176, 236, 189, 120, 212, 23, 94, 14, 69, 215, 42, 216, 217, 0, 102, 222, 250, 29, 60, 191, 255, 87, 104, 175, 255, 28, 148, 109, 93, 64, 56, 4, 238, 232, 1, 176, 71, 247, 67, 177, 196, 182, 95, 147, 97, 10, 203, 83, 120, 1, 93, 26, 6, 135, 221, 139, 227, 13, 12, 20, 104, 97, 82, 11, 11, 215, 80, 120, 135, 34, 41, 227, 245, 248, 81, 103, 170, 141, 5, 147, 227, 131, 78, 6, 163, 1, 86, 139, 21, 215, 222, 124, 5, 0, 224, 39, 79, 190, 16, 51, 196, 108, 166, 3, 217, 144, 141, 241, 223, 117, 215, 93, 80, 40, 20, 21, 103, 252, 153, 40, 249, 163, 41, 94, 16, 28, 172, 29, 163, 238, 179, 9, 129, 197, 182, 170, 142, 130, 91, 122, 113, 71, 246, 33, 112, 225, 229, 240, 217, 38, 48, 247, 155, 221, 80, 223, 248, 37, 168, 55, 109, 5, 20, 0, 123, 244, 61, 176, 199, 150, 54, 126, 0, 24, 11, 41, 177, 156, 90, 124, 76, 53, 173, 196, 21, 198, 170, 69, 113, 128, 75, 141, 186, 180, 43, 1, 190, 174, 194, 123, 20, 74, 149, 92, 210, 97, 139, 33, 2, 196, 248, 11, 67, 116, 223, 52, 57, 176, 104, 158, 27, 139, 44, 61, 210, 58, 232, 104, 29, 154, 116, 45, 104, 208, 54, 229, 116, 206, 176, 223, 11, 231, 79, 191, 5, 168, 212, 96, 62, 243, 151, 160, 47, 222, 6, 62, 200, 130, 63, 250, 14, 216, 255, 122, 37, 171, 32, 224, 31, 130, 106, 92, 73, 167, 158, 211, 247, 232, 52, 104, 97, 84, 89, 45, 3, 2, 128, 227, 202, 117, 98, 95, 230, 162, 177, 84, 58, 172, 193, 104, 88, 148, 245, 152, 44, 2, 44, 27, 137, 218, 167, 34, 42, 2, 233, 120, 232, 161, 135, 240, 253, 239, 127, 63, 237, 251, 196, 248, 51, 67, 75, 45, 40, 21, 10, 135, 192, 5, 89, 112, 65, 22, 78, 214, 1, 139, 103, 34, 97, 165, 161, 134, 169, 67, 187, 190, 51, 243, 137, 88, 22, 225, 48, 64, 85, 233, 17, 6, 48, 113, 98, 20, 179, 239, 157, 70, 135, 207, 151, 85, 254, 243, 48, 84, 24, 8, 41, 177, 134, 74, 189, 26, 80, 77, 43, 209, 179, 132, 209, 71, 241, 244, 180, 130, 107, 168, 17, 251, 178, 22, 141, 168, 251, 15, 68, 130, 201, 51, 118, 71, 198, 172, 199, 120, 17, 184, 235, 174, 187, 0, 96, 73, 17, 240, 249, 22, 23, 103, 109, 218, 180, 9, 91, 183, 166, 55, 232, 231, 159, 127, 30, 247, 221, 119, 31, 49, 254, 56, 82, 217, 186, 232, 30, 64, 54, 68, 5, 33, 186, 218, 48, 236, 28, 132, 70, 169, 134, 134, 214, 192, 168, 174, 75, 155, 194, 172, 224, 88, 176, 175, 60, 15, 254, 234, 219, 48, 49, 228, 65, 64, 213, 12, 218, 176, 1, 45, 174, 126, 80, 8, 103, 252, 189, 154, 155, 84, 192, 27, 65, 32, 207, 222, 32, 97, 37, 5, 243, 231, 175, 18, 251, 242, 21, 149, 124, 211, 97, 115, 17, 1, 173, 118, 241, 62, 15, 75, 213, 82, 16, 227, 207, 30, 89, 8, 64, 50, 129, 16, 155, 176, 218, 112, 206, 181, 196, 110, 52, 156, 31, 138, 63, 236, 134, 174, 230, 34, 184, 116, 29, 176, 87, 119, 131, 87, 170, 209, 225, 248, 0, 20, 210, 79, 5, 158, 252, 10, 135, 79, 93, 14, 216, 41, 26, 83, 175, 231, 183, 38, 48, 245, 169, 139, 17, 104, 172, 17, 251, 114, 21, 149, 232, 83, 197, 60, 97, 201, 57, 152, 156, 139, 8, 100, 75, 188, 241, 255, 229, 35, 183, 226, 178, 235, 55, 139, 125, 137, 36, 141, 44, 5, 32, 153, 76, 221, 123, 148, 97, 30, 93, 142, 247, 1, 199, 251, 89, 157, 239, 201, 175, 112, 248, 179, 203, 35, 143, 125, 211, 13, 74, 120, 6, 130, 240, 158, 206, 236, 49, 196, 227, 233, 105, 197, 244, 117, 23, 138, 125, 105, 36, 79, 178, 8, 176, 108, 100, 73, 47, 31, 226, 141, 255, 158, 199, 239, 192, 165, 159, 216, 152, 85, 130, 91, 37, 67, 202, 129, 147, 136, 55, 126, 0, 128, 66, 129, 182, 237, 12, 168, 26, 38, 235, 115, 132, 141, 70, 140, 127, 249, 134, 188, 171, 8, 43, 141, 107, 111, 190, 34, 150, 39, 112, 223, 125, 247, 225, 249, 231, 159, 207, 249, 28, 187, 118, 237, 138, 25, 255, 221, 255, 235, 118, 92, 123, 243, 21, 100, 131, 208, 44, 32, 2, 16, 199, 34, 227, 159, 199, 161, 217, 12, 207, 23, 238, 201, 250, 60, 190, 47, 126, 25, 193, 234, 242, 170, 247, 207, 68, 161, 233, 176, 215, 222, 124, 5, 238, 122, 236, 75, 0, 114, 23, 129, 93, 187, 118, 225, 225, 135, 31, 134, 66, 161, 192, 23, 254, 250, 102, 172, 219, 186, 170, 40, 27, 161, 148, 35, 68, 0, 230, 73, 103, 252, 211, 252, 197, 56, 199, 222, 138, 96, 119, 15, 184, 109, 215, 102, 60, 15, 119, 245, 53, 8, 173, 236, 46, 201, 152, 211, 53, 26, 77, 78, 205, 149, 11, 55, 124, 110, 91, 206, 34, 16, 111, 252, 247, 60, 126, 7, 62, 251, 229, 79, 203, 162, 68, 87, 42, 16, 1, 64, 102, 227, 143, 194, 221, 240, 105, 132, 140, 53, 105, 207, 19, 170, 51, 129, 251, 228, 103, 74, 54, 238, 116, 141, 70, 147, 83, 115, 229, 68, 178, 8, 236, 218, 181, 43, 237, 177, 201, 198, 31, 141, 39, 16, 178, 167, 226, 5, 32, 157, 241, 115, 218, 107, 224, 53, 62, 144, 248, 162, 74, 5, 246, 150, 91, 211, 158, 139, 253, 252, 109, 128, 74, 152, 157, 137, 178, 33, 93, 163, 209, 228, 134, 164, 114, 35, 94, 4, 30, 126, 248, 225, 148, 34, 240, 228, 147, 79, 38, 184, 253, 196, 248, 243, 163, 162, 5, 32, 147, 241, 27, 244, 6, 52, 54, 38, 166, 241, 6, 215, 172, 71, 112, 213, 234, 69, 159, 9, 174, 90, 141, 96, 247, 106, 136, 9, 69, 81, 89, 165, 230, 202, 129, 100, 17, 248, 193, 15, 126, 16, 123, 239, 153, 103, 158, 193, 19, 79, 60, 145, 224, 246, 39, 67, 92, 255, 236, 40, 139, 101, 192, 124, 200, 246, 201, 111, 208, 71, 154, 58, 78, 78, 46, 148, 82, 114, 87, 108, 131, 118, 48, 177, 101, 26, 119, 229, 182, 146, 127, 135, 84, 41, 183, 169, 82, 115, 229, 186, 12, 118, 195, 231, 34, 215, 244, 23, 223, 121, 17, 15, 62, 248, 32, 212, 106, 53, 88, 150, 197, 35, 143, 60, 146, 16, 237, 39, 100, 134, 166, 104, 52, 209, 139, 19, 170, 42, 82, 0, 210, 25, 255, 107, 254, 110, 120, 212, 159, 69, 242, 243, 36, 89, 4, 130, 107, 214, 33, 100, 170, 7, 101, 159, 6, 16, 153, 251, 7, 47, 40, 125, 190, 191, 193, 104, 88, 148, 114, 59, 61, 57, 189, 40, 53, 183, 90, 47, 79, 1, 0, 34, 34, 160, 173, 210, 224, 71, 143, 253, 18, 247, 222, 123, 47, 128, 72, 166, 225, 125, 223, 38, 25, 126, 217, 98, 164, 212, 139, 202, 128, 163, 84, 156, 0, 44, 101, 252, 223, 116, 95, 5, 184, 143, 2, 0, 62, 93, 219, 145, 240, 126, 130, 8, 40, 20, 8, 108, 190, 20, 234, 223, 71, 182, 195, 10, 92, 178, 69, 148, 53, 255, 84, 41, 183, 169, 82, 115, 229, 206, 149, 55, 110, 5, 173, 162, 241, 119, 15, 255, 4, 0, 240, 240, 223, 127, 13, 151, 108, 219, 36, 246, 176, 100, 65, 27, 179, 180, 248, 87, 148, 0, 100, 52, 254, 121, 190, 53, 158, 89, 4, 66, 221, 61, 177, 215, 67, 43, 165, 211, 239, 175, 144, 212, 92, 41, 115, 217, 245, 155, 161, 98, 104, 168, 181, 106, 244, 94, 186, 182, 240, 19, 150, 57, 26, 138, 134, 41, 133, 203, 159, 76, 197, 8, 64, 182, 198, 31, 37, 163, 8, 4, 131, 192, 124, 155, 241, 224, 178, 46, 177, 191, 94, 69, 176, 249, 106, 242, 212, 207, 134, 165, 92, 254, 100, 42, 66, 0, 114, 53, 254, 40, 153, 68, 192, 181, 124, 190, 10, 81, 153, 185, 44, 152, 64, 200, 23, 62, 16, 153, 214, 53, 54, 54, 128, 86, 209, 9, 27, 125, 232, 116, 58, 24, 141, 122, 40, 230, 155, 174, 100, 114, 249, 147, 41, 251, 101, 192, 124, 141, 63, 202, 183, 198, 143, 226, 119, 142, 177, 69, 175, 27, 244, 6, 232, 46, 189, 12, 225, 246, 44, 122, 19, 20, 25, 169, 119, 165, 37, 20, 70, 54, 59, 77, 27, 41, 117, 206, 198, 15, 228, 233, 1, 164, 83, 160, 100, 165, 18, 155, 66, 141, 63, 74, 58, 79, 160, 238, 47, 190, 0, 218, 237, 74, 88, 34, 44, 71, 254, 245, 39, 191, 22, 123, 8, 146, 228, 243, 247, 252, 247, 146, 252, 158, 76, 61, 23, 89, 199, 92, 214, 46, 127, 50, 121, 89, 233, 130, 2, 213, 193, 229, 116, 193, 233, 116, 163, 166, 214, 152, 160, 84, 98, 175, 61, 11, 101, 252, 81, 178, 9, 12, 150, 43, 175, 252, 148, 8, 64, 42, 62, 123, 231, 141, 69, 127, 224, 101, 74, 236, 234, 208, 24, 128, 150, 252, 55, 32, 205, 107, 212, 169, 186, 190, 214, 212, 26, 23, 41, 149, 88, 8, 109, 252, 81, 42, 89, 4, 128, 200, 14, 62, 132, 72, 26, 50, 128, 146, 60, 240, 210, 245, 92, 12, 186, 253, 232, 104, 104, 44, 248, 252, 130, 200, 86, 186, 20, 84, 49, 166, 1, 197, 50, 254, 40, 149, 44, 2, 79, 60, 241, 132, 216, 67, 144, 4, 81, 1, 40, 197, 3, 47, 85, 207, 197, 158, 214, 246, 188, 93, 254, 100, 242, 178, 80, 169, 166, 160, 22, 219, 248, 163, 84, 178, 8, 16, 22, 40, 197, 3, 47, 62, 177, 139, 86, 40, 209, 94, 215, 32, 152, 241, 3, 121, 174, 2, 196, 111, 53, 20, 12, 241, 145, 74, 52, 143, 31, 26, 77, 36, 241, 64, 140, 78, 44, 165, 50, 254, 40, 75, 173, 14, 24, 141, 181, 37, 253, 238, 4, 113, 72, 126, 224, 21, 131, 104, 79, 3, 131, 90, 139, 77, 93, 43, 209, 96, 200, 127, 190, 159, 138, 188, 36, 75, 138, 41, 168, 215, 94, 180, 184, 193, 103, 177, 140, 63, 74, 58, 79, 32, 28, 38, 173, 192, 42, 129, 248, 7, 94, 49, 167, 1, 109, 140, 30, 104, 45, 206, 185, 5, 243, 89, 196, 78, 65, 221, 127, 174, 25, 159, 92, 99, 137, 253, 187, 216, 198, 31, 37, 149, 8, 124, 56, 212, 135, 112, 147, 56, 49, 16, 66, 233, 40, 246, 3, 47, 219, 116, 222, 66, 40, 155, 59, 212, 187, 242, 147, 216, 51, 240, 91, 172, 107, 158, 193, 1, 170, 19, 223, 228, 74, 215, 143, 255, 91, 227, 71, 209, 74, 169, 209, 30, 160, 240, 209, 217, 147, 240, 25, 205, 128, 31, 162, 47, 133, 10, 141, 130, 52, 57, 77, 160, 181, 173, 165, 104, 15, 188, 92, 210, 121, 11, 161, 108, 4, 0, 0, 102, 87, 126, 6, 239, 205, 255, 252, 140, 54, 243, 246, 95, 249, 18, 159, 8, 21, 45, 195, 29, 60, 253, 58, 166, 77, 181, 160, 155, 104, 104, 2, 242, 47, 195, 37, 136, 71, 62, 25, 125, 249, 82, 144, 0, 84, 106, 10, 170, 20, 99, 32, 197, 228, 185, 255, 247, 116, 44, 239, 35, 20, 10, 99, 210, 54, 137, 250, 122, 19, 156, 46, 39, 76, 38, 19, 236, 118, 59, 140, 6, 99, 217, 79, 121, 138, 61, 189, 213, 64, 13, 19, 83, 252, 167, 126, 60, 229, 253, 63, 86, 66, 196, 142, 129, 20, 19, 169, 46, 251, 150, 154, 98, 62, 240, 244, 42, 3, 12, 138, 220, 54, 159, 17, 130, 178, 47, 6, 34, 20, 142, 20, 151, 125, 203, 137, 54, 70, 47, 138, 241, 3, 50, 244, 0, 114, 41, 141, 36, 8, 67, 165, 77, 121, 74, 69, 169, 2, 125, 75, 33, 59, 75, 201, 166, 52, 178, 84, 84, 106, 12, 36, 250, 61, 201, 6, 28, 249, 19, 53, 254, 169, 201, 105, 81, 199, 33, 59, 1, 72, 238, 121, 159, 174, 55, 62, 129, 32, 69, 52, 20, 13, 35, 165, 198, 220, 212, 12, 92, 46, 23, 212, 26, 226, 1, 100, 77, 54, 61, 239, 41, 226, 254, 151, 13, 229, 182, 245, 153, 145, 82, 195, 68, 107, 225, 155, 113, 1, 0, 88, 63, 7, 131, 192, 169, 189, 185, 34, 43, 107, 73, 87, 26, 233, 114, 186, 16, 10, 133, 99, 17, 106, 66, 113, 40, 245, 148, 167, 156, 182, 62, 107, 99, 244, 240, 205, 184, 34, 129, 212, 249, 13, 75, 131, 193, 72, 96, 213, 106, 17, 174, 120, 44, 215, 115, 201, 42, 8, 152, 170, 52, 178, 190, 177, 126, 81, 111, 124, 66, 121, 32, 245, 190, 19, 217, 16, 159, 206, 219, 208, 88, 47, 246, 112, 22, 33, 43, 15, 32, 26, 121, 142, 223, 250, 57, 26, 161, 6, 0, 147, 201, 4, 138, 34, 233, 170, 229, 138, 220, 182, 62, 139, 186, 252, 165, 32, 57, 152, 152, 109, 112, 81, 86, 30, 64, 57, 39, 219, 16, 22, 35, 231, 4, 164, 82, 166, 243, 2, 128, 90, 195, 192, 229, 138, 196, 22, 114, 9, 46, 202, 202, 3, 32, 84, 22, 114, 76, 64, 202, 183, 59, 111, 193, 215, 202, 96, 0, 235, 231, 0, 228, 22, 92, 148, 149, 7, 64, 168, 44, 228, 150, 128, 36, 86, 98, 79, 114, 224, 47, 26, 92, 4, 16, 11, 160, 166, 67, 150, 2, 80, 201, 9, 56, 149, 142, 20, 167, 129, 233, 118, 222, 45, 21, 153, 140, 124, 201, 177, 139, 54, 106, 2, 161, 12, 144, 66, 58, 111, 33, 16, 1, 32, 16, 242, 68, 140, 185, 190, 208, 16, 1, 32, 72, 30, 169, 77, 249, 74, 209, 170, 171, 84, 144, 85, 0, 153, 144, 156, 254, 154, 46, 77, 150, 80, 92, 74, 185, 182, 95, 10, 136, 0, 200, 4, 41, 85, 65, 86, 42, 26, 138, 150, 245, 124, 63, 21, 68, 0, 100, 2, 169, 130, 20, 31, 127, 136, 199, 4, 231, 134, 157, 151, 79, 13, 66, 38, 72, 12, 64, 6, 144, 42, 72, 105, 17, 21, 2, 64, 254, 241, 0, 34, 0, 50, 96, 169, 42, 200, 248, 52, 89, 66, 233, 137, 23, 3, 154, 162, 81, 5, 165, 172, 166, 9, 228, 177, 33, 3, 82, 165, 191, 166, 74, 147, 37, 136, 11, 31, 226, 225, 12, 177, 152, 224, 220, 152, 224, 220, 152, 227, 57, 184, 61, 156, 216, 195, 90, 18, 193, 61, 128, 116, 61, 250, 146, 123, 249, 17, 178, 39, 85, 250, 107, 170, 52, 89, 130, 180, 112, 134, 88, 152, 39, 45, 104, 110, 105, 70, 45, 173, 65, 53, 205, 96, 142, 231, 36, 229, 33, 8, 238, 1, 148, 83, 19, 7, 169, 64, 250, 239, 201, 31, 103, 136, 197, 152, 223, 133, 211, 19, 163, 176, 115, 28, 166, 230, 43, 247, 196, 70, 112, 1, 72, 23, 157, 78, 142, 98, 151, 3, 229, 214, 178, 138, 32, 60, 169, 58, 86, 77, 187, 167, 49, 19, 152, 195, 4, 231, 198, 148, 203, 37, 234, 52, 161, 232, 49, 0, 185, 53, 113, 200, 5, 226, 237, 16, 50, 145, 169, 164, 217, 171, 12, 98, 216, 62, 30, 139, 25, 148, 26, 193, 5, 32, 149, 226, 165, 138, 98, 151, 3, 165, 246, 118, 164, 150, 18, 75, 200, 76, 170, 142, 85, 169, 58, 91, 1, 88, 20, 64, 44, 201, 248, 132, 62, 161, 28, 155, 56, 8, 69, 57, 123, 59, 4, 225, 200, 38, 166, 147, 73, 12, 132, 106, 36, 42, 120, 56, 94, 110, 77, 28, 10, 65, 206, 45, 171, 42, 25, 185, 237, 46, 229, 12, 177, 112, 114, 44, 128, 133, 196, 35, 93, 149, 78, 144, 115, 151, 228, 91, 150, 107, 20, 187, 146, 189, 29, 57, 35, 231, 186, 138, 104, 226, 145, 91, 163, 128, 157, 247, 21, 60, 85, 144, 142, 204, 201, 144, 92, 230, 119, 149, 140, 212, 42, 25, 197, 168, 171, 40, 70, 252, 198, 31, 151, 120, 100, 203, 83, 12, 72, 70, 142, 192, 72, 177, 101, 149, 216, 196, 63, 113, 171, 85, 250, 184, 39, 110, 29, 92, 78, 23, 156, 78, 55, 106, 106, 141, 37, 25, 75, 185, 214, 85, 240, 33, 30, 78, 240, 177, 169, 66, 192, 233, 67, 87, 67, 99, 198, 207, 21, 237, 155, 146, 136, 53, 33, 138, 148, 42, 25, 43, 101, 119, 169, 169, 185, 89, 28, 57, 59, 136, 35, 103, 7, 49, 108, 49, 167, 77, 60, 146, 159, 212, 17, 100, 133, 212, 158, 184, 149, 82, 87, 17, 31, 107, 211, 154, 244, 224, 52, 10, 216, 82, 148, 49, 147, 41, 128, 0, 16, 111, 39, 61, 82, 171, 100, 172, 228, 186, 10, 62, 196, 199, 226, 4, 209, 122, 4, 226, 1, 16, 138, 138, 212, 158, 184, 229, 186, 34, 149, 45, 206, 16, 11, 235, 212, 194, 182, 97, 196, 3, 32, 20, 149, 74, 126, 226, 74, 129, 84, 2, 167, 53, 45, 228, 165, 16, 1, 32, 20, 21, 178, 42, 34, 77, 38, 56, 55, 218, 24, 61, 254, 63, 91, 213, 67, 145, 233, 13, 169, 197, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130]; +final faviconTileBytes = [ + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 32, + 0, + 201, + 45, + 0, + 0, + 22, + 0, + 0, + 0, + 137, + 80, + 78, + 71, + 13, + 10, + 26, + 10, + 0, + 0, + 0, + 13, + 73, + 72, + 68, + 82, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 8, + 6, + 0, + 0, + 0, + 92, + 114, + 168, + 102, + 0, + 0, + 45, + 144, + 73, + 68, + 65, + 84, + 120, + 218, + 237, + 157, + 123, + 124, + 27, + 229, + 153, + 239, + 127, + 26, + 141, + 70, + 23, + 91, + 146, + 109, + 249, + 126, + 139, + 147, + 216, + 9, + 185, + 185, + 9, + 16, + 72, + 40, + 183, + 64, + 129, + 246, + 208, + 238, + 129, + 237, + 210, + 82, + 122, + 202, + 161, + 91, + 2, + 11, + 45, + 44, + 108, + 161, + 13, + 112, + 246, + 0, + 165, + 52, + 180, + 205, + 46, + 109, + 225, + 244, + 190, + 103, + 217, + 148, + 101, + 89, + 216, + 93, + 232, + 133, + 179, + 101, + 11, + 9, + 105, + 32, + 9, + 73, + 73, + 76, + 156, + 224, + 196, + 151, + 92, + 124, + 209, + 197, + 182, + 44, + 75, + 178, + 46, + 51, + 26, + 73, + 231, + 15, + 89, + 178, + 36, + 75, + 214, + 109, + 164, + 153, + 145, + 222, + 239, + 231, + 147, + 79, + 28, + 105, + 52, + 126, + 53, + 153, + 231, + 55, + 207, + 251, + 188, + 207, + 243, + 188, + 10, + 254, + 163, + 23, + 195, + 40, + 0, + 191, + 215, + 9, + 243, + 88, + 63, + 204, + 163, + 199, + 97, + 29, + 59, + 1, + 149, + 190, + 25, + 205, + 61, + 215, + 160, + 121, + 213, + 39, + 80, + 219, + 177, + 9, + 52, + 83, + 93, + 200, + 233, + 9, + 4, + 66, + 17, + 81, + 20, + 42, + 0, + 201, + 204, + 76, + 157, + 195, + 196, + 249, + 62, + 88, + 198, + 250, + 225, + 152, + 49, + 163, + 174, + 227, + 34, + 52, + 245, + 92, + 131, + 166, + 238, + 109, + 208, + 55, + 174, + 18, + 251, + 251, + 18, + 8, + 132, + 56, + 4, + 23, + 128, + 120, + 120, + 158, + 133, + 101, + 52, + 226, + 29, + 152, + 199, + 250, + 17, + 166, + 52, + 104, + 90, + 181, + 13, + 77, + 221, + 219, + 208, + 216, + 125, + 37, + 241, + 14, + 8, + 4, + 145, + 201, + 73, + 0, + 38, + 236, + 62, + 184, + 61, + 172, + 216, + 99, + 134, + 182, + 190, + 153, + 76, + 47, + 8, + 4, + 1, + 160, + 179, + 61, + 112, + 194, + 238, + 131, + 66, + 93, + 139, + 21, + 203, + 87, + 138, + 61, + 102, + 76, + 155, + 71, + 224, + 24, + 59, + 134, + 134, + 149, + 87, + 136, + 61, + 20, + 2, + 65, + 214, + 80, + 217, + 30, + 232, + 246, + 176, + 168, + 111, + 21, + 223, + 248, + 1, + 160, + 190, + 117, + 37, + 124, + 211, + 86, + 177, + 135, + 65, + 32, + 200, + 158, + 172, + 5, + 128, + 64, + 32, + 148, + 31, + 178, + 22, + 0, + 235, + 224, + 31, + 192, + 115, + 115, + 98, + 15, + 131, + 64, + 144, + 45, + 89, + 199, + 0, + 164, + 200, + 200, + 219, + 79, + 227, + 200, + 244, + 121, + 212, + 117, + 94, + 130, + 166, + 158, + 107, + 208, + 176, + 242, + 10, + 24, + 155, + 214, + 138, + 61, + 44, + 2, + 65, + 54, + 228, + 45, + 0, + 111, + 255, + 231, + 191, + 225, + 143, + 111, + 255, + 22, + 0, + 160, + 209, + 106, + 209, + 220, + 186, + 12, + 87, + 95, + 127, + 19, + 150, + 175, + 92, + 19, + 59, + 230, + 223, + 94, + 252, + 49, + 250, + 251, + 222, + 143, + 253, + 123, + 109, + 239, + 197, + 248, + 252, + 237, + 247, + 1, + 0, + 206, + 142, + 12, + 224, + 159, + 255, + 225, + 239, + 97, + 53, + 143, + 97, + 237, + 134, + 139, + 241, + 63, + 255, + 234, + 155, + 168, + 170, + 210, + 3, + 0, + 142, + 31, + 61, + 128, + 87, + 95, + 252, + 9, + 156, + 179, + 51, + 184, + 120, + 203, + 85, + 184, + 253, + 174, + 111, + 164, + 28, + 195, + 117, + 55, + 61, + 6, + 158, + 103, + 97, + 29, + 63, + 9, + 203, + 232, + 126, + 28, + 57, + 240, + 99, + 4, + 194, + 10, + 52, + 245, + 92, + 131, + 198, + 149, + 87, + 162, + 177, + 231, + 106, + 48, + 154, + 26, + 177, + 175, + 49, + 129, + 32, + 89, + 242, + 22, + 0, + 135, + 125, + 18, + 77, + 45, + 29, + 248, + 243, + 47, + 109, + 199, + 156, + 195, + 137, + 211, + 39, + 251, + 240, + 204, + 223, + 222, + 139, + 47, + 221, + 245, + 16, + 46, + 191, + 250, + 70, + 0, + 192, + 208, + 233, + 227, + 184, + 230, + 147, + 159, + 197, + 138, + 158, + 200, + 83, + 89, + 171, + 173, + 2, + 0, + 112, + 156, + 31, + 127, + 247, + 212, + 131, + 248, + 226, + 95, + 62, + 136, + 222, + 139, + 46, + 195, + 203, + 255, + 247, + 7, + 120, + 101, + 247, + 243, + 248, + 242, + 61, + 143, + 192, + 238, + 180, + 227, + 255, + 236, + 122, + 12, + 247, + 239, + 248, + 30, + 58, + 186, + 186, + 241, + 179, + 103, + 255, + 55, + 222, + 248, + 143, + 221, + 184, + 241, + 207, + 111, + 79, + 253, + 5, + 104, + 53, + 218, + 187, + 46, + 68, + 123, + 215, + 133, + 0, + 0, + 183, + 203, + 6, + 243, + 249, + 15, + 49, + 241, + 254, + 79, + 113, + 236, + 245, + 7, + 161, + 111, + 90, + 131, + 166, + 149, + 87, + 161, + 185, + 231, + 19, + 168, + 237, + 188, + 72, + 236, + 235, + 77, + 32, + 72, + 138, + 130, + 166, + 0, + 90, + 93, + 21, + 154, + 27, + 151, + 1, + 141, + 64, + 247, + 234, + 94, + 116, + 116, + 117, + 227, + 185, + 239, + 237, + 192, + 37, + 151, + 93, + 11, + 134, + 209, + 192, + 53, + 235, + 192, + 138, + 158, + 181, + 232, + 88, + 214, + 157, + 240, + 185, + 161, + 83, + 253, + 48, + 24, + 106, + 176, + 245, + 202, + 27, + 0, + 0, + 55, + 221, + 126, + 47, + 118, + 220, + 125, + 51, + 190, + 248, + 149, + 7, + 209, + 119, + 96, + 47, + 214, + 125, + 108, + 51, + 214, + 245, + 110, + 6, + 0, + 252, + 217, + 95, + 124, + 25, + 191, + 124, + 254, + 219, + 105, + 5, + 32, + 25, + 189, + 161, + 9, + 171, + 55, + 92, + 143, + 213, + 27, + 174, + 7, + 207, + 179, + 176, + 219, + 206, + 96, + 226, + 124, + 31, + 250, + 254, + 253, + 85, + 120, + 125, + 30, + 52, + 173, + 186, + 38, + 226, + 33, + 116, + 95, + 5, + 117, + 85, + 189, + 216, + 215, + 159, + 64, + 16, + 21, + 65, + 99, + 0, + 189, + 23, + 94, + 6, + 138, + 82, + 98, + 232, + 84, + 63, + 214, + 245, + 110, + 134, + 219, + 237, + 68, + 223, + 159, + 246, + 227, + 248, + 7, + 7, + 208, + 209, + 213, + 141, + 222, + 11, + 47, + 3, + 0, + 88, + 39, + 206, + 163, + 61, + 78, + 20, + 76, + 70, + 19, + 0, + 192, + 53, + 235, + 128, + 213, + 60, + 138, + 214, + 182, + 174, + 216, + 123, + 237, + 93, + 61, + 152, + 180, + 78, + 228, + 247, + 229, + 104, + 53, + 154, + 218, + 214, + 160, + 169, + 109, + 13, + 112, + 217, + 23, + 224, + 247, + 58, + 49, + 113, + 190, + 15, + 230, + 99, + 187, + 113, + 252, + 119, + 223, + 132, + 198, + 216, + 129, + 166, + 85, + 215, + 162, + 105, + 229, + 85, + 36, + 177, + 136, + 80, + 145, + 8, + 30, + 4, + 172, + 51, + 53, + 194, + 237, + 114, + 0, + 0, + 62, + 245, + 103, + 183, + 197, + 94, + 255, + 247, + 151, + 126, + 134, + 55, + 127, + 251, + 47, + 120, + 248, + 241, + 231, + 224, + 247, + 121, + 161, + 102, + 212, + 9, + 159, + 211, + 234, + 244, + 112, + 187, + 103, + 193, + 113, + 44, + 106, + 106, + 23, + 158, + 204, + 85, + 85, + 122, + 4, + 2, + 28, + 60, + 30, + 119, + 44, + 70, + 144, + 47, + 26, + 157, + 17, + 43, + 215, + 92, + 133, + 149, + 107, + 174, + 2, + 0, + 76, + 217, + 134, + 97, + 29, + 59, + 129, + 83, + 255, + 185, + 3, + 179, + 179, + 54, + 152, + 150, + 109, + 65, + 211, + 170, + 107, + 208, + 188, + 234, + 90, + 84, + 213, + 46, + 43, + 222, + 85, + 39, + 16, + 36, + 130, + 224, + 2, + 48, + 61, + 101, + 129, + 222, + 80, + 11, + 0, + 9, + 110, + 251, + 117, + 159, + 254, + 28, + 190, + 122, + 251, + 245, + 24, + 59, + 63, + 140, + 106, + 67, + 13, + 216, + 179, + 131, + 9, + 159, + 243, + 121, + 221, + 208, + 84, + 49, + 208, + 235, + 141, + 240, + 121, + 23, + 150, + 246, + 60, + 30, + 55, + 0, + 20, + 108, + 252, + 169, + 104, + 104, + 234, + 70, + 67, + 83, + 55, + 54, + 92, + 124, + 19, + 252, + 94, + 39, + 108, + 230, + 83, + 176, + 12, + 255, + 30, + 251, + 247, + 124, + 15, + 148, + 218, + 128, + 166, + 85, + 159, + 64, + 211, + 170, + 109, + 168, + 239, + 218, + 74, + 188, + 3, + 66, + 89, + 66, + 191, + 251, + 135, + 31, + 163, + 181, + 179, + 23, + 173, + 29, + 27, + 160, + 209, + 25, + 11, + 58, + 217, + 161, + 119, + 255, + 11, + 20, + 165, + 68, + 207, + 5, + 27, + 22, + 189, + 199, + 48, + 26, + 104, + 117, + 122, + 240, + 124, + 0, + 109, + 29, + 93, + 120, + 243, + 55, + 47, + 197, + 222, + 179, + 59, + 237, + 224, + 88, + 22, + 166, + 186, + 54, + 52, + 183, + 47, + 195, + 209, + 247, + 247, + 197, + 222, + 27, + 63, + 55, + 132, + 150, + 214, + 206, + 162, + 95, + 8, + 141, + 206, + 136, + 101, + 221, + 151, + 98, + 89, + 247, + 165, + 0, + 0, + 215, + 172, + 5, + 227, + 231, + 142, + 225, + 204, + 158, + 157, + 56, + 50, + 117, + 22, + 198, + 214, + 141, + 104, + 238, + 185, + 6, + 77, + 171, + 175, + 37, + 75, + 141, + 132, + 178, + 129, + 174, + 93, + 115, + 51, + 206, + 13, + 239, + 197, + 7, + 7, + 94, + 65, + 85, + 149, + 30, + 45, + 29, + 189, + 104, + 237, + 236, + 141, + 204, + 155, + 51, + 224, + 243, + 122, + 96, + 157, + 60, + 15, + 231, + 148, + 29, + 253, + 199, + 14, + 225, + 205, + 223, + 189, + 140, + 237, + 247, + 63, + 30, + 9, + 0, + 186, + 28, + 24, + 57, + 221, + 143, + 85, + 107, + 55, + 1, + 0, + 222, + 121, + 243, + 53, + 168, + 213, + 106, + 180, + 117, + 44, + 7, + 195, + 104, + 16, + 8, + 112, + 120, + 247, + 157, + 55, + 176, + 105, + 243, + 149, + 120, + 125, + 247, + 143, + 113, + 233, + 229, + 215, + 129, + 97, + 52, + 216, + 180, + 249, + 74, + 188, + 244, + 15, + 207, + 226, + 228, + 241, + 35, + 232, + 232, + 234, + 198, + 111, + 254, + 237, + 31, + 99, + 193, + 194, + 82, + 98, + 168, + 105, + 193, + 218, + 141, + 45, + 88, + 187, + 241, + 191, + 197, + 150, + 26, + 173, + 227, + 135, + 113, + 228, + 240, + 47, + 17, + 8, + 43, + 208, + 184, + 242, + 42, + 52, + 245, + 108, + 67, + 195, + 138, + 203, + 73, + 48, + 145, + 32, + 91, + 20, + 46, + 135, + 45, + 86, + 13, + 232, + 24, + 253, + 0, + 214, + 161, + 183, + 96, + 27, + 217, + 7, + 183, + 109, + 0, + 77, + 173, + 23, + 160, + 181, + 243, + 99, + 104, + 237, + 236, + 197, + 248, + 172, + 10, + 43, + 214, + 127, + 60, + 246, + 193, + 228, + 60, + 128, + 182, + 142, + 21, + 216, + 118, + 195, + 159, + 199, + 34, + 254, + 30, + 143, + 27, + 255, + 240, + 252, + 83, + 56, + 63, + 114, + 26, + 148, + 82, + 137, + 229, + 221, + 107, + 241, + 185, + 47, + 125, + 21, + 245, + 141, + 45, + 0, + 128, + 177, + 243, + 195, + 248, + 167, + 159, + 125, + 23, + 147, + 86, + 51, + 46, + 88, + 183, + 41, + 33, + 15, + 224, + 228, + 241, + 35, + 120, + 249, + 133, + 31, + 97, + 110, + 206, + 137, + 141, + 23, + 95, + 142, + 47, + 220, + 113, + 63, + 24, + 70, + 147, + 48, + 240, + 51, + 39, + 222, + 195, + 5, + 157, + 53, + 162, + 92, + 180, + 57, + 215, + 84, + 164, + 196, + 121, + 244, + 67, + 216, + 204, + 167, + 200, + 82, + 35, + 65, + 182, + 36, + 8, + 64, + 60, + 172, + 103, + 26, + 83, + 103, + 222, + 133, + 109, + 104, + 47, + 172, + 131, + 111, + 99, + 195, + 182, + 199, + 19, + 4, + 64, + 108, + 196, + 20, + 128, + 100, + 108, + 19, + 3, + 176, + 140, + 245, + 195, + 60, + 250, + 33, + 60, + 30, + 55, + 76, + 203, + 183, + 160, + 169, + 123, + 27, + 154, + 87, + 93, + 11, + 173, + 177, + 77, + 236, + 225, + 17, + 8, + 105, + 73, + 43, + 0, + 201, + 140, + 190, + 255, + 42, + 17, + 128, + 44, + 32, + 45, + 210, + 8, + 114, + 66, + 214, + 181, + 0, + 82, + 68, + 163, + 51, + 98, + 197, + 234, + 203, + 177, + 98, + 245, + 229, + 0, + 22, + 90, + 164, + 157, + 126, + 243, + 49, + 56, + 102, + 204, + 48, + 45, + 219, + 130, + 198, + 149, + 87, + 144, + 22, + 105, + 4, + 73, + 64, + 4, + 160, + 200, + 212, + 53, + 116, + 161, + 174, + 161, + 11, + 27, + 46, + 190, + 105, + 161, + 69, + 218, + 249, + 119, + 112, + 224, + 221, + 231, + 72, + 139, + 52, + 130, + 232, + 100, + 61, + 5, + 152, + 26, + 217, + 15, + 85, + 8, + 146, + 104, + 10, + 50, + 109, + 30, + 65, + 152, + 117, + 160, + 205, + 164, + 21, + 123, + 40, + 5, + 17, + 93, + 106, + 180, + 140, + 245, + 99, + 218, + 54, + 2, + 99, + 75, + 47, + 154, + 87, + 125, + 2, + 77, + 221, + 87, + 195, + 216, + 186, + 161, + 240, + 95, + 64, + 32, + 100, + 32, + 107, + 1, + 224, + 185, + 57, + 56, + 198, + 142, + 73, + 162, + 19, + 143, + 190, + 74, + 141, + 38, + 35, + 5, + 154, + 86, + 23, + 126, + 50, + 137, + 192, + 243, + 44, + 166, + 204, + 167, + 99, + 241, + 3, + 63, + 199, + 161, + 169, + 251, + 42, + 82, + 183, + 64, + 40, + 42, + 89, + 11, + 128, + 80, + 36, + 47, + 53, + 54, + 182, + 172, + 70, + 107, + 231, + 6, + 180, + 116, + 108, + 128, + 161, + 166, + 69, + 236, + 235, + 33, + 25, + 230, + 92, + 83, + 176, + 77, + 124, + 20, + 105, + 177, + 62, + 126, + 18, + 213, + 166, + 110, + 52, + 175, + 254, + 4, + 26, + 86, + 92, + 137, + 250, + 229, + 91, + 197, + 30, + 30, + 161, + 76, + 40, + 185, + 0, + 196, + 195, + 249, + 103, + 49, + 125, + 230, + 0, + 172, + 131, + 111, + 193, + 54, + 180, + 7, + 84, + 152, + 71, + 107, + 199, + 6, + 180, + 117, + 109, + 68, + 115, + 251, + 186, + 178, + 122, + 194, + 23, + 202, + 148, + 109, + 24, + 230, + 115, + 125, + 48, + 143, + 126, + 136, + 57, + 247, + 52, + 76, + 43, + 34, + 129, + 196, + 166, + 158, + 109, + 208, + 213, + 116, + 136, + 61, + 60, + 130, + 76, + 17, + 85, + 0, + 146, + 113, + 218, + 62, + 194, + 212, + 200, + 126, + 216, + 134, + 246, + 96, + 102, + 244, + 48, + 234, + 234, + 151, + 161, + 165, + 115, + 3, + 90, + 59, + 122, + 81, + 215, + 208, + 37, + 246, + 240, + 36, + 67, + 116, + 169, + 209, + 58, + 118, + 2, + 214, + 241, + 147, + 80, + 86, + 53, + 160, + 113, + 197, + 21, + 164, + 110, + 129, + 144, + 51, + 146, + 18, + 128, + 120, + 120, + 110, + 14, + 211, + 231, + 14, + 98, + 106, + 228, + 93, + 88, + 135, + 246, + 32, + 224, + 182, + 162, + 181, + 179, + 23, + 205, + 29, + 235, + 5, + 169, + 91, + 40, + 39, + 102, + 166, + 206, + 193, + 60, + 118, + 28, + 150, + 209, + 126, + 204, + 144, + 22, + 105, + 132, + 28, + 144, + 172, + 0, + 36, + 227, + 157, + 29, + 131, + 109, + 104, + 47, + 108, + 195, + 123, + 97, + 63, + 179, + 31, + 213, + 250, + 250, + 72, + 154, + 114, + 215, + 70, + 52, + 52, + 117, + 23, + 254, + 11, + 202, + 132, + 133, + 22, + 105, + 253, + 176, + 140, + 245, + 147, + 22, + 105, + 132, + 37, + 145, + 141, + 0, + 36, + 51, + 125, + 246, + 32, + 166, + 206, + 252, + 17, + 214, + 211, + 111, + 97, + 206, + 62, + 140, + 150, + 246, + 117, + 104, + 110, + 95, + 143, + 214, + 206, + 94, + 84, + 27, + 26, + 196, + 30, + 158, + 100, + 136, + 182, + 72, + 51, + 143, + 246, + 99, + 210, + 114, + 154, + 212, + 45, + 16, + 18, + 144, + 173, + 0, + 196, + 195, + 122, + 166, + 49, + 57, + 188, + 15, + 182, + 161, + 61, + 176, + 13, + 239, + 131, + 134, + 97, + 98, + 37, + 206, + 13, + 173, + 171, + 73, + 48, + 113, + 158, + 248, + 22, + 105, + 150, + 209, + 227, + 164, + 69, + 26, + 161, + 60, + 4, + 32, + 25, + 167, + 185, + 31, + 182, + 225, + 119, + 96, + 29, + 124, + 11, + 78, + 203, + 113, + 212, + 55, + 173, + 68, + 75, + 199, + 6, + 180, + 119, + 109, + 34, + 75, + 141, + 113, + 196, + 90, + 164, + 141, + 246, + 195, + 58, + 113, + 146, + 180, + 72, + 171, + 64, + 202, + 82, + 0, + 226, + 225, + 185, + 57, + 76, + 14, + 255, + 17, + 182, + 225, + 189, + 176, + 13, + 238, + 133, + 34, + 228, + 71, + 107, + 199, + 6, + 180, + 118, + 246, + 162, + 165, + 115, + 3, + 241, + 14, + 226, + 136, + 182, + 72, + 51, + 159, + 239, + 35, + 45, + 210, + 42, + 132, + 178, + 23, + 128, + 100, + 220, + 147, + 131, + 17, + 49, + 152, + 95, + 106, + 172, + 53, + 117, + 160, + 165, + 99, + 3, + 218, + 150, + 109, + 36, + 75, + 141, + 113, + 68, + 91, + 164, + 153, + 199, + 142, + 195, + 114, + 254, + 120, + 66, + 139, + 52, + 125, + 195, + 42, + 208, + 76, + 21, + 153, + 50, + 148, + 1, + 21, + 39, + 0, + 241, + 68, + 211, + 155, + 173, + 131, + 111, + 193, + 58, + 180, + 7, + 156, + 219, + 130, + 150, + 121, + 239, + 128, + 44, + 53, + 38, + 50, + 51, + 117, + 14, + 214, + 137, + 143, + 96, + 62, + 255, + 33, + 188, + 115, + 118, + 176, + 172, + 7, + 1, + 206, + 15, + 5, + 165, + 4, + 173, + 214, + 67, + 165, + 174, + 134, + 74, + 173, + 135, + 82, + 93, + 13, + 70, + 87, + 3, + 90, + 165, + 131, + 74, + 99, + 4, + 163, + 53, + 66, + 169, + 210, + 65, + 165, + 53, + 130, + 102, + 170, + 160, + 210, + 26, + 64, + 171, + 170, + 161, + 82, + 87, + 131, + 214, + 26, + 136, + 144, + 136, + 76, + 69, + 11, + 64, + 50, + 62, + 231, + 4, + 172, + 131, + 111, + 71, + 150, + 26, + 207, + 30, + 202, + 185, + 69, + 90, + 37, + 194, + 243, + 44, + 120, + 206, + 143, + 0, + 239, + 71, + 128, + 245, + 33, + 192, + 249, + 16, + 8, + 248, + 192, + 177, + 94, + 4, + 56, + 47, + 2, + 129, + 200, + 235, + 28, + 235, + 137, + 252, + 204, + 249, + 16, + 224, + 188, + 224, + 3, + 126, + 176, + 108, + 228, + 239, + 69, + 66, + 162, + 49, + 66, + 165, + 49, + 128, + 214, + 84, + 167, + 20, + 18, + 149, + 198, + 0, + 90, + 93, + 5, + 149, + 218, + 0, + 90, + 163, + 135, + 74, + 173, + 7, + 173, + 209, + 147, + 37, + 206, + 60, + 32, + 2, + 176, + 4, + 75, + 181, + 72, + 35, + 75, + 141, + 194, + 146, + 74, + 72, + 22, + 68, + 195, + 11, + 142, + 245, + 130, + 15, + 176, + 139, + 132, + 36, + 192, + 122, + 193, + 5, + 252, + 224, + 3, + 126, + 112, + 172, + 7, + 180, + 74, + 3, + 74, + 85, + 181, + 72, + 72, + 84, + 234, + 136, + 183, + 65, + 132, + 36, + 17, + 34, + 0, + 89, + 18, + 223, + 34, + 109, + 114, + 100, + 31, + 84, + 138, + 48, + 90, + 58, + 54, + 160, + 165, + 115, + 3, + 169, + 91, + 144, + 16, + 81, + 33, + 225, + 184, + 136, + 96, + 68, + 133, + 132, + 99, + 61, + 224, + 121, + 118, + 222, + 51, + 137, + 122, + 42, + 139, + 133, + 132, + 99, + 61, + 224, + 3, + 126, + 208, + 42, + 13, + 104, + 141, + 17, + 180, + 90, + 63, + 239, + 133, + 44, + 8, + 9, + 163, + 173, + 1, + 205, + 232, + 202, + 66, + 72, + 136, + 0, + 228, + 137, + 211, + 246, + 17, + 108, + 167, + 223, + 134, + 117, + 104, + 15, + 156, + 230, + 62, + 152, + 26, + 150, + 163, + 117, + 217, + 199, + 208, + 220, + 182, + 150, + 4, + 19, + 203, + 128, + 120, + 33, + 97, + 89, + 15, + 120, + 214, + 11, + 158, + 231, + 82, + 10, + 9, + 199, + 121, + 192, + 7, + 184, + 140, + 66, + 162, + 154, + 23, + 7, + 37, + 163, + 75, + 16, + 18, + 149, + 182, + 6, + 74, + 149, + 54, + 38, + 36, + 106, + 77, + 29, + 40, + 181, + 22, + 140, + 198, + 56, + 31, + 59, + 41, + 222, + 114, + 44, + 17, + 0, + 1, + 136, + 214, + 45, + 216, + 6, + 247, + 194, + 54, + 248, + 22, + 66, + 172, + 11, + 45, + 203, + 122, + 209, + 218, + 209, + 139, + 166, + 214, + 11, + 72, + 48, + 177, + 130, + 89, + 74, + 72, + 88, + 214, + 131, + 32, + 207, + 45, + 8, + 9, + 235, + 141, + 136, + 9, + 231, + 3, + 199, + 249, + 22, + 4, + 39, + 224, + 7, + 205, + 84, + 129, + 86, + 87, + 47, + 18, + 18, + 70, + 91, + 27, + 241, + 56, + 210, + 8, + 9, + 173, + 209, + 71, + 188, + 147, + 52, + 66, + 66, + 4, + 160, + 8, + 120, + 28, + 231, + 35, + 193, + 196, + 193, + 61, + 176, + 159, + 63, + 132, + 154, + 154, + 38, + 180, + 46, + 219, + 136, + 230, + 142, + 245, + 164, + 110, + 129, + 144, + 23, + 169, + 132, + 132, + 227, + 124, + 243, + 193, + 212, + 136, + 144, + 112, + 172, + 7, + 1, + 214, + 139, + 0, + 239, + 143, + 196, + 76, + 230, + 133, + 36, + 242, + 26, + 155, + 82, + 72, + 136, + 0, + 20, + 153, + 232, + 82, + 163, + 109, + 100, + 31, + 108, + 131, + 111, + 195, + 239, + 28, + 67, + 115, + 219, + 58, + 180, + 118, + 70, + 114, + 15, + 136, + 119, + 64, + 40, + 37, + 60, + 207, + 194, + 239, + 117, + 33, + 20, + 226, + 193, + 178, + 30, + 34, + 0, + 165, + 198, + 231, + 156, + 192, + 244, + 185, + 67, + 145, + 186, + 133, + 193, + 61, + 208, + 105, + 171, + 208, + 210, + 217, + 139, + 150, + 246, + 117, + 164, + 110, + 129, + 80, + 114, + 136, + 0, + 136, + 12, + 105, + 145, + 70, + 16, + 19, + 34, + 0, + 18, + 130, + 243, + 207, + 98, + 114, + 232, + 29, + 76, + 142, + 252, + 17, + 182, + 161, + 61, + 100, + 169, + 145, + 80, + 116, + 136, + 0, + 72, + 24, + 210, + 34, + 141, + 80, + 108, + 136, + 0, + 200, + 132, + 132, + 165, + 198, + 161, + 61, + 8, + 249, + 103, + 209, + 220, + 190, + 142, + 180, + 72, + 35, + 20, + 4, + 17, + 0, + 153, + 226, + 113, + 156, + 143, + 52, + 65, + 33, + 45, + 210, + 8, + 5, + 64, + 4, + 160, + 76, + 152, + 62, + 123, + 16, + 214, + 161, + 183, + 49, + 53, + 188, + 47, + 214, + 34, + 173, + 109, + 217, + 70, + 52, + 181, + 173, + 37, + 117, + 11, + 132, + 180, + 16, + 1, + 40, + 67, + 72, + 139, + 52, + 66, + 182, + 16, + 1, + 168, + 0, + 72, + 139, + 52, + 66, + 58, + 136, + 0, + 84, + 24, + 164, + 69, + 26, + 33, + 30, + 34, + 0, + 21, + 78, + 66, + 139, + 180, + 177, + 15, + 80, + 91, + 215, + 74, + 90, + 164, + 85, + 16, + 68, + 0, + 8, + 49, + 146, + 91, + 164, + 5, + 220, + 86, + 52, + 119, + 172, + 71, + 93, + 67, + 23, + 12, + 53, + 205, + 208, + 85, + 213, + 129, + 86, + 169, + 193, + 48, + 58, + 208, + 140, + 134, + 120, + 11, + 101, + 0, + 17, + 0, + 66, + 90, + 162, + 117, + 11, + 179, + 230, + 227, + 112, + 79, + 143, + 128, + 157, + 155, + 66, + 192, + 239, + 68, + 128, + 157, + 67, + 40, + 176, + 80, + 239, + 206, + 168, + 171, + 192, + 168, + 52, + 80, + 169, + 117, + 80, + 49, + 58, + 168, + 24, + 45, + 84, + 42, + 77, + 228, + 111, + 70, + 11, + 38, + 250, + 250, + 252, + 177, + 42, + 70, + 75, + 132, + 68, + 34, + 16, + 1, + 32, + 20, + 4, + 231, + 159, + 5, + 239, + 119, + 35, + 192, + 186, + 231, + 255, + 118, + 129, + 103, + 61, + 8, + 248, + 93, + 8, + 248, + 156, + 8, + 6, + 188, + 224, + 124, + 78, + 240, + 156, + 39, + 242, + 158, + 127, + 46, + 242, + 222, + 188, + 144, + 240, + 172, + 27, + 225, + 80, + 16, + 42, + 70, + 19, + 17, + 147, + 20, + 66, + 194, + 168, + 171, + 34, + 130, + 145, + 66, + 72, + 84, + 106, + 45, + 84, + 180, + 134, + 8, + 73, + 158, + 16, + 1, + 32, + 72, + 2, + 214, + 51, + 13, + 158, + 243, + 68, + 254, + 204, + 11, + 73, + 192, + 231, + 138, + 8, + 71, + 156, + 144, + 4, + 252, + 78, + 240, + 1, + 47, + 56, + 239, + 44, + 130, + 156, + 55, + 173, + 144, + 168, + 213, + 58, + 208, + 42, + 77, + 76, + 72, + 212, + 234, + 170, + 200, + 191, + 213, + 81, + 239, + 68, + 23, + 17, + 20, + 149, + 182, + 162, + 133, + 132, + 8, + 0, + 161, + 172, + 136, + 9, + 137, + 207, + 21, + 17, + 134, + 192, + 92, + 130, + 144, + 112, + 126, + 103, + 76, + 56, + 162, + 158, + 73, + 144, + 157, + 139, + 8, + 9, + 231, + 77, + 16, + 18, + 245, + 188, + 231, + 17, + 21, + 17, + 38, + 234, + 149, + 48, + 90, + 208, + 140, + 166, + 44, + 132, + 132, + 8, + 0, + 129, + 144, + 4, + 207, + 205, + 33, + 24, + 240, + 39, + 8, + 73, + 128, + 117, + 130, + 103, + 61, + 17, + 1, + 137, + 254, + 157, + 78, + 72, + 230, + 167, + 68, + 0, + 192, + 196, + 98, + 32, + 218, + 121, + 143, + 100, + 177, + 144, + 48, + 76, + 220, + 20, + 71, + 21, + 141, + 155, + 84, + 129, + 166, + 153, + 162, + 215, + 120, + 16, + 1, + 32, + 16, + 138, + 68, + 182, + 66, + 18, + 152, + 23, + 19, + 206, + 231, + 4, + 207, + 186, + 17, + 240, + 187, + 16, + 228, + 60, + 105, + 133, + 68, + 197, + 232, + 98, + 65, + 212, + 168, + 88, + 40, + 85, + 76, + 94, + 66, + 66, + 4, + 128, + 64, + 144, + 56, + 201, + 66, + 194, + 249, + 102, + 17, + 96, + 221, + 8, + 6, + 124, + 25, + 133, + 132, + 103, + 61, + 145, + 159, + 211, + 8, + 9, + 17, + 0, + 2, + 161, + 66, + 136, + 10, + 9, + 231, + 153, + 65, + 136, + 103, + 193, + 249, + 102, + 137, + 0, + 16, + 8, + 149, + 12, + 37, + 246, + 0, + 8, + 4, + 130, + 120, + 16, + 1, + 32, + 16, + 42, + 24, + 34, + 0, + 4, + 66, + 5, + 67, + 4, + 128, + 64, + 168, + 96, + 136, + 0, + 16, + 8, + 21, + 12, + 17, + 0, + 2, + 161, + 130, + 161, + 197, + 30, + 0, + 129, + 16, + 101, + 192, + 113, + 2, + 78, + 191, + 3, + 0, + 64, + 81, + 74, + 208, + 97, + 26, + 148, + 82, + 1, + 53, + 173, + 5, + 173, + 160, + 161, + 85, + 234, + 208, + 174, + 239, + 20, + 123, + 152, + 101, + 5, + 201, + 3, + 32, + 136, + 206, + 128, + 227, + 4, + 166, + 125, + 147, + 240, + 243, + 190, + 172, + 142, + 167, + 20, + 20, + 40, + 5, + 5, + 5, + 148, + 80, + 82, + 20, + 40, + 80, + 80, + 42, + 148, + 160, + 40, + 37, + 40, + 80, + 160, + 41, + 37, + 104, + 74, + 5, + 37, + 69, + 67, + 163, + 212, + 194, + 200, + 212, + 160, + 78, + 99, + 18, + 251, + 107, + 74, + 18, + 226, + 1, + 16, + 68, + 35, + 87, + 195, + 143, + 18, + 10, + 135, + 16, + 10, + 135, + 0, + 240, + 8, + 132, + 178, + 255, + 28, + 77, + 69, + 110, + 119, + 74, + 161, + 140, + 136, + 72, + 156, + 112, + 68, + 189, + 13, + 154, + 82, + 65, + 173, + 212, + 84, + 140, + 183, + 65, + 60, + 0, + 66, + 201, + 201, + 215, + 240, + 197, + 32, + 87, + 111, + 163, + 90, + 85, + 141, + 6, + 109, + 147, + 216, + 195, + 206, + 26, + 226, + 1, + 16, + 74, + 134, + 156, + 12, + 63, + 74, + 185, + 123, + 27, + 196, + 3, + 32, + 20, + 29, + 57, + 26, + 190, + 24, + 228, + 228, + 109, + 64, + 141, + 106, + 141, + 161, + 96, + 111, + 131, + 120, + 0, + 132, + 162, + 81, + 206, + 134, + 207, + 7, + 120, + 76, + 78, + 78, + 161, + 177, + 177, + 1, + 180, + 138, + 70, + 40, + 20, + 134, + 195, + 49, + 3, + 214, + 207, + 65, + 167, + 211, + 193, + 104, + 212, + 67, + 65, + 229, + 182, + 202, + 158, + 179, + 183, + 17, + 169, + 240, + 205, + 217, + 219, + 48, + 168, + 141, + 48, + 48, + 145, + 254, + 0, + 196, + 3, + 32, + 8, + 78, + 57, + 27, + 126, + 148, + 57, + 183, + 27, + 44, + 199, + 65, + 205, + 48, + 168, + 214, + 235, + 49, + 235, + 112, + 2, + 0, + 12, + 70, + 3, + 92, + 78, + 23, + 0, + 160, + 166, + 86, + 186, + 59, + 54, + 71, + 189, + 13, + 146, + 8, + 68, + 16, + 140, + 1, + 199, + 9, + 236, + 55, + 239, + 193, + 184, + 251, + 124, + 89, + 27, + 63, + 0, + 120, + 61, + 126, + 24, + 13, + 70, + 120, + 61, + 254, + 200, + 191, + 189, + 94, + 24, + 140, + 6, + 80, + 148, + 2, + 6, + 163, + 1, + 94, + 175, + 87, + 236, + 33, + 46, + 73, + 40, + 28, + 2, + 31, + 226, + 201, + 20, + 128, + 80, + 56, + 149, + 240, + 196, + 143, + 135, + 15, + 240, + 80, + 170, + 40, + 208, + 42, + 26, + 74, + 21, + 5, + 62, + 192, + 47, + 58, + 134, + 202, + 209, + 253, + 23, + 11, + 34, + 0, + 132, + 188, + 17, + 218, + 240, + 139, + 49, + 175, + 46, + 6, + 126, + 191, + 15, + 106, + 134, + 1, + 0, + 168, + 25, + 6, + 126, + 191, + 15, + 58, + 157, + 14, + 46, + 167, + 43, + 54, + 5, + 208, + 104, + 52, + 98, + 15, + 51, + 43, + 196, + 191, + 154, + 4, + 217, + 81, + 44, + 87, + 223, + 239, + 247, + 65, + 173, + 137, + 24, + 20, + 0, + 184, + 156, + 46, + 40, + 41, + 26, + 205, + 45, + 205, + 0, + 0, + 167, + 211, + 45, + 246, + 87, + 7, + 16, + 113, + 255, + 53, + 26, + 45, + 0, + 64, + 163, + 209, + 194, + 235, + 241, + 195, + 96, + 52, + 32, + 24, + 226, + 97, + 181, + 88, + 17, + 12, + 241, + 48, + 24, + 13, + 98, + 15, + 51, + 43, + 136, + 7, + 64, + 200, + 154, + 98, + 187, + 250, + 94, + 143, + 31, + 117, + 166, + 90, + 204, + 216, + 29, + 168, + 214, + 235, + 225, + 245, + 122, + 209, + 220, + 210, + 28, + 155, + 87, + 91, + 45, + 86, + 73, + 4, + 214, + 248, + 96, + 196, + 83, + 137, + 135, + 162, + 20, + 48, + 153, + 76, + 48, + 79, + 88, + 96, + 50, + 201, + 39, + 237, + 152, + 8, + 0, + 33, + 35, + 165, + 152, + 227, + 203, + 105, + 94, + 221, + 218, + 214, + 2, + 0, + 48, + 79, + 88, + 98, + 63, + 203, + 21, + 34, + 0, + 132, + 180, + 148, + 50, + 184, + 87, + 78, + 243, + 106, + 57, + 65, + 4, + 128, + 176, + 8, + 49, + 162, + 250, + 81, + 247, + 31, + 136, + 204, + 171, + 103, + 236, + 14, + 212, + 55, + 214, + 195, + 225, + 152, + 129, + 213, + 98, + 133, + 90, + 195, + 160, + 182, + 182, + 78, + 236, + 75, + 83, + 118, + 144, + 68, + 32, + 66, + 12, + 49, + 151, + 243, + 204, + 19, + 150, + 69, + 175, + 149, + 147, + 171, + 45, + 4, + 233, + 86, + 69, + 146, + 87, + 79, + 114, + 129, + 120, + 0, + 4, + 73, + 172, + 227, + 19, + 99, + 207, + 204, + 194, + 170, + 72, + 29, + 92, + 78, + 23, + 156, + 78, + 55, + 106, + 106, + 141, + 9, + 171, + 39, + 213, + 42, + 125, + 78, + 231, + 148, + 70, + 84, + 133, + 32, + 10, + 149, + 148, + 185, + 87, + 14, + 164, + 203, + 54, + 76, + 206, + 74, + 204, + 5, + 226, + 1, + 84, + 32, + 82, + 120, + 226, + 19, + 10, + 135, + 154, + 119, + 255, + 147, + 87, + 79, + 114, + 153, + 6, + 144, + 24, + 64, + 5, + 65, + 12, + 95, + 222, + 164, + 42, + 56, + 162, + 233, + 136, + 19, + 95, + 173, + 215, + 99, + 206, + 237, + 142, + 253, + 156, + 45, + 68, + 0, + 42, + 0, + 98, + 248, + 229, + 65, + 124, + 16, + 48, + 186, + 42, + 50, + 61, + 57, + 141, + 58, + 83, + 45, + 104, + 21, + 13, + 62, + 192, + 99, + 198, + 238, + 64, + 99, + 115, + 67, + 194, + 231, + 194, + 161, + 16, + 154, + 221, + 141, + 9, + 175, + 241, + 170, + 32, + 0, + 50, + 5, + 40, + 107, + 136, + 225, + 151, + 23, + 169, + 178, + 13, + 83, + 101, + 37, + 46, + 194, + 27, + 68, + 107, + 221, + 10, + 212, + 183, + 45, + 8, + 3, + 207, + 205, + 97, + 206, + 21, + 34, + 2, + 80, + 142, + 16, + 195, + 175, + 28, + 178, + 90, + 61, + 209, + 41, + 17, + 240, + 37, + 222, + 11, + 52, + 83, + 141, + 154, + 122, + 226, + 1, + 148, + 21, + 196, + 240, + 9, + 169, + 80, + 80, + 20, + 248, + 32, + 159, + 242, + 61, + 34, + 0, + 101, + 0, + 49, + 124, + 66, + 190, + 16, + 1, + 144, + 57, + 135, + 44, + 251, + 225, + 14, + 184, + 196, + 30, + 6, + 161, + 132, + 164, + 114, + 245, + 51, + 37, + 79, + 133, + 66, + 169, + 61, + 0, + 146, + 8, + 36, + 115, + 124, + 65, + 105, + 183, + 158, + 34, + 72, + 27, + 34, + 0, + 50, + 102, + 220, + 61, + 10, + 62, + 141, + 178, + 19, + 8, + 217, + 64, + 4, + 64, + 198, + 204, + 114, + 51, + 98, + 15, + 129, + 32, + 115, + 136, + 0, + 200, + 24, + 63, + 159, + 123, + 238, + 55, + 161, + 50, + 225, + 130, + 108, + 202, + 215, + 137, + 0, + 200, + 24, + 54, + 72, + 4, + 32, + 19, + 161, + 80, + 24, + 118, + 187, + 29, + 230, + 9, + 11, + 102, + 29, + 78, + 132, + 67, + 145, + 29, + 55, + 248, + 0, + 15, + 243, + 132, + 37, + 101, + 231, + 161, + 74, + 130, + 8, + 128, + 140, + 153, + 117, + 146, + 27, + 58, + 19, + 233, + 26, + 139, + 38, + 55, + 32, + 173, + 84, + 136, + 0, + 200, + 24, + 46, + 228, + 35, + 55, + 116, + 6, + 138, + 81, + 66, + 43, + 71, + 248, + 112, + 32, + 229, + 235, + 68, + 0, + 100, + 202, + 89, + 215, + 8, + 116, + 85, + 213, + 21, + 123, + 67, + 231, + 75, + 186, + 18, + 218, + 114, + 39, + 0, + 34, + 0, + 101, + 133, + 39, + 16, + 121, + 242, + 87, + 234, + 13, + 157, + 45, + 209, + 198, + 162, + 161, + 80, + 56, + 214, + 88, + 52, + 85, + 3, + 210, + 74, + 133, + 8, + 128, + 76, + 241, + 242, + 94, + 114, + 67, + 103, + 65, + 170, + 13, + 59, + 82, + 109, + 236, + 81, + 238, + 240, + 170, + 32, + 102, + 167, + 23, + 103, + 140, + 18, + 1, + 144, + 41, + 211, + 147, + 230, + 138, + 190, + 161, + 179, + 37, + 90, + 66, + 11, + 0, + 38, + 147, + 9, + 20, + 165, + 136, + 149, + 208, + 154, + 39, + 44, + 152, + 156, + 156, + 74, + 91, + 40, + 83, + 78, + 76, + 135, + 38, + 83, + 190, + 78, + 106, + 1, + 100, + 138, + 39, + 196, + 229, + 86, + 19, + 94, + 36, + 138, + 209, + 169, + 182, + 216, + 84, + 100, + 3, + 82, + 154, + 2, + 207, + 46, + 206, + 5, + 32, + 30, + 128, + 76, + 241, + 251, + 23, + 158, + 242, + 173, + 109, + 45, + 177, + 27, + 57, + 254, + 231, + 82, + 64, + 150, + 217, + 100, + 2, + 163, + 128, + 223, + 51, + 183, + 232, + 101, + 34, + 0, + 50, + 100, + 200, + 121, + 26, + 161, + 249, + 245, + 127, + 177, + 33, + 203, + 108, + 242, + 64, + 65, + 81, + 8, + 135, + 23, + 119, + 255, + 35, + 2, + 32, + 67, + 60, + 156, + 116, + 203, + 127, + 165, + 186, + 42, + 145, + 79, + 9, + 109, + 33, + 72, + 49, + 3, + 49, + 85, + 73, + 48, + 17, + 0, + 25, + 194, + 133, + 2, + 37, + 191, + 161, + 211, + 65, + 150, + 217, + 82, + 35, + 151, + 169, + 17, + 17, + 0, + 25, + 34, + 165, + 26, + 0, + 178, + 204, + 150, + 26, + 185, + 76, + 141, + 164, + 21, + 158, + 37, + 100, + 5, + 31, + 10, + 20, + 126, + 18, + 129, + 200, + 187, + 83, + 109, + 133, + 33, + 196, + 38, + 30, + 197, + 128, + 8, + 128, + 204, + 152, + 241, + 219, + 37, + 223, + 4, + 164, + 34, + 151, + 217, + 146, + 72, + 181, + 181, + 121, + 170, + 169, + 81, + 174, + 123, + 249, + 21, + 66, + 170, + 146, + 96, + 50, + 5, + 144, + 25, + 54, + 159, + 165, + 240, + 147, + 16, + 138, + 142, + 92, + 166, + 70, + 196, + 3, + 144, + 25, + 44, + 233, + 252, + 43, + 11, + 228, + 50, + 53, + 34, + 2, + 32, + 51, + 164, + 218, + 5, + 72, + 42, + 171, + 18, + 82, + 70, + 236, + 169, + 81, + 170, + 146, + 96, + 50, + 5, + 144, + 25, + 129, + 176, + 116, + 2, + 128, + 4, + 121, + 145, + 170, + 36, + 152, + 8, + 128, + 204, + 144, + 210, + 10, + 0, + 65, + 254, + 16, + 1, + 144, + 17, + 22, + 239, + 132, + 228, + 87, + 0, + 8, + 137, + 72, + 105, + 106, + 196, + 171, + 130, + 224, + 185, + 196, + 122, + 0, + 34, + 0, + 50, + 194, + 230, + 49, + 139, + 61, + 4, + 130, + 140, + 9, + 168, + 188, + 152, + 115, + 37, + 214, + 144, + 144, + 32, + 160, + 76, + 56, + 106, + 61, + 12, + 59, + 39, + 173, + 8, + 50, + 65, + 94, + 56, + 217, + 185, + 69, + 37, + 193, + 68, + 0, + 100, + 192, + 97, + 219, + 1, + 56, + 57, + 135, + 216, + 195, + 32, + 72, + 156, + 76, + 189, + 25, + 26, + 76, + 245, + 139, + 183, + 9, + 23, + 123, + 208, + 132, + 165, + 169, + 228, + 205, + 63, + 127, + 254, + 244, + 175, + 48, + 126, + 102, + 233, + 105, + 79, + 199, + 202, + 54, + 108, + 127, + 244, + 127, + 136, + 61, + 84, + 73, + 176, + 80, + 128, + 84, + 7, + 151, + 211, + 5, + 167, + 211, + 141, + 154, + 90, + 99, + 172, + 0, + 137, + 13, + 248, + 193, + 211, + 137, + 49, + 36, + 34, + 0, + 18, + 197, + 197, + 57, + 241, + 145, + 253, + 120, + 197, + 26, + 127, + 40, + 20, + 198, + 96, + 255, + 8, + 206, + 14, + 140, + 46, + 121, + 92, + 128, + 35, + 65, + 209, + 40, + 94, + 175, + 23, + 205, + 45, + 205, + 177, + 2, + 36, + 171, + 197, + 138, + 154, + 218, + 72, + 225, + 81, + 157, + 169, + 22, + 51, + 118, + 7, + 66, + 122, + 34, + 0, + 146, + 103, + 198, + 111, + 199, + 192, + 76, + 63, + 188, + 188, + 71, + 236, + 161, + 136, + 134, + 203, + 233, + 130, + 66, + 161, + 0, + 0, + 236, + 220, + 185, + 19, + 91, + 182, + 108, + 73, + 120, + 255, + 224, + 193, + 131, + 120, + 244, + 209, + 71, + 197, + 30, + 166, + 164, + 73, + 85, + 128, + 148, + 12, + 17, + 0, + 137, + 97, + 241, + 78, + 96, + 120, + 246, + 52, + 252, + 21, + 158, + 242, + 235, + 245, + 122, + 161, + 84, + 42, + 1, + 0, + 87, + 95, + 125, + 245, + 34, + 1, + 80, + 171, + 213, + 98, + 15, + 81, + 114, + 100, + 83, + 128, + 148, + 92, + 16, + 68, + 4, + 64, + 66, + 140, + 187, + 71, + 49, + 226, + 26, + 76, + 187, + 145, + 35, + 129, + 176, + 20, + 6, + 163, + 1, + 14, + 199, + 12, + 172, + 22, + 43, + 212, + 26, + 6, + 181, + 181, + 117, + 152, + 158, + 156, + 70, + 157, + 169, + 22, + 64, + 164, + 0, + 105, + 118, + 102, + 22, + 227, + 131, + 103, + 65, + 205, + 183, + 7, + 19, + 77, + 0, + 44, + 222, + 9, + 76, + 184, + 199, + 176, + 194, + 216, + 131, + 58, + 141, + 73, + 236, + 107, + 39, + 58, + 103, + 93, + 35, + 56, + 231, + 26, + 38, + 137, + 62, + 243, + 232, + 116, + 58, + 4, + 131, + 65, + 177, + 135, + 33, + 43, + 178, + 41, + 64, + 82, + 53, + 48, + 216, + 220, + 189, + 60, + 246, + 111, + 81, + 4, + 96, + 198, + 111, + 143, + 185, + 185, + 31, + 78, + 59, + 209, + 92, + 213, + 134, + 53, + 181, + 235, + 69, + 190, + 124, + 226, + 49, + 228, + 60, + 141, + 113, + 247, + 57, + 98, + 252, + 113, + 24, + 140, + 134, + 148, + 77, + 44, + 9, + 185, + 145, + 92, + 128, + 100, + 80, + 27, + 19, + 222, + 23, + 69, + 0, + 6, + 102, + 250, + 99, + 115, + 92, + 62, + 196, + 99, + 220, + 125, + 30, + 78, + 191, + 3, + 203, + 140, + 43, + 208, + 162, + 107, + 19, + 249, + 146, + 149, + 248, + 90, + 56, + 78, + 192, + 234, + 33, + 41, + 190, + 201, + 80, + 148, + 2, + 52, + 77, + 102, + 168, + 197, + 166, + 228, + 169, + 192, + 135, + 44, + 251, + 83, + 70, + 183, + 221, + 1, + 23, + 78, + 205, + 156, + 192, + 9, + 123, + 159, + 216, + 215, + 164, + 100, + 12, + 56, + 78, + 192, + 60, + 55, + 86, + 144, + 241, + 75, + 177, + 251, + 44, + 65, + 62, + 148, + 84, + 98, + 15, + 219, + 14, + 44, + 185, + 174, + 205, + 135, + 120, + 88, + 60, + 19, + 112, + 176, + 51, + 104, + 215, + 118, + 96, + 121, + 109, + 143, + 216, + 215, + 167, + 104, + 156, + 176, + 247, + 193, + 230, + 181, + 32, + 20, + 46, + 172, + 191, + 127, + 166, + 228, + 143, + 82, + 183, + 157, + 34, + 136, + 207, + 82, + 5, + 72, + 20, + 40, + 88, + 92, + 67, + 177, + 215, + 75, + 230, + 1, + 252, + 201, + 118, + 8, + 78, + 54, + 187, + 116, + 86, + 235, + 148, + 21, + 125, + 230, + 15, + 208, + 55, + 117, + 164, + 84, + 195, + 43, + 41, + 125, + 83, + 71, + 96, + 241, + 76, + 20, + 108, + 252, + 128, + 124, + 186, + 207, + 18, + 164, + 129, + 46, + 204, + 193, + 225, + 181, + 196, + 254, + 148, + 68, + 0, + 250, + 166, + 142, + 192, + 193, + 218, + 179, + 62, + 222, + 235, + 245, + 66, + 87, + 85, + 141, + 41, + 223, + 36, + 222, + 25, + 127, + 11, + 3, + 142, + 19, + 162, + 93, + 48, + 161, + 57, + 106, + 61, + 140, + 41, + 223, + 100, + 225, + 39, + 74, + 131, + 84, + 55, + 230, + 32, + 72, + 147, + 162, + 11, + 192, + 9, + 123, + 95, + 65, + 55, + 60, + 203, + 251, + 113, + 116, + 232, + 79, + 56, + 100, + 217, + 143, + 25, + 127, + 246, + 34, + 34, + 69, + 14, + 219, + 14, + 8, + 94, + 209, + 71, + 54, + 230, + 32, + 20, + 66, + 81, + 5, + 96, + 192, + 113, + 2, + 22, + 207, + 68, + 206, + 159, + 75, + 190, + 169, + 25, + 53, + 3, + 203, + 204, + 4, + 14, + 141, + 238, + 151, + 173, + 55, + 112, + 200, + 178, + 63, + 235, + 41, + 80, + 46, + 200, + 165, + 251, + 44, + 65, + 154, + 20, + 45, + 8, + 24, + 141, + 112, + 231, + 67, + 186, + 140, + 38, + 141, + 73, + 139, + 113, + 247, + 121, + 204, + 248, + 166, + 177, + 162, + 166, + 71, + 22, + 75, + 134, + 197, + 46, + 234, + 145, + 75, + 247, + 89, + 130, + 52, + 41, + 138, + 0, + 12, + 57, + 79, + 195, + 60, + 55, + 150, + 119, + 144, + 43, + 155, + 155, + 122, + 204, + 54, + 138, + 245, + 157, + 189, + 88, + 111, + 218, + 88, + 250, + 171, + 150, + 37, + 98, + 21, + 245, + 136, + 221, + 125, + 182, + 156, + 200, + 84, + 99, + 223, + 216, + 216, + 32, + 250, + 238, + 62, + 133, + 32, + 248, + 20, + 224, + 172, + 107, + 4, + 227, + 238, + 115, + 130, + 68, + 184, + 227, + 137, + 223, + 247, + 190, + 181, + 173, + 5, + 38, + 147, + 9, + 22, + 207, + 4, + 222, + 51, + 191, + 131, + 113, + 247, + 104, + 129, + 103, + 23, + 30, + 139, + 119, + 2, + 39, + 103, + 62, + 172, + 232, + 138, + 190, + 114, + 64, + 46, + 155, + 124, + 230, + 139, + 160, + 2, + 48, + 238, + 30, + 45, + 121, + 62, + 187, + 151, + 247, + 224, + 244, + 236, + 73, + 73, + 45, + 25, + 142, + 187, + 71, + 49, + 232, + 24, + 168, + 248, + 138, + 190, + 114, + 160, + 220, + 151, + 89, + 5, + 19, + 128, + 41, + 159, + 13, + 35, + 174, + 65, + 81, + 82, + 90, + 67, + 225, + 16, + 166, + 124, + 147, + 216, + 111, + 222, + 131, + 179, + 174, + 145, + 146, + 255, + 254, + 120, + 206, + 186, + 70, + 48, + 228, + 28, + 40, + 121, + 69, + 159, + 148, + 186, + 207, + 150, + 51, + 229, + 182, + 204, + 42, + 136, + 0, + 204, + 248, + 237, + 56, + 229, + 56, + 41, + 248, + 77, + 159, + 235, + 77, + 237, + 231, + 125, + 56, + 227, + 28, + 196, + 159, + 108, + 135, + 4, + 190, + 76, + 217, + 49, + 228, + 60, + 77, + 42, + 250, + 202, + 140, + 114, + 95, + 102, + 21, + 68, + 0, + 226, + 139, + 123, + 196, + 38, + 20, + 14, + 193, + 193, + 218, + 177, + 111, + 226, + 45, + 12, + 205, + 158, + 42, + 217, + 239, + 29, + 112, + 156, + 192, + 168, + 235, + 12, + 49, + 254, + 50, + 163, + 220, + 151, + 89, + 11, + 14, + 95, + 166, + 43, + 238, + 17, + 27, + 46, + 200, + 226, + 156, + 107, + 4, + 124, + 152, + 47, + 122, + 169, + 113, + 223, + 212, + 17, + 156, + 60, + 123, + 18, + 140, + 154, + 41, + 187, + 40, + 113, + 165, + 83, + 238, + 203, + 172, + 5, + 121, + 0, + 153, + 138, + 123, + 42, + 129, + 163, + 214, + 195, + 56, + 59, + 57, + 130, + 250, + 198, + 122, + 0, + 229, + 23, + 37, + 38, + 44, + 38, + 121, + 69, + 74, + 206, + 177, + 150, + 188, + 5, + 32, + 151, + 226, + 30, + 49, + 169, + 215, + 52, + 20, + 237, + 220, + 125, + 83, + 71, + 112, + 242, + 252, + 9, + 232, + 170, + 170, + 203, + 54, + 74, + 76, + 40, + 111, + 242, + 18, + 128, + 92, + 139, + 123, + 196, + 130, + 166, + 104, + 52, + 104, + 155, + 138, + 114, + 238, + 104, + 81, + 79, + 40, + 148, + 152, + 239, + 80, + 110, + 81, + 98, + 66, + 121, + 147, + 151, + 0, + 84, + 49, + 6, + 168, + 40, + 233, + 119, + 101, + 165, + 20, + 202, + 162, + 156, + 55, + 190, + 168, + 167, + 220, + 163, + 196, + 132, + 8, + 197, + 94, + 102, + 21, + 171, + 177, + 75, + 94, + 2, + 208, + 99, + 92, + 141, + 222, + 250, + 77, + 208, + 209, + 85, + 146, + 238, + 58, + 67, + 43, + 132, + 15, + 190, + 37, + 23, + 245, + 148, + 123, + 148, + 56, + 27, + 146, + 111, + 210, + 116, + 55, + 51, + 33, + 61, + 98, + 101, + 28, + 230, + 29, + 3, + 168, + 211, + 152, + 240, + 241, + 214, + 171, + 177, + 97, + 121, + 47, + 66, + 156, + 8, + 87, + 44, + 11, + 84, + 74, + 70, + 176, + 115, + 185, + 56, + 103, + 202, + 109, + 186, + 162, + 81, + 98, + 0, + 48, + 153, + 76, + 160, + 40, + 69, + 44, + 74, + 108, + 158, + 176, + 96, + 114, + 114, + 10, + 124, + 80, + 154, + 2, + 41, + 20, + 201, + 55, + 105, + 186, + 155, + 153, + 144, + 30, + 177, + 50, + 14, + 11, + 206, + 3, + 184, + 176, + 249, + 18, + 172, + 109, + 91, + 15, + 29, + 93, + 85, + 218, + 43, + 150, + 5, + 12, + 165, + 18, + 228, + 60, + 51, + 126, + 59, + 250, + 167, + 143, + 101, + 189, + 226, + 81, + 78, + 81, + 226, + 108, + 72, + 190, + 73, + 211, + 221, + 204, + 132, + 236, + 41, + 85, + 44, + 73, + 144, + 68, + 160, + 229, + 134, + 149, + 248, + 120, + 235, + 213, + 104, + 208, + 54, + 130, + 82, + 148, + 188, + 207, + 104, + 90, + 170, + 4, + 232, + 133, + 71, + 138, + 122, + 150, + 38, + 155, + 155, + 148, + 162, + 164, + 115, + 79, + 72, + 21, + 177, + 98, + 73, + 130, + 254, + 207, + 108, + 108, + 216, + 140, + 78, + 195, + 10, + 201, + 4, + 8, + 77, + 2, + 44, + 1, + 218, + 125, + 83, + 146, + 201, + 114, + 148, + 34, + 169, + 110, + 210, + 84, + 55, + 51, + 97, + 105, + 196, + 138, + 37, + 9, + 46, + 205, + 209, + 0, + 161, + 94, + 101, + 40, + 254, + 85, + 91, + 2, + 154, + 162, + 5, + 217, + 113, + 200, + 207, + 103, + 119, + 209, + 43, + 181, + 24, + 39, + 213, + 77, + 154, + 234, + 102, + 38, + 44, + 141, + 88, + 177, + 164, + 162, + 228, + 168, + 214, + 105, + 76, + 216, + 210, + 114, + 197, + 124, + 63, + 64, + 91, + 73, + 243, + 227, + 163, + 13, + 28, + 148, + 20, + 13, + 180, + 23, + 126, + 62, + 62, + 20, + 40, + 217, + 216, + 229, + 72, + 170, + 180, + 216, + 84, + 233, + 179, + 132, + 220, + 41, + 69, + 99, + 151, + 162, + 38, + 169, + 175, + 55, + 109, + 196, + 184, + 123, + 20, + 231, + 221, + 103, + 74, + 54, + 135, + 142, + 70, + 160, + 91, + 27, + 90, + 5, + 57, + 95, + 32, + 76, + 4, + 96, + 41, + 72, + 247, + 33, + 121, + 83, + 244, + 232, + 76, + 187, + 190, + 179, + 164, + 1, + 194, + 104, + 4, + 90, + 168, + 37, + 192, + 32, + 89, + 195, + 38, + 148, + 49, + 37, + 11, + 207, + 150, + 58, + 64, + 168, + 163, + 117, + 130, + 156, + 39, + 72, + 60, + 0, + 66, + 9, + 41, + 117, + 44, + 169, + 164, + 235, + 51, + 165, + 8, + 16, + 70, + 35, + 208, + 106, + 101, + 225, + 145, + 103, + 139, + 87, + 152, + 221, + 123, + 42, + 1, + 177, + 130, + 160, + 28, + 199, + 145, + 61, + 17, + 11, + 160, + 228, + 11, + 180, + 209, + 0, + 97, + 75, + 85, + 27, + 104, + 74, + 248, + 16, + 68, + 52, + 2, + 45, + 196, + 18, + 224, + 172, + 12, + 170, + 29, + 43, + 29, + 134, + 137, + 36, + 123, + 145, + 50, + 236, + 252, + 16, + 45, + 67, + 99, + 189, + 105, + 35, + 122, + 140, + 107, + 4, + 207, + 32, + 164, + 40, + 5, + 154, + 26, + 154, + 4, + 89, + 2, + 100, + 201, + 250, + 191, + 12, + 32, + 101, + 216, + 133, + 32, + 106, + 138, + 86, + 177, + 2, + 132, + 180, + 64, + 41, + 192, + 28, + 89, + 2, + 148, + 13, + 164, + 12, + 59, + 63, + 36, + 145, + 163, + 41, + 116, + 128, + 80, + 165, + 16, + 70, + 0, + 66, + 161, + 160, + 152, + 151, + 37, + 143, + 241, + 138, + 83, + 82, + 42, + 46, + 164, + 12, + 187, + 16, + 36, + 33, + 0, + 128, + 176, + 1, + 66, + 161, + 60, + 0, + 185, + 229, + 0, + 148, + 251, + 38, + 22, + 169, + 224, + 184, + 64, + 197, + 150, + 97, + 11, + 129, + 100, + 4, + 0, + 16, + 46, + 64, + 168, + 161, + 133, + 201, + 61, + 151, + 91, + 22, + 160, + 216, + 155, + 88, + 240, + 1, + 30, + 167, + 250, + 134, + 75, + 254, + 189, + 133, + 74, + 157, + 61, + 117, + 108, + 168, + 76, + 189, + 164, + 244, + 72, + 74, + 0, + 162, + 20, + 26, + 32, + 84, + 211, + 90, + 65, + 198, + 33, + 247, + 22, + 223, + 165, + 156, + 23, + 7, + 184, + 0, + 190, + 115, + 223, + 15, + 241, + 248, + 157, + 223, + 195, + 177, + 247, + 250, + 69, + 249, + 190, + 133, + 148, + 97, + 31, + 123, + 175, + 31, + 143, + 111, + 255, + 62, + 190, + 115, + 223, + 15, + 43, + 74, + 4, + 36, + 41, + 0, + 64, + 97, + 1, + 194, + 38, + 109, + 115, + 193, + 191, + 95, + 236, + 29, + 134, + 242, + 65, + 172, + 146, + 210, + 0, + 23, + 192, + 83, + 247, + 62, + 139, + 15, + 15, + 158, + 4, + 31, + 224, + 241, + 204, + 95, + 63, + 39, + 154, + 8, + 228, + 195, + 177, + 247, + 250, + 241, + 204, + 95, + 63, + 7, + 62, + 192, + 227, + 195, + 131, + 39, + 241, + 157, + 251, + 126, + 136, + 0, + 39, + 47, + 239, + 47, + 95, + 36, + 43, + 0, + 81, + 114, + 13, + 16, + 210, + 20, + 13, + 3, + 99, + 44, + 248, + 247, + 122, + 2, + 242, + 235, + 98, + 35, + 70, + 73, + 41, + 235, + 231, + 240, + 212, + 189, + 207, + 226, + 228, + 145, + 83, + 88, + 182, + 108, + 25, + 190, + 254, + 245, + 175, + 203, + 74, + 4, + 226, + 141, + 223, + 100, + 50, + 129, + 97, + 24, + 124, + 120, + 240, + 36, + 158, + 186, + 247, + 217, + 138, + 16, + 1, + 201, + 11, + 0, + 144, + 91, + 128, + 80, + 168, + 0, + 160, + 220, + 230, + 255, + 64, + 233, + 75, + 74, + 89, + 63, + 135, + 39, + 239, + 222, + 133, + 147, + 71, + 78, + 161, + 189, + 189, + 29, + 239, + 190, + 251, + 46, + 118, + 237, + 218, + 133, + 103, + 159, + 125, + 54, + 38, + 2, + 253, + 135, + 7, + 138, + 250, + 157, + 25, + 102, + 113, + 205, + 71, + 182, + 174, + 127, + 188, + 241, + 223, + 124, + 243, + 205, + 176, + 217, + 108, + 216, + 183, + 111, + 31, + 170, + 170, + 170, + 112, + 242, + 200, + 41, + 60, + 117, + 239, + 179, + 96, + 253, + 18, + 237, + 119, + 39, + 16, + 178, + 16, + 0, + 32, + 251, + 0, + 161, + 80, + 75, + 128, + 229, + 146, + 3, + 80, + 172, + 246, + 100, + 62, + 175, + 31, + 79, + 222, + 189, + 11, + 167, + 251, + 134, + 209, + 222, + 222, + 142, + 253, + 251, + 247, + 163, + 189, + 61, + 82, + 127, + 253, + 192, + 3, + 15, + 196, + 68, + 224, + 233, + 175, + 254, + 160, + 232, + 34, + 144, + 15, + 241, + 198, + 127, + 235, + 173, + 183, + 226, + 213, + 87, + 95, + 133, + 82, + 169, + 196, + 150, + 45, + 91, + 176, + 111, + 223, + 62, + 24, + 12, + 6, + 156, + 60, + 114, + 10, + 79, + 222, + 189, + 171, + 172, + 69, + 64, + 54, + 2, + 16, + 37, + 83, + 128, + 80, + 176, + 37, + 192, + 96, + 249, + 254, + 167, + 23, + 138, + 207, + 235, + 199, + 19, + 219, + 191, + 159, + 96, + 252, + 93, + 93, + 93, + 9, + 199, + 68, + 69, + 32, + 192, + 5, + 36, + 39, + 2, + 253, + 135, + 7, + 18, + 140, + 255, + 165, + 151, + 94, + 130, + 82, + 185, + 208, + 66, + 254, + 162, + 139, + 46, + 194, + 254, + 253, + 251, + 81, + 83, + 83, + 131, + 211, + 125, + 195, + 120, + 242, + 238, + 93, + 240, + 121, + 203, + 115, + 73, + 81, + 118, + 2, + 0, + 44, + 29, + 32, + 20, + 108, + 9, + 48, + 92, + 57, + 145, + 224, + 92, + 57, + 221, + 55, + 140, + 225, + 19, + 103, + 1, + 0, + 59, + 118, + 236, + 88, + 100, + 252, + 81, + 164, + 40, + 2, + 253, + 135, + 7, + 240, + 244, + 87, + 127, + 144, + 96, + 252, + 10, + 133, + 98, + 209, + 113, + 189, + 189, + 189, + 120, + 244, + 209, + 71, + 99, + 223, + 119, + 232, + 248, + 25, + 177, + 135, + 94, + 20, + 100, + 41, + 0, + 81, + 162, + 1, + 66, + 70, + 185, + 16, + 32, + 20, + 162, + 17, + 40, + 0, + 132, + 194, + 242, + 202, + 2, + 140, + 167, + 216, + 149, + 121, + 27, + 47, + 91, + 143, + 135, + 118, + 221, + 11, + 74, + 73, + 225, + 129, + 7, + 30, + 192, + 27, + 111, + 188, + 145, + 246, + 88, + 41, + 137, + 64, + 212, + 248, + 3, + 92, + 96, + 73, + 227, + 7, + 128, + 215, + 94, + 123, + 13, + 143, + 62, + 250, + 40, + 40, + 37, + 133, + 135, + 118, + 221, + 139, + 222, + 45, + 107, + 83, + 30, + 39, + 247, + 61, + 17, + 100, + 45, + 0, + 64, + 36, + 64, + 184, + 193, + 20, + 9, + 16, + 82, + 10, + 10, + 203, + 13, + 43, + 11, + 62, + 167, + 139, + 115, + 22, + 156, + 3, + 32, + 247, + 27, + 35, + 19, + 91, + 175, + 187, + 24, + 127, + 243, + 221, + 191, + 66, + 40, + 28, + 194, + 77, + 55, + 221, + 36, + 121, + 17, + 200, + 213, + 248, + 111, + 185, + 229, + 22, + 132, + 194, + 33, + 252, + 205, + 119, + 255, + 10, + 91, + 175, + 187, + 56, + 237, + 121, + 229, + 190, + 39, + 130, + 236, + 5, + 0, + 88, + 8, + 16, + 118, + 234, + 151, + 11, + 114, + 62, + 155, + 207, + 154, + 242, + 117, + 69, + 128, + 135, + 225, + 131, + 97, + 180, + 253, + 234, + 109, + 172, + 248, + 238, + 43, + 184, + 224, + 161, + 95, + 96, + 245, + 55, + 127, + 137, + 21, + 207, + 188, + 130, + 182, + 95, + 189, + 13, + 195, + 7, + 195, + 80, + 204, + 27, + 188, + 220, + 111, + 140, + 108, + 136, + 138, + 64, + 48, + 24, + 196, + 77, + 55, + 221, + 132, + 215, + 94, + 123, + 45, + 237, + 177, + 15, + 60, + 240, + 0, + 118, + 238, + 220, + 41, + 138, + 8, + 196, + 27, + 255, + 237, + 183, + 223, + 158, + 149, + 241, + 135, + 17, + 206, + 104, + 252, + 128, + 252, + 247, + 68, + 40, + 11, + 1, + 136, + 210, + 83, + 115, + 129, + 32, + 231, + 73, + 85, + 6, + 92, + 253, + 209, + 40, + 86, + 62, + 253, + 47, + 104, + 255, + 167, + 63, + 192, + 120, + 100, + 16, + 154, + 9, + 59, + 40, + 142, + 135, + 210, + 23, + 128, + 198, + 108, + 135, + 241, + 200, + 32, + 218, + 255, + 233, + 15, + 88, + 249, + 244, + 191, + 160, + 250, + 163, + 81, + 217, + 223, + 24, + 217, + 178, + 245, + 186, + 139, + 113, + 255, + 211, + 119, + 34, + 24, + 12, + 226, + 150, + 91, + 110, + 89, + 82, + 4, + 118, + 236, + 216, + 145, + 32, + 2, + 3, + 199, + 6, + 139, + 62, + 190, + 100, + 227, + 127, + 225, + 133, + 23, + 4, + 51, + 254, + 114, + 216, + 19, + 65, + 218, + 163, + 19, + 137, + 228, + 86, + 224, + 245, + 111, + 126, + 128, + 206, + 159, + 190, + 1, + 102, + 102, + 46, + 227, + 103, + 153, + 153, + 57, + 116, + 254, + 244, + 13, + 44, + 63, + 120, + 74, + 214, + 55, + 70, + 46, + 92, + 121, + 227, + 86, + 220, + 255, + 244, + 157, + 8, + 133, + 66, + 57, + 137, + 192, + 83, + 247, + 60, + 91, + 84, + 17, + 200, + 197, + 248, + 95, + 126, + 249, + 229, + 156, + 140, + 31, + 40, + 143, + 61, + 17, + 202, + 231, + 46, + 20, + 144, + 248, + 36, + 160, + 250, + 55, + 63, + 64, + 227, + 27, + 135, + 115, + 62, + 199, + 178, + 189, + 39, + 80, + 255, + 230, + 7, + 178, + 189, + 49, + 114, + 37, + 31, + 17, + 96, + 125, + 108, + 209, + 68, + 96, + 224, + 216, + 96, + 78, + 198, + 127, + 219, + 109, + 183, + 229, + 100, + 252, + 64, + 121, + 236, + 137, + 64, + 191, + 49, + 240, + 107, + 180, + 212, + 182, + 160, + 86, + 103, + 18, + 36, + 128, + 38, + 22, + 238, + 57, + 15, + 94, + 248, + 231, + 215, + 240, + 238, + 193, + 163, + 176, + 216, + 166, + 10, + 63, + 33, + 128, + 13, + 138, + 0, + 30, + 81, + 185, + 129, + 52, + 55, + 78, + 38, + 26, + 223, + 56, + 140, + 185, + 118, + 19, + 70, + 77, + 85, + 168, + 111, + 172, + 135, + 195, + 49, + 3, + 171, + 197, + 10, + 181, + 134, + 65, + 109, + 109, + 157, + 216, + 151, + 76, + 112, + 174, + 188, + 113, + 43, + 0, + 224, + 71, + 143, + 253, + 18, + 183, + 220, + 114, + 11, + 94, + 124, + 241, + 69, + 220, + 122, + 235, + 173, + 41, + 143, + 221, + 177, + 99, + 7, + 0, + 224, + 145, + 71, + 30, + 193, + 83, + 247, + 60, + 139, + 191, + 253, + 201, + 131, + 88, + 179, + 105, + 149, + 32, + 227, + 24, + 56, + 54, + 136, + 167, + 238, + 137, + 4, + 29, + 183, + 111, + 223, + 142, + 159, + 253, + 236, + 103, + 25, + 141, + 95, + 65, + 41, + 114, + 50, + 126, + 64, + 62, + 123, + 34, + 68, + 247, + 202, + 96, + 253, + 28, + 140, + 29, + 106, + 84, + 211, + 11, + 217, + 147, + 148, + 219, + 239, + 196, + 121, + 251, + 25, + 12, + 207, + 158, + 194, + 222, + 241, + 55, + 241, + 158, + 249, + 29, + 28, + 181, + 30, + 198, + 144, + 243, + 180, + 216, + 227, + 206, + 137, + 127, + 124, + 241, + 63, + 240, + 234, + 235, + 111, + 46, + 105, + 252, + 52, + 157, + 125, + 137, + 49, + 131, + 48, + 238, + 86, + 121, + 210, + 222, + 56, + 217, + 210, + 250, + 210, + 59, + 8, + 249, + 217, + 148, + 105, + 186, + 229, + 72, + 188, + 39, + 112, + 219, + 109, + 183, + 225, + 229, + 151, + 95, + 78, + 123, + 108, + 49, + 60, + 129, + 168, + 241, + 179, + 62, + 22, + 219, + 183, + 111, + 199, + 207, + 127, + 254, + 243, + 140, + 198, + 15, + 32, + 103, + 227, + 7, + 228, + 179, + 9, + 108, + 114, + 0, + 58, + 30, + 218, + 104, + 48, + 98, + 198, + 238, + 64, + 181, + 94, + 15, + 62, + 196, + 131, + 15, + 241, + 240, + 194, + 3, + 59, + 55, + 133, + 113, + 247, + 57, + 208, + 148, + 10, + 90, + 165, + 14, + 85, + 76, + 53, + 214, + 212, + 174, + 23, + 251, + 187, + 164, + 229, + 205, + 183, + 222, + 77, + 255, + 166, + 174, + 26, + 45, + 219, + 191, + 129, + 150, + 143, + 93, + 136, + 41, + 127, + 16, + 142, + 131, + 123, + 225, + 126, + 233, + 121, + 40, + 150, + 200, + 139, + 191, + 68, + 193, + 193, + 164, + 8, + 167, + 125, + 127, + 142, + 15, + 194, + 50, + 95, + 44, + 210, + 194, + 168, + 80, + 77, + 43, + 83, + 30, + 167, + 113, + 251, + 176, + 122, + 210, + 11, + 103, + 151, + 216, + 87, + 168, + 116, + 196, + 123, + 2, + 81, + 3, + 91, + 202, + 19, + 96, + 89, + 22, + 79, + 60, + 241, + 68, + 193, + 158, + 64, + 42, + 227, + 79, + 199, + 238, + 221, + 187, + 113, + 199, + 29, + 119, + 0, + 0, + 238, + 127, + 250, + 206, + 156, + 141, + 95, + 78, + 120, + 189, + 94, + 52, + 183, + 52, + 131, + 162, + 20, + 9, + 79, + 127, + 0, + 160, + 227, + 3, + 85, + 180, + 42, + 241, + 9, + 25, + 21, + 4, + 63, + 239, + 131, + 131, + 181, + 195, + 60, + 55, + 6, + 70, + 169, + 134, + 90, + 169, + 129, + 158, + 49, + 160, + 73, + 219, + 34, + 72, + 243, + 77, + 33, + 112, + 123, + 210, + 68, + 213, + 181, + 85, + 104, + 186, + 243, + 27, + 168, + 89, + 191, + 9, + 238, + 64, + 16, + 246, + 183, + 126, + 13, + 239, + 235, + 47, + 64, + 145, + 97, + 29, + 126, + 147, + 50, + 125, + 45, + 192, + 144, + 215, + 143, + 253, + 206, + 196, + 157, + 142, + 174, + 48, + 86, + 161, + 71, + 151, + 122, + 94, + 95, + 125, + 242, + 28, + 156, + 151, + 8, + 227, + 222, + 202, + 133, + 92, + 68, + 224, + 241, + 199, + 31, + 135, + 223, + 239, + 199, + 51, + 207, + 60, + 147, + 183, + 8, + 20, + 98, + 252, + 209, + 177, + 86, + 34, + 52, + 176, + 16, + 193, + 172, + 206, + 144, + 69, + 23, + 10, + 135, + 224, + 231, + 125, + 240, + 243, + 62, + 56, + 89, + 7, + 198, + 221, + 231, + 35, + 130, + 64, + 169, + 81, + 205, + 232, + 97, + 210, + 54, + 160, + 69, + 215, + 38, + 246, + 119, + 138, + 161, + 168, + 50, + 96, + 217, + 87, + 31, + 5, + 213, + 189, + 1, + 254, + 96, + 16, + 83, + 7, + 246, + 194, + 243, + 250, + 238, + 140, + 198, + 15, + 0, + 203, + 21, + 169, + 51, + 1, + 231, + 248, + 224, + 34, + 227, + 7, + 128, + 253, + 78, + 79, + 90, + 79, + 64, + 59, + 110, + 23, + 251, + 82, + 136, + 66, + 178, + 8, + 112, + 28, + 135, + 219, + 111, + 191, + 61, + 229, + 177, + 59, + 119, + 238, + 4, + 128, + 152, + 8, + 60, + 241, + 139, + 135, + 179, + 254, + 61, + 241, + 198, + 255, + 181, + 175, + 125, + 13, + 207, + 61, + 247, + 92, + 218, + 99, + 139, + 97, + 252, + 82, + 223, + 24, + 54, + 26, + 128, + 54, + 24, + 13, + 152, + 227, + 185, + 196, + 24, + 0, + 80, + 88, + 157, + 56, + 23, + 100, + 225, + 14, + 184, + 96, + 241, + 76, + 224, + 196, + 116, + 31, + 222, + 25, + 127, + 11, + 135, + 44, + 251, + 209, + 55, + 117, + 4, + 227, + 238, + 81, + 209, + 190, + 180, + 66, + 163, + 67, + 199, + 87, + 30, + 132, + 113, + 213, + 90, + 240, + 60, + 15, + 219, + 127, + 189, + 14, + 247, + 139, + 207, + 67, + 145, + 101, + 134, + 95, + 157, + 34, + 181, + 72, + 156, + 91, + 162, + 50, + 204, + 146, + 166, + 126, + 156, + 158, + 93, + 72, + 250, + 145, + 210, + 141, + 33, + 20, + 75, + 101, + 61, + 246, + 94, + 182, + 22, + 247, + 125, + 251, + 43, + 0, + 128, + 59, + 238, + 184, + 3, + 187, + 119, + 239, + 78, + 123, + 158, + 157, + 59, + 119, + 70, + 166, + 4, + 62, + 22, + 223, + 186, + 107, + 23, + 134, + 250, + 51, + 231, + 223, + 15, + 30, + 31, + 17, + 213, + 248, + 229, + 64, + 252, + 202, + 68, + 50, + 148, + 208, + 117, + 226, + 129, + 80, + 68, + 16, + 166, + 124, + 147, + 24, + 112, + 244, + 199, + 2, + 139, + 125, + 83, + 71, + 74, + 215, + 101, + 167, + 74, + 143, + 186, + 175, + 125, + 11, + 117, + 31, + 219, + 12, + 132, + 21, + 152, + 57, + 184, + 23, + 222, + 223, + 254, + 10, + 10, + 74, + 1, + 208, + 42, + 132, + 149, + 153, + 43, + 6, + 133, + 44, + 5, + 10, + 169, + 138, + 186, + 7, + 171, + 232, + 100, + 202, + 122, + 252, + 216, + 199, + 215, + 225, + 158, + 199, + 239, + 0, + 144, + 189, + 8, + 228, + 82, + 125, + 151, + 141, + 241, + 255, + 226, + 23, + 191, + 168, + 72, + 227, + 7, + 18, + 251, + 68, + 44, + 138, + 1, + 0, + 197, + 125, + 42, + 197, + 2, + 139, + 188, + 7, + 83, + 190, + 73, + 156, + 115, + 13, + 131, + 161, + 34, + 113, + 4, + 163, + 186, + 70, + 176, + 236, + 189, + 120, + 212, + 23, + 94, + 14, + 69, + 215, + 42, + 216, + 217, + 0, + 102, + 222, + 250, + 29, + 60, + 191, + 255, + 87, + 104, + 175, + 255, + 28, + 148, + 109, + 93, + 64, + 56, + 4, + 238, + 232, + 1, + 176, + 71, + 247, + 67, + 177, + 196, + 182, + 95, + 147, + 97, + 10, + 203, + 83, + 120, + 1, + 93, + 26, + 6, + 135, + 221, + 139, + 227, + 13, + 12, + 20, + 104, + 97, + 82, + 11, + 11, + 215, + 80, + 120, + 135, + 34, + 41, + 227, + 245, + 248, + 81, + 103, + 170, + 141, + 5, + 147, + 227, + 131, + 78, + 6, + 163, + 1, + 86, + 139, + 21, + 215, + 222, + 124, + 5, + 0, + 224, + 39, + 79, + 190, + 16, + 51, + 196, + 108, + 166, + 3, + 217, + 144, + 141, + 241, + 223, + 117, + 215, + 93, + 80, + 40, + 20, + 21, + 103, + 252, + 153, + 40, + 249, + 163, + 41, + 94, + 16, + 28, + 172, + 29, + 163, + 238, + 179, + 9, + 129, + 197, + 182, + 170, + 142, + 130, + 91, + 122, + 113, + 71, + 246, + 33, + 112, + 225, + 229, + 240, + 217, + 38, + 48, + 247, + 155, + 221, + 80, + 223, + 248, + 37, + 168, + 55, + 109, + 5, + 20, + 0, + 123, + 244, + 61, + 176, + 199, + 150, + 54, + 126, + 0, + 24, + 11, + 41, + 177, + 156, + 90, + 124, + 76, + 53, + 173, + 196, + 21, + 198, + 170, + 69, + 113, + 128, + 75, + 141, + 186, + 180, + 43, + 1, + 190, + 174, + 194, + 123, + 20, + 74, + 149, + 92, + 210, + 97, + 139, + 33, + 2, + 196, + 248, + 11, + 67, + 116, + 223, + 52, + 57, + 176, + 104, + 158, + 27, + 139, + 44, + 61, + 210, + 58, + 232, + 104, + 29, + 154, + 116, + 45, + 104, + 208, + 54, + 229, + 116, + 206, + 176, + 223, + 11, + 231, + 79, + 191, + 5, + 168, + 212, + 96, + 62, + 243, + 151, + 160, + 47, + 222, + 6, + 62, + 200, + 130, + 63, + 250, + 14, + 216, + 255, + 122, + 37, + 171, + 32, + 224, + 31, + 130, + 106, + 92, + 73, + 167, + 158, + 211, + 247, + 232, + 52, + 104, + 97, + 84, + 89, + 45, + 3, + 2, + 128, + 227, + 202, + 117, + 98, + 95, + 230, + 162, + 177, + 84, + 58, + 172, + 193, + 104, + 88, + 148, + 245, + 152, + 44, + 2, + 44, + 27, + 137, + 218, + 167, + 34, + 42, + 2, + 233, + 120, + 232, + 161, + 135, + 240, + 253, + 239, + 127, + 63, + 237, + 251, + 196, + 248, + 51, + 67, + 75, + 45, + 40, + 21, + 10, + 135, + 192, + 5, + 89, + 112, + 65, + 22, + 78, + 214, + 1, + 139, + 103, + 34, + 97, + 165, + 161, + 134, + 169, + 67, + 187, + 190, + 51, + 243, + 137, + 88, + 22, + 225, + 48, + 64, + 85, + 233, + 17, + 6, + 48, + 113, + 98, + 20, + 179, + 239, + 157, + 70, + 135, + 207, + 151, + 85, + 254, + 243, + 48, + 84, + 24, + 8, + 41, + 177, + 134, + 74, + 189, + 26, + 80, + 77, + 43, + 209, + 179, + 132, + 209, + 71, + 241, + 244, + 180, + 130, + 107, + 168, + 17, + 251, + 178, + 22, + 141, + 168, + 251, + 15, + 68, + 130, + 201, + 51, + 118, + 71, + 198, + 172, + 199, + 120, + 17, + 184, + 235, + 174, + 187, + 0, + 96, + 73, + 17, + 240, + 249, + 22, + 23, + 103, + 109, + 218, + 180, + 9, + 91, + 183, + 166, + 55, + 232, + 231, + 159, + 127, + 30, + 247, + 221, + 119, + 31, + 49, + 254, + 56, + 82, + 217, + 186, + 232, + 30, + 64, + 54, + 68, + 5, + 33, + 186, + 218, + 48, + 236, + 28, + 132, + 70, + 169, + 134, + 134, + 214, + 192, + 168, + 174, + 75, + 155, + 194, + 172, + 224, + 88, + 176, + 175, + 60, + 15, + 254, + 234, + 219, + 48, + 49, + 228, + 65, + 64, + 213, + 12, + 218, + 176, + 1, + 45, + 174, + 126, + 80, + 8, + 103, + 252, + 189, + 154, + 155, + 84, + 192, + 27, + 65, + 32, + 207, + 222, + 32, + 97, + 37, + 5, + 243, + 231, + 175, + 18, + 251, + 242, + 21, + 149, + 124, + 211, + 97, + 115, + 17, + 1, + 173, + 118, + 241, + 62, + 15, + 75, + 213, + 82, + 16, + 227, + 207, + 30, + 89, + 8, + 64, + 50, + 129, + 16, + 155, + 176, + 218, + 112, + 206, + 181, + 196, + 110, + 52, + 156, + 31, + 138, + 63, + 236, + 134, + 174, + 230, + 34, + 184, + 116, + 29, + 176, + 87, + 119, + 131, + 87, + 170, + 209, + 225, + 248, + 0, + 20, + 210, + 79, + 5, + 158, + 252, + 10, + 135, + 79, + 93, + 14, + 216, + 41, + 26, + 83, + 175, + 231, + 183, + 38, + 48, + 245, + 169, + 139, + 17, + 104, + 172, + 17, + 251, + 114, + 21, + 149, + 232, + 83, + 197, + 60, + 97, + 201, + 57, + 152, + 156, + 139, + 8, + 100, + 75, + 188, + 241, + 255, + 229, + 35, + 183, + 226, + 178, + 235, + 55, + 139, + 125, + 137, + 36, + 141, + 44, + 5, + 32, + 153, + 76, + 221, + 123, + 148, + 97, + 30, + 93, + 142, + 247, + 1, + 199, + 251, + 89, + 157, + 239, + 201, + 175, + 112, + 248, + 179, + 203, + 35, + 143, + 125, + 211, + 13, + 74, + 120, + 6, + 130, + 240, + 158, + 206, + 236, + 49, + 196, + 227, + 233, + 105, + 197, + 244, + 117, + 23, + 138, + 125, + 105, + 36, + 79, + 178, + 8, + 176, + 108, + 100, + 73, + 47, + 31, + 226, + 141, + 255, + 158, + 199, + 239, + 192, + 165, + 159, + 216, + 152, + 85, + 130, + 91, + 37, + 67, + 202, + 129, + 147, + 136, + 55, + 126, + 0, + 128, + 66, + 129, + 182, + 237, + 12, + 168, + 26, + 38, + 235, + 115, + 132, + 141, + 70, + 140, + 127, + 249, + 134, + 188, + 171, + 8, + 43, + 141, + 107, + 111, + 190, + 34, + 150, + 39, + 112, + 223, + 125, + 247, + 225, + 249, + 231, + 159, + 207, + 249, + 28, + 187, + 118, + 237, + 138, + 25, + 255, + 221, + 255, + 235, + 118, + 92, + 123, + 243, + 21, + 100, + 131, + 208, + 44, + 32, + 2, + 16, + 199, + 34, + 227, + 159, + 199, + 161, + 217, + 12, + 207, + 23, + 238, + 201, + 250, + 60, + 190, + 47, + 126, + 25, + 193, + 234, + 242, + 170, + 247, + 207, + 68, + 161, + 233, + 176, + 215, + 222, + 124, + 5, + 238, + 122, + 236, + 75, + 0, + 114, + 23, + 129, + 93, + 187, + 118, + 225, + 225, + 135, + 31, + 134, + 66, + 161, + 192, + 23, + 254, + 250, + 102, + 172, + 219, + 186, + 170, + 40, + 27, + 161, + 148, + 35, + 68, + 0, + 230, + 73, + 103, + 252, + 211, + 252, + 197, + 56, + 199, + 222, + 138, + 96, + 119, + 15, + 184, + 109, + 215, + 102, + 60, + 15, + 119, + 245, + 53, + 8, + 173, + 236, + 46, + 201, + 152, + 211, + 53, + 26, + 77, + 78, + 205, + 149, + 11, + 55, + 124, + 110, + 91, + 206, + 34, + 16, + 111, + 252, + 247, + 60, + 126, + 7, + 62, + 251, + 229, + 79, + 203, + 162, + 68, + 87, + 42, + 16, + 1, + 64, + 102, + 227, + 143, + 194, + 221, + 240, + 105, + 132, + 140, + 53, + 105, + 207, + 19, + 170, + 51, + 129, + 251, + 228, + 103, + 74, + 54, + 238, + 116, + 141, + 70, + 147, + 83, + 115, + 229, + 68, + 178, + 8, + 236, + 218, + 181, + 43, + 237, + 177, + 201, + 198, + 31, + 141, + 39, + 16, + 178, + 167, + 226, + 5, + 32, + 157, + 241, + 115, + 218, + 107, + 224, + 53, + 62, + 144, + 248, + 162, + 74, + 5, + 246, + 150, + 91, + 211, + 158, + 139, + 253, + 252, + 109, + 128, + 74, + 152, + 157, + 137, + 178, + 33, + 93, + 163, + 209, + 228, + 134, + 164, + 114, + 35, + 94, + 4, + 30, + 126, + 248, + 225, + 148, + 34, + 240, + 228, + 147, + 79, + 38, + 184, + 253, + 196, + 248, + 243, + 163, + 162, + 5, + 32, + 147, + 241, + 27, + 244, + 6, + 52, + 54, + 38, + 166, + 241, + 6, + 215, + 172, + 71, + 112, + 213, + 234, + 69, + 159, + 9, + 174, + 90, + 141, + 96, + 247, + 106, + 136, + 9, + 69, + 81, + 89, + 165, + 230, + 202, + 129, + 100, + 17, + 248, + 193, + 15, + 126, + 16, + 123, + 239, + 153, + 103, + 158, + 193, + 19, + 79, + 60, + 145, + 224, + 246, + 39, + 67, + 92, + 255, + 236, + 40, + 139, + 101, + 192, + 124, + 200, + 246, + 201, + 111, + 208, + 71, + 154, + 58, + 78, + 78, + 46, + 148, + 82, + 114, + 87, + 108, + 131, + 118, + 48, + 177, + 101, + 26, + 119, + 229, + 182, + 146, + 127, + 135, + 84, + 41, + 183, + 169, + 82, + 115, + 229, + 186, + 12, + 118, + 195, + 231, + 34, + 215, + 244, + 23, + 223, + 121, + 17, + 15, + 62, + 248, + 32, + 212, + 106, + 53, + 88, + 150, + 197, + 35, + 143, + 60, + 146, + 16, + 237, + 39, + 100, + 134, + 166, + 104, + 52, + 209, + 139, + 19, + 170, + 42, + 82, + 0, + 210, + 25, + 255, + 107, + 254, + 110, + 120, + 212, + 159, + 69, + 242, + 243, + 36, + 89, + 4, + 130, + 107, + 214, + 33, + 100, + 170, + 7, + 101, + 159, + 6, + 16, + 153, + 251, + 7, + 47, + 40, + 125, + 190, + 191, + 193, + 104, + 88, + 148, + 114, + 59, + 61, + 57, + 189, + 40, + 53, + 183, + 90, + 47, + 79, + 1, + 0, + 34, + 34, + 160, + 173, + 210, + 224, + 71, + 143, + 253, + 18, + 247, + 222, + 123, + 47, + 128, + 72, + 166, + 225, + 125, + 223, + 38, + 25, + 126, + 217, + 98, + 164, + 212, + 139, + 202, + 128, + 163, + 84, + 156, + 0, + 44, + 101, + 252, + 223, + 116, + 95, + 5, + 184, + 143, + 2, + 0, + 62, + 93, + 219, + 145, + 240, + 126, + 130, + 8, + 40, + 20, + 8, + 108, + 190, + 20, + 234, + 223, + 71, + 182, + 195, + 10, + 92, + 178, + 69, + 148, + 53, + 255, + 84, + 41, + 183, + 169, + 82, + 115, + 229, + 206, + 149, + 55, + 110, + 5, + 173, + 162, + 241, + 119, + 15, + 255, + 4, + 0, + 240, + 240, + 223, + 127, + 13, + 151, + 108, + 219, + 36, + 246, + 176, + 100, + 65, + 27, + 179, + 180, + 248, + 87, + 148, + 0, + 100, + 52, + 254, + 121, + 190, + 53, + 158, + 89, + 4, + 66, + 221, + 61, + 177, + 215, + 67, + 43, + 165, + 211, + 239, + 175, + 144, + 212, + 92, + 41, + 115, + 217, + 245, + 155, + 161, + 98, + 104, + 168, + 181, + 106, + 244, + 94, + 186, + 182, + 240, + 19, + 150, + 57, + 26, + 138, + 134, + 41, + 133, + 203, + 159, + 76, + 197, + 8, + 64, + 182, + 198, + 31, + 37, + 163, + 8, + 4, + 131, + 192, + 124, + 155, + 241, + 224, + 178, + 46, + 177, + 191, + 94, + 69, + 176, + 249, + 106, + 242, + 212, + 207, + 134, + 165, + 92, + 254, + 100, + 42, + 66, + 0, + 114, + 53, + 254, + 40, + 153, + 68, + 192, + 181, + 124, + 190, + 10, + 81, + 153, + 185, + 44, + 152, + 64, + 200, + 23, + 62, + 16, + 153, + 214, + 53, + 54, + 54, + 128, + 86, + 209, + 9, + 27, + 125, + 232, + 116, + 58, + 24, + 141, + 122, + 40, + 230, + 155, + 174, + 100, + 114, + 249, + 147, + 41, + 251, + 101, + 192, + 124, + 141, + 63, + 202, + 183, + 198, + 143, + 226, + 119, + 142, + 177, + 69, + 175, + 27, + 244, + 6, + 232, + 46, + 189, + 12, + 225, + 246, + 44, + 122, + 19, + 20, + 25, + 169, + 119, + 165, + 37, + 20, + 70, + 54, + 59, + 77, + 27, + 41, + 117, + 206, + 198, + 15, + 228, + 233, + 1, + 164, + 83, + 160, + 100, + 165, + 18, + 155, + 66, + 141, + 63, + 74, + 58, + 79, + 160, + 238, + 47, + 190, + 0, + 218, + 237, + 74, + 88, + 34, + 44, + 71, + 254, + 245, + 39, + 191, + 22, + 123, + 8, + 146, + 228, + 243, + 247, + 252, + 247, + 146, + 252, + 158, + 76, + 61, + 23, + 89, + 199, + 92, + 214, + 46, + 127, + 50, + 121, + 89, + 233, + 130, + 2, + 213, + 193, + 229, + 116, + 193, + 233, + 116, + 163, + 166, + 214, + 152, + 160, + 84, + 98, + 175, + 61, + 11, + 101, + 252, + 81, + 178, + 9, + 12, + 150, + 43, + 175, + 252, + 148, + 8, + 64, + 42, + 62, + 123, + 231, + 141, + 69, + 127, + 224, + 101, + 74, + 236, + 234, + 208, + 24, + 128, + 150, + 252, + 55, + 32, + 205, + 107, + 212, + 169, + 186, + 190, + 214, + 212, + 26, + 23, + 41, + 149, + 88, + 8, + 109, + 252, + 81, + 42, + 89, + 4, + 128, + 200, + 14, + 62, + 132, + 72, + 26, + 50, + 128, + 146, + 60, + 240, + 210, + 245, + 92, + 12, + 186, + 253, + 232, + 104, + 104, + 44, + 248, + 252, + 130, + 200, + 86, + 186, + 20, + 84, + 49, + 166, + 1, + 197, + 50, + 254, + 40, + 149, + 44, + 2, + 79, + 60, + 241, + 132, + 216, + 67, + 144, + 4, + 81, + 1, + 40, + 197, + 3, + 47, + 85, + 207, + 197, + 158, + 214, + 246, + 188, + 93, + 254, + 100, + 242, + 178, + 80, + 169, + 166, + 160, + 22, + 219, + 248, + 163, + 84, + 178, + 8, + 16, + 22, + 40, + 197, + 3, + 47, + 62, + 177, + 139, + 86, + 40, + 209, + 94, + 215, + 32, + 152, + 241, + 3, + 121, + 174, + 2, + 196, + 111, + 53, + 20, + 12, + 241, + 145, + 74, + 52, + 143, + 31, + 26, + 77, + 36, + 241, + 64, + 140, + 78, + 44, + 165, + 50, + 254, + 40, + 75, + 173, + 14, + 24, + 141, + 181, + 37, + 253, + 238, + 4, + 113, + 72, + 126, + 224, + 21, + 131, + 104, + 79, + 3, + 131, + 90, + 139, + 77, + 93, + 43, + 209, + 96, + 200, + 127, + 190, + 159, + 138, + 188, + 36, + 75, + 138, + 41, + 168, + 215, + 94, + 180, + 184, + 193, + 103, + 177, + 140, + 63, + 74, + 58, + 79, + 32, + 28, + 38, + 173, + 192, + 42, + 129, + 248, + 7, + 94, + 49, + 167, + 1, + 109, + 140, + 30, + 104, + 45, + 206, + 185, + 5, + 243, + 89, + 196, + 78, + 65, + 221, + 127, + 174, + 25, + 159, + 92, + 99, + 137, + 253, + 187, + 216, + 198, + 31, + 37, + 149, + 8, + 124, + 56, + 212, + 135, + 112, + 147, + 56, + 49, + 16, + 66, + 233, + 40, + 246, + 3, + 47, + 219, + 116, + 222, + 66, + 40, + 155, + 59, + 212, + 187, + 242, + 147, + 216, + 51, + 240, + 91, + 172, + 107, + 158, + 193, + 1, + 170, + 19, + 223, + 228, + 74, + 215, + 143, + 255, + 91, + 227, + 71, + 209, + 74, + 169, + 209, + 30, + 160, + 240, + 209, + 217, + 147, + 240, + 25, + 205, + 128, + 31, + 162, + 47, + 133, + 10, + 141, + 130, + 52, + 57, + 77, + 160, + 181, + 173, + 165, + 104, + 15, + 188, + 92, + 210, + 121, + 11, + 161, + 108, + 4, + 0, + 0, + 102, + 87, + 126, + 6, + 239, + 205, + 255, + 252, + 140, + 54, + 243, + 246, + 95, + 249, + 18, + 159, + 8, + 21, + 45, + 195, + 29, + 60, + 253, + 58, + 166, + 77, + 181, + 160, + 155, + 104, + 104, + 2, + 242, + 47, + 195, + 37, + 136, + 71, + 62, + 25, + 125, + 249, + 82, + 144, + 0, + 84, + 106, + 10, + 170, + 20, + 99, + 32, + 197, + 228, + 185, + 255, + 247, + 116, + 44, + 239, + 35, + 20, + 10, + 99, + 210, + 54, + 137, + 250, + 122, + 19, + 156, + 46, + 39, + 76, + 38, + 19, + 236, + 118, + 59, + 140, + 6, + 99, + 217, + 79, + 121, + 138, + 61, + 189, + 213, + 64, + 13, + 19, + 83, + 252, + 167, + 126, + 60, + 229, + 253, + 63, + 86, + 66, + 196, + 142, + 129, + 20, + 19, + 169, + 46, + 251, + 150, + 154, + 98, + 62, + 240, + 244, + 42, + 3, + 12, + 138, + 220, + 54, + 159, + 17, + 130, + 178, + 47, + 6, + 34, + 20, + 142, + 20, + 151, + 125, + 203, + 137, + 54, + 70, + 47, + 138, + 241, + 3, + 50, + 244, + 0, + 114, + 41, + 141, + 36, + 8, + 67, + 165, + 77, + 121, + 74, + 69, + 169, + 2, + 125, + 75, + 33, + 59, + 75, + 201, + 166, + 52, + 178, + 84, + 84, + 106, + 12, + 36, + 250, + 61, + 201, + 6, + 28, + 249, + 19, + 53, + 254, + 169, + 201, + 105, + 81, + 199, + 33, + 59, + 1, + 72, + 238, + 121, + 159, + 174, + 55, + 62, + 129, + 32, + 69, + 52, + 20, + 13, + 35, + 165, + 198, + 220, + 212, + 12, + 92, + 46, + 23, + 212, + 26, + 226, + 1, + 100, + 77, + 54, + 61, + 239, + 41, + 226, + 254, + 151, + 13, + 229, + 182, + 245, + 153, + 145, + 82, + 195, + 68, + 107, + 225, + 155, + 113, + 1, + 0, + 88, + 63, + 7, + 131, + 192, + 169, + 189, + 185, + 34, + 43, + 107, + 73, + 87, + 26, + 233, + 114, + 186, + 16, + 10, + 133, + 99, + 17, + 106, + 66, + 113, + 40, + 245, + 148, + 167, + 156, + 182, + 62, + 107, + 99, + 244, + 240, + 205, + 184, + 34, + 129, + 212, + 249, + 13, + 75, + 131, + 193, + 72, + 96, + 213, + 106, + 17, + 174, + 120, + 44, + 215, + 115, + 201, + 42, + 8, + 152, + 170, + 52, + 178, + 190, + 177, + 126, + 81, + 111, + 124, + 66, + 121, + 32, + 245, + 190, + 19, + 217, + 16, + 159, + 206, + 219, + 208, + 88, + 47, + 246, + 112, + 22, + 33, + 43, + 15, + 32, + 26, + 121, + 142, + 223, + 250, + 57, + 26, + 161, + 6, + 0, + 147, + 201, + 4, + 138, + 34, + 233, + 170, + 229, + 138, + 220, + 182, + 62, + 139, + 186, + 252, + 165, + 32, + 57, + 152, + 152, + 109, + 112, + 81, + 86, + 30, + 64, + 57, + 39, + 219, + 16, + 22, + 35, + 231, + 4, + 164, + 82, + 166, + 243, + 2, + 128, + 90, + 195, + 192, + 229, + 138, + 196, + 22, + 114, + 9, + 46, + 202, + 202, + 3, + 32, + 84, + 22, + 114, + 76, + 64, + 202, + 183, + 59, + 111, + 193, + 215, + 202, + 96, + 0, + 235, + 231, + 0, + 228, + 22, + 92, + 148, + 149, + 7, + 64, + 168, + 44, + 228, + 150, + 128, + 36, + 86, + 98, + 79, + 114, + 224, + 47, + 26, + 92, + 4, + 16, + 11, + 160, + 166, + 67, + 150, + 2, + 80, + 201, + 9, + 56, + 149, + 142, + 20, + 167, + 129, + 233, + 118, + 222, + 45, + 21, + 153, + 140, + 124, + 201, + 177, + 139, + 54, + 106, + 2, + 161, + 12, + 144, + 66, + 58, + 111, + 33, + 16, + 1, + 32, + 16, + 242, + 68, + 140, + 185, + 190, + 208, + 16, + 1, + 32, + 72, + 30, + 169, + 77, + 249, + 74, + 209, + 170, + 171, + 84, + 144, + 85, + 0, + 153, + 144, + 156, + 254, + 154, + 46, + 77, + 150, + 80, + 92, + 74, + 185, + 182, + 95, + 10, + 136, + 0, + 200, + 4, + 41, + 85, + 65, + 86, + 42, + 26, + 138, + 150, + 245, + 124, + 63, + 21, + 68, + 0, + 100, + 2, + 169, + 130, + 20, + 31, + 127, + 136, + 199, + 4, + 231, + 134, + 157, + 151, + 79, + 13, + 66, + 38, + 72, + 12, + 64, + 6, + 144, + 42, + 72, + 105, + 17, + 21, + 2, + 64, + 254, + 241, + 0, + 34, + 0, + 50, + 96, + 169, + 42, + 200, + 248, + 52, + 89, + 66, + 233, + 137, + 23, + 3, + 154, + 162, + 81, + 5, + 165, + 172, + 166, + 9, + 228, + 177, + 33, + 3, + 82, + 165, + 191, + 166, + 74, + 147, + 37, + 136, + 11, + 31, + 226, + 225, + 12, + 177, + 152, + 224, + 220, + 152, + 224, + 220, + 152, + 227, + 57, + 184, + 61, + 156, + 216, + 195, + 90, + 18, + 193, + 61, + 128, + 116, + 61, + 250, + 146, + 123, + 249, + 17, + 178, + 39, + 85, + 250, + 107, + 170, + 52, + 89, + 130, + 180, + 112, + 134, + 88, + 152, + 39, + 45, + 104, + 110, + 105, + 70, + 45, + 173, + 65, + 53, + 205, + 96, + 142, + 231, + 36, + 229, + 33, + 8, + 238, + 1, + 148, + 83, + 19, + 7, + 169, + 64, + 250, + 239, + 201, + 31, + 103, + 136, + 197, + 152, + 223, + 133, + 211, + 19, + 163, + 176, + 115, + 28, + 166, + 230, + 43, + 247, + 196, + 70, + 112, + 1, + 72, + 23, + 157, + 78, + 142, + 98, + 151, + 3, + 229, + 214, + 178, + 138, + 32, + 60, + 169, + 58, + 86, + 77, + 187, + 167, + 49, + 19, + 152, + 195, + 4, + 231, + 198, + 148, + 203, + 37, + 234, + 52, + 161, + 232, + 49, + 0, + 185, + 53, + 113, + 200, + 5, + 226, + 237, + 16, + 50, + 145, + 169, + 164, + 217, + 171, + 12, + 98, + 216, + 62, + 30, + 139, + 25, + 148, + 26, + 193, + 5, + 32, + 149, + 226, + 165, + 138, + 98, + 151, + 3, + 165, + 246, + 118, + 164, + 150, + 18, + 75, + 200, + 76, + 170, + 142, + 85, + 169, + 58, + 91, + 1, + 88, + 20, + 64, + 44, + 201, + 248, + 132, + 62, + 161, + 28, + 155, + 56, + 8, + 69, + 57, + 123, + 59, + 4, + 225, + 200, + 38, + 166, + 147, + 73, + 12, + 132, + 106, + 36, + 42, + 120, + 56, + 94, + 110, + 77, + 28, + 10, + 65, + 206, + 45, + 171, + 42, + 25, + 185, + 237, + 46, + 229, + 12, + 177, + 112, + 114, + 44, + 128, + 133, + 196, + 35, + 93, + 149, + 78, + 144, + 115, + 151, + 228, + 91, + 150, + 107, + 20, + 187, + 146, + 189, + 29, + 57, + 35, + 231, + 186, + 138, + 104, + 226, + 145, + 91, + 163, + 128, + 157, + 247, + 21, + 60, + 85, + 144, + 142, + 204, + 201, + 144, + 92, + 230, + 119, + 149, + 140, + 212, + 42, + 25, + 197, + 168, + 171, + 40, + 70, + 252, + 198, + 31, + 151, + 120, + 100, + 203, + 83, + 12, + 72, + 70, + 142, + 192, + 72, + 177, + 101, + 149, + 216, + 196, + 63, + 113, + 171, + 85, + 250, + 184, + 39, + 110, + 29, + 92, + 78, + 23, + 156, + 78, + 55, + 106, + 106, + 141, + 37, + 25, + 75, + 185, + 214, + 85, + 240, + 33, + 30, + 78, + 240, + 177, + 169, + 66, + 192, + 233, + 67, + 87, + 67, + 99, + 198, + 207, + 21, + 237, + 155, + 146, + 136, + 53, + 33, + 138, + 148, + 42, + 25, + 43, + 101, + 119, + 169, + 169, + 185, + 89, + 28, + 57, + 59, + 136, + 35, + 103, + 7, + 49, + 108, + 49, + 167, + 77, + 60, + 146, + 159, + 212, + 17, + 100, + 133, + 212, + 158, + 184, + 149, + 82, + 87, + 17, + 31, + 107, + 211, + 154, + 244, + 224, + 52, + 10, + 216, + 82, + 148, + 49, + 147, + 41, + 128, + 0, + 16, + 111, + 39, + 61, + 82, + 171, + 100, + 172, + 228, + 186, + 10, + 62, + 196, + 199, + 226, + 4, + 209, + 122, + 4, + 226, + 1, + 16, + 138, + 138, + 212, + 158, + 184, + 229, + 186, + 34, + 149, + 45, + 206, + 16, + 11, + 235, + 212, + 194, + 182, + 97, + 196, + 3, + 32, + 20, + 149, + 74, + 126, + 226, + 74, + 129, + 84, + 2, + 167, + 53, + 45, + 228, + 165, + 16, + 1, + 32, + 20, + 21, + 178, + 42, + 34, + 77, + 38, + 56, + 55, + 218, + 24, + 61, + 254, + 63, + 91, + 213, + 67, + 145, + 233, + 13, + 169, + 197, + 0, + 0, + 0, + 0, + 73, + 69, + 78, + 68, + 174, + 66, + 96, + 130 +]; diff --git a/test/tools/tile_server/static/generated/land.dart b/test/tools/tile_server/static/generated/land.dart index b271f0eb..085ec8f8 100644 --- a/test/tools/tile_server/static/generated/land.dart +++ b/test/tools/tile_server/static/generated/land.dart @@ -1 +1,18691 @@ -final landTileBytes = [137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 1, 0, 0, 0, 1, 0, 8, 3, 0, 0, 0, 107, 172, 88, 84, 0, 0, 3, 0, 80, 76, 84, 69, 51, 52, 49, 72, 52, 55, 60, 65, 58, 76, 85, 24, 157, 17, 17, 83, 80, 48, 162, 28, 28, 73, 74, 72, 82, 75, 68, 78, 83, 73, 92, 100, 43, 83, 86, 71, 86, 87, 84, 169, 46, 46, 108, 79, 76, 124, 90, 55, 104, 112, 57, 89, 109, 86, 129, 96, 61, 102, 108, 85, 113, 94, 97, 131, 89, 89, 134, 105, 70, 104, 105, 102, 94, 131, 93, 104, 115, 103, 121, 128, 77, 139, 112, 78, 141, 89, 99, 141, 101, 88, 113, 117, 107, 182, 78, 78, 142, 116, 84, 117, 117, 116, 149, 107, 95, 146, 121, 90, 151, 150, 59, 117, 141, 112, 147, 111, 113, 128, 134, 111, 138, 144, 96, 148, 150, 83, 130, 132, 125, 168, 127, 94, 154, 135, 104, 177, 114, 110, 129, 155, 119, 177, 153, 75, 136, 137, 133, 141, 168, 98, 147, 153, 107, 150, 171, 87, 156, 139, 114, 211, 118, 88, 153, 166, 102, 157, 149, 115, 165, 165, 91, 135, 165, 124, 125, 185, 115, 202, 111, 113, 152, 137, 138, 141, 150, 137, 134, 167, 129, 218, 89, 126, 224, 83, 126, 206, 146, 84, 142, 189, 107, 166, 171, 101, 172, 150, 116, 150, 150, 139, 153, 171, 115, 156, 180, 104, 132, 189, 120, 153, 139, 150, 221, 92, 129, 182, 122, 144, 224, 93, 131, 161, 165, 123, 210, 110, 130, 168, 179, 105, 140, 184, 131, 149, 169, 137, 180, 173, 102, 206, 141, 108, 206, 164, 86, 152, 187, 120, 155, 155, 151, 142, 195, 125, 147, 182, 133, 149, 178, 137, 142, 194, 129, 168, 154, 145, 149, 184, 135, 182, 183, 104, 157, 182, 131, 183, 166, 121, 172, 165, 134, 168, 184, 120, 151, 185, 137, 154, 179, 141, 157, 165, 153, 147, 197, 133, 157, 187, 134, 228, 110, 142, 148, 205, 128, 154, 189, 139, 150, 197, 137, 155, 196, 133, 155, 184, 147, 172, 146, 168, 181, 168, 139, 184, 186, 118, 228, 142, 118, 170, 189, 130, 204, 173, 112, 165, 169, 156, 157, 192, 142, 153, 206, 133, 208, 143, 141, 186, 195, 115, 155, 201, 141, 156, 209, 135, 158, 198, 145, 168, 168, 165, 161, 196, 145, 182, 172, 148, 169, 196, 138, 182, 186, 135, 168, 184, 157, 163, 204, 149, 167, 193, 156, 172, 199, 145, 182, 183, 151, 163, 213, 141, 171, 182, 166, 177, 172, 170, 195, 202, 122, 188, 197, 136, 231, 141, 149, 172, 203, 148, 203, 154, 166, 167, 205, 152, 171, 196, 157, 204, 179, 141, 184, 167, 179, 195, 200, 136, 171, 205, 156, 180, 199, 154, 198, 173, 163, 228, 186, 120, 185, 183, 167, 172, 202, 163, 173, 209, 158, 232, 146, 162, 175, 210, 161, 201, 210, 136, 181, 214, 155, 197, 186, 167, 212, 199, 139, 182, 202, 168, 218, 168, 166, 178, 212, 163, 198, 201, 155, 205, 169, 180, 187, 186, 183, 182, 216, 164, 213, 184, 165, 182, 213, 169, 187, 212, 165, 196, 216, 153, 198, 198, 169, 210, 215, 140, 186, 199, 181, 214, 200, 152, 184, 225, 158, 194, 189, 187, 186, 219, 166, 187, 214, 171, 209, 172, 191, 188, 219, 171, 248, 177, 155, 190, 225, 166, 189, 216, 178, 197, 201, 185, 215, 220, 148, 199, 215, 171, 214, 202, 169, 215, 187, 183, 191, 206, 193, 241, 203, 146, 198, 197, 196, 235, 181, 178, 215, 199, 182, 216, 212, 170, 226, 202, 170, 198, 228, 173, 199, 218, 183, 221, 226, 155, 225, 215, 164, 224, 229, 159, 210, 204, 200, 205, 234, 176, 214, 216, 185, 248, 193, 174, 204, 216, 197, 232, 201, 184, 236, 188, 197, 245, 197, 183, 204, 226, 196, 210, 230, 187, 229, 233, 166, 213, 217, 200, 252, 214, 164, 233, 200, 199, 228, 218, 187, 215, 228, 201, 244, 200, 202, 231, 218, 198, 232, 231, 184, 217, 217, 215, 245, 220, 185, 217, 240, 195, 237, 241, 178, 244, 202, 210, 249, 230, 178, 226, 221, 219, 229, 233, 204, 228, 227, 212, 220, 234, 214, 252, 215, 204, 228, 226, 220, 229, 234, 211, 232, 229, 213, 245, 229, 201, 244, 216, 219, 227, 236, 219, 235, 238, 211, 237, 228, 219, 247, 250, 191, 234, 234, 222, 247, 249, 194, 238, 240, 213, 245, 233, 214, 232, 231, 230, 248, 219, 226, 241, 242, 214, 234, 243, 229, 243, 238, 233, 243, 243, 242, 246, 247, 235, 251, 238, 241, 244, 249, 241, 251, 243, 244, 251, 251, 245, 253, 246, 248, 254, 254, 254, 65, 248, 164, 27, 0, 0, 69, 188, 73, 68, 65, 84, 120, 156, 205, 125, 15, 96, 84, 213, 153, 239, 172, 221, 87, 197, 133, 85, 233, 43, 20, 125, 75, 228, 165, 246, 45, 44, 42, 187, 90, 153, 74, 89, 183, 27, 144, 170, 200, 172, 65, 134, 171, 109, 22, 162, 99, 145, 170, 132, 56, 188, 68, 95, 32, 140, 153, 104, 128, 148, 65, 111, 90, 184, 25, 192, 100, 220, 164, 3, 17, 155, 228, 133, 204, 52, 74, 131, 118, 174, 175, 51, 105, 74, 39, 206, 194, 213, 55, 187, 242, 38, 25, 34, 75, 130, 60, 54, 144, 73, 128, 48, 239, 59, 231, 220, 255, 127, 102, 238, 4, 218, 183, 159, 114, 51, 127, 238, 189, 115, 207, 239, 124, 255, 207, 119, 206, 177, 48, 255, 81, 201, 251, 222, 219, 231, 3, 44, 165, 250, 180, 189, 203, 221, 37, 80, 40, 210, 204, 48, 161, 72, 22, 234, 234, 98, 51, 125, 109, 249, 255, 209, 54, 83, 228, 27, 126, 121, 248, 124, 31, 165, 250, 244, 0, 0, 80, 39, 32, 16, 241, 121, 59, 100, 109, 209, 197, 130, 237, 202, 12, 209, 127, 92, 0, 152, 254, 143, 62, 58, 207, 246, 177, 170, 79, 235, 220, 117, 117, 18, 15, 48, 2, 0, 29, 29, 161, 102, 166, 57, 11, 51, 132, 116, 152, 225, 15, 11, 192, 214, 173, 215, 112, 113, 223, 240, 203, 231, 207, 159, 239, 87, 125, 10, 0, 144, 198, 151, 188, 3, 44, 32, 244, 110, 136, 124, 153, 165, 249, 136, 105, 254, 184, 0, 60, 249, 164, 222, 167, 59, 169, 157, 12, 189, 158, 218, 68, 103, 185, 186, 239, 237, 207, 0, 1, 37, 11, 248, 170, 221, 64, 34, 0, 2, 117, 162, 239, 218, 12, 153, 29, 244, 64, 40, 210, 245, 199, 7, 160, 228, 123, 223, 211, 99, 129, 205, 235, 55, 51, 155, 54, 209, 155, 54, 101, 187, 190, 107, 23, 0, 192, 41, 63, 34, 4, 175, 182, 111, 197, 210, 221, 73, 90, 29, 242, 42, 212, 129, 170, 249, 2, 233, 168, 195, 220, 1, 48, 219, 127, 12, 67, 127, 15, 72, 231, 180, 103, 118, 62, 195, 80, 52, 67, 83, 89, 239, 128, 100, 192, 175, 248, 4, 183, 227, 0, 126, 89, 72, 90, 192, 243, 125, 200, 64, 5, 242, 172, 207, 155, 141, 235, 1, 128, 233, 254, 99, 94, 70, 0, 172, 209, 124, 188, 115, 61, 179, 126, 39, 2, 224, 135, 89, 239, 240, 214, 137, 243, 195, 202, 79, 14, 64, 227, 125, 228, 101, 17, 105, 65, 155, 172, 53, 29, 62, 21, 27, 32, 166, 103, 69, 0, 16, 78, 254, 16, 2, 171, 243, 26, 0, 48, 221, 127, 117, 223, 195, 84, 167, 254, 124, 243, 102, 248, 223, 28, 132, 125, 39, 52, 74, 80, 162, 183, 72, 135, 50, 62, 190, 41, 240, 182, 67, 173, 6, 112, 163, 101, 237, 239, 96, 188, 33, 172, 50, 67, 147, 6, 192, 124, 255, 189, 76, 0, 120, 89, 253, 249, 51, 59, 25, 192, 112, 61, 181, 62, 187, 16, 185, 183, 114, 237, 134, 95, 242, 109, 16, 94, 132, 186, 180, 82, 64, 90, 205, 226, 63, 232, 75, 220, 114, 130, 192, 164, 1, 200, 161, 255, 12, 136, 66, 4, 127, 76, 156, 234, 182, 101, 254, 190, 77, 232, 205, 16, 81, 242, 42, 4, 26, 186, 176, 18, 136, 72, 186, 31, 97, 212, 6, 151, 25, 0, 96, 70, 191, 229, 208, 127, 134, 68, 137, 135, 108, 100, 205, 248, 109, 41, 121, 134, 230, 136, 92, 202, 229, 12, 32, 189, 148, 65, 131, 125, 6, 125, 17, 48, 163, 223, 114, 232, 63, 227, 123, 136, 135, 108, 100, 205, 248, 109, 41, 207, 32, 146, 157, 211, 218, 121, 177, 209, 242, 246, 203, 92, 70, 5, 0, 230, 244, 155, 249, 199, 191, 246, 59, 88, 51, 243, 89, 129, 213, 90, 80, 169, 0, 64, 31, 1, 212, 104, 252, 167, 185, 141, 92, 168, 15, 128, 73, 253, 102, 254, 241, 175, 157, 10, 178, 10, 90, 45, 252, 123, 167, 48, 147, 169, 15, 225, 70, 135, 34, 157, 240, 183, 51, 4, 212, 44, 247, 25, 45, 178, 95, 48, 169, 223, 40, 241, 240, 135, 39, 171, 153, 147, 192, 45, 222, 110, 196, 0, 157, 200, 76, 32, 10, 33, 46, 208, 129, 199, 82, 108, 173, 21, 110, 116, 61, 244, 219, 117, 38, 107, 246, 83, 218, 161, 217, 219, 183, 122, 15, 160, 238, 15, 225, 22, 203, 136, 102, 58, 59, 165, 83, 51, 122, 130, 182, 210, 235, 162, 223, 152, 118, 150, 5, 211, 237, 13, 4, 192, 99, 99, 217, 128, 247, 90, 238, 85, 106, 45, 206, 126, 18, 106, 121, 73, 29, 227, 37, 221, 175, 108, 101, 243, 51, 242, 51, 59, 117, 218, 47, 1, 64, 151, 50, 215, 131, 187, 163, 189, 107, 31, 239, 101, 253, 201, 234, 96, 192, 159, 220, 178, 176, 58, 57, 105, 4, 104, 155, 181, 88, 135, 21, 137, 153, 70, 6, 91, 32, 64, 160, 208, 203, 248, 48, 7, 224, 238, 150, 26, 186, 158, 146, 95, 170, 27, 45, 40, 29, 33, 138, 161, 233, 201, 3, 0, 79, 229, 227, 166, 239, 222, 61, 125, 40, 57, 127, 250, 150, 64, 244, 254, 37, 193, 252, 106, 150, 241, 70, 147, 28, 23, 141, 114, 81, 127, 246, 91, 200, 168, 184, 82, 247, 99, 162, 163, 144, 193, 22, 9, 68, 192, 139, 25, 1, 71, 137, 216, 61, 2, 213, 215, 129, 253, 35, 249, 165, 122, 237, 215, 0, 224, 182, 150, 82, 57, 61, 167, 140, 224, 169, 252, 189, 51, 146, 220, 244, 36, 87, 189, 4, 0, 200, 111, 73, 174, 93, 203, 50, 129, 150, 45, 243, 243, 91, 214, 222, 185, 112, 18, 220, 80, 170, 249, 132, 152, 105, 100, 176, 5, 106, 199, 1, 162, 20, 38, 162, 216, 32, 180, 30, 196, 48, 18, 17, 52, 32, 33, 125, 0, 100, 249, 6, 110, 120, 184, 159, 251, 66, 157, 131, 50, 79, 232, 169, 184, 181, 119, 222, 185, 133, 11, 36, 1, 128, 0, 59, 99, 126, 126, 210, 207, 4, 170, 111, 225, 122, 111, 169, 78, 230, 239, 15, 152, 189, 147, 104, 253, 172, 154, 175, 176, 153, 198, 6, 155, 188, 63, 32, 185, 0, 40, 108, 240, 117, 18, 86, 223, 236, 37, 127, 101, 87, 234, 39, 204, 44, 77, 193, 6, 254, 215, 56, 8, 190, 207, 15, 247, 247, 225, 12, 68, 45, 147, 51, 161, 167, 98, 98, 249, 107, 231, 207, 79, 122, 89, 196, 1, 213, 249, 107, 103, 176, 1, 0, 96, 126, 52, 121, 75, 18, 125, 100, 124, 177, 66, 180, 109, 98, 199, 23, 184, 213, 39, 98, 17, 192, 6, 91, 209, 126, 146, 34, 32, 226, 143, 101, 61, 68, 108, 189, 236, 74, 221, 246, 71, 44, 233, 177, 193, 112, 119, 211, 27, 78, 87, 67, 79, 63, 70, 0, 254, 231, 250, 251, 75, 144, 4, 114, 95, 244, 247, 153, 7, 0, 61, 149, 127, 203, 66, 142, 203, 111, 105, 71, 173, 77, 78, 79, 70, 91, 242, 163, 0, 192, 253, 4, 128, 13, 122, 0, 180, 17, 63, 69, 71, 180, 17, 213, 22, 104, 46, 64, 102, 154, 24, 108, 68, 72, 247, 117, 109, 23, 66, 238, 16, 31, 0, 71, 36, 231, 223, 199, 72, 223, 105, 41, 196, 88, 210, 60, 141, 141, 196, 187, 91, 155, 90, 99, 95, 16, 20, 254, 237, 215, 61, 93, 125, 232, 149, 121, 0, 208, 83, 121, 246, 223, 25, 139, 205, 232, 37, 0, 204, 8, 38, 55, 220, 159, 17, 128, 102, 146, 209, 234, 208, 136, 182, 91, 96, 95, 29, 95, 152, 18, 3, 18, 76, 7, 186, 14, 128, 190, 236, 192, 74, 47, 34, 37, 62, 24, 62, 81, 210, 145, 9, 0, 144, 23, 17, 0, 158, 70, 194, 173, 77, 65, 238, 95, 48, 253, 27, 206, 200, 177, 156, 73, 237, 141, 30, 234, 135, 220, 134, 59, 243, 119, 195, 69, 213, 45, 1, 182, 119, 126, 254, 66, 136, 231, 3, 193, 45, 209, 228, 253, 73, 244, 145, 230, 26, 145, 83, 149, 162, 29, 217, 94, 216, 73, 60, 248, 74, 45, 11, 80, 226, 65, 160, 210, 82, 194, 223, 66, 242, 11, 249, 67, 56, 72, 6, 125, 216, 44, 156, 228, 211, 34, 128, 140, 166, 165, 169, 169, 181, 59, 62, 56, 38, 7, 97, 44, 222, 221, 180, 255, 67, 64, 96, 24, 84, 2, 128, 96, 90, 12, 224, 169, 188, 44, 27, 101, 193, 42, 71, 163, 94, 38, 64, 76, 159, 55, 26, 245, 7, 162, 1, 252, 145, 154, 80, 122, 2, 39, 40, 68, 209, 166, 193, 105, 7, 117, 21, 42, 122, 139, 176, 111, 177, 198, 23, 210, 2, 80, 89, 204, 200, 219, 143, 88, 0, 161, 8, 183, 13, 69, 100, 134, 48, 18, 217, 91, 94, 4, 196, 39, 80, 246, 21, 35, 85, 99, 65, 28, 223, 31, 11, 54, 53, 53, 117, 135, 19, 50, 28, 6, 187, 155, 130, 88, 41, 156, 83, 229, 101, 51, 3, 144, 163, 31, 229, 139, 132, 182, 17, 65, 229, 69, 27, 245, 19, 60, 35, 124, 246, 86, 97, 209, 65, 116, 74, 129, 218, 31, 208, 254, 74, 109, 1, 86, 126, 56, 249, 215, 215, 39, 1, 0, 93, 28, 145, 123, 194, 161, 131, 251, 246, 49, 109, 40, 32, 58, 88, 96, 45, 176, 149, 214, 10, 0, 240, 244, 5, 224, 208, 26, 30, 145, 56, 33, 188, 27, 84, 226, 224, 224, 192, 31, 14, 0, 232, 161, 78, 94, 99, 53, 135, 58, 55, 131, 96, 163, 134, 176, 7, 139, 10, 139, 138, 238, 141, 188, 5, 158, 76, 136, 214, 250, 2, 26, 178, 34, 65, 98, 187, 160, 237, 125, 8, 6, 22, 0, 232, 68, 247, 109, 150, 78, 169, 180, 226, 63, 26, 71, 82, 14, 0, 143, 66, 107, 83, 119, 156, 103, 133, 177, 166, 216, 96, 34, 145, 200, 165, 73, 57, 145, 32, 140, 44, 159, 176, 15, 81, 50, 99, 181, 119, 27, 249, 107, 226, 62, 52, 186, 21, 106, 124, 159, 144, 7, 12, 225, 192, 72, 58, 195, 205, 107, 83, 141, 181, 209, 0, 128, 137, 3, 86, 136, 99, 8, 226, 77, 241, 68, 162, 183, 183, 247, 122, 55, 157, 144, 198, 42, 237, 227, 95, 224, 70, 188, 69, 94, 155, 138, 77, 155, 241, 37, 125, 17, 1, 0, 232, 124, 159, 252, 251, 2, 222, 177, 209, 56, 146, 250, 0, 96, 16, 118, 199, 137, 28, 116, 55, 133, 19, 137, 201, 251, 135, 153, 40, 20, 209, 27, 211, 69, 236, 12, 13, 225, 185, 162, 211, 106, 230, 78, 116, 9, 18, 127, 116, 241, 129, 3, 237, 10, 213, 71, 168, 152, 23, 36, 181, 35, 153, 1, 0, 208, 141, 77, 9, 34, 9, 97, 64, 32, 102, 156, 159, 158, 52, 225, 182, 31, 44, 58, 168, 104, 126, 51, 207, 24, 88, 44, 74, 66, 33, 171, 57, 167, 180, 212, 250, 14, 200, 128, 156, 235, 49, 85, 90, 201, 245, 2, 0, 42, 71, 18, 0, 248, 232, 68, 191, 49, 4, 177, 38, 162, 12, 154, 64, 17, 196, 175, 83, 171, 101, 132, 1, 104, 59, 136, 212, 85, 73, 209, 54, 192, 1, 84, 116, 179, 194, 99, 57, 88, 104, 178, 253, 64, 37, 96, 54, 66, 29, 170, 15, 75, 233, 90, 171, 27, 132, 136, 22, 4, 73, 233, 72, 34, 0, 222, 219, 245, 182, 49, 2, 195, 193, 166, 110, 48, 11, 35, 131, 35, 32, 5, 62, 36, 85, 62, 63, 24, 116, 54, 10, 1, 174, 233, 208, 198, 144, 58, 121, 111, 132, 112, 194, 182, 194, 18, 162, 181, 197, 140, 93, 91, 199, 190, 44, 89, 81, 37, 161, 203, 107, 53, 81, 180, 91, 25, 80, 80, 74, 71, 82, 16, 129, 247, 79, 160, 227, 197, 241, 139, 106, 8, 6, 19, 77, 233, 244, 68, 247, 216, 96, 34, 57, 62, 26, 101, 216, 36, 208, 145, 36, 215, 59, 122, 36, 153, 91, 124, 175, 247, 192, 168, 149, 168, 207, 218, 58, 145, 75, 28, 58, 88, 200, 251, 125, 68, 248, 153, 230, 182, 156, 218, 143, 137, 182, 89, 69, 187, 169, 137, 164, 16, 81, 226, 1, 147, 229, 226, 197, 243, 23, 225, 95, 255, 123, 39, 224, 207, 120, 10, 35, 112, 17, 17, 249, 11, 0, 0, 11, 196, 111, 28, 27, 25, 28, 111, 170, 72, 122, 147, 111, 44, 29, 29, 154, 51, 111, 74, 172, 247, 198, 100, 192, 239, 103, 188, 237, 237, 192, 23, 126, 95, 123, 187, 215, 215, 158, 43, 34, 161, 136, 74, 91, 249, 112, 123, 139, 109, 194, 128, 103, 200, 154, 227, 29, 17, 209, 197, 0, 65, 177, 21, 72, 215, 131, 160, 196, 3, 38, 203, 248, 229, 203, 227, 64, 163, 240, 239, 242, 68, 236, 107, 227, 227, 169, 243, 227, 132, 46, 226, 227, 185, 145, 49, 0, 96, 10, 2, 192, 62, 111, 212, 155, 180, 207, 74, 14, 37, 23, 28, 26, 5, 0, 4, 138, 114, 226, 203, 220, 52, 101, 155, 86, 91, 99, 170, 180, 150, 16, 41, 216, 167, 141, 4, 76, 81, 113, 109, 169, 33, 235, 80, 226, 1, 147, 101, 106, 124, 100, 206, 212, 121, 169, 137, 214, 169, 83, 231, 164, 166, 90, 166, 110, 249, 172, 127, 188, 162, 102, 230, 84, 251, 248, 229, 209, 5, 83, 103, 198, 198, 198, 154, 102, 206, 116, 76, 77, 97, 0, 146, 8, 128, 209, 161, 197, 83, 166, 54, 133, 111, 76, 114, 179, 122, 47, 191, 49, 117, 202, 210, 161, 209, 197, 111, 220, 54, 245, 13, 199, 212, 219, 142, 36, 115, 123, 208, 78, 159, 209, 55, 5, 133, 160, 19, 67, 238, 220, 243, 211, 149, 54, 51, 153, 84, 137, 44, 137, 137, 169, 21, 227, 11, 230, 77, 88, 6, 199, 195, 19, 77, 51, 71, 199, 83, 195, 227, 11, 110, 236, 233, 185, 113, 247, 196, 236, 5, 163, 77, 55, 164, 6, 45, 241, 145, 153, 83, 199, 70, 18, 99, 0, 192, 102, 4, 128, 125, 74, 111, 111, 18, 56, 96, 170, 99, 60, 120, 195, 145, 222, 169, 246, 203, 51, 167, 198, 14, 89, 230, 113, 139, 103, 94, 187, 94, 16, 136, 182, 21, 148, 228, 156, 149, 1, 5, 96, 211, 79, 37, 26, 146, 229, 242, 136, 165, 166, 102, 233, 141, 19, 51, 231, 196, 198, 1, 128, 241, 241, 243, 231, 199, 23, 44, 24, 31, 119, 206, 185, 106, 73, 141, 95, 158, 218, 84, 51, 123, 34, 213, 52, 21, 148, 32, 0, 48, 190, 153, 179, 207, 26, 155, 234, 76, 70, 135, 130, 83, 110, 91, 156, 28, 157, 183, 32, 153, 124, 99, 234, 229, 153, 246, 100, 210, 210, 155, 60, 2, 122, 33, 215, 103, 206, 64, 149, 57, 117, 37, 34, 58, 199, 214, 51, 72, 7, 196, 45, 118, 187, 189, 98, 226, 114, 197, 148, 217, 8, 128, 139, 8, 128, 165, 227, 227, 53, 51, 199, 44, 169, 212, 216, 204, 154, 138, 121, 169, 241, 238, 169, 19, 9, 12, 128, 47, 186, 116, 246, 229, 41, 111, 112, 126, 238, 144, 101, 234, 204, 161, 209, 217, 75, 147, 209, 67, 83, 46, 207, 116, 2, 0, 156, 22, 0, 109, 149, 152, 54, 171, 109, 76, 181, 86, 116, 212, 198, 195, 122, 167, 150, 22, 23, 152, 8, 154, 116, 200, 50, 62, 110, 73, 140, 143, 79, 76, 128, 42, 188, 49, 222, 10, 28, 48, 14, 0, 192, 159, 121, 11, 38, 110, 104, 253, 108, 228, 134, 120, 247, 148, 241, 241, 138, 169, 99, 111, 15, 142, 119, 223, 56, 62, 196, 205, 92, 124, 121, 22, 116, 59, 82, 130, 192, 251, 75, 103, 37, 147, 139, 103, 27, 2, 160, 173, 18, 51, 72, 125, 233, 19, 180, 189, 22, 73, 116, 150, 42, 1, 56, 169, 160, 184, 116, 18, 89, 76, 68, 150, 209, 241, 138, 27, 230, 205, 89, 60, 49, 117, 193, 188, 41, 227, 35, 55, 204, 11, 34, 0, 166, 204, 158, 115, 195, 224, 68, 211, 141, 243, 166, 46, 72, 143, 205, 156, 57, 111, 230, 212, 212, 137, 183, 71, 199, 231, 220, 56, 107, 202, 84, 110, 232, 200, 13, 179, 102, 85, 128, 18, 236, 189, 225, 200, 232, 212, 153, 179, 111, 236, 29, 55, 0, 64, 167, 74, 76, 155, 213, 206, 68, 5, 196, 145, 213, 166, 70, 175, 31, 89, 46, 142, 142, 159, 107, 13, 143, 79, 12, 182, 118, 131, 69, 60, 215, 122, 14, 1, 96, 79, 4, 83, 99, 35, 169, 17, 112, 1, 70, 82, 151, 195, 221, 169, 196, 216, 197, 247, 222, 27, 226, 130, 111, 28, 226, 184, 246, 100, 236, 141, 221, 67, 224, 8, 37, 123, 123, 199, 135, 14, 29, 74, 14, 13, 245, 130, 33, 12, 98, 15, 73, 174, 4, 245, 170, 196, 52, 193, 72, 6, 18, 235, 53, 42, 179, 177, 192, 53, 144, 5, 185, 127, 96, 252, 241, 1, 191, 188, 136, 0, 24, 3, 7, 32, 145, 72, 167, 6, 39, 38, 226, 9, 64, 34, 61, 242, 197, 249, 247, 134, 3, 1, 46, 186, 231, 135, 212, 250, 6, 52, 210, 19, 13, 180, 195, 63, 22, 94, 250, 2, 209, 168, 159, 141, 178, 62, 86, 233, 30, 235, 85, 137, 105, 130, 145, 12, 36, 213, 107, 88, 179, 156, 233, 214, 40, 191, 206, 72, 200, 199, 168, 3, 3, 61, 210, 139, 6, 199, 237, 21, 99, 241, 88, 60, 145, 30, 139, 39, 194, 40, 26, 2, 0, 18, 195, 40, 66, 166, 208, 37, 148, 137, 219, 34, 210, 175, 18, 83, 7, 35, 25, 72, 170, 215, 144, 123, 196, 109, 40, 213, 161, 106, 26, 109, 85, 9, 9, 142, 179, 155, 15, 32, 125, 187, 41, 196, 71, 24, 250, 169, 21, 21, 0, 195, 3, 177, 88, 124, 100, 12, 8, 103, 136, 33, 8, 12, 163, 236, 208, 216, 96, 12, 127, 75, 161, 75, 168, 236, 143, 142, 201, 160, 74, 140, 82, 6, 35, 198, 36, 171, 215, 40, 149, 201, 0, 9, 33, 85, 231, 170, 198, 15, 58, 248, 188, 194, 230, 77, 128, 215, 62, 226, 109, 25, 228, 150, 100, 0, 160, 182, 139, 233, 225, 177, 68, 188, 59, 28, 135, 246, 67, 44, 24, 15, 115, 95, 144, 83, 230, 210, 140, 33, 0, 102, 43, 72, 41, 241, 160, 71, 224, 202, 88, 173, 164, 185, 242, 122, 13, 137, 5, 240, 136, 159, 214, 131, 182, 202, 223, 240, 209, 100, 200, 231, 163, 124, 161, 200, 62, 156, 36, 208, 178, 13, 33, 9, 128, 47, 112, 246, 99, 108, 48, 30, 14, 3, 223, 119, 135, 19, 19, 233, 120, 119, 247, 88, 122, 36, 54, 44, 242, 71, 101, 134, 232, 204, 108, 5, 41, 37, 30, 244, 8, 141, 9, 251, 136, 212, 200, 235, 53, 100, 67, 197, 161, 236, 0, 136, 169, 37, 250, 135, 62, 146, 115, 13, 133, 12, 24, 128, 177, 12, 15, 15, 247, 247, 115, 125, 129, 190, 225, 112, 28, 218, 30, 71, 169, 143, 65, 194, 4, 152, 27, 226, 3, 50, 1, 97, 50, 32, 96, 182, 130, 148, 18, 15, 70, 228, 109, 14, 133, 188, 110, 69, 189, 134, 77, 248, 89, 60, 144, 20, 210, 14, 48, 40, 68, 64, 224, 128, 72, 27, 10, 43, 153, 205, 252, 128, 185, 238, 143, 137, 195, 227, 253, 56, 246, 199, 20, 31, 155, 136, 143, 196, 195, 241, 48, 202, 134, 13, 159, 151, 3, 192, 184, 173, 6, 222, 166, 32, 177, 126, 20, 26, 70, 51, 181, 207, 152, 64, 147, 241, 18, 208, 220, 108, 179, 233, 98, 213, 28, 49, 96, 101, 57, 181, 225, 124, 120, 243, 250, 245, 235, 105, 186, 13, 177, 81, 7, 202, 51, 234, 159, 43, 2, 208, 215, 47, 2, 144, 136, 167, 227, 152, 19, 128, 255, 101, 253, 127, 30, 143, 17, 25, 165, 233, 5, 137, 221, 19, 204, 223, 176, 144, 31, 74, 241, 27, 6, 123, 186, 84, 202, 119, 99, 41, 86, 233, 168, 102, 7, 253, 165, 148, 39, 241, 93, 25, 242, 9, 7, 61, 34, 3, 174, 166, 244, 173, 8, 192, 230, 205, 125, 220, 224, 0, 199, 245, 245, 69, 184, 240, 68, 10, 1, 16, 158, 72, 135, 85, 70, 130, 156, 75, 235, 120, 102, 162, 196, 38, 91, 150, 36, 73, 14, 153, 141, 113, 185, 32, 192, 15, 93, 136, 196, 151, 106, 80, 224, 233, 202, 126, 15, 51, 245, 206, 77, 40, 93, 78, 135, 20, 242, 168, 141, 3, 41, 241, 96, 76, 34, 0, 168, 1, 175, 17, 217, 225, 226, 35, 19, 97, 204, 8, 131, 95, 168, 92, 4, 50, 76, 72, 23, 104, 93, 51, 65, 98, 217, 96, 254, 140, 249, 100, 60, 53, 58, 191, 37, 135, 4, 137, 219, 170, 254, 68, 8, 111, 132, 246, 99, 123, 222, 12, 38, 206, 183, 121, 61, 176, 244, 129, 144, 79, 161, 112, 105, 205, 13, 40, 241, 96, 76, 34, 0, 120, 108, 151, 7, 0, 154, 62, 50, 152, 136, 79, 140, 197, 212, 62, 210, 48, 63, 62, 80, 172, 147, 172, 37, 63, 151, 188, 37, 8, 94, 49, 210, 4, 81, 0, 0, 142, 237, 160, 19, 134, 134, 224, 144, 121, 148, 185, 214, 88, 189, 242, 13, 11, 97, 41, 14, 161, 164, 255, 51, 184, 175, 66, 234, 17, 66, 253, 39, 202, 244, 171, 50, 0, 248, 51, 67, 24, 128, 240, 200, 4, 30, 15, 208, 122, 137, 2, 2, 110, 171, 134, 9, 200, 207, 37, 111, 137, 69, 3, 254, 228, 252, 59, 102, 180, 36, 231, 231, 231, 79, 223, 157, 92, 187, 118, 198, 218, 253, 243, 17, 95, 100, 122, 144, 90, 195, 246, 243, 61, 27, 34, 238, 79, 40, 226, 197, 10, 23, 213, 253, 11, 37, 173, 228, 82, 243, 9, 116, 57, 169, 1, 112, 183, 33, 0, 18, 200, 25, 8, 111, 24, 214, 2, 32, 77, 96, 208, 252, 28, 190, 158, 138, 238, 190, 101, 97, 140, 171, 158, 159, 228, 166, 143, 206, 95, 146, 228, 110, 25, 93, 114, 71, 50, 89, 13, 255, 102, 196, 114, 211, 137, 152, 108, 162, 4, 68, 136, 37, 7, 39, 159, 40, 92, 250, 96, 225, 220, 185, 214, 202, 218, 82, 190, 233, 98, 37, 65, 101, 78, 64, 88, 92, 34, 242, 168, 1, 205, 155, 65, 198, 56, 222, 24, 236, 210, 105, 191, 178, 100, 68, 39, 255, 228, 141, 38, 55, 76, 231, 150, 204, 200, 207, 191, 101, 116, 254, 238, 0, 55, 125, 96, 201, 90, 54, 176, 101, 97, 52, 122, 103, 48, 163, 12, 232, 231, 61, 10, 220, 124, 193, 16, 46, 122, 11, 225, 170, 239, 16, 86, 184, 197, 214, 31, 130, 149, 43, 64, 141, 47, 69, 215, 86, 210, 116, 37, 122, 28, 91, 110, 73, 49, 139, 147, 97, 60, 226, 59, 226, 62, 32, 0, 226, 177, 24, 167, 11, 128, 162, 92, 162, 22, 204, 182, 77, 105, 18, 88, 54, 192, 45, 172, 94, 187, 22, 73, 254, 252, 106, 54, 121, 203, 208, 146, 13, 8, 0, 54, 27, 0, 250, 58, 160, 212, 202, 119, 44, 180, 189, 19, 121, 116, 200, 10, 212, 34, 133, 101, 149, 73, 55, 186, 182, 214, 106, 53, 40, 45, 204, 72, 32, 2, 180, 221, 197, 191, 241, 18, 37, 216, 151, 24, 140, 66, 67, 250, 244, 1, 80, 87, 13, 41, 51, 119, 94, 110, 73, 117, 245, 244, 88, 108, 250, 150, 253, 91, 134, 230, 223, 209, 242, 120, 190, 25, 0, 74, 11, 138, 245, 147, 248, 12, 115, 47, 223, 40, 48, 237, 72, 237, 225, 74, 160, 72, 39, 133, 6, 251, 40, 233, 25, 172, 181, 149, 242, 172, 73, 14, 201, 100, 164, 3, 92, 194, 21, 194, 148, 171, 1, 232, 102, 150, 59, 175, 15, 192, 121, 189, 129, 226, 98, 107, 1, 159, 137, 79, 110, 89, 178, 182, 55, 202, 246, 174, 93, 219, 146, 108, 137, 173, 221, 144, 140, 6, 131, 129, 64, 111, 11, 27, 221, 173, 235, 21, 16, 231, 218, 56, 143, 79, 23, 20, 243, 6, 0, 123, 245, 33, 60, 4, 30, 161, 144, 105, 144, 0, 0, 86, 84, 92, 100, 37, 127, 124, 38, 148, 14, 175, 4, 157, 78, 90, 4, 224, 95, 65, 247, 5, 128, 151, 207, 27, 140, 154, 234, 143, 148, 211, 197, 86, 220, 20, 95, 128, 101, 161, 167, 253, 32, 10, 190, 0, 188, 246, 225, 186, 233, 64, 192, 235, 99, 181, 78, 1, 92, 100, 51, 211, 91, 66, 201, 139, 72, 20, 18, 123, 202, 240, 124, 94, 13, 24, 184, 255, 10, 226, 1, 240, 56, 237, 72, 19, 52, 119, 132, 250, 134, 249, 70, 6, 244, 155, 127, 254, 11, 19, 15, 108, 146, 74, 141, 248, 94, 77, 32, 252, 111, 21, 225, 130, 145, 189, 219, 138, 138, 138, 172, 5, 214, 204, 51, 41, 104, 98, 18, 155, 141, 34, 32, 25, 137, 102, 208, 37, 104, 2, 110, 24, 3, 224, 215, 179, 129, 106, 37, 168, 165, 82, 177, 75, 233, 220, 71, 117, 12, 233, 96, 81, 209, 246, 142, 131, 200, 16, 188, 133, 142, 69, 217, 206, 239, 8, 21, 226, 191, 128, 88, 182, 250, 100, 201, 15, 16, 17, 64, 16, 244, 51, 237, 231, 13, 68, 32, 75, 205, 88, 32, 122, 226, 125, 224, 117, 182, 103, 123, 193, 214, 247, 63, 226, 184, 235, 50, 82, 130, 84, 191, 172, 212, 183, 48, 27, 182, 145, 200, 65, 220, 245, 29, 57, 112, 0, 136, 129, 136, 0, 211, 119, 190, 143, 61, 111, 160, 4, 51, 3, 208, 30, 203, 207, 207, 255, 48, 192, 237, 191, 51, 201, 36, 239, 156, 49, 227, 126, 14, 12, 89, 169, 27, 180, 145, 87, 209, 23, 149, 5, 57, 57, 110, 252, 20, 0, 190, 152, 42, 107, 213, 128, 24, 254, 119, 228, 192, 1, 50, 227, 65, 244, 128, 1, 0, 153, 171, 133, 162, 247, 87, 39, 183, 44, 76, 110, 152, 127, 11, 0, 48, 29, 156, 1, 56, 157, 229, 184, 30, 136, 13, 184, 46, 155, 213, 134, 67, 93, 52, 132, 151, 163, 223, 42, 47, 27, 9, 133, 42, 51, 15, 128, 242, 229, 21, 248, 101, 54, 22, 80, 205, 28, 117, 210, 126, 182, 79, 96, 253, 93, 122, 50, 32, 115, 3, 154, 201, 132, 24, 31, 142, 210, 72, 145, 57, 195, 174, 93, 152, 92, 184, 129, 139, 37, 111, 73, 122, 185, 25, 16, 11, 65, 223, 115, 119, 220, 159, 63, 125, 109, 254, 140, 37, 81, 218, 93, 140, 236, 85, 229, 36, 28, 22, 9, 1, 220, 164, 226, 76, 76, 192, 231, 131, 200, 67, 102, 185, 175, 26, 0, 215, 105, 169, 173, 253, 47, 235, 32, 32, 1, 208, 62, 124, 126, 216, 139, 196, 5, 7, 72, 240, 6, 65, 224, 79, 222, 113, 75, 126, 210, 239, 7, 0, 124, 220, 140, 59, 167, 175, 133, 15, 185, 25, 213, 220, 134, 124, 46, 118, 75, 142, 131, 231, 10, 34, 89, 45, 169, 254, 75, 27, 61, 75, 132, 192, 58, 88, 110, 166, 192, 80, 3, 0, 195, 244, 15, 100, 70, 64, 2, 0, 151, 17, 251, 241, 52, 3, 150, 33, 39, 14, 247, 143, 174, 93, 24, 155, 191, 54, 202, 0, 0, 136, 245, 147, 211, 123, 125, 0, 64, 48, 80, 189, 48, 144, 188, 38, 0, 208, 120, 0, 244, 166, 84, 5, 102, 204, 69, 216, 161, 221, 251, 86, 54, 0, 72, 102, 77, 3, 64, 207, 155, 221, 138, 52, 224, 176, 218, 28, 138, 103, 178, 253, 253, 220, 48, 231, 133, 174, 239, 243, 242, 90, 227, 124, 63, 154, 36, 192, 205, 224, 16, 0, 222, 40, 132, 5, 119, 244, 2, 68, 51, 122, 27, 170, 23, 66, 88, 96, 164, 63, 115, 25, 50, 54, 65, 33, 62, 35, 154, 241, 36, 95, 136, 232, 71, 237, 236, 241, 214, 55, 79, 139, 98, 128, 114, 24, 15, 23, 190, 125, 66, 14, 128, 148, 146, 100, 81, 171, 155, 217, 62, 78, 194, 136, 139, 222, 127, 255, 254, 249, 75, 48, 7, 4, 130, 75, 170, 23, 130, 49, 64, 0, 248, 51, 2, 144, 211, 144, 113, 118, 34, 234, 34, 179, 240, 55, 11, 90, 82, 111, 250, 60, 119, 250, 159, 248, 84, 8, 122, 226, 103, 118, 86, 22, 204, 45, 234, 98, 57, 196, 11, 195, 195, 178, 5, 13, 200, 28, 155, 128, 95, 18, 25, 0, 54, 217, 178, 161, 37, 233, 103, 146, 224, 249, 39, 119, 111, 216, 143, 170, 134, 216, 253, 92, 67, 111, 208, 207, 85, 27, 37, 139, 115, 27, 50, 22, 200, 176, 126, 40, 100, 156, 4, 23, 169, 67, 128, 72, 119, 253, 128, 96, 119, 236, 252, 105, 80, 106, 88, 224, 197, 244, 188, 187, 0, 165, 172, 101, 202, 183, 143, 8, 137, 79, 76, 28, 34, 116, 188, 1, 22, 249, 62, 190, 128, 15, 191, 68, 108, 182, 190, 221, 191, 179, 170, 125, 253, 250, 61, 70, 54, 57, 151, 33, 99, 137, 172, 70, 95, 248, 68, 35, 152, 129, 58, 248, 41, 117, 6, 11, 40, 112, 167, 91, 187, 161, 235, 136, 219, 75, 137, 7, 80, 61, 50, 239, 157, 229, 251, 189, 79, 18, 1, 93, 63, 57, 235, 140, 212, 92, 134, 140, 37, 42, 214, 9, 36, 248, 241, 57, 38, 100, 34, 12, 34, 100, 184, 130, 68, 211, 35, 118, 23, 159, 41, 161, 196, 3, 33, 136, 80, 73, 236, 205, 145, 86, 251, 3, 253, 106, 0, 252, 28, 199, 245, 247, 127, 209, 223, 207, 234, 222, 65, 77, 57, 12, 25, 139, 20, 10, 233, 132, 82, 230, 87, 120, 17, 200, 120, 9, 13, 151, 253, 17, 187, 19, 191, 162, 196, 3, 79, 116, 169, 13, 143, 223, 4, 192, 5, 24, 198, 173, 230, 161, 16, 220, 68, 150, 235, 240, 183, 131, 21, 236, 63, 175, 229, 33, 93, 162, 204, 14, 25, 75, 4, 46, 65, 137, 56, 144, 42, 80, 198, 241, 57, 52, 240, 170, 113, 159, 50, 173, 33, 226, 178, 219, 237, 116, 198, 168, 238, 4, 71, 251, 250, 112, 47, 19, 38, 16, 156, 4, 145, 35, 120, 149, 73, 137, 7, 3, 202, 122, 130, 14, 137, 195, 157, 210, 3, 102, 94, 1, 193, 86, 170, 51, 7, 43, 227, 34, 42, 85, 118, 167, 171, 202, 105, 215, 249, 134, 70, 41, 4, 15, 179, 189, 176, 128, 239, 3, 150, 101, 251, 165, 64, 137, 7, 96, 56, 151, 233, 86, 198, 0, 232, 15, 188, 251, 68, 0, 10, 196, 192, 32, 251, 10, 8, 54, 117, 16, 145, 17, 0, 218, 97, 183, 195, 255, 46, 218, 165, 230, 2, 135, 139, 241, 56, 156, 4, 124, 228, 146, 129, 91, 124, 94, 90, 241, 166, 131, 55, 162, 230, 211, 224, 148, 120, 208, 33, 125, 193, 110, 147, 6, 188, 197, 121, 85, 25, 86, 64, 16, 180, 183, 188, 154, 14, 76, 85, 123, 102, 14, 112, 217, 29, 85, 140, 7, 180, 193, 35, 252, 39, 180, 203, 233, 116, 216, 113, 26, 17, 181, 222, 99, 231, 245, 100, 241, 71, 138, 30, 239, 151, 233, 3, 66, 154, 66, 254, 28, 72, 38, 216, 78, 89, 203, 36, 0, 104, 65, 180, 51, 216, 27, 81, 250, 173, 232, 80, 91, 90, 9, 239, 185, 112, 50, 110, 201, 144, 91, 162, 171, 60, 118, 210, 247, 46, 135, 131, 113, 226, 196, 153, 221, 233, 116, 73, 105, 116, 4, 132, 11, 62, 160, 25, 91, 73, 215, 38, 182, 63, 0, 224, 163, 128, 23, 20, 131, 114, 210, 109, 91, 164, 99, 18, 163, 34, 132, 228, 130, 45, 101, 109, 48, 0, 194, 61, 37, 209, 166, 196, 131, 146, 164, 114, 75, 212, 160, 130, 130, 98, 80, 136, 5, 201, 151, 94, 106, 181, 84, 102, 200, 203, 209, 50, 222, 119, 61, 2, 93, 255, 8, 121, 75, 187, 28, 146, 98, 112, 193, 231, 118, 167, 167, 242, 129, 185, 15, 0, 163, 218, 230, 62, 92, 160, 45, 33, 240, 226, 169, 252, 205, 147, 227, 2, 133, 96, 67, 31, 56, 121, 12, 34, 178, 162, 15, 113, 186, 53, 37, 30, 20, 68, 138, 78, 121, 226, 155, 236, 143, 142, 191, 244, 82, 210, 66, 230, 232, 103, 207, 223, 185, 92, 85, 30, 116, 150, 3, 169, 69, 80, 13, 242, 239, 64, 48, 236, 246, 103, 118, 46, 117, 18, 70, 165, 53, 217, 10, 92, 214, 210, 38, 250, 167, 190, 156, 22, 18, 80, 9, 54, 237, 178, 139, 55, 149, 252, 125, 33, 191, 68, 137, 7, 5, 41, 170, 200, 106, 105, 52, 31, 56, 16, 127, 179, 123, 244, 205, 55, 121, 29, 80, 108, 186, 198, 220, 227, 4, 5, 8, 34, 81, 165, 248, 212, 97, 119, 56, 156, 207, 58, 159, 162, 157, 46, 222, 2, 217, 172, 197, 98, 210, 167, 141, 207, 80, 8, 61, 102, 106, 46, 160, 72, 162, 96, 163, 62, 128, 247, 85, 60, 248, 222, 144, 44, 222, 81, 116, 177, 134, 72, 165, 165, 24, 116, 250, 184, 112, 107, 52, 249, 82, 56, 201, 13, 188, 36, 42, 193, 98, 235, 3, 38, 98, 82, 33, 227, 235, 178, 171, 121, 230, 197, 23, 65, 46, 128, 190, 251, 44, 67, 148, 4, 93, 74, 146, 215, 224, 71, 72, 217, 28, 212, 245, 62, 148, 214, 200, 137, 7, 40, 114, 0, 97, 3, 240, 153, 42, 221, 115, 50, 86, 211, 18, 13, 40, 6, 157, 209, 238, 71, 90, 185, 240, 75, 225, 214, 55, 99, 49, 201, 10, 108, 90, 85, 96, 62, 38, 213, 122, 71, 192, 168, 47, 218, 237, 207, 62, 245, 172, 3, 112, 96, 170, 156, 88, 53, 98, 43, 105, 181, 150, 202, 150, 254, 196, 229, 58, 29, 217, 211, 213, 114, 162, 196, 131, 199, 94, 149, 203, 184, 23, 79, 188, 134, 16, 131, 206, 246, 100, 252, 145, 216, 80, 211, 155, 173, 221, 47, 113, 24, 0, 214, 39, 255, 58, 231, 251, 147, 187, 83, 212, 19, 118, 39, 50, 85, 200, 86, 128, 190, 4, 226, 109, 8, 26, 56, 171, 109, 70, 35, 155, 178, 245, 14, 39, 9, 128, 195, 174, 225, 61, 129, 140, 103, 201, 200, 102, 141, 242, 65, 39, 219, 253, 82, 50, 154, 76, 134, 223, 228, 44, 43, 26, 144, 27, 7, 18, 69, 190, 158, 235, 206, 45, 38, 149, 63, 38, 237, 240, 144, 199, 196, 252, 129, 28, 233, 71, 156, 252, 119, 238, 90, 52, 126, 87, 42, 46, 112, 147, 45, 88, 53, 32, 164, 109, 29, 78, 131, 47, 221, 217, 102, 139, 200, 131, 78, 238, 205, 55, 147, 67, 47, 189, 153, 244, 91, 234, 151, 255, 52, 192, 165, 216, 100, 212, 31, 245, 195, 215, 155, 10, 172, 255, 56, 185, 135, 83, 43, 224, 42, 16, 88, 165, 164, 84, 22, 131, 237, 125, 43, 210, 214, 102, 106, 14, 132, 30, 121, 104, 87, 21, 227, 208, 87, 2, 12, 154, 33, 168, 87, 86, 94, 41, 62, 132, 44, 232, 244, 37, 95, 106, 141, 15, 113, 126, 198, 82, 95, 191, 110, 93, 11, 155, 74, 182, 251, 55, 145, 175, 221, 15, 232, 196, 76, 38, 168, 189, 23, 185, 126, 20, 122, 117, 121, 2, 94, 54, 36, 122, 145, 50, 180, 123, 180, 167, 210, 197, 215, 82, 255, 238, 114, 56, 13, 33, 208, 44, 54, 160, 172, 157, 162, 164, 160, 51, 218, 250, 18, 158, 220, 0, 0, 212, 215, 47, 95, 209, 224, 147, 127, 93, 105, 84, 13, 153, 137, 70, 203, 174, 240, 142, 25, 87, 86, 6, 81, 209, 232, 221, 103, 208, 15, 184, 92, 122, 39, 235, 60, 104, 14, 4, 16, 72, 93, 164, 140, 83, 180, 227, 77, 242, 159, 162, 196, 3, 19, 141, 145, 57, 126, 24, 0, 128, 160, 140, 150, 127, 77, 235, 35, 144, 105, 188, 221, 155, 254, 106, 26, 180, 201, 232, 232, 196, 208, 104, 222, 177, 116, 210, 151, 62, 126, 117, 52, 125, 121, 212, 96, 48, 221, 236, 184, 176, 1, 185, 28, 2, 99, 129, 86, 144, 67, 172, 241, 7, 220, 242, 0, 152, 18, 15, 16, 192, 99, 53, 236, 226, 1, 168, 223, 177, 124, 197, 22, 217, 215, 180, 36, 5, 236, 144, 144, 203, 100, 175, 92, 65, 3, 93, 151, 117, 211, 251, 129, 147, 121, 163, 12, 123, 41, 47, 239, 104, 99, 122, 90, 99, 94, 217, 232, 165, 69, 233, 178, 198, 188, 188, 84, 187, 75, 87, 107, 93, 27, 0, 160, 98, 69, 49, 240, 56, 228, 8, 168, 74, 164, 104, 133, 56, 83, 226, 129, 191, 212, 105, 23, 0, 0, 90, 247, 80, 89, 157, 142, 27, 57, 177, 40, 205, 191, 74, 46, 106, 4, 44, 46, 231, 165, 244, 74, 93, 162, 63, 41, 75, 194, 119, 141, 87, 167, 53, 94, 250, 211, 198, 171, 95, 189, 122, 116, 209, 68, 94, 222, 165, 188, 198, 40, 10, 96, 244, 149, 138, 45, 163, 247, 146, 153, 170, 36, 4, 104, 99, 181, 200, 100, 20, 102, 15, 132, 250, 150, 29, 18, 2, 192, 6, 143, 150, 85, 139, 95, 147, 146, 35, 127, 106, 218, 101, 16, 235, 116, 58, 125, 249, 242, 180, 11, 233, 81, 127, 250, 66, 26, 21, 17, 170, 83, 220, 67, 139, 142, 70, 129, 11, 224, 164, 51, 71, 243, 46, 79, 124, 245, 106, 89, 99, 250, 79, 83, 233, 188, 15, 88, 102, 207, 30, 239, 27, 246, 61, 94, 45, 213, 22, 88, 221, 58, 31, 155, 163, 42, 135, 240, 106, 143, 195, 94, 37, 191, 43, 45, 189, 46, 40, 149, 125, 193, 168, 124, 15, 143, 29, 4, 201, 178, 98, 121, 189, 130, 214, 61, 186, 98, 3, 169, 215, 39, 234, 51, 122, 116, 209, 16, 195, 157, 156, 150, 7, 13, 154, 182, 104, 90, 217, 232, 201, 69, 19, 139, 202, 166, 77, 75, 73, 5, 47, 232, 222, 204, 229, 105, 151, 134, 70, 27, 23, 141, 94, 248, 106, 186, 172, 108, 40, 53, 45, 157, 119, 242, 204, 180, 203, 233, 175, 94, 194, 122, 195, 227, 226, 115, 8, 42, 162, 117, 66, 71, 179, 36, 56, 4, 40, 54, 149, 235, 1, 89, 205, 169, 94, 226, 88, 246, 227, 78, 15, 202, 8, 85, 175, 88, 167, 132, 160, 126, 7, 128, 80, 182, 133, 38, 213, 219, 136, 239, 125, 87, 167, 157, 73, 127, 245, 248, 201, 63, 61, 121, 21, 53, 47, 253, 213, 21, 233, 188, 163, 81, 54, 26, 229, 184, 100, 138, 16, 119, 229, 171, 103, 206, 164, 26, 243, 174, 228, 229, 93, 205, 91, 4, 156, 15, 58, 17, 193, 129, 216, 135, 255, 57, 187, 174, 46, 152, 196, 100, 79, 190, 217, 224, 111, 226, 168, 3, 94, 57, 156, 114, 61, 224, 54, 87, 120, 4, 228, 36, 41, 49, 45, 4, 8, 132, 229, 143, 174, 184, 251, 175, 182, 84, 143, 230, 157, 9, 68, 161, 41, 105, 96, 234, 69, 67, 208, 172, 69, 199, 82, 127, 122, 53, 13, 140, 190, 238, 239, 214, 237, 216, 177, 142, 167, 250, 11, 121, 160, 255, 174, 46, 202, 59, 222, 8, 204, 159, 87, 118, 233, 210, 138, 75, 141, 199, 134, 78, 150, 13, 73, 143, 172, 143, 192, 36, 9, 57, 197, 14, 232, 123, 23, 142, 212, 161, 45, 242, 0, 189, 18, 141, 225, 100, 159, 121, 142, 180, 19, 9, 134, 170, 213, 130, 32, 192, 176, 98, 29, 52, 185, 254, 88, 89, 217, 165, 99, 211, 174, 46, 106, 60, 115, 60, 47, 61, 237, 210, 209, 188, 11, 87, 224, 83, 37, 157, 60, 115, 230, 204, 201, 163, 103, 46, 92, 58, 115, 236, 194, 133, 11, 39, 235, 63, 61, 115, 236, 204, 201, 117, 199, 142, 183, 72, 191, 71, 219, 117, 125, 2, 147, 179, 99, 85, 84, 5, 17, 184, 139, 217, 249, 148, 144, 153, 164, 245, 114, 183, 217, 0, 112, 73, 73, 209, 186, 199, 30, 213, 178, 1, 208, 209, 51, 211, 26, 27, 47, 52, 230, 29, 207, 91, 4, 42, 224, 248, 180, 227, 87, 166, 93, 93, 81, 118, 225, 88, 222, 5, 93, 196, 180, 180, 238, 209, 50, 113, 226, 156, 107, 233, 19, 250, 17, 183, 45, 119, 207, 112, 211, 122, 231, 179, 235, 81, 240, 42, 116, 189, 97, 144, 100, 68, 248, 124, 135, 44, 41, 186, 97, 197, 242, 29, 154, 231, 255, 201, 153, 178, 178, 178, 51, 87, 225, 223, 241, 116, 227, 177, 69, 199, 47, 164, 26, 47, 53, 158, 57, 115, 252, 216, 201, 178, 178, 250, 250, 111, 153, 129, 0, 92, 12, 222, 180, 108, 122, 214, 190, 84, 63, 226, 206, 88, 240, 161, 75, 154, 224, 213, 233, 200, 173, 46, 13, 171, 64, 218, 169, 200, 10, 215, 149, 173, 88, 174, 226, 131, 187, 227, 159, 77, 212, 28, 171, 63, 121, 242, 228, 177, 163, 232, 80, 127, 236, 211, 163, 159, 162, 99, 253, 183, 30, 171, 175, 255, 115, 125, 166, 65, 210, 160, 248, 104, 249, 138, 58, 254, 161, 171, 40, 230, 9, 189, 136, 59, 211, 124, 52, 67, 0, 148, 3, 170, 46, 172, 21, 76, 223, 192, 133, 163, 20, 90, 147, 22, 223, 178, 226, 81, 5, 8, 83, 207, 110, 40, 211, 237, 216, 187, 239, 54, 0, 224, 234, 34, 160, 99, 199, 148, 31, 18, 95, 27, 61, 180, 103, 169, 83, 47, 226, 54, 158, 47, 160, 79, 250, 3, 170, 85, 134, 193, 178, 150, 136, 51, 173, 55, 46, 80, 183, 165, 12, 161, 176, 14, 11, 196, 99, 250, 205, 175, 175, 47, 3, 14, 184, 91, 246, 30, 89, 132, 229, 143, 62, 186, 98, 197, 165, 227, 199, 143, 79, 59, 3, 65, 166, 226, 244, 29, 15, 109, 152, 228, 40, 176, 1, 233, 15, 168, 58, 120, 219, 152, 141, 196, 115, 140, 7, 70, 0, 134, 199, 86, 172, 120, 244, 209, 229, 0, 5, 216, 187, 250, 250, 79, 47, 32, 82, 162, 128, 48, 90, 183, 28, 44, 230, 138, 178, 178, 45, 213, 68, 219, 249, 254, 21, 28, 162, 241, 80, 221, 6, 64, 81, 174, 84, 214, 173, 240, 240, 15, 237, 89, 170, 219, 221, 40, 112, 15, 105, 214, 130, 50, 36, 74, 103, 64, 181, 202, 1, 134, 209, 238, 204, 10, 1, 54, 72, 120, 121, 7, 19, 27, 44, 212, 85, 111, 217, 176, 1, 84, 33, 248, 56, 121, 121, 199, 234, 31, 125, 244, 161, 229, 162, 144, 192, 223, 29, 101, 138, 138, 148, 206, 72, 164, 239, 202, 180, 75, 125, 56, 241, 91, 87, 166, 176, 45, 15, 109, 225, 31, 122, 169, 46, 163, 86, 90, 113, 225, 198, 129, 205, 230, 234, 133, 40, 241, 32, 181, 203, 133, 211, 166, 250, 225, 151, 130, 16, 68, 104, 121, 7, 173, 14, 48, 36, 239, 196, 149, 43, 169, 105, 87, 112, 181, 122, 228, 231, 101, 82, 231, 150, 241, 9, 79, 146, 230, 2, 0, 70, 23, 149, 141, 70, 58, 219, 72, 87, 42, 92, 140, 117, 119, 147, 135, 246, 232, 11, 124, 165, 21, 79, 113, 237, 52, 151, 155, 165, 196, 131, 140, 240, 80, 14, 173, 151, 133, 145, 19, 254, 26, 47, 239, 224, 201, 97, 139, 141, 182, 200, 248, 162, 198, 30, 33, 173, 23, 18, 33, 216, 177, 66, 172, 224, 12, 161, 165, 77, 255, 245, 204, 180, 116, 36, 132, 75, 123, 241, 202, 96, 93, 50, 8, 118, 172, 16, 242, 14, 154, 241, 86, 68, 165, 214, 16, 73, 152, 154, 201, 205, 82, 226, 65, 67, 46, 35, 151, 75, 248, 154, 17, 150, 119, 200, 5, 0, 134, 133, 150, 29, 180, 30, 20, 19, 187, 101, 127, 183, 67, 80, 241, 242, 101, 237, 39, 242, 142, 125, 17, 233, 144, 198, 2, 154, 229, 190, 246, 142, 135, 234, 240, 67, 123, 244, 31, 177, 210, 90, 27, 138, 116, 208, 116, 174, 245, 66, 106, 170, 178, 27, 184, 92, 136, 136, 1, 36, 203, 59, 56, 115, 1, 224, 114, 222, 113, 46, 210, 89, 41, 78, 77, 6, 73, 224, 69, 28, 124, 157, 247, 133, 246, 254, 107, 170, 108, 34, 18, 121, 38, 212, 209, 214, 214, 129, 102, 57, 226, 241, 176, 13, 143, 74, 138, 128, 120, 69, 180, 193, 120, 220, 166, 77, 161, 200, 206, 107, 182, 20, 155, 94, 180, 191, 104, 36, 72, 132, 61, 248, 229, 29, 126, 144, 3, 0, 28, 196, 249, 160, 10, 124, 205, 251, 14, 10, 0, 68, 34, 239, 61, 196, 115, 193, 186, 117, 63, 231, 17, 248, 226, 124, 95, 164, 153, 230, 81, 234, 104, 38, 195, 96, 117, 18, 19, 60, 42, 164, 28, 244, 37, 117, 61, 101, 45, 121, 184, 54, 183, 122, 33, 13, 81, 160, 14, 13, 6, 57, 248, 136, 68, 88, 222, 193, 242, 138, 250, 123, 163, 170, 77, 255, 213, 105, 23, 80, 150, 179, 35, 18, 178, 21, 32, 8, 154, 9, 4, 63, 23, 60, 232, 117, 203, 5, 8, 34, 145, 205, 146, 68, 8, 245, 90, 27, 150, 171, 17, 112, 234, 35, 64, 209, 149, 15, 204, 157, 59, 151, 154, 100, 219, 249, 123, 48, 206, 39, 116, 11, 239, 104, 133, 171, 228, 97, 44, 223, 156, 171, 12, 200, 13, 171, 54, 163, 23, 142, 146, 208, 22, 77, 220, 40, 182, 22, 20, 90, 75, 249, 149, 255, 68, 125, 184, 110, 249, 46, 190, 213, 12, 209, 137, 29, 29, 178, 201, 238, 213, 15, 169, 17, 208, 215, 85, 148, 120, 40, 181, 78, 118, 129, 32, 104, 197, 179, 246, 23, 245, 4, 201, 229, 144, 73, 30, 68, 232, 150, 183, 223, 126, 242, 222, 2, 217, 111, 24, 143, 144, 177, 56, 11, 230, 245, 122, 65, 182, 81, 114, 137, 70, 249, 150, 226, 130, 18, 100, 186, 118, 241, 158, 193, 142, 71, 177, 46, 8, 53, 11, 166, 17, 113, 64, 39, 63, 14, 84, 39, 34, 240, 80, 93, 6, 4, 36, 0, 24, 188, 68, 212, 164, 102, 196, 174, 127, 2, 2, 3, 221, 194, 59, 185, 230, 241, 184, 92, 150, 109, 111, 191, 253, 171, 95, 61, 121, 175, 152, 67, 209, 6, 25, 237, 100, 103, 4, 31, 203, 178, 94, 175, 118, 72, 207, 109, 181, 97, 125, 200, 11, 57, 146, 131, 66, 90, 102, 20, 58, 219, 196, 145, 176, 58, 65, 97, 212, 63, 196, 255, 156, 75, 71, 10, 40, 241, 32, 251, 141, 130, 202, 218, 210, 92, 114, 71, 52, 42, 110, 210, 22, 222, 105, 221, 100, 75, 9, 66, 224, 159, 255, 249, 103, 79, 222, 75, 82, 245, 90, 127, 221, 159, 74, 250, 0, 130, 246, 148, 209, 34, 154, 219, 201, 250, 183, 239, 19, 8, 150, 119, 69, 10, 133, 198, 35, 99, 136, 87, 56, 230, 135, 242, 233, 21, 2, 2, 43, 132, 39, 114, 232, 223, 83, 77, 181, 197, 5, 136, 21, 244, 86, 46, 208, 167, 167, 170, 60, 78, 23, 165, 250, 80, 101, 124, 209, 91, 75, 9, 143, 192, 63, 255, 243, 175, 0, 132, 123, 239, 181, 130, 254, 177, 206, 181, 22, 88, 231, 22, 20, 216, 138, 139, 75, 43, 221, 76, 52, 149, 74, 5, 252, 108, 74, 59, 239, 15, 147, 173, 80, 176, 249, 4, 130, 21, 37, 123, 101, 12, 32, 238, 128, 70, 60, 69, 1, 129, 29, 101, 252, 213, 180, 126, 182, 212, 128, 244, 86, 46, 208, 39, 10, 233, 59, 135, 218, 210, 42, 223, 227, 148, 216, 127, 187, 111, 251, 91, 60, 2, 152, 126, 37, 167, 159, 253, 236, 103, 47, 63, 249, 228, 247, 158, 124, 242, 229, 183, 127, 253, 153, 1, 0, 160, 9, 36, 167, 167, 12, 218, 183, 188, 48, 34, 7, 64, 246, 186, 77, 134, 192, 58, 49, 255, 238, 145, 137, 1, 237, 113, 58, 51, 135, 50, 165, 38, 39, 91, 81, 232, 240, 132, 98, 196, 128, 118, 33, 43, 142, 94, 145, 35, 152, 33, 154, 177, 212, 204, 190, 235, 225, 174, 247, 100, 8, 232, 211, 175, 0, 137, 239, 221, 139, 56, 196, 86, 92, 169, 245, 96, 196, 110, 70, 14, 207, 202, 136, 17, 161, 167, 120, 72, 37, 4, 152, 120, 217, 164, 237, 118, 39, 170, 55, 114, 17, 119, 157, 214, 131, 162, 54, 151, 68, 122, 149, 92, 194, 156, 14, 154, 77, 198, 146, 81, 95, 20, 142, 1, 198, 159, 140, 181, 162, 21, 37, 43, 102, 125, 227, 59, 30, 174, 231, 95, 178, 32, 32, 113, 8, 98, 10, 12, 69, 129, 13, 100, 164, 184, 20, 164, 164, 150, 70, 83, 187, 145, 180, 255, 29, 210, 112, 134, 0, 160, 248, 72, 176, 5, 59, 228, 230, 8, 185, 4, 40, 134, 35, 17, 130, 199, 131, 197, 211, 99, 183, 163, 89, 76, 154, 86, 229, 50, 170, 38, 79, 149, 186, 60, 129, 224, 140, 249, 51, 90, 146, 91, 238, 152, 63, 131, 99, 185, 252, 252, 94, 0, 96, 222, 156, 217, 179, 110, 251, 198, 139, 135, 122, 199, 39, 254, 253, 223, 204, 162, 32, 8, 203, 207, 48, 189, 12, 136, 0, 97, 6, 89, 4, 77, 91, 116, 80, 119, 75, 35, 158, 5, 252, 213, 200, 35, 58, 124, 184, 190, 76, 254, 160, 30, 52, 127, 89, 245, 240, 30, 0, 130, 70, 176, 120, 104, 218, 131, 7, 65, 61, 57, 168, 11, 66, 226, 32, 42, 22, 127, 52, 169, 175, 101, 254, 232, 116, 46, 185, 176, 122, 40, 127, 3, 6, 96, 164, 6, 35, 240, 157, 245, 47, 30, 138, 197, 7, 207, 141, 79, 32, 74, 161, 127, 255, 254, 217, 175, 223, 219, 245, 50, 208, 207, 178, 201, 135, 146, 254, 207, 39, 31, 191, 187, 119, 219, 182, 242, 162, 114, 157, 101, 195, 25, 95, 42, 85, 191, 3, 218, 127, 248, 240, 215, 205, 38, 193, 60, 168, 248, 202, 238, 244, 160, 202, 84, 89, 107, 76, 145, 200, 64, 56, 68, 102, 247, 223, 201, 230, 239, 78, 222, 191, 176, 229, 142, 24, 203, 5, 49, 0, 13, 225, 193, 165, 128, 192, 109, 119, 253, 195, 143, 105, 102, 79, 67, 67, 67, 48, 60, 146, 78, 28, 58, 4, 175, 248, 25, 30, 181, 165, 54, 43, 244, 45, 104, 194, 159, 253, 202, 20, 20, 255, 231, 75, 158, 182, 109, 215, 97, 1, 46, 149, 90, 142, 1, 40, 187, 223, 108, 43, 104, 90, 150, 240, 197, 58, 210, 180, 30, 16, 149, 0, 210, 128, 32, 246, 119, 78, 191, 147, 99, 217, 233, 183, 44, 73, 110, 174, 106, 193, 0, 48, 76, 67, 60, 60, 239, 182, 219, 238, 153, 117, 207, 143, 241, 175, 248, 82, 137, 145, 244, 72, 131, 206, 99, 84, 22, 99, 36, 238, 253, 30, 64, 145, 137, 43, 68, 0, 190, 252, 252, 118, 45, 0, 126, 54, 186, 127, 29, 2, 224, 112, 189, 249, 128, 79, 138, 80, 60, 78, 60, 0, 96, 122, 226, 173, 67, 174, 72, 145, 8, 84, 231, 143, 206, 232, 77, 206, 223, 210, 224, 219, 207, 3, 192, 120, 15, 37, 106, 110, 123, 202, 191, 120, 214, 223, 98, 4, 184, 212, 161, 65, 93, 4, 164, 199, 169, 219, 94, 92, 128, 177, 192, 112, 240, 36, 50, 200, 151, 18, 125, 100, 83, 139, 0, 166, 175, 175, 196, 8, 60, 102, 26, 0, 121, 132, 226, 65, 186, 210, 244, 34, 147, 46, 60, 241, 133, 215, 47, 220, 140, 88, 52, 57, 125, 104, 122, 50, 90, 125, 127, 192, 191, 63, 63, 42, 228, 4, 247, 28, 9, 223, 117, 215, 158, 67, 119, 221, 133, 204, 102, 52, 197, 246, 198, 211, 169, 67, 134, 247, 244, 129, 216, 112, 237, 126, 84, 240, 138, 198, 156, 3, 169, 20, 56, 73, 180, 187, 180, 216, 134, 80, 121, 178, 168, 188, 124, 219, 182, 189, 239, 126, 66, 120, 192, 166, 211, 126, 134, 190, 9, 3, 240, 219, 181, 102, 1, 80, 70, 40, 200, 68, 88, 77, 93, 71, 147, 146, 255, 71, 30, 33, 254, 0, 91, 125, 199, 194, 59, 182, 12, 205, 207, 191, 127, 70, 208, 239, 111, 149, 0, 64, 16, 60, 241, 141, 123, 162, 111, 204, 94, 76, 179, 104, 176, 55, 14, 8, 24, 241, 128, 255, 80, 119, 119, 107, 107, 119, 124, 36, 133, 198, 134, 3, 62, 64, 108, 72, 83, 59, 67, 187, 87, 21, 124, 132, 16, 248, 205, 178, 182, 80, 72, 213, 126, 134, 249, 206, 127, 197, 8, 28, 203, 5, 0, 89, 132, 2, 156, 106, 53, 147, 252, 102, 240, 24, 162, 253, 17, 167, 16, 117, 69, 193, 250, 69, 253, 209, 222, 96, 178, 97, 167, 55, 22, 102, 229, 89, 225, 61, 47, 222, 243, 141, 55, 162, 115, 230, 188, 49, 10, 237, 241, 122, 227, 6, 82, 224, 13, 180, 118, 99, 106, 69, 32, 32, 176, 252, 16, 37, 232, 254, 246, 173, 159, 35, 4, 62, 46, 29, 74, 13, 69, 3, 29, 138, 109, 31, 188, 83, 49, 0, 167, 170, 117, 47, 212, 146, 54, 66, 113, 98, 179, 144, 157, 170, 236, 143, 32, 253, 15, 110, 49, 118, 182, 188, 126, 60, 145, 207, 239, 69, 11, 103, 33, 126, 146, 101, 132, 118, 254, 195, 223, 126, 227, 174, 89, 179, 102, 47, 29, 65, 237, 105, 72, 164, 227, 58, 213, 156, 62, 150, 111, 191, 0, 66, 120, 144, 245, 233, 112, 0, 166, 251, 8, 2, 239, 224, 10, 2, 86, 113, 183, 134, 175, 99, 4, 206, 152, 45, 24, 213, 14, 131, 160, 2, 129, 236, 3, 0, 224, 10, 241, 21, 101, 85, 184, 176, 138, 44, 245, 76, 225, 3, 46, 96, 83, 108, 186, 186, 122, 211, 61, 179, 110, 155, 53, 123, 78, 77, 131, 151, 137, 38, 82, 105, 173, 26, 240, 31, 145, 183, 31, 83, 83, 138, 77, 166, 244, 235, 199, 252, 54, 5, 2, 228, 67, 190, 201, 127, 70, 88, 224, 131, 236, 45, 32, 68, 105, 135, 65, 220, 224, 28, 101, 25, 1, 240, 200, 87, 73, 66, 8, 144, 165, 158, 241, 93, 40, 7, 50, 145, 202, 141, 151, 191, 255, 224, 139, 119, 205, 154, 61, 123, 78, 69, 60, 120, 238, 220, 17, 173, 16, 176, 9, 77, 251, 187, 91, 195, 208, 56, 221, 173, 4, 123, 19, 9, 27, 214, 132, 159, 148, 156, 195, 178, 130, 56, 48, 48, 228, 15, 32, 131, 70, 253, 5, 86, 131, 39, 133, 177, 115, 111, 150, 197, 7, 41, 241, 32, 146, 149, 81, 76, 219, 209, 33, 85, 186, 193, 229, 88, 67, 150, 122, 38, 55, 195, 206, 133, 50, 41, 250, 218, 131, 223, 127, 238, 111, 231, 204, 158, 61, 123, 241, 96, 60, 234, 77, 164, 195, 252, 231, 188, 25, 222, 156, 210, 52, 31, 33, 144, 74, 13, 233, 4, 138, 94, 188, 38, 163, 149, 119, 137, 74, 222, 63, 151, 66, 219, 144, 98, 94, 240, 33, 131, 70, 88, 160, 190, 49, 64, 26, 238, 141, 114, 129, 76, 43, 77, 105, 1, 16, 202, 42, 144, 135, 168, 59, 30, 168, 232, 126, 130, 128, 118, 169, 103, 85, 86, 248, 251, 15, 62, 253, 220, 230, 121, 179, 43, 106, 230, 116, 179, 204, 161, 9, 193, 18, 240, 102, 152, 77, 169, 24, 32, 30, 71, 199, 115, 81, 159, 78, 166, 136, 65, 0, 36, 214, 20, 9, 62, 209, 222, 242, 173, 149, 8, 128, 161, 88, 180, 151, 221, 180, 243, 153, 123, 238, 38, 8, 156, 33, 203, 236, 249, 192, 67, 204, 180, 224, 138, 6, 0, 249, 20, 64, 23, 216, 121, 117, 232, 200, 123, 76, 10, 90, 163, 88, 234, 25, 79, 8, 84, 0, 112, 224, 192, 211, 15, 174, 126, 238, 53, 102, 233, 188, 193, 248, 188, 154, 6, 102, 80, 208, 2, 188, 25, 78, 170, 36, 32, 49, 50, 18, 70, 40, 36, 89, 157, 125, 212, 24, 63, 94, 144, 109, 176, 228, 19, 209, 45, 250, 120, 91, 121, 121, 73, 121, 73, 145, 109, 251, 1, 239, 250, 157, 127, 78, 0, 56, 154, 66, 211, 235, 252, 208, 254, 220, 182, 167, 80, 22, 20, 208, 218, 161, 22, 173, 134, 116, 43, 151, 122, 198, 89, 105, 37, 0, 117, 79, 3, 7, 236, 100, 14, 184, 230, 180, 142, 204, 171, 104, 141, 11, 50, 192, 155, 97, 182, 91, 213, 254, 4, 250, 19, 14, 27, 116, 29, 139, 0, 24, 73, 189, 245, 238, 151, 26, 250, 228, 221, 251, 30, 190, 13, 3, 112, 244, 210, 25, 224, 10, 110, 104, 100, 132, 211, 91, 165, 209, 144, 180, 155, 15, 32, 15, 81, 104, 52, 173, 171, 28, 85, 75, 61, 187, 52, 0, 48, 93, 79, 63, 248, 220, 115, 128, 108, 87, 195, 188, 154, 177, 5, 142, 116, 154, 223, 92, 136, 55, 195, 190, 67, 10, 254, 31, 73, 12, 146, 87, 156, 129, 49, 139, 99, 0, 82, 49, 219, 222, 207, 181, 24, 124, 254, 192, 95, 97, 59, 144, 190, 128, 245, 194, 96, 98, 164, 206, 93, 224, 187, 182, 29, 26, 80, 50, 5, 99, 224, 49, 83, 39, 129, 189, 35, 37, 0, 59, 87, 175, 126, 238, 199, 76, 59, 218, 199, 117, 129, 99, 204, 190, 120, 66, 176, 3, 188, 25, 222, 163, 96, 128, 193, 238, 48, 136, 64, 120, 176, 219, 143, 182, 31, 210, 249, 5, 16, 130, 93, 184, 117, 167, 223, 41, 41, 255, 141, 6, 2, 44, 3, 191, 77, 167, 143, 195, 25, 35, 137, 196, 135, 41, 111, 97, 52, 165, 95, 92, 157, 19, 6, 102, 235, 100, 116, 0, 120, 229, 251, 207, 61, 87, 203, 111, 90, 184, 120, 105, 124, 206, 130, 177, 176, 223, 239, 71, 218, 153, 194, 102, 120, 147, 66, 4, 64, 254, 1, 129, 145, 110, 84, 65, 173, 255, 224, 241, 216, 254, 148, 64, 61, 219, 139, 182, 253, 242, 147, 207, 101, 188, 240, 53, 44, 3, 87, 211, 169, 146, 237, 239, 39, 18, 225, 53, 189, 76, 97, 42, 167, 117, 104, 229, 36, 197, 139, 180, 126, 133, 190, 150, 60, 2, 0, 18, 3, 255, 248, 193, 23, 94, 243, 10, 219, 54, 46, 5, 123, 56, 47, 158, 72, 97, 237, 68, 161, 175, 41, 175, 198, 6, 198, 71, 226, 221, 73, 136, 135, 88, 93, 41, 0, 79, 128, 248, 64, 108, 42, 137, 146, 234, 149, 171, 138, 139, 139, 151, 253, 181, 173, 8, 43, 198, 199, 203, 16, 0, 23, 210, 233, 79, 191, 220, 118, 111, 225, 46, 206, 231, 99, 77, 182, 95, 39, 51, 42, 139, 23, 205, 166, 204, 240, 40, 161, 165, 161, 97, 143, 143, 223, 177, 153, 97, 158, 126, 240, 5, 183, 180, 145, 175, 3, 28, 130, 57, 77, 131, 216, 60, 241, 0, 168, 253, 160, 48, 232, 193, 115, 41, 63, 50, 237, 122, 20, 72, 12, 162, 246, 71, 253, 76, 59, 27, 197, 167, 28, 168, 235, 234, 218, 220, 213, 181, 111, 153, 109, 219, 151, 159, 96, 95, 232, 211, 244, 213, 127, 67, 42, 161, 40, 6, 254, 148, 185, 77, 106, 212, 43, 16, 227, 103, 147, 226, 69, 179, 201, 18, 194, 1, 19, 19, 169, 48, 200, 60, 233, 192, 239, 63, 184, 89, 6, 64, 56, 12, 60, 48, 167, 102, 80, 2, 192, 175, 105, 127, 28, 28, 65, 206, 103, 40, 185, 137, 4, 135, 0, 16, 223, 251, 16, 0, 64, 104, 109, 177, 226, 34, 162, 4, 254, 253, 255, 18, 137, 120, 247, 45, 179, 155, 244, 20, 232, 88, 139, 73, 172, 67, 66, 116, 192, 200, 200, 68, 58, 33, 52, 249, 233, 213, 110, 183, 180, 147, 115, 48, 221, 61, 15, 152, 160, 34, 193, 91, 121, 47, 27, 212, 180, 31, 249, 129, 108, 192, 144, 115, 227, 137, 56, 10, 175, 69, 213, 206, 183, 31, 16, 240, 118, 165, 122, 176, 25, 56, 44, 0, 240, 229, 111, 204, 86, 204, 90, 117, 62, 155, 68, 5, 26, 118, 162, 45, 94, 239, 161, 145, 244, 32, 191, 125, 243, 234, 213, 110, 65, 7, 118, 245, 196, 209, 10, 219, 243, 102, 207, 89, 80, 17, 39, 29, 220, 158, 138, 183, 106, 218, 15, 17, 49, 103, 44, 185, 108, 47, 27, 72, 113, 81, 86, 58, 161, 78, 64, 184, 231, 92, 215, 174, 29, 68, 13, 78, 92, 36, 8, 236, 53, 201, 188, 186, 217, 32, 89, 188, 104, 114, 0, 141, 248, 1, 208, 185, 13, 131, 233, 56, 126, 166, 125, 171, 95, 144, 0, 136, 37, 194, 233, 177, 56, 59, 207, 14, 14, 65, 28, 59, 105, 224, 199, 42, 21, 96, 28, 7, 2, 172, 34, 140, 81, 214, 23, 192, 221, 105, 159, 98, 243, 203, 174, 174, 246, 3, 7, 152, 230, 174, 46, 142, 235, 122, 103, 35, 6, 224, 247, 233, 244, 196, 255, 194, 8, 44, 51, 7, 128, 62, 81, 98, 188, 168, 93, 100, 91, 151, 60, 40, 80, 182, 112, 104, 134, 219, 216, 68, 16, 181, 185, 22, 0, 216, 39, 0, 16, 143, 143, 164, 209, 94, 155, 11, 42, 198, 22, 47, 238, 198, 87, 36, 83, 234, 96, 160, 53, 161, 246, 2, 149, 245, 5, 94, 13, 191, 182, 31, 96, 200, 253, 135, 216, 174, 151, 15, 19, 2, 59, 80, 95, 136, 0, 40, 191, 70, 0, 132, 104, 33, 243, 130, 155, 152, 132, 108, 177, 5, 55, 0, 34, 95, 132, 0, 243, 227, 87, 36, 29, 40, 44, 178, 207, 46, 176, 143, 217, 23, 28, 97, 188, 129, 246, 33, 112, 88, 148, 8, 180, 158, 83, 235, 127, 101, 246, 78, 11, 0, 64, 64, 238, 255, 69, 87, 215, 74, 30, 0, 84, 86, 219, 184, 23, 5, 11, 215, 50, 155, 142, 18, 15, 102, 200, 78, 123, 240, 152, 129, 5, 139, 183, 55, 158, 158, 24, 73, 4, 247, 84, 29, 58, 18, 227, 98, 74, 0, 18, 236, 226, 197, 99, 21, 11, 186, 219, 177, 69, 87, 133, 67, 173, 131, 41, 149, 3, 160, 212, 198, 94, 198, 173, 39, 176, 93, 0, 115, 127, 87, 215, 46, 34, 2, 135, 27, 209, 80, 89, 137, 121, 25, 208, 46, 80, 32, 182, 157, 50, 217, 126, 218, 193, 71, 79, 22, 144, 224, 64, 128, 105, 143, 163, 253, 150, 247, 188, 216, 112, 228, 195, 120, 92, 0, 96, 132, 32, 48, 200, 62, 177, 32, 213, 52, 47, 30, 30, 68, 74, 79, 37, 2, 41, 53, 0, 74, 109, 236, 101, 106, 117, 21, 18, 0, 192, 193, 111, 124, 27, 183, 127, 199, 113, 4, 192, 239, 76, 3, 96, 48, 171, 209, 240, 116, 189, 170, 39, 23, 41, 174, 175, 114, 90, 2, 169, 40, 120, 122, 192, 150, 193, 238, 68, 32, 16, 109, 216, 192, 183, 191, 7, 156, 211, 20, 217, 105, 39, 222, 229, 156, 87, 49, 123, 206, 200, 200, 238, 45, 27, 246, 139, 109, 111, 34, 0, 12, 169, 127, 79, 145, 189, 51, 76, 249, 117, 29, 64, 0, 244, 16, 4, 26, 63, 172, 63, 153, 158, 64, 0, 216, 204, 236, 86, 99, 98, 231, 45, 57, 139, 168, 171, 158, 112, 192, 232, 176, 187, 104, 218, 131, 0, 72, 14, 121, 163, 126, 210, 230, 186, 127, 168, 138, 14, 165, 71, 186, 121, 35, 0, 8, 12, 166, 241, 78, 11, 108, 151, 11, 185, 68, 225, 234, 221, 187, 215, 108, 17, 250, 126, 13, 254, 147, 210, 46, 145, 71, 201, 178, 119, 198, 57, 79, 47, 6, 0, 59, 195, 135, 191, 205, 29, 7, 254, 67, 238, 192, 54, 218, 196, 106, 88, 214, 172, 237, 87, 156, 162, 170, 122, 2, 247, 167, 10, 229, 202, 29, 40, 161, 232, 114, 90, 192, 255, 108, 247, 226, 109, 203, 145, 17, 168, 2, 167, 32, 157, 10, 131, 70, 196, 226, 31, 15, 98, 4, 64, 43, 84, 35, 4, 154, 100, 204, 191, 101, 119, 55, 206, 7, 106, 125, 64, 74, 60, 100, 2, 128, 65, 155, 164, 119, 253, 142, 104, 129, 141, 31, 94, 74, 167, 145, 51, 240, 249, 125, 217, 119, 171, 49, 99, 228, 212, 0, 200, 125, 68, 26, 45, 68, 225, 176, 67, 228, 236, 241, 31, 233, 181, 36, 185, 84, 187, 143, 99, 32, 30, 232, 170, 92, 141, 34, 129, 96, 98, 34, 61, 22, 38, 0, 36, 214, 132, 211, 35, 240, 10, 177, 199, 156, 217, 118, 94, 1, 236, 70, 109, 95, 211, 74, 116, 160, 214, 123, 85, 1, 96, 196, 175, 24, 116, 142, 200, 192, 198, 207, 128, 5, 176, 12, 252, 230, 71, 25, 87, 195, 194, 100, 66, 3, 200, 1, 208, 245, 17, 209, 74, 48, 78, 127, 114, 97, 190, 133, 1, 9, 224, 192, 89, 111, 111, 239, 122, 97, 117, 37, 182, 130, 193, 248, 4, 8, 63, 66, 32, 182, 43, 158, 72, 199, 145, 12, 116, 117, 53, 60, 193, 246, 242, 8, 44, 225, 255, 33, 55, 64, 235, 4, 170, 0, 208, 115, 220, 17, 249, 251, 36, 37, 112, 120, 99, 234, 2, 145, 129, 47, 109, 89, 87, 195, 50, 67, 10, 53, 161, 55, 181, 194, 3, 44, 64, 247, 206, 95, 27, 179, 4, 162, 120, 222, 163, 175, 93, 30, 9, 4, 7, 201, 102, 91, 107, 208, 86, 51, 35, 251, 19, 61, 130, 251, 74, 16, 216, 45, 168, 128, 214, 120, 150, 4, 142, 151, 223, 253, 64, 143, 2, 92, 31, 247, 197, 239, 136, 55, 188, 177, 231, 195, 244, 5, 108, 8, 191, 188, 207, 112, 53, 172, 28, 168, 22, 135, 196, 194, 174, 35, 148, 102, 76, 193, 133, 203, 112, 184, 233, 225, 55, 44, 124, 186, 226, 28, 116, 178, 60, 18, 8, 38, 208, 62, 83, 137, 48, 50, 134, 187, 18, 49, 49, 64, 80, 217, 193, 96, 150, 7, 241, 234, 237, 125, 34, 144, 47, 0, 63, 204, 179, 192, 202, 174, 227, 233, 48, 14, 9, 126, 243, 215, 70, 171, 97, 137, 100, 26, 27, 158, 17, 40, 241, 192, 19, 180, 31, 217, 130, 238, 59, 119, 199, 5, 0, 82, 44, 68, 2, 207, 73, 0, 0, 4, 168, 82, 34, 157, 128, 144, 232, 74, 88, 66, 0, 152, 64, 6, 65, 171, 241, 16, 50, 33, 164, 4, 141, 141, 22, 2, 128, 247, 133, 54, 190, 255, 254, 213, 120, 250, 99, 60, 134, 80, 201, 100, 1, 192, 154, 229, 87, 17, 225, 81, 3, 126, 238, 48, 37, 30, 48, 33, 13, 136, 49, 108, 8, 231, 231, 139, 0, 112, 202, 72, 128, 80, 119, 10, 52, 0, 138, 137, 18, 113, 65, 10, 186, 88, 25, 23, 180, 102, 11, 224, 51, 15, 253, 33, 233, 251, 140, 71, 160, 172, 235, 248, 96, 122, 43, 78, 152, 217, 104, 198, 71, 101, 186, 206, 154, 229, 87, 17, 97, 205, 99, 37, 107, 88, 83, 226, 1, 147, 84, 160, 203, 38, 147, 34, 0, 169, 158, 85, 90, 0, 186, 194, 0, 0, 222, 112, 176, 91, 206, 4, 92, 24, 143, 13, 119, 183, 102, 147, 128, 204, 0, 120, 147, 169, 145, 84, 138, 15, 8, 190, 221, 213, 146, 26, 27, 33, 195, 40, 247, 249, 82, 209, 118, 227, 75, 77, 5, 123, 232, 156, 74, 155, 77, 171, 130, 93, 146, 8, 249, 185, 222, 106, 9, 128, 212, 166, 213, 242, 124, 24, 239, 15, 167, 227, 131, 97, 180, 239, 220, 88, 56, 28, 150, 62, 102, 123, 82, 231, 226, 225, 238, 112, 214, 12, 78, 102, 14, 104, 239, 29, 140, 167, 142, 19, 22, 216, 177, 43, 152, 30, 57, 182, 163, 28, 143, 26, 44, 227, 82, 169, 33, 86, 39, 203, 192, 111, 65, 106, 98, 74, 149, 85, 247, 83, 112, 254, 236, 210, 120, 97, 32, 56, 255, 113, 25, 0, 254, 23, 86, 85, 170, 218, 31, 156, 72, 133, 227, 169, 116, 56, 30, 79, 143, 77, 140, 176, 50, 4, 128, 123, 163, 108, 32, 107, 246, 5, 1, 144, 193, 108, 31, 73, 199, 69, 53, 248, 109, 224, 182, 250, 250, 50, 60, 138, 242, 203, 82, 36, 148, 58, 23, 32, 251, 248, 35, 235, 143, 76, 76, 169, 210, 55, 190, 16, 0, 202, 135, 17, 217, 40, 43, 1, 112, 206, 231, 251, 145, 91, 5, 64, 24, 111, 60, 136, 66, 34, 120, 21, 23, 210, 89, 93, 61, 236, 122, 159, 239, 135, 102, 22, 65, 69, 0, 24, 57, 2, 64, 135, 32, 12, 15, 127, 192, 123, 131, 35, 233, 32, 42, 51, 197, 9, 227, 109, 117, 40, 149, 24, 208, 48, 16, 118, 17, 107, 205, 76, 169, 178, 89, 149, 203, 135, 96, 114, 105, 135, 211, 37, 0, 146, 62, 127, 109, 173, 10, 128, 4, 248, 129, 241, 48, 7, 93, 95, 23, 12, 174, 17, 0, 240, 250, 252, 248, 1, 168, 172, 79, 65, 56, 192, 184, 158, 105, 207, 8, 90, 142, 227, 42, 41, 24, 74, 39, 186, 126, 94, 127, 244, 18, 241, 6, 182, 85, 202, 74, 10, 36, 2, 0, 106, 77, 166, 62, 43, 181, 155, 129, 121, 116, 214, 225, 146, 0, 136, 85, 85, 53, 28, 137, 199, 130, 114, 9, 72, 143, 12, 6, 24, 175, 15, 13, 21, 212, 185, 249, 246, 31, 192, 15, 34, 30, 50, 19, 238, 66, 171, 234, 67, 89, 124, 218, 16, 31, 73, 196, 70, 82, 120, 140, 44, 190, 129, 97, 142, 130, 71, 76, 20, 225, 187, 184, 170, 66, 229, 103, 209, 15, 204, 181, 62, 12, 110, 45, 157, 53, 245, 169, 55, 147, 156, 118, 232, 204, 207, 32, 0, 64, 63, 199, 143, 4, 162, 201, 81, 188, 215, 172, 136, 65, 56, 29, 110, 199, 131, 215, 93, 7, 188, 13, 110, 55, 242, 19, 249, 1, 144, 204, 0, 176, 177, 211, 3, 177, 24, 43, 0, 160, 126, 22, 121, 124, 138, 166, 96, 52, 124, 248, 251, 223, 66, 76, 124, 234, 4, 195, 180, 92, 73, 167, 199, 203, 73, 142, 184, 4, 35, 192, 75, 129, 251, 222, 90, 212, 169, 149, 235, 169, 127, 180, 206, 181, 90, 43, 179, 201, 128, 222, 234, 33, 78, 189, 249, 41, 150, 64, 87, 207, 57, 188, 201, 104, 116, 207, 158, 61, 135, 130, 225, 4, 216, 188, 17, 164, 225, 99, 225, 32, 0, 192, 111, 13, 226, 99, 200, 128, 17, 107, 98, 236, 146, 59, 43, 80, 148, 145, 206, 151, 213, 243, 104, 103, 229, 108, 40, 43, 251, 244, 212, 169, 83, 63, 223, 202, 124, 8, 79, 114, 154, 32, 240, 121, 81, 15, 146, 76, 22, 155, 154, 82, 193, 157, 162, 144, 91, 251, 240, 3, 84, 150, 135, 208, 42, 30, 143, 110, 251, 25, 11, 3, 156, 62, 6, 162, 30, 254, 225, 63, 8, 161, 80, 42, 45, 236, 189, 155, 78, 200, 212, 144, 159, 5, 202, 58, 134, 31, 19, 155, 127, 118, 0, 129, 17, 31, 32, 218, 156, 150, 126, 93, 103, 12, 99, 37, 106, 255, 169, 83, 27, 183, 50, 39, 225, 119, 143, 23, 144, 52, 249, 47, 183, 158, 59, 183, 189, 188, 92, 33, 203, 184, 229, 84, 214, 113, 116, 13, 7, 160, 74, 124, 189, 19, 1, 128, 224, 68, 26, 153, 248, 213, 210, 160, 72, 176, 59, 1, 50, 1, 129, 96, 122, 196, 228, 88, 141, 64, 1, 212, 254, 56, 252, 119, 54, 134, 218, 207, 68, 79, 35, 36, 122, 149, 39, 233, 196, 167, 181, 239, 96, 0, 78, 173, 116, 215, 165, 78, 28, 171, 175, 183, 146, 49, 212, 207, 203, 17, 51, 124, 110, 180, 26, 86, 6, 162, 213, 17, 136, 126, 255, 99, 0, 70, 112, 251, 15, 172, 126, 90, 30, 9, 160, 138, 149, 20, 88, 0, 214, 159, 203, 202, 135, 66, 247, 15, 240, 12, 192, 120, 79, 35, 4, 98, 140, 194, 1, 211, 141, 79, 223, 39, 8, 84, 51, 107, 208, 130, 5, 141, 203, 126, 41, 27, 71, 255, 101, 165, 116, 49, 37, 30, 50, 147, 10, 0, 163, 217, 196, 180, 101, 79, 55, 25, 23, 66, 145, 64, 157, 4, 192, 1, 47, 14, 147, 123, 15, 29, 49, 207, 3, 129, 129, 179, 137, 211, 146, 4, 32, 37, 200, 13, 182, 54, 4, 17, 0, 180, 195, 197, 216, 93, 178, 86, 168, 107, 222, 214, 236, 32, 8, 108, 45, 248, 41, 74, 145, 126, 80, 90, 36, 13, 164, 127, 190, 74, 93, 17, 146, 107, 250, 156, 54, 154, 152, 84, 101, 129, 136, 15, 107, 125, 247, 234, 23, 54, 211, 50, 19, 216, 14, 161, 26, 23, 237, 61, 114, 196, 116, 229, 78, 44, 142, 26, 61, 16, 71, 93, 94, 211, 20, 59, 132, 74, 144, 26, 236, 118, 135, 131, 127, 4, 84, 181, 42, 172, 0, 45, 30, 36, 90, 115, 24, 3, 176, 235, 175, 152, 70, 240, 6, 210, 31, 210, 18, 19, 124, 190, 76, 29, 0, 100, 94, 34, 74, 67, 198, 115, 211, 156, 150, 9, 52, 36, 2, 202, 239, 149, 7, 159, 126, 238, 53, 121, 40, 224, 227, 2, 94, 63, 123, 164, 215, 100, 205, 74, 59, 102, 123, 6, 47, 179, 222, 78, 87, 57, 92, 168, 229, 180, 211, 30, 166, 101, 206, 135, 176, 6, 28, 37, 30, 100, 180, 242, 247, 208, 254, 79, 127, 187, 113, 141, 247, 232, 85, 240, 142, 90, 192, 131, 218, 203, 215, 83, 124, 188, 140, 86, 114, 128, 113, 146, 65, 143, 104, 131, 73, 186, 232, 43, 75, 16, 187, 54, 93, 7, 190, 79, 202, 163, 160, 235, 5, 0, 114, 210, 127, 188, 244, 15, 28, 114, 224, 181, 199, 241, 242, 175, 85, 46, 123, 69, 171, 130, 129, 92, 120, 230, 190, 195, 249, 4, 122, 67, 169, 239, 113, 224, 212, 169, 223, 131, 71, 116, 247, 93, 117, 187, 222, 79, 167, 175, 180, 48, 237, 7, 86, 45, 187, 143, 48, 65, 193, 92, 101, 81, 152, 53, 203, 243, 200, 57, 68, 91, 47, 40, 35, 75, 23, 121, 66, 250, 193, 7, 159, 123, 142, 127, 14, 30, 129, 44, 63, 33, 167, 118, 196, 248, 136, 5, 154, 28, 221, 241, 24, 230, 25, 36, 115, 180, 189, 53, 22, 67, 85, 70, 78, 178, 16, 115, 21, 89, 146, 27, 254, 232, 175, 127, 183, 235, 20, 118, 138, 255, 226, 174, 234, 75, 96, 12, 47, 181, 128, 137, 102, 126, 244, 49, 65, 224, 147, 162, 101, 114, 8, 172, 70, 143, 66, 134, 21, 30, 120, 88, 58, 217, 227, 200, 180, 160, 132, 80, 35, 132, 106, 68, 127, 204, 191, 230, 57, 32, 91, 171, 37, 130, 238, 7, 229, 151, 56, 27, 174, 177, 215, 156, 29, 136, 113, 14, 180, 208, 151, 179, 102, 61, 21, 59, 27, 179, 87, 52, 245, 186, 28, 104, 137, 89, 135, 11, 55, 27, 61, 153, 83, 71, 38, 225, 247, 220, 124, 110, 224, 235, 247, 128, 71, 56, 241, 105, 253, 110, 232, 11, 102, 153, 80, 101, 247, 121, 145, 172, 91, 75, 141, 100, 128, 12, 43, 216, 100, 57, 85, 35, 3, 72, 72, 0, 224, 21, 0, 224, 21, 254, 245, 1, 148, 140, 48, 191, 249, 17, 145, 254, 112, 83, 141, 179, 2, 248, 31, 58, 60, 214, 228, 168, 169, 168, 176, 55, 57, 192, 44, 0, 48, 77, 200, 9, 116, 56, 232, 42, 162, 138, 112, 17, 23, 216, 3, 81, 49, 181, 15, 12, 4, 24, 118, 0, 213, 166, 59, 151, 16, 4, 254, 252, 59, 45, 87, 143, 173, 92, 185, 178, 112, 59, 200, 103, 229, 125, 66, 129, 217, 54, 19, 202, 95, 51, 172, 224, 201, 92, 80, 45, 0, 240, 28, 0, 240, 63, 228, 95, 152, 222, 252, 168, 247, 244, 217, 193, 214, 38, 123, 205, 238, 134, 216, 233, 32, 64, 16, 27, 136, 67, 147, 155, 226, 206, 138, 110, 2, 13, 118, 8, 92, 18, 211, 123, 156, 104, 114, 164, 244, 137, 255, 44, 98, 160, 1, 228, 49, 58, 239, 249, 58, 143, 192, 83, 251, 241, 228, 194, 93, 93, 72, 68, 87, 149, 243, 38, 81, 25, 223, 233, 5, 60, 218, 77, 54, 140, 21, 160, 2, 128, 213, 200, 8, 200, 191, 48, 187, 208, 52, 210, 126, 173, 53, 173, 113, 104, 193, 64, 47, 93, 21, 24, 136, 215, 216, 29, 85, 235, 99, 3, 53, 208, 114, 248, 50, 129, 151, 109, 166, 21, 15, 235, 66, 171, 29, 161, 79, 208, 204, 64, 28, 59, 116, 55, 57, 223, 232, 6, 165, 233, 185, 231, 47, 4, 4, 156, 208, 254, 75, 151, 90, 176, 142, 166, 151, 217, 176, 42, 216, 171, 112, 129, 221, 58, 131, 164, 224, 93, 254, 200, 42, 31, 86, 112, 100, 169, 168, 23, 0, 32, 70, 64, 5, 128, 137, 200, 155, 3, 237, 23, 182, 199, 207, 246, 114, 16, 247, 208, 85, 206, 67, 65, 59, 252, 228, 166, 205, 76, 140, 183, 11, 113, 189, 171, 104, 59, 222, 187, 3, 44, 165, 195, 225, 26, 168, 169, 176, 87, 212, 160, 119, 40, 87, 123, 23, 153, 76, 115, 248, 166, 197, 246, 122, 80, 133, 87, 63, 228, 47, 41, 221, 198, 123, 68, 242, 219, 104, 215, 166, 5, 7, 211, 74, 201, 135, 21, 208, 100, 220, 76, 105, 116, 11, 17, 114, 153, 17, 224, 201, 76, 209, 81, 128, 67, 142, 127, 83, 69, 24, 115, 57, 131, 54, 31, 112, 56, 60, 85, 78, 215, 179, 224, 231, 6, 101, 14, 161, 154, 208, 98, 128, 100, 233, 117, 52, 70, 229, 168, 105, 170, 113, 160, 5, 250, 113, 137, 231, 44, 1, 129, 77, 40, 54, 78, 167, 5, 4, 108, 122, 195, 231, 154, 122, 65, 136, 20, 209, 226, 27, 148, 248, 1, 174, 158, 213, 123, 120, 94, 201, 89, 214, 111, 238, 5, 187, 37, 55, 2, 60, 101, 217, 252, 168, 61, 24, 27, 72, 160, 6, 182, 58, 226, 103, 99, 138, 86, 122, 92, 216, 207, 133, 72, 48, 62, 32, 64, 163, 36, 143, 67, 62, 37, 216, 131, 108, 132, 211, 89, 133, 192, 112, 185, 28, 51, 239, 230, 17, 216, 220, 130, 172, 97, 250, 40, 169, 238, 94, 166, 85, 2, 186, 219, 10, 96, 135, 129, 146, 127, 162, 207, 2, 188, 146, 179, 4, 27, 80, 43, 218, 101, 70, 64, 32, 74, 103, 173, 46, 145, 216, 10, 187, 189, 6, 91, 126, 187, 189, 233, 172, 238, 24, 57, 195, 190, 17, 12, 170, 35, 65, 66, 180, 203, 35, 215, 76, 85, 14, 59, 63, 5, 202, 227, 116, 84, 85, 221, 38, 32, 240, 90, 203, 133, 116, 250, 100, 125, 61, 70, 64, 23, 0, 157, 220, 231, 92, 225, 231, 179, 16, 175, 228, 44, 103, 131, 3, 241, 193, 179, 175, 169, 141, 0, 147, 57, 238, 10, 84, 216, 91, 227, 21, 224, 243, 87, 84, 56, 237, 78, 157, 225, 1, 116, 29, 253, 132, 179, 166, 187, 23, 47, 93, 154, 153, 104, 112, 20, 248, 147, 122, 89, 127, 59, 115, 27, 239, 15, 220, 180, 179, 225, 12, 94, 189, 109, 63, 124, 243, 194, 39, 230, 170, 168, 180, 143, 237, 210, 76, 203, 198, 167, 16, 37, 103, 57, 27, 235, 61, 205, 177, 26, 35, 160, 123, 39, 145, 2, 187, 155, 90, 43, 104, 187, 19, 148, 57, 27, 220, 173, 103, 103, 208, 117, 244, 19, 246, 138, 112, 76, 59, 43, 92, 75, 158, 42, 212, 253, 222, 134, 134, 166, 166, 26, 59, 136, 211, 109, 164, 112, 98, 199, 77, 180, 23, 215, 15, 93, 58, 238, 101, 106, 145, 67, 180, 45, 235, 173, 244, 238, 174, 107, 8, 121, 37, 103, 65, 66, 238, 213, 26, 1, 38, 35, 0, 193, 138, 65, 224, 237, 42, 82, 150, 109, 188, 146, 37, 112, 118, 171, 62, 252, 122, 228, 173, 169, 104, 106, 109, 114, 217, 145, 47, 59, 133, 32, 80, 182, 146, 102, 32, 58, 6, 65, 56, 211, 194, 32, 51, 240, 75, 237, 64, 99, 109, 182, 73, 180, 180, 193, 96, 42, 81, 114, 22, 16, 114, 118, 192, 167, 49, 2, 25, 137, 181, 135, 73, 150, 195, 248, 71, 105, 228, 242, 33, 59, 103, 188, 20, 190, 154, 2, 103, 207, 114, 3, 12, 253, 162, 29, 124, 217, 245, 127, 86, 198, 15, 151, 212, 50, 251, 143, 33, 85, 120, 229, 67, 148, 28, 250, 132, 216, 65, 239, 59, 89, 72, 118, 91, 227, 133, 213, 40, 164, 228, 44, 20, 218, 27, 253, 53, 0, 224, 199, 6, 167, 105, 137, 109, 106, 61, 155, 25, 0, 180, 207, 136, 211, 229, 82, 237, 198, 147, 141, 218, 113, 252, 249, 140, 235, 153, 222, 96, 128, 190, 137, 47, 28, 248, 182, 155, 105, 56, 137, 51, 148, 133, 146, 29, 124, 167, 231, 252, 197, 12, 116, 190, 71, 66, 128, 54, 14, 133, 40, 116, 176, 80, 49, 191, 50, 18, 200, 78, 193, 10, 100, 56, 178, 229, 9, 240, 6, 76, 224, 220, 184, 170, 114, 42, 117, 0, 223, 171, 97, 224, 244, 0, 237, 184, 137, 175, 162, 252, 246, 90, 134, 249, 16, 60, 130, 147, 24, 128, 251, 150, 189, 0, 183, 123, 39, 99, 251, 1, 1, 9, 128, 12, 43, 235, 81, 232, 96, 193, 71, 77, 36, 144, 137, 2, 224, 248, 36, 194, 167, 201, 27, 65, 1, 232, 55, 146, 246, 120, 80, 8, 12, 114, 128, 215, 62, 174, 50, 49, 211, 21, 28, 47, 79, 239, 217, 196, 233, 88, 195, 15, 26, 249, 210, 129, 53, 12, 211, 114, 230, 82, 253, 18, 62, 46, 222, 182, 140, 126, 39, 115, 251, 47, 94, 124, 199, 45, 60, 144, 195, 120, 189, 97, 220, 116, 226, 10, 63, 173, 99, 4, 228, 212, 219, 59, 112, 182, 162, 162, 34, 140, 123, 189, 117, 119, 156, 56, 120, 52, 74, 181, 224, 77, 79, 156, 153, 153, 157, 44, 0, 2, 241, 95, 70, 167, 148, 16, 242, 189, 60, 103, 7, 226, 30, 186, 106, 169, 80, 60, 1, 170, 144, 249, 121, 125, 153, 56, 235, 104, 89, 118, 0, 74, 121, 39, 57, 203, 210, 138, 2, 0, 122, 70, 64, 70, 120, 168, 35, 30, 107, 173, 104, 5, 171, 30, 176, 199, 79, 227, 8, 15, 140, 128, 48, 95, 17, 26, 150, 69, 215, 129, 135, 227, 180, 103, 123, 24, 68, 216, 247, 98, 79, 163, 210, 19, 215, 221, 59, 4, 49, 0, 229, 95, 215, 88, 40, 0, 240, 46, 15, 64, 79, 79, 207, 137, 139, 23, 251, 91, 250, 209, 155, 225, 174, 139, 253, 240, 65, 79, 63, 225, 128, 130, 101, 182, 242, 109, 69, 203, 150, 102, 91, 112, 26, 3, 160, 141, 4, 20, 196, 138, 35, 29, 142, 214, 1, 174, 169, 105, 16, 15, 250, 32, 35, 151, 109, 237, 78, 57, 33, 119, 207, 148, 69, 160, 196, 131, 107, 165, 192, 3, 72, 12, 152, 159, 22, 9, 217, 33, 2, 192, 240, 87, 110, 191, 253, 241, 139, 61, 55, 255, 229, 205, 61, 240, 238, 111, 190, 114, 177, 238, 246, 219, 111, 255, 74, 53, 6, 192, 182, 23, 159, 249, 193, 134, 108, 187, 153, 96, 0, 50, 27, 1, 60, 214, 49, 120, 54, 145, 56, 59, 208, 10, 109, 174, 24, 224, 67, 252, 172, 145, 166, 154, 170, 28, 14, 51, 128, 81, 226, 129, 97, 214, 240, 67, 231, 135, 113, 64, 224, 221, 255, 206, 55, 113, 142, 148, 0, 112, 226, 86, 116, 252, 203, 234, 139, 239, 220, 126, 241, 98, 203, 237, 55, 163, 119, 253, 55, 15, 99, 0, 8, 82, 61, 63, 253, 224, 203, 119, 51, 215, 31, 99, 0, 50, 27, 1, 212, 96, 28, 246, 12, 180, 54, 57, 80, 206, 11, 236, 53, 142, 165, 60, 246, 170, 44, 9, 35, 245, 160, 138, 203, 97, 98, 198, 63, 37, 30, 128, 182, 226, 242, 137, 141, 63, 61, 117, 170, 5, 49, 65, 3, 25, 58, 38, 0, 116, 221, 186, 21, 204, 1, 116, 255, 240, 205, 23, 251, 111, 237, 255, 10, 250, 236, 241, 133, 23, 37, 0, 78, 252, 116, 255, 255, 70, 213, 167, 25, 195, 97, 116, 200, 108, 4, 184, 179, 97, 62, 180, 63, 221, 234, 116, 116, 3, 16, 113, 228, 72, 2, 75, 59, 179, 37, 140, 180, 163, 74, 154, 125, 234, 50, 16, 126, 110, 247, 183, 55, 238, 216, 88, 246, 193, 169, 83, 167, 14, 175, 92, 195, 7, 197, 60, 0, 253, 143, 175, 189, 253, 214, 139, 55, 131, 208, 127, 229, 226, 237, 45, 23, 17, 0, 231, 111, 238, 151, 0, 248, 205, 150, 159, 158, 192, 167, 91, 51, 252, 8, 6, 32, 139, 17, 128, 46, 63, 61, 128, 178, 59, 103, 195, 221, 21, 177, 248, 217, 112, 47, 10, 37, 160, 251, 179, 38, 140, 180, 0, 208, 235, 209, 142, 100, 56, 117, 235, 201, 166, 56, 121, 90, 178, 114, 139, 189, 6, 15, 154, 28, 61, 192, 44, 251, 92, 2, 0, 209, 87, 250, 111, 237, 1, 190, 127, 231, 230, 199, 31, 255, 202, 218, 139, 23, 215, 254, 37, 111, 5, 190, 252, 188, 124, 217, 11, 149, 15, 148, 139, 3, 43, 25, 1, 200, 98, 4, 8, 129, 167, 26, 70, 201, 187, 4, 86, 0, 79, 129, 73, 203, 158, 48, 210, 2, 128, 66, 144, 103, 237, 142, 23, 55, 87, 57, 157, 230, 182, 156, 64, 171, 101, 219, 143, 147, 113, 179, 83, 191, 91, 137, 151, 229, 224, 149, 32, 48, 193, 87, 206, 255, 205, 227, 23, 171, 111, 239, 217, 186, 117, 43, 82, 127, 88, 29, 98, 37, 184, 138, 70, 243, 135, 233, 101, 216, 118, 22, 105, 238, 138, 22, 181, 5, 238, 253, 206, 83, 24, 128, 7, 201, 148, 233, 44, 20, 59, 11, 234, 159, 29, 136, 159, 109, 71, 254, 165, 195, 204, 44, 53, 45, 0, 56, 10, 135, 48, 17, 173, 1, 110, 206, 67, 68, 190, 134, 235, 196, 41, 129, 14, 223, 107, 43, 47, 34, 0, 84, 223, 124, 235, 205, 213, 32, 253, 183, 146, 102, 131, 8, 84, 223, 46, 248, 1, 140, 144, 13, 197, 44, 163, 137, 162, 239, 249, 90, 93, 221, 254, 27, 143, 31, 255, 238, 119, 17, 0, 166, 35, 129, 40, 10, 0, 118, 7, 161, 83, 92, 230, 118, 203, 214, 7, 0, 56, 135, 118, 152, 114, 10, 48, 85, 65, 75, 182, 226, 113, 51, 66, 159, 117, 173, 228, 69, 224, 124, 191, 192, 9, 26, 71, 8, 93, 136, 245, 45, 206, 163, 148, 51, 175, 241, 104, 215, 181, 180, 124, 120, 252, 248, 153, 178, 63, 135, 24, 243, 198, 116, 186, 123, 158, 133, 9, 133, 94, 121, 240, 191, 239, 219, 103, 106, 183, 242, 129, 179, 3, 177, 128, 131, 232, 49, 42, 83, 194, 200, 16, 0, 49, 213, 248, 162, 9, 109, 200, 243, 8, 90, 15, 140, 94, 179, 81, 130, 224, 84, 118, 79, 16, 93, 140, 183, 123, 124, 247, 203, 47, 255, 247, 159, 89, 44, 55, 237, 255, 240, 248, 201, 163, 101, 101, 95, 175, 72, 143, 204, 156, 233, 56, 250, 39, 245, 245, 103, 0, 128, 196, 76, 75, 36, 18, 217, 247, 252, 235, 251, 126, 33, 174, 251, 153, 137, 144, 75, 24, 115, 100, 24, 224, 204, 10, 128, 140, 115, 64, 184, 179, 4, 203, 188, 201, 228, 211, 14, 91, 187, 62, 21, 1, 200, 26, 12, 157, 57, 246, 210, 99, 141, 199, 206, 156, 193, 102, 115, 239, 183, 230, 164, 107, 194, 233, 121, 63, 249, 250, 159, 60, 118, 227, 200, 188, 238, 176, 229, 83, 0, 224, 228, 148, 137, 244, 200, 84, 4, 192, 235, 0, 0, 90, 1, 48, 251, 46, 136, 200, 18, 8, 249, 191, 73, 2, 32, 231, 28, 136, 148, 204, 56, 135, 98, 2, 85, 228, 130, 143, 178, 133, 195, 61, 233, 227, 143, 253, 164, 254, 100, 186, 7, 87, 29, 46, 121, 236, 63, 167, 29, 21, 233, 165, 63, 248, 47, 255, 165, 254, 91, 77, 179, 103, 87, 180, 158, 252, 147, 250, 198, 15, 230, 125, 109, 233, 148, 187, 16, 0, 175, 2, 0, 230, 54, 54, 64, 67, 61, 66, 150, 243, 26, 0, 144, 93, 87, 133, 85, 129, 65, 82, 137, 111, 184, 228, 206, 211, 75, 86, 254, 142, 32, 144, 57, 31, 210, 51, 145, 254, 159, 24, 0, 82, 121, 107, 253, 171, 155, 38, 154, 22, 164, 43, 190, 123, 247, 215, 235, 191, 53, 239, 7, 55, 253, 96, 118, 221, 13, 232, 158, 247, 124, 237, 59, 12, 2, 224, 249, 231, 247, 237, 51, 185, 19, 238, 64, 123, 76, 55, 205, 107, 64, 217, 1, 32, 123, 197, 85, 233, 234, 3, 126, 80, 75, 53, 146, 122, 96, 199, 41, 57, 253, 254, 112, 99, 215, 174, 149, 43, 215, 108, 5, 203, 86, 215, 178, 255, 167, 141, 141, 199, 46, 92, 186, 116, 229, 202, 79, 30, 171, 175, 255, 244, 106, 57, 63, 174, 250, 159, 246, 206, 189, 97, 203, 77, 95, 123, 118, 10, 243, 212, 93, 204, 119, 190, 118, 143, 20, 190, 3, 0, 161, 231, 65, 7, 202, 23, 250, 98, 154, 59, 141, 164, 33, 199, 21, 62, 76, 0, 128, 138, 103, 92, 14, 93, 73, 112, 160, 202, 86, 90, 179, 109, 138, 123, 229, 198, 223, 158, 82, 211, 239, 127, 119, 226, 231, 27, 87, 174, 220, 234, 93, 179, 102, 205, 214, 58, 111, 93, 245, 15, 182, 212, 1, 217, 248, 248, 241, 107, 95, 126, 121, 235, 159, 81, 79, 209, 207, 104, 166, 227, 1, 0, 191, 120, 254, 213, 125, 191, 16, 90, 220, 44, 44, 158, 79, 86, 13, 15, 25, 66, 97, 138, 76, 21, 88, 225, 5, 239, 116, 85, 1, 10, 230, 117, 22, 85, 5, 125, 120, 224, 215, 26, 8, 68, 40, 142, 30, 254, 117, 207, 174, 149, 119, 175, 172, 6, 48, 10, 164, 21, 108, 222, 165, 116, 167, 227, 89, 4, 35, 128, 85, 64, 115, 91, 40, 212, 214, 204, 239, 140, 237, 107, 238, 232, 8, 73, 203, 130, 162, 253, 35, 180, 235, 226, 93, 7, 0, 112, 164, 236, 241, 232, 68, 73, 168, 200, 198, 32, 158, 119, 175, 92, 121, 216, 16, 131, 83, 167, 62, 253, 201, 63, 17, 48, 94, 46, 218, 198, 231, 81, 62, 182, 46, 169, 213, 153, 142, 103, 17, 140, 64, 51, 191, 67, 54, 179, 89, 177, 51, 184, 79, 185, 52, 50, 15, 133, 217, 157, 96, 76, 151, 216, 185, 132, 90, 42, 5, 217, 237, 85, 25, 246, 75, 217, 186, 181, 235, 119, 191, 55, 0, 224, 131, 159, 124, 32, 190, 254, 245, 46, 108, 11, 203, 127, 255, 219, 223, 125, 216, 184, 113, 227, 198, 158, 30, 56, 108, 252, 136, 63, 90, 120, 35, 16, 226, 55, 8, 71, 132, 23, 196, 247, 117, 192, 31, 111, 91, 73, 200, 108, 111, 235, 81, 46, 53, 134, 58, 188, 142, 246, 140, 201, 104, 38, 107, 215, 172, 92, 89, 214, 115, 226, 183, 159, 170, 1, 248, 167, 159, 200, 63, 194, 170, 112, 111, 201, 191, 232, 65, 101, 33, 70, 32, 36, 91, 30, 91, 100, 246, 80, 91, 40, 178, 175, 252, 143, 5, 192, 100, 182, 139, 227, 175, 92, 179, 100, 229, 202, 246, 234, 141, 59, 14, 11, 202, 241, 184, 140, 1, 128, 248, 245, 204, 182, 149, 232, 2, 128, 141, 0, 209, 125, 161, 230, 54, 53, 179, 231, 248, 40, 170, 186, 154, 140, 0, 52, 135, 58, 178, 251, 158, 230, 137, 162, 107, 183, 62, 190, 243, 31, 87, 2, 117, 237, 127, 172, 241, 176, 76, 58, 118, 9, 203, 153, 149, 107, 33, 176, 96, 35, 176, 19, 115, 62, 218, 222, 69, 209, 126, 153, 168, 155, 91, 149, 67, 93, 87, 147, 17, 0, 159, 108, 247, 137, 235, 64, 138, 130, 134, 218, 173, 91, 215, 48, 117, 24, 140, 247, 55, 30, 62, 92, 184, 87, 40, 180, 42, 223, 165, 1, 96, 223, 243, 175, 50, 191, 0, 77, 23, 242, 169, 181, 157, 212, 126, 183, 201, 109, 130, 213, 117, 53, 153, 69, 32, 148, 211, 14, 236, 89, 73, 138, 50, 92, 138, 196, 27, 237, 222, 186, 213, 109, 21, 107, 205, 202, 75, 176, 250, 67, 170, 112, 199, 142, 30, 80, 130, 175, 3, 7, 116, 134, 152, 72, 71, 179, 74, 6, 38, 241, 12, 234, 186, 154, 108, 0, 248, 24, 166, 35, 196, 215, 227, 153, 47, 75, 51, 252, 117, 33, 202, 208, 171, 139, 170, 20, 43, 111, 63, 47, 95, 134, 75, 150, 200, 122, 213, 150, 200, 171, 171, 193, 10, 242, 58, 48, 212, 38, 110, 15, 147, 195, 174, 103, 18, 169, 235, 106, 178, 139, 64, 165, 184, 216, 172, 233, 178, 52, 67, 162, 196, 131, 254, 94, 110, 54, 17, 130, 34, 41, 71, 102, 193, 70, 128, 102, 120, 37, 8, 215, 19, 125, 216, 1, 93, 51, 137, 103, 80, 165, 73, 50, 2, 128, 182, 168, 177, 189, 37, 178, 154, 217, 178, 52, 99, 162, 248, 131, 203, 104, 189, 221, 23, 108, 31, 139, 16, 8, 229, 86, 150, 16, 0, 240, 58, 136, 64, 167, 248, 36, 33, 193, 22, 78, 242, 33, 100, 105, 146, 44, 50, 30, 10, 213, 122, 69, 99, 51, 137, 181, 176, 180, 191, 141, 15, 25, 134, 3, 37, 8, 126, 115, 31, 249, 29, 203, 62, 20, 9, 244, 97, 119, 143, 180, 184, 243, 26, 116, 128, 58, 212, 201, 2, 0, 90, 140, 215, 139, 119, 37, 107, 158, 212, 90, 88, 250, 148, 185, 52, 244, 71, 54, 97, 136, 177, 8, 143, 25, 89, 94, 121, 254, 213, 215, 127, 1, 26, 95, 210, 250, 161, 63, 26, 0, 60, 193, 111, 33, 69, 104, 42, 203, 104, 130, 178, 148, 134, 74, 16, 148, 35, 4, 44, 96, 4, 94, 111, 131, 30, 144, 56, 158, 168, 131, 206, 146, 131, 154, 75, 179, 47, 240, 50, 41, 0, 218, 120, 222, 163, 76, 100, 25, 179, 147, 137, 100, 251, 42, 178, 224, 51, 154, 166, 206, 88, 94, 125, 254, 199, 208, 100, 89, 62, 144, 55, 132, 109, 109, 90, 0, 178, 47, 240, 162, 34, 147, 118, 190, 141, 88, 66, 74, 60, 92, 11, 101, 29, 16, 71, 180, 108, 175, 48, 96, 98, 121, 254, 249, 231, 94, 87, 4, 184, 136, 1, 246, 234, 43, 64, 177, 18, 219, 108, 205, 71, 70, 0, 52, 27, 132, 83, 226, 225, 90, 72, 103, 122, 172, 154, 94, 176, 149, 111, 251, 146, 31, 48, 65, 0, 236, 67, 42, 144, 79, 124, 144, 238, 47, 210, 245, 1, 118, 174, 167, 231, 22, 96, 101, 237, 182, 154, 91, 250, 47, 19, 0, 252, 196, 54, 90, 194, 129, 18, 15, 58, 103, 155, 117, 147, 76, 48, 128, 52, 25, 105, 155, 8, 0, 67, 218, 223, 204, 155, 128, 131, 50, 14, 160, 221, 5, 86, 235, 189, 232, 105, 55, 231, 188, 192, 139, 9, 17, 40, 54, 55, 1, 204, 180, 155, 148, 189, 46, 107, 149, 180, 210, 51, 230, 128, 213, 178, 145, 113, 201, 17, 38, 44, 64, 163, 77, 220, 11, 138, 209, 42, 166, 52, 169, 94, 201, 109, 129, 151, 235, 231, 235, 155, 117, 147, 244, 118, 111, 83, 81, 185, 216, 254, 79, 176, 14, 80, 140, 140, 119, 10, 94, 128, 94, 42, 144, 87, 209, 148, 249, 199, 54, 6, 192, 6, 200, 154, 191, 143, 105, 55, 41, 203, 244, 16, 68, 149, 216, 21, 122, 247, 221, 143, 127, 89, 142, 28, 98, 196, 1, 138, 145, 241, 80, 38, 39, 128, 18, 15, 230, 232, 250, 113, 128, 89, 55, 41, 251, 86, 76, 171, 144, 9, 44, 170, 173, 37, 235, 171, 0, 0, 170, 145, 113, 25, 0, 248, 148, 74, 177, 30, 187, 178, 88, 9, 128, 137, 133, 192, 175, 99, 184, 107, 202, 77, 210, 203, 172, 50, 170, 217, 95, 88, 5, 72, 227, 229, 8, 0, 61, 14, 232, 68, 73, 128, 218, 226, 2, 171, 108, 117, 74, 155, 141, 66, 127, 40, 242, 206, 204, 220, 69, 93, 0, 116, 106, 252, 205, 16, 101, 194, 77, 210, 115, 2, 155, 85, 226, 236, 70, 54, 96, 175, 216, 233, 42, 29, 192, 111, 149, 25, 41, 57, 88, 104, 45, 40, 45, 80, 237, 247, 89, 44, 111, 179, 219, 132, 37, 212, 5, 32, 151, 53, 228, 101, 68, 137, 7, 67, 210, 157, 33, 220, 172, 206, 59, 225, 9, 233, 226, 220, 27, 4, 192, 43, 242, 179, 121, 59, 248, 205, 125, 122, 191, 80, 169, 93, 151, 36, 35, 233, 1, 96, 184, 182, 88, 22, 162, 196, 131, 33, 121, 244, 18, 1, 94, 245, 168, 183, 114, 238, 141, 229, 121, 101, 137, 32, 223, 254, 66, 131, 124, 72, 165, 233, 45, 47, 201, 143, 235, 124, 54, 217, 69, 210, 40, 241, 96, 68, 166, 156, 96, 177, 100, 130, 39, 203, 171, 223, 87, 104, 65, 180, 77, 108, 251, 222, 162, 156, 243, 193, 250, 116, 93, 115, 126, 89, 41, 67, 30, 64, 78, 181, 123, 229, 50, 96, 121, 125, 245, 106, 69, 137, 28, 234, 255, 162, 76, 99, 95, 250, 171, 164, 234, 147, 2, 0, 19, 177, 228, 53, 145, 73, 6, 224, 101, 64, 176, 3, 150, 125, 207, 43, 230, 139, 33, 37, 176, 173, 232, 96, 196, 56, 99, 15, 158, 241, 228, 60, 65, 228, 199, 254, 227, 195, 215, 184, 82, 162, 49, 209, 38, 162, 32, 66, 216, 16, 10, 165, 115, 150, 78, 0, 64, 46, 3, 33, 99, 79, 80, 32, 243, 170, 80, 1, 0, 242, 99, 31, 40, 205, 176, 88, 232, 181, 145, 105, 6, 224, 89, 0, 47, 100, 238, 101, 45, 12, 146, 1, 153, 39, 112, 240, 160, 106, 72, 68, 135, 240, 162, 62, 102, 72, 14, 0, 246, 99, 231, 22, 23, 103, 88, 44, 244, 90, 40, 235, 166, 83, 50, 90, 69, 138, 7, 107, 233, 202, 150, 207, 44, 204, 62, 185, 33, 116, 91, 75, 66, 38, 210, 97, 86, 164, 7, 10, 138, 179, 106, 3, 57, 0, 216, 143, 253, 230, 3, 59, 65, 4, 220, 185, 45, 128, 97, 134, 52, 237, 207, 24, 63, 147, 85, 25, 10, 42, 183, 191, 191, 213, 194, 180, 65, 52, 32, 148, 73, 66, 208, 151, 93, 2, 152, 98, 50, 105, 153, 174, 204, 26, 209, 200, 1, 192, 126, 172, 21, 199, 146, 116, 49, 127, 93, 78, 91, 232, 102, 34, 151, 176, 165, 154, 72, 25, 227, 231, 157, 100, 184, 180, 232, 179, 0, 42, 149, 125, 85, 244, 134, 43, 11, 219, 120, 71, 96, 115, 46, 131, 52, 165, 214, 98, 3, 165, 32, 0, 80, 90, 92, 74, 252, 88, 171, 232, 72, 163, 67, 78, 27, 11, 101, 34, 157, 85, 226, 50, 199, 207, 165, 100, 164, 108, 219, 42, 4, 192, 235, 0, 192, 43, 248, 227, 131, 17, 33, 35, 16, 50, 64, 79, 127, 89, 115, 128, 64, 127, 205, 60, 2, 192, 214, 194, 173, 216, 129, 122, 192, 10, 29, 78, 201, 239, 150, 161, 77, 185, 145, 182, 236, 54, 75, 252, 188, 138, 100, 133, 62, 94, 6, 0, 52, 63, 255, 60, 177, 3, 72, 246, 249, 124, 64, 200, 0, 61, 155, 17, 203, 10, 161, 145, 102, 127, 5, 166, 13, 109, 69, 206, 63, 19, 125, 29, 114, 126, 90, 210, 157, 154, 153, 45, 126, 94, 69, 6, 140, 63, 71, 181, 194, 175, 255, 253, 243, 207, 253, 143, 182, 237, 88, 249, 9, 99, 2, 6, 232, 217, 178, 233, 61, 205, 254, 10, 29, 82, 122, 129, 18, 15, 215, 151, 156, 106, 249, 199, 148, 45, 126, 126, 129, 232, 1, 0, 32, 100, 253, 230, 125, 223, 188, 175, 112, 47, 30, 28, 108, 70, 142, 0, 99, 136, 94, 105, 54, 165, 165, 218, 95, 193, 237, 38, 128, 254, 1, 125, 98, 151, 145, 40, 81, 153, 226, 103, 224, 209, 90, 188, 96, 153, 5, 60, 223, 123, 161, 253, 184, 86, 54, 20, 226, 213, 255, 228, 6, 105, 80, 180, 172, 20, 61, 183, 149, 38, 28, 48, 217, 253, 163, 178, 147, 177, 253, 167, 196, 131, 14, 225, 33, 14, 180, 58, 11, 0, 240, 86, 209, 235, 247, 61, 143, 170, 165, 101, 37, 49, 212, 164, 6, 105, 74, 173, 74, 209, 195, 46, 99, 232, 15, 200, 1, 59, 41, 104, 191, 81, 120, 65, 137, 7, 29, 34, 67, 28, 171, 182, 33, 0, 58, 75, 66, 115, 81, 157, 20, 174, 143, 48, 115, 177, 49, 65, 139, 101, 204, 195, 111, 8, 229, 109, 235, 184, 166, 106, 203, 12, 180, 249, 217, 165, 78, 195, 60, 61, 37, 30, 180, 196, 47, 54, 211, 118, 208, 6, 0, 20, 118, 70, 150, 253, 61, 174, 23, 55, 119, 113, 38, 170, 180, 74, 204, 67, 91, 105, 157, 142, 247, 50, 62, 115, 165, 81, 102, 130, 199, 103, 170, 190, 107, 207, 184, 23, 129, 17, 145, 33, 142, 205, 145, 182, 77, 150, 208, 189, 224, 252, 239, 251, 251, 87, 65, 6, 66, 146, 164, 82, 226, 65, 73, 181, 217, 226, 160, 90, 197, 165, 106, 0, 124, 29, 109, 145, 80, 134, 72, 83, 241, 140, 217, 7, 34, 161, 31, 159, 181, 47, 205, 184, 23, 129, 1, 97, 30, 245, 118, 118, 108, 166, 45, 86, 84, 163, 17, 34, 181, 130, 38, 200, 109, 205, 118, 6, 37, 30, 20, 0, 248, 80, 69, 16, 95, 124, 98, 242, 25, 179, 236, 52, 65, 250, 241, 217, 165, 246, 73, 68, 216, 152, 71, 193, 221, 67, 21, 34, 88, 75, 163, 106, 81, 115, 53, 65, 217, 13, 97, 173, 21, 252, 94, 178, 176, 189, 0, 0, 95, 130, 141, 142, 222, 144, 57, 0, 228, 75, 66, 129, 163, 137, 126, 149, 86, 255, 52, 238, 71, 231, 210, 73, 237, 69, 64, 161, 26, 45, 10, 215, 10, 35, 66, 117, 34, 160, 5, 38, 81, 23, 165, 67, 16, 235, 240, 65, 142, 8, 64, 68, 70, 230, 76, 162, 114, 32, 146, 164, 160, 11, 84, 142, 56, 238, 71, 143, 157, 154, 204, 67, 82, 72, 20, 41, 17, 128, 208, 125, 196, 14, 92, 31, 4, 68, 82, 3, 224, 99, 154, 59, 76, 90, 68, 221, 129, 200, 98, 117, 240, 73, 161, 209, 176, 39, 38, 243, 104, 181, 208, 126, 247, 125, 229, 229, 60, 0, 145, 85, 127, 253, 250, 206, 95, 252, 193, 0, 16, 38, 33, 228, 112, 169, 56, 16, 153, 49, 235, 64, 161, 69, 26, 168, 73, 60, 89, 40, 178, 189, 152, 46, 32, 158, 32, 166, 95, 204, 253, 239, 175, 239, 51, 249, 132, 58, 27, 94, 26, 144, 0, 64, 155, 207, 155, 43, 0, 162, 54, 205, 104, 119, 40, 241, 144, 27, 117, 130, 203, 91, 252, 77, 228, 10, 255, 63, 107, 43, 43, 245, 20, 183, 135, 247, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130]; +final landTileBytes = [ + 137, + 80, + 78, + 71, + 13, + 10, + 26, + 10, + 0, + 0, + 0, + 13, + 73, + 72, + 68, + 82, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 8, + 3, + 0, + 0, + 0, + 107, + 172, + 88, + 84, + 0, + 0, + 3, + 0, + 80, + 76, + 84, + 69, + 51, + 52, + 49, + 72, + 52, + 55, + 60, + 65, + 58, + 76, + 85, + 24, + 157, + 17, + 17, + 83, + 80, + 48, + 162, + 28, + 28, + 73, + 74, + 72, + 82, + 75, + 68, + 78, + 83, + 73, + 92, + 100, + 43, + 83, + 86, + 71, + 86, + 87, + 84, + 169, + 46, + 46, + 108, + 79, + 76, + 124, + 90, + 55, + 104, + 112, + 57, + 89, + 109, + 86, + 129, + 96, + 61, + 102, + 108, + 85, + 113, + 94, + 97, + 131, + 89, + 89, + 134, + 105, + 70, + 104, + 105, + 102, + 94, + 131, + 93, + 104, + 115, + 103, + 121, + 128, + 77, + 139, + 112, + 78, + 141, + 89, + 99, + 141, + 101, + 88, + 113, + 117, + 107, + 182, + 78, + 78, + 142, + 116, + 84, + 117, + 117, + 116, + 149, + 107, + 95, + 146, + 121, + 90, + 151, + 150, + 59, + 117, + 141, + 112, + 147, + 111, + 113, + 128, + 134, + 111, + 138, + 144, + 96, + 148, + 150, + 83, + 130, + 132, + 125, + 168, + 127, + 94, + 154, + 135, + 104, + 177, + 114, + 110, + 129, + 155, + 119, + 177, + 153, + 75, + 136, + 137, + 133, + 141, + 168, + 98, + 147, + 153, + 107, + 150, + 171, + 87, + 156, + 139, + 114, + 211, + 118, + 88, + 153, + 166, + 102, + 157, + 149, + 115, + 165, + 165, + 91, + 135, + 165, + 124, + 125, + 185, + 115, + 202, + 111, + 113, + 152, + 137, + 138, + 141, + 150, + 137, + 134, + 167, + 129, + 218, + 89, + 126, + 224, + 83, + 126, + 206, + 146, + 84, + 142, + 189, + 107, + 166, + 171, + 101, + 172, + 150, + 116, + 150, + 150, + 139, + 153, + 171, + 115, + 156, + 180, + 104, + 132, + 189, + 120, + 153, + 139, + 150, + 221, + 92, + 129, + 182, + 122, + 144, + 224, + 93, + 131, + 161, + 165, + 123, + 210, + 110, + 130, + 168, + 179, + 105, + 140, + 184, + 131, + 149, + 169, + 137, + 180, + 173, + 102, + 206, + 141, + 108, + 206, + 164, + 86, + 152, + 187, + 120, + 155, + 155, + 151, + 142, + 195, + 125, + 147, + 182, + 133, + 149, + 178, + 137, + 142, + 194, + 129, + 168, + 154, + 145, + 149, + 184, + 135, + 182, + 183, + 104, + 157, + 182, + 131, + 183, + 166, + 121, + 172, + 165, + 134, + 168, + 184, + 120, + 151, + 185, + 137, + 154, + 179, + 141, + 157, + 165, + 153, + 147, + 197, + 133, + 157, + 187, + 134, + 228, + 110, + 142, + 148, + 205, + 128, + 154, + 189, + 139, + 150, + 197, + 137, + 155, + 196, + 133, + 155, + 184, + 147, + 172, + 146, + 168, + 181, + 168, + 139, + 184, + 186, + 118, + 228, + 142, + 118, + 170, + 189, + 130, + 204, + 173, + 112, + 165, + 169, + 156, + 157, + 192, + 142, + 153, + 206, + 133, + 208, + 143, + 141, + 186, + 195, + 115, + 155, + 201, + 141, + 156, + 209, + 135, + 158, + 198, + 145, + 168, + 168, + 165, + 161, + 196, + 145, + 182, + 172, + 148, + 169, + 196, + 138, + 182, + 186, + 135, + 168, + 184, + 157, + 163, + 204, + 149, + 167, + 193, + 156, + 172, + 199, + 145, + 182, + 183, + 151, + 163, + 213, + 141, + 171, + 182, + 166, + 177, + 172, + 170, + 195, + 202, + 122, + 188, + 197, + 136, + 231, + 141, + 149, + 172, + 203, + 148, + 203, + 154, + 166, + 167, + 205, + 152, + 171, + 196, + 157, + 204, + 179, + 141, + 184, + 167, + 179, + 195, + 200, + 136, + 171, + 205, + 156, + 180, + 199, + 154, + 198, + 173, + 163, + 228, + 186, + 120, + 185, + 183, + 167, + 172, + 202, + 163, + 173, + 209, + 158, + 232, + 146, + 162, + 175, + 210, + 161, + 201, + 210, + 136, + 181, + 214, + 155, + 197, + 186, + 167, + 212, + 199, + 139, + 182, + 202, + 168, + 218, + 168, + 166, + 178, + 212, + 163, + 198, + 201, + 155, + 205, + 169, + 180, + 187, + 186, + 183, + 182, + 216, + 164, + 213, + 184, + 165, + 182, + 213, + 169, + 187, + 212, + 165, + 196, + 216, + 153, + 198, + 198, + 169, + 210, + 215, + 140, + 186, + 199, + 181, + 214, + 200, + 152, + 184, + 225, + 158, + 194, + 189, + 187, + 186, + 219, + 166, + 187, + 214, + 171, + 209, + 172, + 191, + 188, + 219, + 171, + 248, + 177, + 155, + 190, + 225, + 166, + 189, + 216, + 178, + 197, + 201, + 185, + 215, + 220, + 148, + 199, + 215, + 171, + 214, + 202, + 169, + 215, + 187, + 183, + 191, + 206, + 193, + 241, + 203, + 146, + 198, + 197, + 196, + 235, + 181, + 178, + 215, + 199, + 182, + 216, + 212, + 170, + 226, + 202, + 170, + 198, + 228, + 173, + 199, + 218, + 183, + 221, + 226, + 155, + 225, + 215, + 164, + 224, + 229, + 159, + 210, + 204, + 200, + 205, + 234, + 176, + 214, + 216, + 185, + 248, + 193, + 174, + 204, + 216, + 197, + 232, + 201, + 184, + 236, + 188, + 197, + 245, + 197, + 183, + 204, + 226, + 196, + 210, + 230, + 187, + 229, + 233, + 166, + 213, + 217, + 200, + 252, + 214, + 164, + 233, + 200, + 199, + 228, + 218, + 187, + 215, + 228, + 201, + 244, + 200, + 202, + 231, + 218, + 198, + 232, + 231, + 184, + 217, + 217, + 215, + 245, + 220, + 185, + 217, + 240, + 195, + 237, + 241, + 178, + 244, + 202, + 210, + 249, + 230, + 178, + 226, + 221, + 219, + 229, + 233, + 204, + 228, + 227, + 212, + 220, + 234, + 214, + 252, + 215, + 204, + 228, + 226, + 220, + 229, + 234, + 211, + 232, + 229, + 213, + 245, + 229, + 201, + 244, + 216, + 219, + 227, + 236, + 219, + 235, + 238, + 211, + 237, + 228, + 219, + 247, + 250, + 191, + 234, + 234, + 222, + 247, + 249, + 194, + 238, + 240, + 213, + 245, + 233, + 214, + 232, + 231, + 230, + 248, + 219, + 226, + 241, + 242, + 214, + 234, + 243, + 229, + 243, + 238, + 233, + 243, + 243, + 242, + 246, + 247, + 235, + 251, + 238, + 241, + 244, + 249, + 241, + 251, + 243, + 244, + 251, + 251, + 245, + 253, + 246, + 248, + 254, + 254, + 254, + 65, + 248, + 164, + 27, + 0, + 0, + 69, + 188, + 73, + 68, + 65, + 84, + 120, + 156, + 205, + 125, + 15, + 96, + 84, + 213, + 153, + 239, + 172, + 221, + 87, + 197, + 133, + 85, + 233, + 43, + 20, + 125, + 75, + 228, + 165, + 246, + 45, + 44, + 42, + 187, + 90, + 153, + 74, + 89, + 183, + 27, + 144, + 170, + 200, + 172, + 65, + 134, + 171, + 109, + 22, + 162, + 99, + 145, + 170, + 132, + 56, + 188, + 68, + 95, + 32, + 140, + 153, + 104, + 128, + 148, + 65, + 111, + 90, + 184, + 25, + 192, + 100, + 220, + 164, + 3, + 17, + 155, + 228, + 133, + 204, + 52, + 74, + 131, + 118, + 174, + 175, + 51, + 105, + 74, + 39, + 206, + 194, + 213, + 55, + 187, + 242, + 38, + 25, + 34, + 75, + 130, + 60, + 54, + 144, + 73, + 128, + 48, + 239, + 59, + 231, + 220, + 255, + 127, + 102, + 238, + 4, + 218, + 183, + 159, + 114, + 51, + 127, + 238, + 189, + 115, + 207, + 239, + 124, + 255, + 207, + 119, + 206, + 177, + 48, + 255, + 81, + 201, + 251, + 222, + 219, + 231, + 3, + 44, + 165, + 250, + 180, + 189, + 203, + 221, + 37, + 80, + 40, + 210, + 204, + 48, + 161, + 72, + 22, + 234, + 234, + 98, + 51, + 125, + 109, + 249, + 255, + 209, + 54, + 83, + 228, + 27, + 126, + 121, + 248, + 124, + 31, + 165, + 250, + 244, + 0, + 0, + 80, + 39, + 32, + 16, + 241, + 121, + 59, + 100, + 109, + 209, + 197, + 130, + 237, + 202, + 12, + 209, + 127, + 92, + 0, + 152, + 254, + 143, + 62, + 58, + 207, + 246, + 177, + 170, + 79, + 235, + 220, + 117, + 117, + 18, + 15, + 48, + 2, + 0, + 29, + 29, + 161, + 102, + 166, + 57, + 11, + 51, + 132, + 116, + 152, + 225, + 15, + 11, + 192, + 214, + 173, + 215, + 112, + 113, + 223, + 240, + 203, + 231, + 207, + 159, + 239, + 87, + 125, + 10, + 0, + 144, + 198, + 151, + 188, + 3, + 44, + 32, + 244, + 110, + 136, + 124, + 153, + 165, + 249, + 136, + 105, + 254, + 184, + 0, + 60, + 249, + 164, + 222, + 167, + 59, + 169, + 157, + 12, + 189, + 158, + 218, + 68, + 103, + 185, + 186, + 239, + 237, + 207, + 0, + 1, + 37, + 11, + 248, + 170, + 221, + 64, + 34, + 0, + 2, + 117, + 162, + 239, + 218, + 12, + 153, + 29, + 244, + 64, + 40, + 210, + 245, + 199, + 7, + 160, + 228, + 123, + 223, + 211, + 99, + 129, + 205, + 235, + 55, + 51, + 155, + 54, + 209, + 155, + 54, + 101, + 187, + 190, + 107, + 23, + 0, + 192, + 41, + 63, + 34, + 4, + 175, + 182, + 111, + 197, + 210, + 221, + 73, + 90, + 29, + 242, + 42, + 212, + 129, + 170, + 249, + 2, + 233, + 168, + 195, + 220, + 1, + 48, + 219, + 127, + 12, + 67, + 127, + 15, + 72, + 231, + 180, + 103, + 118, + 62, + 195, + 80, + 52, + 67, + 83, + 89, + 239, + 128, + 100, + 192, + 175, + 248, + 4, + 183, + 227, + 0, + 126, + 89, + 72, + 90, + 192, + 243, + 125, + 200, + 64, + 5, + 242, + 172, + 207, + 155, + 141, + 235, + 1, + 128, + 233, + 254, + 99, + 94, + 70, + 0, + 172, + 209, + 124, + 188, + 115, + 61, + 179, + 126, + 39, + 2, + 224, + 135, + 89, + 239, + 240, + 214, + 137, + 243, + 195, + 202, + 79, + 14, + 64, + 227, + 125, + 228, + 101, + 17, + 105, + 65, + 155, + 172, + 53, + 29, + 62, + 21, + 27, + 32, + 166, + 103, + 69, + 0, + 16, + 78, + 254, + 16, + 2, + 171, + 243, + 26, + 0, + 48, + 221, + 127, + 117, + 223, + 195, + 84, + 167, + 254, + 124, + 243, + 102, + 248, + 223, + 28, + 132, + 125, + 39, + 52, + 74, + 80, + 162, + 183, + 72, + 135, + 50, + 62, + 190, + 41, + 240, + 182, + 67, + 173, + 6, + 112, + 163, + 101, + 237, + 239, + 96, + 188, + 33, + 172, + 50, + 67, + 147, + 6, + 192, + 124, + 255, + 189, + 76, + 0, + 120, + 89, + 253, + 249, + 51, + 59, + 25, + 192, + 112, + 61, + 181, + 62, + 187, + 16, + 185, + 183, + 114, + 237, + 134, + 95, + 242, + 109, + 16, + 94, + 132, + 186, + 180, + 82, + 64, + 90, + 205, + 226, + 63, + 232, + 75, + 220, + 114, + 130, + 192, + 164, + 1, + 200, + 161, + 255, + 12, + 136, + 66, + 4, + 127, + 76, + 156, + 234, + 182, + 101, + 254, + 190, + 77, + 232, + 205, + 16, + 81, + 242, + 42, + 4, + 26, + 186, + 176, + 18, + 136, + 72, + 186, + 31, + 97, + 212, + 6, + 151, + 25, + 0, + 96, + 70, + 191, + 229, + 208, + 127, + 134, + 68, + 137, + 135, + 108, + 100, + 205, + 248, + 109, + 41, + 121, + 134, + 230, + 136, + 92, + 202, + 229, + 12, + 32, + 189, + 148, + 65, + 131, + 125, + 6, + 125, + 17, + 48, + 163, + 223, + 114, + 232, + 63, + 227, + 123, + 136, + 135, + 108, + 100, + 205, + 248, + 109, + 41, + 207, + 32, + 146, + 157, + 211, + 218, + 121, + 177, + 209, + 242, + 246, + 203, + 92, + 70, + 5, + 0, + 230, + 244, + 155, + 249, + 199, + 191, + 246, + 59, + 88, + 51, + 243, + 89, + 129, + 213, + 90, + 80, + 169, + 0, + 64, + 31, + 1, + 212, + 104, + 252, + 167, + 185, + 141, + 92, + 168, + 15, + 128, + 73, + 253, + 102, + 254, + 241, + 175, + 157, + 10, + 178, + 10, + 90, + 45, + 252, + 123, + 167, + 48, + 147, + 169, + 15, + 225, + 70, + 135, + 34, + 157, + 240, + 183, + 51, + 4, + 212, + 44, + 247, + 25, + 45, + 178, + 95, + 48, + 169, + 223, + 40, + 241, + 240, + 135, + 39, + 171, + 153, + 147, + 192, + 45, + 222, + 110, + 196, + 0, + 157, + 200, + 76, + 32, + 10, + 33, + 46, + 208, + 129, + 199, + 82, + 108, + 173, + 21, + 110, + 116, + 61, + 244, + 219, + 117, + 38, + 107, + 246, + 83, + 218, + 161, + 217, + 219, + 183, + 122, + 15, + 160, + 238, + 15, + 225, + 22, + 203, + 136, + 102, + 58, + 59, + 165, + 83, + 51, + 122, + 130, + 182, + 210, + 235, + 162, + 223, + 152, + 118, + 150, + 5, + 211, + 237, + 13, + 4, + 192, + 99, + 99, + 217, + 128, + 247, + 90, + 238, + 85, + 106, + 45, + 206, + 126, + 18, + 106, + 121, + 73, + 29, + 227, + 37, + 221, + 175, + 108, + 101, + 243, + 51, + 242, + 51, + 59, + 117, + 218, + 47, + 1, + 64, + 151, + 50, + 215, + 131, + 187, + 163, + 189, + 107, + 31, + 239, + 101, + 253, + 201, + 234, + 96, + 192, + 159, + 220, + 178, + 176, + 58, + 57, + 105, + 4, + 104, + 155, + 181, + 88, + 135, + 21, + 137, + 153, + 70, + 6, + 91, + 32, + 64, + 160, + 208, + 203, + 248, + 48, + 7, + 224, + 238, + 150, + 26, + 186, + 158, + 146, + 95, + 170, + 27, + 45, + 40, + 29, + 33, + 138, + 161, + 233, + 201, + 3, + 0, + 79, + 229, + 227, + 166, + 239, + 222, + 61, + 125, + 40, + 57, + 127, + 250, + 150, + 64, + 244, + 254, + 37, + 193, + 252, + 106, + 150, + 241, + 70, + 147, + 28, + 23, + 141, + 114, + 81, + 127, + 246, + 91, + 200, + 168, + 184, + 82, + 247, + 99, + 162, + 163, + 144, + 193, + 22, + 9, + 68, + 192, + 139, + 25, + 1, + 71, + 137, + 216, + 61, + 2, + 213, + 215, + 129, + 253, + 35, + 249, + 165, + 122, + 237, + 215, + 0, + 224, + 182, + 150, + 82, + 57, + 61, + 167, + 140, + 224, + 169, + 252, + 189, + 51, + 146, + 220, + 244, + 36, + 87, + 189, + 4, + 0, + 200, + 111, + 73, + 174, + 93, + 203, + 50, + 129, + 150, + 45, + 243, + 243, + 91, + 214, + 222, + 185, + 112, + 18, + 220, + 80, + 170, + 249, + 132, + 152, + 105, + 100, + 176, + 5, + 106, + 199, + 1, + 162, + 20, + 38, + 162, + 216, + 32, + 180, + 30, + 196, + 48, + 18, + 17, + 52, + 32, + 33, + 125, + 0, + 100, + 249, + 6, + 110, + 120, + 184, + 159, + 251, + 66, + 157, + 131, + 50, + 79, + 232, + 169, + 184, + 181, + 119, + 222, + 185, + 133, + 11, + 36, + 1, + 128, + 0, + 59, + 99, + 126, + 126, + 210, + 207, + 4, + 170, + 111, + 225, + 122, + 111, + 169, + 78, + 230, + 239, + 15, + 152, + 189, + 147, + 104, + 253, + 172, + 154, + 175, + 176, + 153, + 198, + 6, + 155, + 188, + 63, + 32, + 185, + 0, + 40, + 108, + 240, + 117, + 18, + 86, + 223, + 236, + 37, + 127, + 101, + 87, + 234, + 39, + 204, + 44, + 77, + 193, + 6, + 254, + 215, + 56, + 8, + 190, + 207, + 15, + 247, + 247, + 225, + 12, + 68, + 45, + 147, + 51, + 161, + 167, + 98, + 98, + 249, + 107, + 231, + 207, + 79, + 122, + 89, + 196, + 1, + 213, + 249, + 107, + 103, + 176, + 1, + 0, + 96, + 126, + 52, + 121, + 75, + 18, + 125, + 100, + 124, + 177, + 66, + 180, + 109, + 98, + 199, + 23, + 184, + 213, + 39, + 98, + 17, + 192, + 6, + 91, + 209, + 126, + 146, + 34, + 32, + 226, + 143, + 101, + 61, + 68, + 108, + 189, + 236, + 74, + 221, + 246, + 71, + 44, + 233, + 177, + 193, + 112, + 119, + 211, + 27, + 78, + 87, + 67, + 79, + 63, + 70, + 0, + 254, + 231, + 250, + 251, + 75, + 144, + 4, + 114, + 95, + 244, + 247, + 153, + 7, + 0, + 61, + 149, + 127, + 203, + 66, + 142, + 203, + 111, + 105, + 71, + 173, + 77, + 78, + 79, + 70, + 91, + 242, + 163, + 0, + 192, + 253, + 4, + 128, + 13, + 122, + 0, + 180, + 17, + 63, + 69, + 71, + 180, + 17, + 213, + 22, + 104, + 46, + 64, + 102, + 154, + 24, + 108, + 68, + 72, + 247, + 117, + 109, + 23, + 66, + 238, + 16, + 31, + 0, + 71, + 36, + 231, + 223, + 199, + 72, + 223, + 105, + 41, + 196, + 88, + 210, + 60, + 141, + 141, + 196, + 187, + 91, + 155, + 90, + 99, + 95, + 16, + 20, + 254, + 237, + 215, + 61, + 93, + 125, + 232, + 149, + 121, + 0, + 208, + 83, + 121, + 246, + 223, + 25, + 139, + 205, + 232, + 37, + 0, + 204, + 8, + 38, + 55, + 220, + 159, + 17, + 128, + 102, + 146, + 209, + 234, + 208, + 136, + 182, + 91, + 96, + 95, + 29, + 95, + 152, + 18, + 3, + 18, + 76, + 7, + 186, + 14, + 128, + 190, + 236, + 192, + 74, + 47, + 34, + 37, + 62, + 24, + 62, + 81, + 210, + 145, + 9, + 0, + 144, + 23, + 17, + 0, + 158, + 70, + 194, + 173, + 77, + 65, + 238, + 95, + 48, + 253, + 27, + 206, + 200, + 177, + 156, + 73, + 237, + 141, + 30, + 234, + 135, + 220, + 134, + 59, + 243, + 119, + 195, + 69, + 213, + 45, + 1, + 182, + 119, + 126, + 254, + 66, + 136, + 231, + 3, + 193, + 45, + 209, + 228, + 253, + 73, + 244, + 145, + 230, + 26, + 145, + 83, + 149, + 162, + 29, + 217, + 94, + 216, + 73, + 60, + 248, + 74, + 45, + 11, + 80, + 226, + 65, + 160, + 210, + 82, + 194, + 223, + 66, + 242, + 11, + 249, + 67, + 56, + 72, + 6, + 125, + 216, + 44, + 156, + 228, + 211, + 34, + 128, + 140, + 166, + 165, + 169, + 169, + 181, + 59, + 62, + 56, + 38, + 7, + 97, + 44, + 222, + 221, + 180, + 255, + 67, + 64, + 96, + 24, + 84, + 2, + 128, + 96, + 90, + 12, + 224, + 169, + 188, + 44, + 27, + 101, + 193, + 42, + 71, + 163, + 94, + 38, + 64, + 76, + 159, + 55, + 26, + 245, + 7, + 162, + 1, + 252, + 145, + 154, + 80, + 122, + 2, + 39, + 40, + 68, + 209, + 166, + 193, + 105, + 7, + 117, + 21, + 42, + 122, + 139, + 176, + 111, + 177, + 198, + 23, + 210, + 2, + 80, + 89, + 204, + 200, + 219, + 143, + 88, + 0, + 161, + 8, + 183, + 13, + 69, + 100, + 134, + 48, + 18, + 217, + 91, + 94, + 4, + 196, + 39, + 80, + 246, + 21, + 35, + 85, + 99, + 65, + 28, + 223, + 31, + 11, + 54, + 53, + 53, + 117, + 135, + 19, + 50, + 28, + 6, + 187, + 155, + 130, + 88, + 41, + 156, + 83, + 229, + 101, + 51, + 3, + 144, + 163, + 31, + 229, + 139, + 132, + 182, + 17, + 65, + 229, + 69, + 27, + 245, + 19, + 60, + 35, + 124, + 246, + 86, + 97, + 209, + 65, + 116, + 74, + 129, + 218, + 31, + 208, + 254, + 74, + 109, + 1, + 86, + 126, + 56, + 249, + 215, + 215, + 39, + 1, + 0, + 93, + 28, + 145, + 123, + 194, + 161, + 131, + 251, + 246, + 49, + 109, + 40, + 32, + 58, + 88, + 96, + 45, + 176, + 149, + 214, + 10, + 0, + 240, + 244, + 5, + 224, + 208, + 26, + 30, + 145, + 56, + 33, + 188, + 27, + 84, + 226, + 224, + 224, + 192, + 31, + 14, + 0, + 232, + 161, + 78, + 94, + 99, + 53, + 135, + 58, + 55, + 131, + 96, + 163, + 134, + 176, + 7, + 139, + 10, + 139, + 138, + 238, + 141, + 188, + 5, + 158, + 76, + 136, + 214, + 250, + 2, + 26, + 178, + 34, + 65, + 98, + 187, + 160, + 237, + 125, + 8, + 6, + 22, + 0, + 232, + 68, + 247, + 109, + 150, + 78, + 169, + 180, + 226, + 63, + 26, + 71, + 82, + 14, + 0, + 143, + 66, + 107, + 83, + 119, + 156, + 103, + 133, + 177, + 166, + 216, + 96, + 34, + 145, + 200, + 165, + 73, + 57, + 145, + 32, + 140, + 44, + 159, + 176, + 15, + 81, + 50, + 99, + 181, + 119, + 27, + 249, + 107, + 226, + 62, + 52, + 186, + 21, + 106, + 124, + 159, + 144, + 7, + 12, + 225, + 192, + 72, + 58, + 195, + 205, + 107, + 83, + 141, + 181, + 209, + 0, + 128, + 137, + 3, + 86, + 136, + 99, + 8, + 226, + 77, + 241, + 68, + 162, + 183, + 183, + 247, + 122, + 55, + 157, + 144, + 198, + 42, + 237, + 227, + 95, + 224, + 70, + 188, + 69, + 94, + 155, + 138, + 77, + 155, + 241, + 37, + 125, + 17, + 1, + 0, + 232, + 124, + 159, + 252, + 251, + 2, + 222, + 177, + 209, + 56, + 146, + 250, + 0, + 96, + 16, + 118, + 199, + 137, + 28, + 116, + 55, + 133, + 19, + 137, + 201, + 251, + 135, + 153, + 40, + 20, + 209, + 27, + 211, + 69, + 236, + 12, + 13, + 225, + 185, + 162, + 211, + 106, + 230, + 78, + 116, + 9, + 18, + 127, + 116, + 241, + 129, + 3, + 237, + 10, + 213, + 71, + 168, + 152, + 23, + 36, + 181, + 35, + 153, + 1, + 0, + 208, + 141, + 77, + 9, + 34, + 9, + 97, + 64, + 32, + 102, + 156, + 159, + 158, + 52, + 225, + 182, + 31, + 44, + 58, + 168, + 104, + 126, + 51, + 207, + 24, + 88, + 44, + 74, + 66, + 33, + 171, + 57, + 167, + 180, + 212, + 250, + 14, + 200, + 128, + 156, + 235, + 49, + 85, + 90, + 201, + 245, + 2, + 0, + 42, + 71, + 18, + 0, + 248, + 232, + 68, + 191, + 49, + 4, + 177, + 38, + 162, + 12, + 154, + 64, + 17, + 196, + 175, + 83, + 171, + 101, + 132, + 1, + 104, + 59, + 136, + 212, + 85, + 73, + 209, + 54, + 192, + 1, + 84, + 116, + 179, + 194, + 99, + 57, + 88, + 104, + 178, + 253, + 64, + 37, + 96, + 54, + 66, + 29, + 170, + 15, + 75, + 233, + 90, + 171, + 27, + 132, + 136, + 22, + 4, + 73, + 233, + 72, + 34, + 0, + 222, + 219, + 245, + 182, + 49, + 2, + 195, + 193, + 166, + 110, + 48, + 11, + 35, + 131, + 35, + 32, + 5, + 62, + 36, + 85, + 62, + 63, + 24, + 116, + 54, + 10, + 1, + 174, + 233, + 208, + 198, + 144, + 58, + 121, + 111, + 132, + 112, + 194, + 182, + 194, + 18, + 162, + 181, + 197, + 140, + 93, + 91, + 199, + 190, + 44, + 89, + 81, + 37, + 161, + 203, + 107, + 53, + 81, + 180, + 91, + 25, + 80, + 80, + 74, + 71, + 82, + 16, + 129, + 247, + 79, + 160, + 227, + 197, + 241, + 139, + 106, + 8, + 6, + 19, + 77, + 233, + 244, + 68, + 247, + 216, + 96, + 34, + 57, + 62, + 26, + 101, + 216, + 36, + 208, + 145, + 36, + 215, + 59, + 122, + 36, + 153, + 91, + 124, + 175, + 247, + 192, + 168, + 149, + 168, + 207, + 218, + 58, + 145, + 75, + 28, + 58, + 88, + 200, + 251, + 125, + 68, + 248, + 153, + 230, + 182, + 156, + 218, + 143, + 137, + 182, + 89, + 69, + 187, + 169, + 137, + 164, + 16, + 81, + 226, + 1, + 147, + 229, + 226, + 197, + 243, + 23, + 225, + 95, + 255, + 123, + 39, + 224, + 207, + 120, + 10, + 35, + 112, + 17, + 17, + 249, + 11, + 0, + 0, + 11, + 196, + 111, + 28, + 27, + 25, + 28, + 111, + 170, + 72, + 122, + 147, + 111, + 44, + 29, + 29, + 154, + 51, + 111, + 74, + 172, + 247, + 198, + 100, + 192, + 239, + 103, + 188, + 237, + 237, + 192, + 23, + 126, + 95, + 123, + 187, + 215, + 215, + 158, + 43, + 34, + 161, + 136, + 74, + 91, + 249, + 112, + 123, + 139, + 109, + 194, + 128, + 103, + 200, + 154, + 227, + 29, + 17, + 209, + 197, + 0, + 65, + 177, + 21, + 72, + 215, + 131, + 160, + 196, + 3, + 38, + 203, + 248, + 229, + 203, + 227, + 64, + 163, + 240, + 239, + 242, + 68, + 236, + 107, + 227, + 227, + 169, + 243, + 227, + 132, + 46, + 226, + 227, + 185, + 145, + 49, + 0, + 96, + 10, + 2, + 192, + 62, + 111, + 212, + 155, + 180, + 207, + 74, + 14, + 37, + 23, + 28, + 26, + 5, + 0, + 4, + 138, + 114, + 226, + 203, + 220, + 52, + 101, + 155, + 86, + 91, + 99, + 170, + 180, + 150, + 16, + 41, + 216, + 167, + 141, + 4, + 76, + 81, + 113, + 109, + 169, + 33, + 235, + 80, + 226, + 1, + 147, + 101, + 106, + 124, + 100, + 206, + 212, + 121, + 169, + 137, + 214, + 169, + 83, + 231, + 164, + 166, + 90, + 166, + 110, + 249, + 172, + 127, + 188, + 162, + 102, + 230, + 84, + 251, + 248, + 229, + 209, + 5, + 83, + 103, + 198, + 198, + 198, + 154, + 102, + 206, + 116, + 76, + 77, + 97, + 0, + 146, + 8, + 128, + 209, + 161, + 197, + 83, + 166, + 54, + 133, + 111, + 76, + 114, + 179, + 122, + 47, + 191, + 49, + 117, + 202, + 210, + 161, + 209, + 197, + 111, + 220, + 54, + 245, + 13, + 199, + 212, + 219, + 142, + 36, + 115, + 123, + 208, + 78, + 159, + 209, + 55, + 5, + 133, + 160, + 19, + 67, + 238, + 220, + 243, + 211, + 149, + 54, + 51, + 153, + 84, + 137, + 44, + 137, + 137, + 169, + 21, + 227, + 11, + 230, + 77, + 88, + 6, + 199, + 195, + 19, + 77, + 51, + 71, + 199, + 83, + 195, + 227, + 11, + 110, + 236, + 233, + 185, + 113, + 247, + 196, + 236, + 5, + 163, + 77, + 55, + 164, + 6, + 45, + 241, + 145, + 153, + 83, + 199, + 70, + 18, + 99, + 0, + 192, + 102, + 4, + 128, + 125, + 74, + 111, + 111, + 18, + 56, + 96, + 170, + 99, + 60, + 120, + 195, + 145, + 222, + 169, + 246, + 203, + 51, + 167, + 198, + 14, + 89, + 230, + 113, + 139, + 103, + 94, + 187, + 94, + 16, + 136, + 182, + 21, + 148, + 228, + 156, + 149, + 1, + 5, + 96, + 211, + 79, + 37, + 26, + 146, + 229, + 242, + 136, + 165, + 166, + 102, + 233, + 141, + 19, + 51, + 231, + 196, + 198, + 1, + 128, + 241, + 241, + 243, + 231, + 199, + 23, + 44, + 24, + 31, + 119, + 206, + 185, + 106, + 73, + 141, + 95, + 158, + 218, + 84, + 51, + 123, + 34, + 213, + 52, + 21, + 148, + 32, + 0, + 48, + 190, + 153, + 179, + 207, + 26, + 155, + 234, + 76, + 70, + 135, + 130, + 83, + 110, + 91, + 156, + 28, + 157, + 183, + 32, + 153, + 124, + 99, + 234, + 229, + 153, + 246, + 100, + 210, + 210, + 155, + 60, + 2, + 122, + 33, + 215, + 103, + 206, + 64, + 149, + 57, + 117, + 37, + 34, + 58, + 199, + 214, + 51, + 72, + 7, + 196, + 45, + 118, + 187, + 189, + 98, + 226, + 114, + 197, + 148, + 217, + 8, + 128, + 139, + 8, + 128, + 165, + 227, + 227, + 53, + 51, + 199, + 44, + 169, + 212, + 216, + 204, + 154, + 138, + 121, + 169, + 241, + 238, + 169, + 19, + 9, + 12, + 128, + 47, + 186, + 116, + 246, + 229, + 41, + 111, + 112, + 126, + 238, + 144, + 101, + 234, + 204, + 161, + 209, + 217, + 75, + 147, + 209, + 67, + 83, + 46, + 207, + 116, + 2, + 0, + 156, + 22, + 0, + 109, + 149, + 152, + 54, + 171, + 109, + 76, + 181, + 86, + 116, + 212, + 198, + 195, + 122, + 167, + 150, + 22, + 23, + 152, + 8, + 154, + 116, + 200, + 50, + 62, + 110, + 73, + 140, + 143, + 79, + 76, + 128, + 42, + 188, + 49, + 222, + 10, + 28, + 48, + 14, + 0, + 192, + 159, + 121, + 11, + 38, + 110, + 104, + 253, + 108, + 228, + 134, + 120, + 247, + 148, + 241, + 241, + 138, + 169, + 99, + 111, + 15, + 142, + 119, + 223, + 56, + 62, + 196, + 205, + 92, + 124, + 121, + 22, + 116, + 59, + 82, + 130, + 192, + 251, + 75, + 103, + 37, + 147, + 139, + 103, + 27, + 2, + 160, + 173, + 18, + 51, + 72, + 125, + 233, + 19, + 180, + 189, + 22, + 73, + 116, + 150, + 42, + 1, + 56, + 169, + 160, + 184, + 116, + 18, + 89, + 76, + 68, + 150, + 209, + 241, + 138, + 27, + 230, + 205, + 89, + 60, + 49, + 117, + 193, + 188, + 41, + 227, + 35, + 55, + 204, + 11, + 34, + 0, + 166, + 204, + 158, + 115, + 195, + 224, + 68, + 211, + 141, + 243, + 166, + 46, + 72, + 143, + 205, + 156, + 57, + 111, + 230, + 212, + 212, + 137, + 183, + 71, + 199, + 231, + 220, + 56, + 107, + 202, + 84, + 110, + 232, + 200, + 13, + 179, + 102, + 85, + 128, + 18, + 236, + 189, + 225, + 200, + 232, + 212, + 153, + 179, + 111, + 236, + 29, + 55, + 0, + 64, + 167, + 74, + 76, + 155, + 213, + 206, + 68, + 5, + 196, + 145, + 213, + 166, + 70, + 175, + 31, + 89, + 46, + 142, + 142, + 159, + 107, + 13, + 143, + 79, + 12, + 182, + 118, + 131, + 69, + 60, + 215, + 122, + 14, + 1, + 96, + 79, + 4, + 83, + 99, + 35, + 169, + 17, + 112, + 1, + 70, + 82, + 151, + 195, + 221, + 169, + 196, + 216, + 197, + 247, + 222, + 27, + 226, + 130, + 111, + 28, + 226, + 184, + 246, + 100, + 236, + 141, + 221, + 67, + 224, + 8, + 37, + 123, + 123, + 199, + 135, + 14, + 29, + 74, + 14, + 13, + 245, + 130, + 33, + 12, + 98, + 15, + 73, + 174, + 4, + 245, + 170, + 196, + 52, + 193, + 72, + 6, + 18, + 235, + 53, + 42, + 179, + 177, + 192, + 53, + 144, + 5, + 185, + 127, + 96, + 252, + 241, + 1, + 191, + 188, + 136, + 0, + 24, + 3, + 7, + 32, + 145, + 72, + 167, + 6, + 39, + 38, + 226, + 9, + 64, + 34, + 61, + 242, + 197, + 249, + 247, + 134, + 3, + 1, + 46, + 186, + 231, + 135, + 212, + 250, + 6, + 52, + 210, + 19, + 13, + 180, + 195, + 63, + 22, + 94, + 250, + 2, + 209, + 168, + 159, + 141, + 178, + 62, + 86, + 233, + 30, + 235, + 85, + 137, + 105, + 130, + 145, + 12, + 36, + 213, + 107, + 88, + 179, + 156, + 233, + 214, + 40, + 191, + 206, + 72, + 200, + 199, + 168, + 3, + 3, + 61, + 210, + 139, + 6, + 199, + 237, + 21, + 99, + 241, + 88, + 60, + 145, + 30, + 139, + 39, + 194, + 40, + 26, + 2, + 0, + 18, + 195, + 40, + 66, + 166, + 208, + 37, + 148, + 137, + 219, + 34, + 210, + 175, + 18, + 83, + 7, + 35, + 25, + 72, + 170, + 215, + 144, + 123, + 196, + 109, + 40, + 213, + 161, + 106, + 26, + 109, + 85, + 9, + 9, + 142, + 179, + 155, + 15, + 32, + 125, + 187, + 41, + 196, + 71, + 24, + 250, + 169, + 21, + 21, + 0, + 195, + 3, + 177, + 88, + 124, + 100, + 12, + 8, + 103, + 136, + 33, + 8, + 12, + 163, + 236, + 208, + 216, + 96, + 12, + 127, + 75, + 161, + 75, + 168, + 236, + 143, + 142, + 201, + 160, + 74, + 140, + 82, + 6, + 35, + 198, + 36, + 171, + 215, + 40, + 149, + 201, + 0, + 9, + 33, + 85, + 231, + 170, + 198, + 15, + 58, + 248, + 188, + 194, + 230, + 77, + 128, + 215, + 62, + 226, + 109, + 25, + 228, + 150, + 100, + 0, + 160, + 182, + 139, + 233, + 225, + 177, + 68, + 188, + 59, + 28, + 135, + 246, + 67, + 44, + 24, + 15, + 115, + 95, + 144, + 83, + 230, + 210, + 140, + 33, + 0, + 102, + 43, + 72, + 41, + 241, + 160, + 71, + 224, + 202, + 88, + 173, + 164, + 185, + 242, + 122, + 13, + 137, + 5, + 240, + 136, + 159, + 214, + 131, + 182, + 202, + 223, + 240, + 209, + 100, + 200, + 231, + 163, + 124, + 161, + 200, + 62, + 156, + 36, + 208, + 178, + 13, + 33, + 9, + 128, + 47, + 112, + 246, + 99, + 108, + 48, + 30, + 14, + 3, + 223, + 119, + 135, + 19, + 19, + 233, + 120, + 119, + 247, + 88, + 122, + 36, + 54, + 44, + 242, + 71, + 101, + 134, + 232, + 204, + 108, + 5, + 41, + 37, + 30, + 244, + 8, + 141, + 9, + 251, + 136, + 212, + 200, + 235, + 53, + 100, + 67, + 197, + 161, + 236, + 0, + 136, + 169, + 37, + 250, + 135, + 62, + 146, + 115, + 13, + 133, + 12, + 24, + 128, + 177, + 12, + 15, + 15, + 247, + 247, + 115, + 125, + 129, + 190, + 225, + 112, + 28, + 218, + 30, + 71, + 169, + 143, + 65, + 194, + 4, + 152, + 27, + 226, + 3, + 50, + 1, + 97, + 50, + 32, + 96, + 182, + 130, + 148, + 18, + 15, + 70, + 228, + 109, + 14, + 133, + 188, + 110, + 69, + 189, + 134, + 77, + 248, + 89, + 60, + 144, + 20, + 210, + 14, + 48, + 40, + 68, + 64, + 224, + 128, + 72, + 27, + 10, + 43, + 153, + 205, + 252, + 128, + 185, + 238, + 143, + 137, + 195, + 227, + 253, + 56, + 246, + 199, + 20, + 31, + 155, + 136, + 143, + 196, + 195, + 241, + 48, + 202, + 134, + 13, + 159, + 151, + 3, + 192, + 184, + 173, + 6, + 222, + 166, + 32, + 177, + 126, + 20, + 26, + 70, + 51, + 181, + 207, + 152, + 64, + 147, + 241, + 18, + 208, + 220, + 108, + 179, + 233, + 98, + 213, + 28, + 49, + 96, + 101, + 57, + 181, + 225, + 124, + 120, + 243, + 250, + 245, + 235, + 105, + 186, + 13, + 177, + 81, + 7, + 202, + 51, + 234, + 159, + 43, + 2, + 208, + 215, + 47, + 2, + 144, + 136, + 167, + 227, + 152, + 19, + 128, + 255, + 101, + 253, + 127, + 30, + 143, + 17, + 25, + 165, + 233, + 5, + 137, + 221, + 19, + 204, + 223, + 176, + 144, + 31, + 74, + 241, + 27, + 6, + 123, + 186, + 84, + 202, + 119, + 99, + 41, + 86, + 233, + 168, + 102, + 7, + 253, + 165, + 148, + 39, + 241, + 93, + 25, + 242, + 9, + 7, + 61, + 34, + 3, + 174, + 166, + 244, + 173, + 8, + 192, + 230, + 205, + 125, + 220, + 224, + 0, + 199, + 245, + 245, + 69, + 184, + 240, + 68, + 10, + 1, + 16, + 158, + 72, + 135, + 85, + 70, + 130, + 156, + 75, + 235, + 120, + 102, + 162, + 196, + 38, + 91, + 150, + 36, + 73, + 14, + 153, + 141, + 113, + 185, + 32, + 192, + 15, + 93, + 136, + 196, + 151, + 106, + 80, + 224, + 233, + 202, + 126, + 15, + 51, + 245, + 206, + 77, + 40, + 93, + 78, + 135, + 20, + 242, + 168, + 141, + 3, + 41, + 241, + 96, + 76, + 34, + 0, + 168, + 1, + 175, + 17, + 217, + 225, + 226, + 35, + 19, + 97, + 204, + 8, + 131, + 95, + 168, + 92, + 4, + 50, + 76, + 72, + 23, + 104, + 93, + 51, + 65, + 98, + 217, + 96, + 254, + 140, + 249, + 100, + 60, + 53, + 58, + 191, + 37, + 135, + 4, + 137, + 219, + 170, + 254, + 68, + 8, + 111, + 132, + 246, + 99, + 123, + 222, + 12, + 38, + 206, + 183, + 121, + 61, + 176, + 244, + 129, + 144, + 79, + 161, + 112, + 105, + 205, + 13, + 40, + 241, + 96, + 76, + 34, + 0, + 120, + 108, + 151, + 7, + 0, + 154, + 62, + 50, + 152, + 136, + 79, + 140, + 197, + 212, + 62, + 210, + 48, + 63, + 62, + 80, + 172, + 147, + 172, + 37, + 63, + 151, + 188, + 37, + 8, + 94, + 49, + 210, + 4, + 81, + 0, + 0, + 142, + 237, + 160, + 19, + 134, + 134, + 224, + 144, + 121, + 148, + 185, + 214, + 88, + 189, + 242, + 13, + 11, + 97, + 41, + 14, + 161, + 164, + 255, + 51, + 184, + 175, + 66, + 234, + 17, + 66, + 253, + 39, + 202, + 244, + 171, + 50, + 0, + 248, + 51, + 67, + 24, + 128, + 240, + 200, + 4, + 30, + 15, + 208, + 122, + 137, + 2, + 2, + 110, + 171, + 134, + 9, + 200, + 207, + 37, + 111, + 137, + 69, + 3, + 254, + 228, + 252, + 59, + 102, + 180, + 36, + 231, + 231, + 231, + 79, + 223, + 157, + 92, + 187, + 118, + 198, + 218, + 253, + 243, + 17, + 95, + 100, + 122, + 144, + 90, + 195, + 246, + 243, + 61, + 27, + 34, + 238, + 79, + 40, + 226, + 197, + 10, + 23, + 213, + 253, + 11, + 37, + 173, + 228, + 82, + 243, + 9, + 116, + 57, + 169, + 1, + 112, + 183, + 33, + 0, + 18, + 200, + 25, + 8, + 111, + 24, + 214, + 2, + 32, + 77, + 96, + 208, + 252, + 28, + 190, + 158, + 138, + 238, + 190, + 101, + 97, + 140, + 171, + 158, + 159, + 228, + 166, + 143, + 206, + 95, + 146, + 228, + 110, + 25, + 93, + 114, + 71, + 50, + 89, + 13, + 255, + 102, + 196, + 114, + 211, + 137, + 152, + 108, + 162, + 4, + 68, + 136, + 37, + 7, + 39, + 159, + 40, + 92, + 250, + 96, + 225, + 220, + 185, + 214, + 202, + 218, + 82, + 190, + 233, + 98, + 37, + 65, + 101, + 78, + 64, + 88, + 92, + 34, + 242, + 168, + 1, + 205, + 155, + 65, + 198, + 56, + 222, + 24, + 236, + 210, + 105, + 191, + 178, + 100, + 68, + 39, + 255, + 228, + 141, + 38, + 55, + 76, + 231, + 150, + 204, + 200, + 207, + 191, + 101, + 116, + 254, + 238, + 0, + 55, + 125, + 96, + 201, + 90, + 54, + 176, + 101, + 97, + 52, + 122, + 103, + 48, + 163, + 12, + 232, + 231, + 61, + 10, + 220, + 124, + 193, + 16, + 46, + 122, + 11, + 225, + 170, + 239, + 16, + 86, + 184, + 197, + 214, + 31, + 130, + 149, + 43, + 64, + 141, + 47, + 69, + 215, + 86, + 210, + 116, + 37, + 122, + 28, + 91, + 110, + 73, + 49, + 139, + 147, + 97, + 60, + 226, + 59, + 226, + 62, + 32, + 0, + 226, + 177, + 24, + 167, + 11, + 128, + 162, + 92, + 162, + 22, + 204, + 182, + 77, + 105, + 18, + 88, + 54, + 192, + 45, + 172, + 94, + 187, + 22, + 73, + 254, + 252, + 106, + 54, + 121, + 203, + 208, + 146, + 13, + 8, + 0, + 54, + 27, + 0, + 250, + 58, + 160, + 212, + 202, + 119, + 44, + 180, + 189, + 19, + 121, + 116, + 200, + 10, + 212, + 34, + 133, + 101, + 149, + 73, + 55, + 186, + 182, + 214, + 106, + 53, + 40, + 45, + 204, + 72, + 32, + 2, + 180, + 221, + 197, + 191, + 241, + 18, + 37, + 216, + 151, + 24, + 140, + 66, + 67, + 250, + 244, + 1, + 80, + 87, + 13, + 41, + 51, + 119, + 94, + 110, + 73, + 117, + 245, + 244, + 88, + 108, + 250, + 150, + 253, + 91, + 134, + 230, + 223, + 209, + 242, + 120, + 190, + 25, + 0, + 74, + 11, + 138, + 245, + 147, + 248, + 12, + 115, + 47, + 223, + 40, + 48, + 237, + 72, + 237, + 225, + 74, + 160, + 72, + 39, + 133, + 6, + 251, + 40, + 233, + 25, + 172, + 181, + 149, + 242, + 172, + 73, + 14, + 201, + 100, + 164, + 3, + 92, + 194, + 21, + 194, + 148, + 171, + 1, + 232, + 102, + 150, + 59, + 175, + 15, + 192, + 121, + 189, + 129, + 226, + 98, + 107, + 1, + 159, + 137, + 79, + 110, + 89, + 178, + 182, + 55, + 202, + 246, + 174, + 93, + 219, + 146, + 108, + 137, + 173, + 221, + 144, + 140, + 6, + 131, + 129, + 64, + 111, + 11, + 27, + 221, + 173, + 235, + 21, + 16, + 231, + 218, + 56, + 143, + 79, + 23, + 20, + 243, + 6, + 0, + 123, + 245, + 33, + 60, + 4, + 30, + 161, + 144, + 105, + 144, + 0, + 0, + 86, + 84, + 92, + 100, + 37, + 127, + 124, + 38, + 148, + 14, + 175, + 4, + 157, + 78, + 90, + 4, + 224, + 95, + 65, + 247, + 5, + 128, + 151, + 207, + 27, + 140, + 154, + 234, + 143, + 148, + 211, + 197, + 86, + 220, + 20, + 95, + 128, + 101, + 161, + 167, + 253, + 32, + 10, + 190, + 0, + 188, + 246, + 225, + 186, + 233, + 64, + 192, + 235, + 99, + 181, + 78, + 1, + 92, + 100, + 51, + 211, + 91, + 66, + 201, + 139, + 72, + 20, + 18, + 123, + 202, + 240, + 124, + 94, + 13, + 24, + 184, + 255, + 10, + 226, + 1, + 240, + 56, + 237, + 72, + 19, + 52, + 119, + 132, + 250, + 134, + 249, + 70, + 6, + 244, + 155, + 127, + 254, + 11, + 19, + 15, + 108, + 146, + 74, + 141, + 248, + 94, + 77, + 32, + 252, + 111, + 21, + 225, + 130, + 145, + 189, + 219, + 138, + 138, + 138, + 172, + 5, + 214, + 204, + 51, + 41, + 104, + 98, + 18, + 155, + 141, + 34, + 32, + 25, + 137, + 102, + 208, + 37, + 104, + 2, + 110, + 24, + 3, + 224, + 215, + 179, + 129, + 106, + 37, + 168, + 165, + 82, + 177, + 75, + 233, + 220, + 71, + 117, + 12, + 233, + 96, + 81, + 209, + 246, + 142, + 131, + 200, + 16, + 188, + 133, + 142, + 69, + 217, + 206, + 239, + 8, + 21, + 226, + 191, + 128, + 88, + 182, + 250, + 100, + 201, + 15, + 16, + 17, + 64, + 16, + 244, + 51, + 237, + 231, + 13, + 68, + 32, + 75, + 205, + 88, + 32, + 122, + 226, + 125, + 224, + 117, + 182, + 103, + 123, + 193, + 214, + 247, + 63, + 226, + 184, + 235, + 50, + 82, + 130, + 84, + 191, + 172, + 212, + 183, + 48, + 27, + 182, + 145, + 200, + 65, + 220, + 245, + 29, + 57, + 112, + 0, + 136, + 129, + 136, + 0, + 211, + 119, + 190, + 143, + 61, + 111, + 160, + 4, + 51, + 3, + 208, + 30, + 203, + 207, + 207, + 255, + 48, + 192, + 237, + 191, + 51, + 201, + 36, + 239, + 156, + 49, + 227, + 126, + 14, + 12, + 89, + 169, + 27, + 180, + 145, + 87, + 209, + 23, + 149, + 5, + 57, + 57, + 110, + 252, + 20, + 0, + 190, + 152, + 42, + 107, + 213, + 128, + 24, + 254, + 119, + 228, + 192, + 1, + 50, + 227, + 65, + 244, + 128, + 1, + 0, + 153, + 171, + 133, + 162, + 247, + 87, + 39, + 183, + 44, + 76, + 110, + 152, + 127, + 11, + 0, + 48, + 29, + 156, + 1, + 56, + 157, + 229, + 184, + 30, + 136, + 13, + 184, + 46, + 155, + 213, + 134, + 67, + 93, + 52, + 132, + 151, + 163, + 223, + 42, + 47, + 27, + 9, + 133, + 42, + 51, + 15, + 128, + 242, + 229, + 21, + 248, + 101, + 54, + 22, + 80, + 205, + 28, + 117, + 210, + 126, + 182, + 79, + 96, + 253, + 93, + 122, + 50, + 32, + 115, + 3, + 154, + 201, + 132, + 24, + 31, + 142, + 210, + 72, + 145, + 57, + 195, + 174, + 93, + 152, + 92, + 184, + 129, + 139, + 37, + 111, + 73, + 122, + 185, + 25, + 16, + 11, + 65, + 223, + 115, + 119, + 220, + 159, + 63, + 125, + 109, + 254, + 140, + 37, + 81, + 218, + 93, + 140, + 236, + 85, + 229, + 36, + 28, + 22, + 9, + 1, + 220, + 164, + 226, + 76, + 76, + 192, + 231, + 131, + 200, + 67, + 102, + 185, + 175, + 26, + 0, + 215, + 105, + 169, + 173, + 253, + 47, + 235, + 32, + 32, + 1, + 208, + 62, + 124, + 126, + 216, + 139, + 196, + 5, + 7, + 72, + 240, + 6, + 65, + 224, + 79, + 222, + 113, + 75, + 126, + 210, + 239, + 7, + 0, + 124, + 220, + 140, + 59, + 167, + 175, + 133, + 15, + 185, + 25, + 213, + 220, + 134, + 124, + 46, + 118, + 75, + 142, + 131, + 231, + 10, + 34, + 89, + 45, + 169, + 254, + 75, + 27, + 61, + 75, + 132, + 192, + 58, + 88, + 110, + 166, + 192, + 80, + 3, + 0, + 195, + 244, + 15, + 100, + 70, + 64, + 2, + 0, + 151, + 17, + 251, + 241, + 52, + 3, + 150, + 33, + 39, + 14, + 247, + 143, + 174, + 93, + 24, + 155, + 191, + 54, + 202, + 0, + 0, + 136, + 245, + 147, + 211, + 123, + 125, + 0, + 64, + 48, + 80, + 189, + 48, + 144, + 188, + 38, + 0, + 208, + 120, + 0, + 244, + 166, + 84, + 5, + 102, + 204, + 69, + 216, + 161, + 221, + 251, + 86, + 54, + 0, + 72, + 102, + 77, + 3, + 64, + 207, + 155, + 221, + 138, + 52, + 224, + 176, + 218, + 28, + 138, + 103, + 178, + 253, + 253, + 220, + 48, + 231, + 133, + 174, + 239, + 243, + 242, + 90, + 227, + 124, + 63, + 154, + 36, + 192, + 205, + 224, + 16, + 0, + 222, + 40, + 132, + 5, + 119, + 244, + 2, + 68, + 51, + 122, + 27, + 170, + 23, + 66, + 88, + 96, + 164, + 63, + 115, + 25, + 50, + 54, + 65, + 33, + 62, + 35, + 154, + 241, + 36, + 95, + 136, + 232, + 71, + 237, + 236, + 241, + 214, + 55, + 79, + 139, + 98, + 128, + 114, + 24, + 15, + 23, + 190, + 125, + 66, + 14, + 128, + 148, + 146, + 100, + 81, + 171, + 155, + 217, + 62, + 78, + 194, + 136, + 139, + 222, + 127, + 255, + 254, + 249, + 75, + 48, + 7, + 4, + 130, + 75, + 170, + 23, + 130, + 49, + 64, + 0, + 248, + 51, + 2, + 144, + 211, + 144, + 113, + 118, + 34, + 234, + 34, + 179, + 240, + 55, + 11, + 90, + 82, + 111, + 250, + 60, + 119, + 250, + 159, + 248, + 84, + 8, + 122, + 226, + 103, + 118, + 86, + 22, + 204, + 45, + 234, + 98, + 57, + 196, + 11, + 195, + 195, + 178, + 5, + 13, + 200, + 28, + 155, + 128, + 95, + 18, + 25, + 0, + 54, + 217, + 178, + 161, + 37, + 233, + 103, + 146, + 224, + 249, + 39, + 119, + 111, + 216, + 143, + 170, + 134, + 216, + 253, + 92, + 67, + 111, + 208, + 207, + 85, + 27, + 37, + 139, + 115, + 27, + 50, + 22, + 200, + 176, + 126, + 40, + 100, + 156, + 4, + 23, + 169, + 67, + 128, + 72, + 119, + 253, + 128, + 96, + 119, + 236, + 252, + 105, + 80, + 106, + 88, + 224, + 197, + 244, + 188, + 187, + 0, + 165, + 172, + 101, + 202, + 183, + 143, + 8, + 137, + 79, + 76, + 28, + 34, + 116, + 188, + 1, + 22, + 249, + 62, + 190, + 128, + 15, + 191, + 68, + 108, + 182, + 190, + 221, + 191, + 179, + 170, + 125, + 253, + 250, + 61, + 70, + 54, + 57, + 151, + 33, + 99, + 137, + 172, + 70, + 95, + 248, + 68, + 35, + 152, + 129, + 58, + 248, + 41, + 117, + 6, + 11, + 40, + 112, + 167, + 91, + 187, + 161, + 235, + 136, + 219, + 75, + 137, + 7, + 80, + 61, + 50, + 239, + 157, + 229, + 251, + 189, + 79, + 18, + 1, + 93, + 63, + 57, + 235, + 140, + 212, + 92, + 134, + 140, + 37, + 42, + 214, + 9, + 36, + 248, + 241, + 57, + 38, + 100, + 34, + 12, + 34, + 100, + 184, + 130, + 68, + 211, + 35, + 118, + 23, + 159, + 41, + 161, + 196, + 3, + 33, + 136, + 80, + 73, + 236, + 205, + 145, + 86, + 251, + 3, + 253, + 106, + 0, + 252, + 28, + 199, + 245, + 247, + 127, + 209, + 223, + 207, + 234, + 222, + 65, + 77, + 57, + 12, + 25, + 139, + 20, + 10, + 233, + 132, + 82, + 230, + 87, + 120, + 17, + 200, + 120, + 9, + 13, + 151, + 253, + 17, + 187, + 19, + 191, + 162, + 196, + 3, + 79, + 116, + 169, + 13, + 143, + 223, + 4, + 192, + 5, + 24, + 198, + 173, + 230, + 161, + 16, + 220, + 68, + 150, + 235, + 240, + 183, + 131, + 21, + 236, + 63, + 175, + 229, + 33, + 93, + 162, + 204, + 14, + 25, + 75, + 4, + 46, + 65, + 137, + 56, + 144, + 42, + 80, + 198, + 241, + 57, + 52, + 240, + 170, + 113, + 159, + 50, + 173, + 33, + 226, + 178, + 219, + 237, + 116, + 198, + 168, + 238, + 4, + 71, + 251, + 250, + 112, + 47, + 19, + 38, + 16, + 156, + 4, + 145, + 35, + 120, + 149, + 73, + 137, + 7, + 3, + 202, + 122, + 130, + 14, + 137, + 195, + 157, + 210, + 3, + 102, + 94, + 1, + 193, + 86, + 170, + 51, + 7, + 43, + 227, + 34, + 42, + 85, + 118, + 167, + 171, + 202, + 105, + 215, + 249, + 134, + 70, + 41, + 4, + 15, + 179, + 189, + 176, + 128, + 239, + 3, + 150, + 101, + 251, + 165, + 64, + 137, + 7, + 96, + 56, + 151, + 233, + 86, + 198, + 0, + 232, + 15, + 188, + 251, + 68, + 0, + 10, + 196, + 192, + 32, + 251, + 10, + 8, + 54, + 117, + 16, + 145, + 17, + 0, + 218, + 97, + 183, + 195, + 255, + 46, + 218, + 165, + 230, + 2, + 135, + 139, + 241, + 56, + 156, + 4, + 124, + 228, + 146, + 129, + 91, + 124, + 94, + 90, + 241, + 166, + 131, + 55, + 162, + 230, + 211, + 224, + 148, + 120, + 208, + 33, + 125, + 193, + 110, + 147, + 6, + 188, + 197, + 121, + 85, + 25, + 86, + 64, + 16, + 180, + 183, + 188, + 154, + 14, + 76, + 85, + 123, + 102, + 14, + 112, + 217, + 29, + 85, + 140, + 7, + 180, + 193, + 35, + 252, + 39, + 180, + 203, + 233, + 116, + 216, + 113, + 26, + 17, + 181, + 222, + 99, + 231, + 245, + 100, + 241, + 71, + 138, + 30, + 239, + 151, + 233, + 3, + 66, + 154, + 66, + 254, + 28, + 72, + 38, + 216, + 78, + 89, + 203, + 36, + 0, + 104, + 65, + 180, + 51, + 216, + 27, + 81, + 250, + 173, + 232, + 80, + 91, + 90, + 9, + 239, + 185, + 112, + 50, + 110, + 201, + 144, + 91, + 162, + 171, + 60, + 118, + 210, + 247, + 46, + 135, + 131, + 113, + 226, + 196, + 153, + 221, + 233, + 116, + 73, + 105, + 116, + 4, + 132, + 11, + 62, + 160, + 25, + 91, + 73, + 215, + 38, + 182, + 63, + 0, + 224, + 163, + 128, + 23, + 20, + 131, + 114, + 210, + 109, + 91, + 164, + 99, + 18, + 163, + 34, + 132, + 228, + 130, + 45, + 101, + 109, + 48, + 0, + 194, + 61, + 37, + 209, + 166, + 196, + 131, + 146, + 164, + 114, + 75, + 212, + 160, + 130, + 130, + 98, + 80, + 136, + 5, + 201, + 151, + 94, + 106, + 181, + 84, + 102, + 200, + 203, + 209, + 50, + 222, + 119, + 61, + 2, + 93, + 255, + 8, + 121, + 75, + 187, + 28, + 146, + 98, + 112, + 193, + 231, + 118, + 167, + 167, + 242, + 129, + 185, + 15, + 0, + 163, + 218, + 230, + 62, + 92, + 160, + 45, + 33, + 240, + 226, + 169, + 252, + 205, + 147, + 227, + 2, + 133, + 96, + 67, + 31, + 56, + 121, + 12, + 34, + 178, + 162, + 15, + 113, + 186, + 53, + 37, + 30, + 20, + 68, + 138, + 78, + 121, + 226, + 155, + 236, + 143, + 142, + 191, + 244, + 82, + 210, + 66, + 230, + 232, + 103, + 207, + 223, + 185, + 92, + 85, + 30, + 116, + 150, + 3, + 169, + 69, + 80, + 13, + 242, + 239, + 64, + 48, + 236, + 246, + 103, + 118, + 46, + 117, + 18, + 70, + 165, + 53, + 217, + 10, + 92, + 214, + 210, + 38, + 250, + 167, + 190, + 156, + 22, + 18, + 80, + 9, + 54, + 237, + 178, + 139, + 55, + 149, + 252, + 125, + 33, + 191, + 68, + 137, + 7, + 5, + 41, + 170, + 200, + 106, + 105, + 52, + 31, + 56, + 16, + 127, + 179, + 123, + 244, + 205, + 55, + 121, + 29, + 80, + 108, + 186, + 198, + 220, + 227, + 4, + 5, + 8, + 34, + 81, + 165, + 248, + 212, + 97, + 119, + 56, + 156, + 207, + 58, + 159, + 162, + 157, + 46, + 222, + 2, + 217, + 172, + 197, + 98, + 210, + 167, + 141, + 207, + 80, + 8, + 61, + 102, + 106, + 46, + 160, + 72, + 162, + 96, + 163, + 62, + 128, + 247, + 85, + 60, + 248, + 222, + 144, + 44, + 222, + 81, + 116, + 177, + 134, + 72, + 165, + 165, + 24, + 116, + 250, + 184, + 112, + 107, + 52, + 249, + 82, + 56, + 201, + 13, + 188, + 36, + 42, + 193, + 98, + 235, + 3, + 38, + 98, + 82, + 33, + 227, + 235, + 178, + 171, + 121, + 230, + 197, + 23, + 65, + 46, + 128, + 190, + 251, + 44, + 67, + 148, + 4, + 93, + 74, + 146, + 215, + 224, + 71, + 72, + 217, + 28, + 212, + 245, + 62, + 148, + 214, + 200, + 137, + 7, + 40, + 114, + 0, + 97, + 3, + 240, + 153, + 42, + 221, + 115, + 50, + 86, + 211, + 18, + 13, + 40, + 6, + 157, + 209, + 238, + 71, + 90, + 185, + 240, + 75, + 225, + 214, + 55, + 99, + 49, + 201, + 10, + 108, + 90, + 85, + 96, + 62, + 38, + 213, + 122, + 71, + 192, + 168, + 47, + 218, + 237, + 207, + 62, + 245, + 172, + 3, + 112, + 96, + 170, + 156, + 88, + 53, + 98, + 43, + 105, + 181, + 150, + 202, + 150, + 254, + 196, + 229, + 58, + 29, + 217, + 211, + 213, + 114, + 162, + 196, + 131, + 199, + 94, + 149, + 203, + 184, + 23, + 79, + 188, + 134, + 16, + 131, + 206, + 246, + 100, + 252, + 145, + 216, + 80, + 211, + 155, + 173, + 221, + 47, + 113, + 24, + 0, + 214, + 39, + 255, + 58, + 231, + 251, + 147, + 187, + 83, + 212, + 19, + 118, + 39, + 50, + 85, + 200, + 86, + 128, + 190, + 4, + 226, + 109, + 8, + 26, + 56, + 171, + 109, + 70, + 35, + 155, + 178, + 245, + 14, + 39, + 9, + 128, + 195, + 174, + 225, + 61, + 129, + 140, + 103, + 201, + 200, + 102, + 141, + 242, + 65, + 39, + 219, + 253, + 82, + 50, + 154, + 76, + 134, + 223, + 228, + 44, + 43, + 26, + 144, + 27, + 7, + 18, + 69, + 190, + 158, + 235, + 206, + 45, + 38, + 149, + 63, + 38, + 237, + 240, + 144, + 199, + 196, + 252, + 129, + 28, + 233, + 71, + 156, + 252, + 119, + 238, + 90, + 52, + 126, + 87, + 42, + 46, + 112, + 147, + 45, + 88, + 53, + 32, + 164, + 109, + 29, + 78, + 131, + 47, + 221, + 217, + 102, + 139, + 200, + 131, + 78, + 238, + 205, + 55, + 147, + 67, + 47, + 189, + 153, + 244, + 91, + 234, + 151, + 255, + 52, + 192, + 165, + 216, + 100, + 212, + 31, + 245, + 195, + 215, + 155, + 10, + 172, + 255, + 56, + 185, + 135, + 83, + 43, + 224, + 42, + 16, + 88, + 165, + 164, + 84, + 22, + 131, + 237, + 125, + 43, + 210, + 214, + 102, + 106, + 14, + 132, + 30, + 121, + 104, + 87, + 21, + 227, + 208, + 87, + 2, + 12, + 154, + 33, + 168, + 87, + 86, + 94, + 41, + 62, + 132, + 44, + 232, + 244, + 37, + 95, + 106, + 141, + 15, + 113, + 126, + 198, + 82, + 95, + 191, + 110, + 93, + 11, + 155, + 74, + 182, + 251, + 55, + 145, + 175, + 221, + 15, + 232, + 196, + 76, + 38, + 168, + 189, + 23, + 185, + 126, + 20, + 122, + 117, + 121, + 2, + 94, + 54, + 36, + 122, + 145, + 50, + 180, + 123, + 180, + 167, + 210, + 197, + 215, + 82, + 255, + 238, + 114, + 56, + 13, + 33, + 208, + 44, + 54, + 160, + 172, + 157, + 162, + 164, + 160, + 51, + 218, + 250, + 18, + 158, + 220, + 0, + 0, + 212, + 215, + 47, + 95, + 209, + 224, + 147, + 127, + 93, + 105, + 84, + 13, + 153, + 137, + 70, + 203, + 174, + 240, + 142, + 25, + 87, + 86, + 6, + 81, + 209, + 232, + 221, + 103, + 208, + 15, + 184, + 92, + 122, + 39, + 235, + 60, + 104, + 14, + 4, + 16, + 72, + 93, + 164, + 140, + 83, + 180, + 227, + 77, + 242, + 159, + 162, + 196, + 3, + 19, + 141, + 145, + 57, + 126, + 24, + 0, + 128, + 160, + 140, + 150, + 127, + 77, + 235, + 35, + 144, + 105, + 188, + 221, + 155, + 254, + 106, + 26, + 180, + 201, + 232, + 232, + 196, + 208, + 104, + 222, + 177, + 116, + 210, + 151, + 62, + 126, + 117, + 52, + 125, + 121, + 212, + 96, + 48, + 221, + 236, + 184, + 176, + 1, + 185, + 28, + 2, + 99, + 129, + 86, + 144, + 67, + 172, + 241, + 7, + 220, + 242, + 0, + 152, + 18, + 15, + 16, + 192, + 99, + 53, + 236, + 226, + 1, + 168, + 223, + 177, + 124, + 197, + 22, + 217, + 215, + 180, + 36, + 5, + 236, + 144, + 144, + 203, + 100, + 175, + 92, + 65, + 3, + 93, + 151, + 117, + 211, + 251, + 129, + 147, + 121, + 163, + 12, + 123, + 41, + 47, + 239, + 104, + 99, + 122, + 90, + 99, + 94, + 217, + 232, + 165, + 69, + 233, + 178, + 198, + 188, + 188, + 84, + 187, + 75, + 87, + 107, + 93, + 27, + 0, + 160, + 98, + 69, + 49, + 240, + 56, + 228, + 8, + 168, + 74, + 164, + 104, + 133, + 56, + 83, + 226, + 129, + 191, + 212, + 105, + 23, + 0, + 0, + 90, + 247, + 80, + 89, + 157, + 142, + 27, + 57, + 177, + 40, + 205, + 191, + 74, + 46, + 106, + 4, + 44, + 46, + 231, + 165, + 244, + 74, + 93, + 162, + 63, + 41, + 75, + 194, + 119, + 141, + 87, + 167, + 53, + 94, + 250, + 211, + 198, + 171, + 95, + 189, + 122, + 116, + 209, + 68, + 94, + 222, + 165, + 188, + 198, + 40, + 10, + 96, + 244, + 149, + 138, + 45, + 163, + 247, + 146, + 153, + 170, + 36, + 4, + 104, + 99, + 181, + 200, + 100, + 20, + 102, + 15, + 132, + 250, + 150, + 29, + 18, + 2, + 192, + 6, + 143, + 150, + 85, + 139, + 95, + 147, + 146, + 35, + 127, + 106, + 218, + 101, + 16, + 235, + 116, + 58, + 125, + 249, + 242, + 180, + 11, + 233, + 81, + 127, + 250, + 66, + 26, + 21, + 17, + 170, + 83, + 220, + 67, + 139, + 142, + 70, + 129, + 11, + 224, + 164, + 51, + 71, + 243, + 46, + 79, + 124, + 245, + 106, + 89, + 99, + 250, + 79, + 83, + 233, + 188, + 15, + 88, + 102, + 207, + 30, + 239, + 27, + 246, + 61, + 94, + 45, + 213, + 22, + 88, + 221, + 58, + 31, + 155, + 163, + 42, + 135, + 240, + 106, + 143, + 195, + 94, + 37, + 191, + 43, + 45, + 189, + 46, + 40, + 149, + 125, + 193, + 168, + 124, + 15, + 143, + 29, + 4, + 201, + 178, + 98, + 121, + 189, + 130, + 214, + 61, + 186, + 98, + 3, + 169, + 215, + 39, + 234, + 51, + 122, + 116, + 209, + 16, + 195, + 157, + 156, + 150, + 7, + 13, + 154, + 182, + 104, + 90, + 217, + 232, + 201, + 69, + 19, + 139, + 202, + 166, + 77, + 75, + 73, + 5, + 47, + 232, + 222, + 204, + 229, + 105, + 151, + 134, + 70, + 27, + 23, + 141, + 94, + 248, + 106, + 186, + 172, + 108, + 40, + 53, + 45, + 157, + 119, + 242, + 204, + 180, + 203, + 233, + 175, + 94, + 194, + 122, + 195, + 227, + 226, + 115, + 8, + 42, + 162, + 117, + 66, + 71, + 179, + 36, + 56, + 4, + 40, + 54, + 149, + 235, + 1, + 89, + 205, + 169, + 94, + 226, + 88, + 246, + 227, + 78, + 15, + 202, + 8, + 85, + 175, + 88, + 167, + 132, + 160, + 126, + 7, + 128, + 80, + 182, + 133, + 38, + 213, + 219, + 136, + 239, + 125, + 87, + 167, + 157, + 73, + 127, + 245, + 248, + 201, + 63, + 61, + 121, + 21, + 53, + 47, + 253, + 213, + 21, + 233, + 188, + 163, + 81, + 54, + 26, + 229, + 184, + 100, + 138, + 16, + 119, + 229, + 171, + 103, + 206, + 164, + 26, + 243, + 174, + 228, + 229, + 93, + 205, + 91, + 4, + 156, + 15, + 58, + 17, + 193, + 129, + 216, + 135, + 255, + 57, + 187, + 174, + 46, + 152, + 196, + 100, + 79, + 190, + 217, + 224, + 111, + 226, + 168, + 3, + 94, + 57, + 156, + 114, + 61, + 224, + 54, + 87, + 120, + 4, + 228, + 36, + 41, + 49, + 45, + 4, + 8, + 132, + 229, + 143, + 174, + 184, + 251, + 175, + 182, + 84, + 143, + 230, + 157, + 9, + 68, + 161, + 41, + 105, + 96, + 234, + 69, + 67, + 208, + 172, + 69, + 199, + 82, + 127, + 122, + 53, + 13, + 140, + 190, + 238, + 239, + 214, + 237, + 216, + 177, + 142, + 167, + 250, + 11, + 121, + 160, + 255, + 174, + 46, + 202, + 59, + 222, + 8, + 204, + 159, + 87, + 118, + 233, + 210, + 138, + 75, + 141, + 199, + 134, + 78, + 150, + 13, + 73, + 143, + 172, + 143, + 192, + 36, + 9, + 57, + 197, + 14, + 232, + 123, + 23, + 142, + 212, + 161, + 45, + 242, + 0, + 189, + 18, + 141, + 225, + 100, + 159, + 121, + 142, + 180, + 19, + 9, + 134, + 170, + 213, + 130, + 32, + 192, + 176, + 98, + 29, + 52, + 185, + 254, + 88, + 89, + 217, + 165, + 99, + 211, + 174, + 46, + 106, + 60, + 115, + 60, + 47, + 61, + 237, + 210, + 209, + 188, + 11, + 87, + 224, + 83, + 37, + 157, + 60, + 115, + 230, + 204, + 201, + 163, + 103, + 46, + 92, + 58, + 115, + 236, + 194, + 133, + 11, + 39, + 235, + 63, + 61, + 115, + 236, + 204, + 201, + 117, + 199, + 142, + 183, + 72, + 191, + 71, + 219, + 117, + 125, + 2, + 147, + 179, + 99, + 85, + 84, + 5, + 17, + 184, + 139, + 217, + 249, + 148, + 144, + 153, + 164, + 245, + 114, + 183, + 217, + 0, + 112, + 73, + 73, + 209, + 186, + 199, + 30, + 213, + 178, + 1, + 208, + 209, + 51, + 211, + 26, + 27, + 47, + 52, + 230, + 29, + 207, + 91, + 4, + 42, + 224, + 248, + 180, + 227, + 87, + 166, + 93, + 93, + 81, + 118, + 225, + 88, + 222, + 5, + 93, + 196, + 180, + 180, + 238, + 209, + 50, + 113, + 226, + 156, + 107, + 233, + 19, + 250, + 17, + 183, + 45, + 119, + 207, + 112, + 211, + 122, + 231, + 179, + 235, + 81, + 240, + 42, + 116, + 189, + 97, + 144, + 100, + 68, + 248, + 124, + 135, + 44, + 41, + 186, + 97, + 197, + 242, + 29, + 154, + 231, + 255, + 201, + 153, + 178, + 178, + 178, + 51, + 87, + 225, + 223, + 241, + 116, + 227, + 177, + 69, + 199, + 47, + 164, + 26, + 47, + 53, + 158, + 57, + 115, + 252, + 216, + 201, + 178, + 178, + 250, + 250, + 111, + 153, + 129, + 0, + 92, + 12, + 222, + 180, + 108, + 122, + 214, + 190, + 84, + 63, + 226, + 206, + 88, + 240, + 161, + 75, + 154, + 224, + 213, + 233, + 200, + 173, + 46, + 13, + 171, + 64, + 218, + 169, + 200, + 10, + 215, + 149, + 173, + 88, + 174, + 226, + 131, + 187, + 227, + 159, + 77, + 212, + 28, + 171, + 63, + 121, + 242, + 228, + 177, + 163, + 232, + 80, + 127, + 236, + 211, + 163, + 159, + 162, + 99, + 253, + 183, + 30, + 171, + 175, + 255, + 115, + 125, + 166, + 65, + 210, + 160, + 248, + 104, + 249, + 138, + 58, + 254, + 161, + 171, + 40, + 230, + 9, + 189, + 136, + 59, + 211, + 124, + 52, + 67, + 0, + 148, + 3, + 170, + 46, + 172, + 21, + 76, + 223, + 192, + 133, + 163, + 20, + 90, + 147, + 22, + 223, + 178, + 226, + 81, + 5, + 8, + 83, + 207, + 110, + 40, + 211, + 237, + 216, + 187, + 239, + 54, + 0, + 224, + 234, + 34, + 160, + 99, + 199, + 148, + 31, + 18, + 95, + 27, + 61, + 180, + 103, + 169, + 83, + 47, + 226, + 54, + 158, + 47, + 160, + 79, + 250, + 3, + 170, + 85, + 134, + 193, + 178, + 150, + 136, + 51, + 173, + 55, + 46, + 80, + 183, + 165, + 12, + 161, + 176, + 14, + 11, + 196, + 99, + 250, + 205, + 175, + 175, + 47, + 3, + 14, + 184, + 91, + 246, + 30, + 89, + 132, + 229, + 143, + 62, + 186, + 98, + 197, + 165, + 227, + 199, + 143, + 79, + 59, + 3, + 65, + 166, + 226, + 244, + 29, + 15, + 109, + 152, + 228, + 40, + 176, + 1, + 233, + 15, + 168, + 58, + 120, + 219, + 152, + 141, + 196, + 115, + 140, + 7, + 70, + 0, + 134, + 199, + 86, + 172, + 120, + 244, + 209, + 229, + 0, + 5, + 216, + 187, + 250, + 250, + 79, + 47, + 32, + 82, + 162, + 128, + 48, + 90, + 183, + 28, + 44, + 230, + 138, + 178, + 178, + 45, + 213, + 68, + 219, + 249, + 254, + 21, + 28, + 162, + 241, + 80, + 221, + 6, + 64, + 81, + 174, + 84, + 214, + 173, + 240, + 240, + 15, + 237, + 89, + 170, + 219, + 221, + 40, + 112, + 15, + 105, + 214, + 130, + 50, + 36, + 74, + 103, + 64, + 181, + 202, + 1, + 134, + 209, + 238, + 204, + 10, + 1, + 54, + 72, + 120, + 121, + 7, + 19, + 27, + 44, + 212, + 85, + 111, + 217, + 176, + 1, + 84, + 33, + 248, + 56, + 121, + 121, + 199, + 234, + 31, + 125, + 244, + 161, + 229, + 162, + 144, + 192, + 223, + 29, + 101, + 138, + 138, + 148, + 206, + 72, + 164, + 239, + 202, + 180, + 75, + 125, + 56, + 241, + 91, + 87, + 166, + 176, + 45, + 15, + 109, + 225, + 31, + 122, + 169, + 46, + 163, + 86, + 90, + 113, + 225, + 198, + 129, + 205, + 230, + 234, + 133, + 40, + 241, + 32, + 181, + 203, + 133, + 211, + 166, + 250, + 225, + 151, + 130, + 16, + 68, + 104, + 121, + 7, + 173, + 14, + 48, + 36, + 239, + 196, + 149, + 43, + 169, + 105, + 87, + 112, + 181, + 122, + 228, + 231, + 101, + 82, + 231, + 150, + 241, + 9, + 79, + 146, + 230, + 2, + 0, + 70, + 23, + 149, + 141, + 70, + 58, + 219, + 72, + 87, + 42, + 92, + 140, + 117, + 119, + 147, + 135, + 246, + 232, + 11, + 124, + 165, + 21, + 79, + 113, + 237, + 52, + 151, + 155, + 165, + 196, + 131, + 140, + 240, + 80, + 14, + 173, + 151, + 133, + 145, + 19, + 254, + 26, + 47, + 239, + 224, + 201, + 97, + 139, + 141, + 182, + 200, + 248, + 162, + 198, + 30, + 33, + 173, + 23, + 18, + 33, + 216, + 177, + 66, + 172, + 224, + 12, + 161, + 165, + 77, + 255, + 245, + 204, + 180, + 116, + 36, + 132, + 75, + 123, + 241, + 202, + 96, + 93, + 50, + 8, + 118, + 172, + 16, + 242, + 14, + 154, + 241, + 86, + 68, + 165, + 214, + 16, + 73, + 152, + 154, + 201, + 205, + 82, + 226, + 65, + 67, + 46, + 35, + 151, + 75, + 248, + 154, + 17, + 150, + 119, + 200, + 5, + 0, + 134, + 133, + 150, + 29, + 180, + 30, + 20, + 19, + 187, + 101, + 127, + 183, + 67, + 80, + 241, + 242, + 101, + 237, + 39, + 242, + 142, + 125, + 17, + 233, + 144, + 198, + 2, + 154, + 229, + 190, + 246, + 142, + 135, + 234, + 240, + 67, + 123, + 244, + 31, + 177, + 210, + 90, + 27, + 138, + 116, + 208, + 116, + 174, + 245, + 66, + 106, + 170, + 178, + 27, + 184, + 92, + 136, + 136, + 1, + 36, + 203, + 59, + 56, + 115, + 1, + 224, + 114, + 222, + 113, + 46, + 210, + 89, + 41, + 78, + 77, + 6, + 73, + 224, + 69, + 28, + 124, + 157, + 247, + 133, + 246, + 254, + 107, + 170, + 108, + 34, + 18, + 121, + 38, + 212, + 209, + 214, + 214, + 129, + 102, + 57, + 226, + 241, + 176, + 13, + 143, + 74, + 138, + 128, + 120, + 69, + 180, + 193, + 120, + 220, + 166, + 77, + 161, + 200, + 206, + 107, + 182, + 20, + 155, + 94, + 180, + 191, + 104, + 36, + 72, + 132, + 61, + 248, + 229, + 29, + 126, + 144, + 3, + 0, + 28, + 196, + 249, + 160, + 10, + 124, + 205, + 251, + 14, + 10, + 0, + 68, + 34, + 239, + 61, + 196, + 115, + 193, + 186, + 117, + 63, + 231, + 17, + 248, + 226, + 124, + 95, + 164, + 153, + 230, + 81, + 234, + 104, + 38, + 195, + 96, + 117, + 18, + 19, + 60, + 42, + 164, + 28, + 244, + 37, + 117, + 61, + 101, + 45, + 121, + 184, + 54, + 183, + 122, + 33, + 13, + 81, + 160, + 14, + 13, + 6, + 57, + 248, + 136, + 68, + 88, + 222, + 193, + 242, + 138, + 250, + 123, + 163, + 170, + 77, + 255, + 213, + 105, + 23, + 80, + 150, + 179, + 35, + 18, + 178, + 21, + 32, + 8, + 154, + 9, + 4, + 63, + 23, + 60, + 232, + 117, + 203, + 5, + 8, + 34, + 145, + 205, + 146, + 68, + 8, + 245, + 90, + 27, + 150, + 171, + 17, + 112, + 234, + 35, + 64, + 209, + 149, + 15, + 204, + 157, + 59, + 151, + 154, + 100, + 219, + 249, + 123, + 48, + 206, + 39, + 116, + 11, + 239, + 104, + 133, + 171, + 228, + 97, + 44, + 223, + 156, + 171, + 12, + 200, + 13, + 171, + 54, + 163, + 23, + 142, + 146, + 208, + 22, + 77, + 220, + 40, + 182, + 22, + 20, + 90, + 75, + 249, + 149, + 255, + 68, + 125, + 184, + 110, + 249, + 46, + 190, + 213, + 12, + 209, + 137, + 29, + 29, + 178, + 201, + 238, + 213, + 15, + 169, + 17, + 208, + 215, + 85, + 148, + 120, + 40, + 181, + 78, + 118, + 129, + 32, + 104, + 197, + 179, + 246, + 23, + 245, + 4, + 201, + 229, + 144, + 73, + 30, + 68, + 232, + 150, + 183, + 223, + 126, + 242, + 222, + 2, + 217, + 111, + 24, + 143, + 144, + 177, + 56, + 11, + 230, + 245, + 122, + 65, + 182, + 81, + 114, + 137, + 70, + 249, + 150, + 226, + 130, + 18, + 100, + 186, + 118, + 241, + 158, + 193, + 142, + 71, + 177, + 46, + 8, + 53, + 11, + 166, + 17, + 113, + 64, + 39, + 63, + 14, + 84, + 39, + 34, + 240, + 80, + 93, + 6, + 4, + 36, + 0, + 24, + 188, + 68, + 212, + 164, + 102, + 196, + 174, + 127, + 2, + 2, + 3, + 221, + 194, + 59, + 185, + 230, + 241, + 184, + 92, + 150, + 109, + 111, + 191, + 253, + 171, + 95, + 61, + 121, + 175, + 152, + 67, + 209, + 6, + 25, + 237, + 100, + 103, + 4, + 31, + 203, + 178, + 94, + 175, + 118, + 72, + 207, + 109, + 181, + 97, + 125, + 200, + 11, + 57, + 146, + 131, + 66, + 90, + 102, + 20, + 58, + 219, + 196, + 145, + 176, + 58, + 65, + 97, + 212, + 63, + 196, + 255, + 156, + 75, + 71, + 10, + 40, + 241, + 32, + 251, + 141, + 130, + 202, + 218, + 210, + 92, + 114, + 71, + 52, + 42, + 110, + 210, + 22, + 222, + 105, + 221, + 100, + 75, + 9, + 66, + 224, + 159, + 255, + 249, + 103, + 79, + 222, + 75, + 82, + 245, + 90, + 127, + 221, + 159, + 74, + 250, + 0, + 130, + 246, + 148, + 209, + 34, + 154, + 219, + 201, + 250, + 183, + 239, + 19, + 8, + 150, + 119, + 69, + 10, + 133, + 198, + 35, + 99, + 136, + 87, + 56, + 230, + 135, + 242, + 233, + 21, + 2, + 2, + 43, + 132, + 39, + 114, + 232, + 223, + 83, + 77, + 181, + 197, + 5, + 136, + 21, + 244, + 86, + 46, + 208, + 167, + 167, + 170, + 60, + 78, + 23, + 165, + 250, + 80, + 101, + 124, + 209, + 91, + 75, + 9, + 143, + 192, + 63, + 255, + 243, + 175, + 0, + 132, + 123, + 239, + 181, + 130, + 254, + 177, + 206, + 181, + 22, + 88, + 231, + 22, + 20, + 216, + 138, + 139, + 75, + 43, + 221, + 76, + 52, + 149, + 74, + 5, + 252, + 108, + 74, + 59, + 239, + 15, + 147, + 173, + 80, + 176, + 249, + 4, + 130, + 21, + 37, + 123, + 101, + 12, + 32, + 238, + 128, + 70, + 60, + 69, + 1, + 129, + 29, + 101, + 252, + 213, + 180, + 126, + 182, + 212, + 128, + 244, + 86, + 46, + 208, + 39, + 10, + 233, + 59, + 135, + 218, + 210, + 42, + 223, + 227, + 148, + 216, + 127, + 187, + 111, + 251, + 91, + 60, + 2, + 152, + 126, + 37, + 167, + 159, + 253, + 236, + 103, + 47, + 63, + 249, + 228, + 247, + 158, + 124, + 242, + 229, + 183, + 127, + 253, + 153, + 1, + 0, + 160, + 9, + 36, + 167, + 167, + 12, + 218, + 183, + 188, + 48, + 34, + 7, + 64, + 246, + 186, + 77, + 134, + 192, + 58, + 49, + 255, + 238, + 145, + 137, + 1, + 237, + 113, + 58, + 51, + 135, + 50, + 165, + 38, + 39, + 91, + 81, + 232, + 240, + 132, + 98, + 196, + 128, + 118, + 33, + 43, + 142, + 94, + 145, + 35, + 152, + 33, + 154, + 177, + 212, + 204, + 190, + 235, + 225, + 174, + 247, + 100, + 8, + 232, + 211, + 175, + 0, + 137, + 239, + 221, + 139, + 56, + 196, + 86, + 92, + 169, + 245, + 96, + 196, + 110, + 70, + 14, + 207, + 202, + 136, + 17, + 161, + 167, + 120, + 72, + 37, + 4, + 152, + 120, + 217, + 164, + 237, + 118, + 39, + 170, + 55, + 114, + 17, + 119, + 157, + 214, + 131, + 162, + 54, + 151, + 68, + 122, + 149, + 92, + 194, + 156, + 14, + 154, + 77, + 198, + 146, + 81, + 95, + 20, + 142, + 1, + 198, + 159, + 140, + 181, + 162, + 21, + 37, + 43, + 102, + 125, + 227, + 59, + 30, + 174, + 231, + 95, + 178, + 32, + 32, + 113, + 8, + 98, + 10, + 12, + 69, + 129, + 13, + 100, + 164, + 184, + 20, + 164, + 164, + 150, + 70, + 83, + 187, + 145, + 180, + 255, + 29, + 210, + 112, + 134, + 0, + 160, + 248, + 72, + 176, + 5, + 59, + 228, + 230, + 8, + 185, + 4, + 40, + 134, + 35, + 17, + 130, + 199, + 131, + 197, + 211, + 99, + 183, + 163, + 89, + 76, + 154, + 86, + 229, + 50, + 170, + 38, + 79, + 149, + 186, + 60, + 129, + 224, + 140, + 249, + 51, + 90, + 146, + 91, + 238, + 152, + 63, + 131, + 99, + 185, + 252, + 252, + 94, + 0, + 96, + 222, + 156, + 217, + 179, + 110, + 251, + 198, + 139, + 135, + 122, + 199, + 39, + 254, + 253, + 223, + 204, + 162, + 32, + 8, + 203, + 207, + 48, + 189, + 12, + 136, + 0, + 97, + 6, + 89, + 4, + 77, + 91, + 116, + 80, + 119, + 75, + 35, + 158, + 5, + 252, + 213, + 200, + 35, + 58, + 124, + 184, + 190, + 76, + 254, + 160, + 30, + 52, + 127, + 89, + 245, + 240, + 30, + 0, + 130, + 70, + 176, + 120, + 104, + 218, + 131, + 7, + 65, + 61, + 57, + 168, + 11, + 66, + 226, + 32, + 42, + 22, + 127, + 52, + 169, + 175, + 101, + 254, + 232, + 116, + 46, + 185, + 176, + 122, + 40, + 127, + 3, + 6, + 96, + 164, + 6, + 35, + 240, + 157, + 245, + 47, + 30, + 138, + 197, + 7, + 207, + 141, + 79, + 32, + 74, + 161, + 127, + 255, + 254, + 217, + 175, + 223, + 219, + 245, + 50, + 208, + 207, + 178, + 201, + 135, + 146, + 254, + 207, + 39, + 31, + 191, + 187, + 119, + 219, + 182, + 242, + 162, + 114, + 157, + 101, + 195, + 25, + 95, + 42, + 85, + 191, + 3, + 218, + 127, + 248, + 240, + 215, + 205, + 38, + 193, + 60, + 168, + 248, + 202, + 238, + 244, + 160, + 202, + 84, + 89, + 107, + 76, + 145, + 200, + 64, + 56, + 68, + 102, + 247, + 223, + 201, + 230, + 239, + 78, + 222, + 191, + 176, + 229, + 142, + 24, + 203, + 5, + 49, + 0, + 13, + 225, + 193, + 165, + 128, + 192, + 109, + 119, + 253, + 195, + 143, + 105, + 102, + 79, + 67, + 67, + 67, + 48, + 60, + 146, + 78, + 28, + 58, + 4, + 175, + 248, + 25, + 30, + 181, + 165, + 54, + 43, + 244, + 45, + 104, + 194, + 159, + 253, + 202, + 20, + 20, + 255, + 231, + 75, + 158, + 182, + 109, + 215, + 97, + 1, + 46, + 149, + 90, + 142, + 1, + 40, + 187, + 223, + 108, + 43, + 104, + 90, + 150, + 240, + 197, + 58, + 210, + 180, + 30, + 16, + 149, + 0, + 210, + 128, + 32, + 246, + 119, + 78, + 191, + 147, + 99, + 217, + 233, + 183, + 44, + 73, + 110, + 174, + 106, + 193, + 0, + 48, + 76, + 67, + 60, + 60, + 239, + 182, + 219, + 238, + 153, + 117, + 207, + 143, + 241, + 175, + 248, + 82, + 137, + 145, + 244, + 72, + 131, + 206, + 99, + 84, + 22, + 99, + 36, + 238, + 253, + 30, + 64, + 145, + 137, + 43, + 68, + 0, + 190, + 252, + 252, + 118, + 45, + 0, + 126, + 54, + 186, + 127, + 29, + 2, + 224, + 112, + 189, + 249, + 128, + 79, + 138, + 80, + 60, + 78, + 60, + 0, + 96, + 122, + 226, + 173, + 67, + 174, + 72, + 145, + 8, + 84, + 231, + 143, + 206, + 232, + 77, + 206, + 223, + 210, + 224, + 219, + 207, + 3, + 192, + 120, + 15, + 37, + 106, + 110, + 123, + 202, + 191, + 120, + 214, + 223, + 98, + 4, + 184, + 212, + 161, + 65, + 93, + 4, + 164, + 199, + 169, + 219, + 94, + 92, + 128, + 177, + 192, + 112, + 240, + 36, + 50, + 200, + 151, + 18, + 125, + 100, + 83, + 139, + 0, + 166, + 175, + 175, + 196, + 8, + 60, + 102, + 26, + 0, + 121, + 132, + 226, + 65, + 186, + 210, + 244, + 34, + 147, + 46, + 60, + 241, + 133, + 215, + 47, + 220, + 140, + 88, + 52, + 57, + 125, + 104, + 122, + 50, + 90, + 125, + 127, + 192, + 191, + 63, + 63, + 42, + 228, + 4, + 247, + 28, + 9, + 223, + 117, + 215, + 158, + 67, + 119, + 221, + 133, + 204, + 102, + 52, + 197, + 246, + 198, + 211, + 169, + 67, + 134, + 247, + 244, + 129, + 216, + 112, + 237, + 126, + 84, + 240, + 138, + 198, + 156, + 3, + 169, + 20, + 56, + 73, + 180, + 187, + 180, + 216, + 134, + 80, + 121, + 178, + 168, + 188, + 124, + 219, + 182, + 189, + 239, + 126, + 66, + 120, + 192, + 166, + 211, + 126, + 134, + 190, + 9, + 3, + 240, + 219, + 181, + 102, + 1, + 80, + 70, + 40, + 200, + 68, + 88, + 77, + 93, + 71, + 147, + 146, + 255, + 71, + 30, + 33, + 254, + 0, + 91, + 125, + 199, + 194, + 59, + 182, + 12, + 205, + 207, + 191, + 127, + 70, + 208, + 239, + 111, + 149, + 0, + 64, + 16, + 60, + 241, + 141, + 123, + 162, + 111, + 204, + 94, + 76, + 179, + 104, + 176, + 55, + 14, + 8, + 24, + 241, + 128, + 255, + 80, + 119, + 119, + 107, + 107, + 119, + 124, + 36, + 133, + 198, + 134, + 3, + 62, + 64, + 108, + 72, + 83, + 59, + 67, + 187, + 87, + 21, + 124, + 132, + 16, + 248, + 205, + 178, + 182, + 80, + 72, + 213, + 126, + 134, + 249, + 206, + 127, + 197, + 8, + 28, + 203, + 5, + 0, + 89, + 132, + 2, + 156, + 106, + 53, + 147, + 252, + 102, + 240, + 24, + 162, + 253, + 17, + 167, + 16, + 117, + 69, + 193, + 250, + 69, + 253, + 209, + 222, + 96, + 178, + 97, + 167, + 55, + 22, + 102, + 229, + 89, + 225, + 61, + 47, + 222, + 243, + 141, + 55, + 162, + 115, + 230, + 188, + 49, + 10, + 237, + 241, + 122, + 227, + 6, + 82, + 224, + 13, + 180, + 118, + 99, + 106, + 69, + 32, + 32, + 176, + 252, + 16, + 37, + 232, + 254, + 246, + 173, + 159, + 35, + 4, + 62, + 46, + 29, + 74, + 13, + 69, + 3, + 29, + 138, + 109, + 31, + 188, + 83, + 49, + 0, + 167, + 170, + 117, + 47, + 212, + 146, + 54, + 66, + 113, + 98, + 179, + 144, + 157, + 170, + 236, + 143, + 32, + 253, + 15, + 110, + 49, + 118, + 182, + 188, + 126, + 60, + 145, + 207, + 239, + 69, + 11, + 103, + 33, + 126, + 146, + 101, + 132, + 118, + 254, + 195, + 223, + 126, + 227, + 174, + 89, + 179, + 102, + 47, + 29, + 65, + 237, + 105, + 72, + 164, + 227, + 58, + 213, + 156, + 62, + 150, + 111, + 191, + 0, + 66, + 120, + 144, + 245, + 233, + 112, + 0, + 166, + 251, + 8, + 2, + 239, + 224, + 10, + 2, + 86, + 113, + 183, + 134, + 175, + 99, + 4, + 206, + 152, + 45, + 24, + 213, + 14, + 131, + 160, + 2, + 129, + 236, + 3, + 0, + 224, + 10, + 241, + 21, + 101, + 85, + 184, + 176, + 138, + 44, + 245, + 76, + 225, + 3, + 46, + 96, + 83, + 108, + 186, + 186, + 122, + 211, + 61, + 179, + 110, + 155, + 53, + 123, + 78, + 77, + 131, + 151, + 137, + 38, + 82, + 105, + 173, + 26, + 240, + 31, + 145, + 183, + 31, + 83, + 83, + 138, + 77, + 166, + 244, + 235, + 199, + 252, + 54, + 5, + 2, + 228, + 67, + 190, + 201, + 127, + 70, + 88, + 224, + 131, + 236, + 45, + 32, + 68, + 105, + 135, + 65, + 220, + 224, + 28, + 101, + 25, + 1, + 240, + 200, + 87, + 73, + 66, + 8, + 144, + 165, + 158, + 241, + 93, + 40, + 7, + 50, + 145, + 202, + 141, + 151, + 191, + 255, + 224, + 139, + 119, + 205, + 154, + 61, + 123, + 78, + 69, + 60, + 120, + 238, + 220, + 17, + 173, + 16, + 176, + 9, + 77, + 251, + 187, + 91, + 195, + 208, + 56, + 221, + 173, + 4, + 123, + 19, + 9, + 27, + 214, + 132, + 159, + 148, + 156, + 195, + 178, + 130, + 56, + 48, + 48, + 228, + 15, + 32, + 131, + 70, + 253, + 5, + 86, + 131, + 39, + 133, + 177, + 115, + 111, + 150, + 197, + 7, + 41, + 241, + 32, + 146, + 149, + 81, + 76, + 219, + 209, + 33, + 85, + 186, + 193, + 229, + 88, + 67, + 150, + 122, + 38, + 55, + 195, + 206, + 133, + 50, + 41, + 250, + 218, + 131, + 223, + 127, + 238, + 111, + 231, + 204, + 158, + 61, + 123, + 241, + 96, + 60, + 234, + 77, + 164, + 195, + 252, + 231, + 188, + 25, + 222, + 156, + 210, + 52, + 31, + 33, + 144, + 74, + 13, + 233, + 4, + 138, + 94, + 188, + 38, + 163, + 149, + 119, + 137, + 74, + 222, + 63, + 151, + 66, + 219, + 144, + 98, + 94, + 240, + 33, + 131, + 70, + 88, + 160, + 190, + 49, + 64, + 26, + 238, + 141, + 114, + 129, + 76, + 43, + 77, + 105, + 1, + 16, + 202, + 42, + 144, + 135, + 168, + 59, + 30, + 168, + 232, + 126, + 130, + 128, + 118, + 169, + 103, + 85, + 86, + 248, + 251, + 15, + 62, + 253, + 220, + 230, + 121, + 179, + 43, + 106, + 230, + 116, + 179, + 204, + 161, + 9, + 193, + 18, + 240, + 102, + 152, + 77, + 169, + 24, + 32, + 30, + 71, + 199, + 115, + 81, + 159, + 78, + 166, + 136, + 65, + 0, + 36, + 214, + 20, + 9, + 62, + 209, + 222, + 242, + 173, + 149, + 8, + 128, + 161, + 88, + 180, + 151, + 221, + 180, + 243, + 153, + 123, + 238, + 38, + 8, + 156, + 33, + 203, + 236, + 249, + 192, + 67, + 204, + 180, + 224, + 138, + 6, + 0, + 249, + 20, + 64, + 23, + 216, + 121, + 117, + 232, + 200, + 123, + 76, + 10, + 90, + 163, + 88, + 234, + 25, + 79, + 8, + 84, + 0, + 112, + 224, + 192, + 211, + 15, + 174, + 126, + 238, + 53, + 102, + 233, + 188, + 193, + 248, + 188, + 154, + 6, + 102, + 80, + 208, + 2, + 188, + 25, + 78, + 170, + 36, + 32, + 49, + 50, + 18, + 70, + 40, + 36, + 89, + 157, + 125, + 212, + 24, + 63, + 94, + 144, + 109, + 176, + 228, + 19, + 209, + 45, + 250, + 120, + 91, + 121, + 121, + 73, + 121, + 73, + 145, + 109, + 251, + 1, + 239, + 250, + 157, + 127, + 78, + 0, + 56, + 154, + 66, + 211, + 235, + 252, + 208, + 254, + 220, + 182, + 167, + 80, + 22, + 20, + 208, + 218, + 161, + 22, + 173, + 134, + 116, + 43, + 151, + 122, + 198, + 89, + 105, + 37, + 0, + 117, + 79, + 3, + 7, + 236, + 100, + 14, + 184, + 230, + 180, + 142, + 204, + 171, + 104, + 141, + 11, + 50, + 192, + 155, + 97, + 182, + 91, + 213, + 254, + 4, + 250, + 19, + 14, + 27, + 116, + 29, + 139, + 0, + 24, + 73, + 189, + 245, + 238, + 151, + 26, + 250, + 228, + 221, + 251, + 30, + 190, + 13, + 3, + 112, + 244, + 210, + 25, + 224, + 10, + 110, + 104, + 100, + 132, + 211, + 91, + 165, + 209, + 144, + 180, + 155, + 15, + 32, + 15, + 81, + 104, + 52, + 173, + 171, + 28, + 85, + 75, + 61, + 187, + 52, + 0, + 48, + 93, + 79, + 63, + 248, + 220, + 115, + 128, + 108, + 87, + 195, + 188, + 154, + 177, + 5, + 142, + 116, + 154, + 223, + 92, + 136, + 55, + 195, + 190, + 67, + 10, + 254, + 31, + 73, + 12, + 146, + 87, + 156, + 129, + 49, + 139, + 99, + 0, + 82, + 49, + 219, + 222, + 207, + 181, + 24, + 124, + 254, + 192, + 95, + 97, + 59, + 144, + 190, + 128, + 245, + 194, + 96, + 98, + 164, + 206, + 93, + 224, + 187, + 182, + 29, + 26, + 80, + 50, + 5, + 99, + 224, + 49, + 83, + 39, + 129, + 189, + 35, + 37, + 0, + 59, + 87, + 175, + 126, + 238, + 199, + 76, + 59, + 218, + 199, + 117, + 129, + 99, + 204, + 190, + 120, + 66, + 176, + 3, + 188, + 25, + 222, + 163, + 96, + 128, + 193, + 238, + 48, + 136, + 64, + 120, + 176, + 219, + 143, + 182, + 31, + 210, + 249, + 5, + 16, + 130, + 93, + 184, + 117, + 167, + 223, + 41, + 41, + 255, + 141, + 6, + 2, + 44, + 3, + 191, + 77, + 167, + 143, + 195, + 25, + 35, + 137, + 196, + 135, + 41, + 111, + 97, + 52, + 165, + 95, + 92, + 157, + 19, + 6, + 102, + 235, + 100, + 116, + 0, + 120, + 229, + 251, + 207, + 61, + 87, + 203, + 111, + 90, + 184, + 120, + 105, + 124, + 206, + 130, + 177, + 176, + 223, + 239, + 71, + 218, + 153, + 194, + 102, + 120, + 147, + 66, + 4, + 64, + 254, + 1, + 129, + 145, + 110, + 84, + 65, + 173, + 255, + 224, + 241, + 216, + 254, + 148, + 64, + 61, + 219, + 139, + 182, + 253, + 242, + 147, + 207, + 101, + 188, + 240, + 53, + 44, + 3, + 87, + 211, + 169, + 146, + 237, + 239, + 39, + 18, + 225, + 53, + 189, + 76, + 97, + 42, + 167, + 117, + 104, + 229, + 36, + 197, + 139, + 180, + 126, + 133, + 190, + 150, + 60, + 2, + 0, + 18, + 3, + 255, + 248, + 193, + 23, + 94, + 243, + 10, + 219, + 54, + 46, + 5, + 123, + 56, + 47, + 158, + 72, + 97, + 237, + 68, + 161, + 175, + 41, + 175, + 198, + 6, + 198, + 71, + 226, + 221, + 73, + 136, + 135, + 88, + 93, + 41, + 0, + 79, + 128, + 248, + 64, + 108, + 42, + 137, + 146, + 234, + 149, + 171, + 138, + 139, + 139, + 151, + 253, + 181, + 173, + 8, + 43, + 198, + 199, + 203, + 16, + 0, + 23, + 210, + 233, + 79, + 191, + 220, + 118, + 111, + 225, + 46, + 206, + 231, + 99, + 77, + 182, + 95, + 39, + 51, + 42, + 139, + 23, + 205, + 166, + 204, + 240, + 40, + 161, + 165, + 161, + 97, + 143, + 143, + 223, + 177, + 153, + 97, + 158, + 126, + 240, + 5, + 183, + 180, + 145, + 175, + 3, + 28, + 130, + 57, + 77, + 131, + 216, + 60, + 241, + 0, + 168, + 253, + 160, + 48, + 232, + 193, + 115, + 41, + 63, + 50, + 237, + 122, + 20, + 72, + 12, + 162, + 246, + 71, + 253, + 76, + 59, + 27, + 197, + 167, + 28, + 168, + 235, + 234, + 218, + 220, + 213, + 181, + 111, + 153, + 109, + 219, + 151, + 159, + 96, + 95, + 232, + 211, + 244, + 213, + 127, + 67, + 42, + 161, + 40, + 6, + 254, + 148, + 185, + 77, + 106, + 212, + 43, + 16, + 227, + 103, + 147, + 226, + 69, + 179, + 201, + 18, + 194, + 1, + 19, + 19, + 169, + 48, + 200, + 60, + 233, + 192, + 239, + 63, + 184, + 89, + 6, + 64, + 56, + 12, + 60, + 48, + 167, + 102, + 80, + 2, + 192, + 175, + 105, + 127, + 28, + 28, + 65, + 206, + 103, + 40, + 185, + 137, + 4, + 135, + 0, + 16, + 223, + 251, + 16, + 0, + 64, + 104, + 109, + 177, + 226, + 34, + 162, + 4, + 254, + 253, + 255, + 18, + 137, + 120, + 247, + 45, + 179, + 155, + 244, + 20, + 232, + 88, + 139, + 73, + 172, + 67, + 66, + 116, + 192, + 200, + 200, + 68, + 58, + 33, + 52, + 249, + 233, + 213, + 110, + 183, + 180, + 147, + 115, + 48, + 221, + 61, + 15, + 152, + 160, + 34, + 193, + 91, + 121, + 47, + 27, + 212, + 180, + 31, + 249, + 129, + 108, + 192, + 144, + 115, + 227, + 137, + 56, + 10, + 175, + 69, + 213, + 206, + 183, + 31, + 16, + 240, + 118, + 165, + 122, + 176, + 25, + 56, + 44, + 0, + 240, + 229, + 111, + 204, + 86, + 204, + 90, + 117, + 62, + 155, + 68, + 5, + 26, + 118, + 162, + 45, + 94, + 239, + 161, + 145, + 244, + 32, + 191, + 125, + 243, + 234, + 213, + 110, + 65, + 7, + 118, + 245, + 196, + 209, + 10, + 219, + 243, + 102, + 207, + 89, + 80, + 17, + 39, + 29, + 220, + 158, + 138, + 183, + 106, + 218, + 15, + 17, + 49, + 103, + 44, + 185, + 108, + 47, + 27, + 72, + 113, + 81, + 86, + 58, + 161, + 78, + 64, + 184, + 231, + 92, + 215, + 174, + 29, + 68, + 13, + 78, + 92, + 36, + 8, + 236, + 53, + 201, + 188, + 186, + 217, + 32, + 89, + 188, + 104, + 114, + 0, + 141, + 248, + 1, + 208, + 185, + 13, + 131, + 233, + 56, + 126, + 166, + 125, + 171, + 95, + 144, + 0, + 136, + 37, + 194, + 233, + 177, + 56, + 59, + 207, + 14, + 14, + 65, + 28, + 59, + 105, + 224, + 199, + 42, + 21, + 96, + 28, + 7, + 2, + 172, + 34, + 140, + 81, + 214, + 23, + 192, + 221, + 105, + 159, + 98, + 243, + 203, + 174, + 174, + 246, + 3, + 7, + 152, + 230, + 174, + 46, + 142, + 235, + 122, + 103, + 35, + 6, + 224, + 247, + 233, + 244, + 196, + 255, + 194, + 8, + 44, + 51, + 7, + 128, + 62, + 81, + 98, + 188, + 168, + 93, + 100, + 91, + 151, + 60, + 40, + 80, + 182, + 112, + 104, + 134, + 219, + 216, + 68, + 16, + 181, + 185, + 22, + 0, + 216, + 39, + 0, + 16, + 143, + 143, + 164, + 209, + 94, + 155, + 11, + 42, + 198, + 22, + 47, + 238, + 198, + 87, + 36, + 83, + 234, + 96, + 160, + 53, + 161, + 246, + 2, + 149, + 245, + 5, + 94, + 13, + 191, + 182, + 31, + 96, + 200, + 253, + 135, + 216, + 174, + 151, + 15, + 19, + 2, + 59, + 80, + 95, + 136, + 0, + 40, + 191, + 70, + 0, + 132, + 104, + 33, + 243, + 130, + 155, + 152, + 132, + 108, + 177, + 5, + 55, + 0, + 34, + 95, + 132, + 0, + 243, + 227, + 87, + 36, + 29, + 40, + 44, + 178, + 207, + 46, + 176, + 143, + 217, + 23, + 28, + 97, + 188, + 129, + 246, + 33, + 112, + 88, + 148, + 8, + 180, + 158, + 83, + 235, + 127, + 101, + 246, + 78, + 11, + 0, + 64, + 64, + 238, + 255, + 69, + 87, + 215, + 74, + 30, + 0, + 84, + 86, + 219, + 184, + 23, + 5, + 11, + 215, + 50, + 155, + 142, + 18, + 15, + 102, + 200, + 78, + 123, + 240, + 152, + 129, + 5, + 139, + 183, + 55, + 158, + 158, + 24, + 73, + 4, + 247, + 84, + 29, + 58, + 18, + 227, + 98, + 74, + 0, + 18, + 236, + 226, + 197, + 99, + 21, + 11, + 186, + 219, + 177, + 69, + 87, + 133, + 67, + 173, + 131, + 41, + 149, + 3, + 160, + 212, + 198, + 94, + 198, + 173, + 39, + 176, + 93, + 0, + 115, + 127, + 87, + 215, + 46, + 34, + 2, + 135, + 27, + 209, + 80, + 89, + 137, + 121, + 25, + 208, + 46, + 80, + 32, + 182, + 157, + 50, + 217, + 126, + 218, + 193, + 71, + 79, + 22, + 144, + 224, + 64, + 128, + 105, + 143, + 163, + 253, + 150, + 247, + 188, + 216, + 112, + 228, + 195, + 120, + 92, + 0, + 96, + 132, + 32, + 48, + 200, + 62, + 177, + 32, + 213, + 52, + 47, + 30, + 30, + 68, + 74, + 79, + 37, + 2, + 41, + 53, + 0, + 74, + 109, + 236, + 101, + 106, + 117, + 21, + 18, + 0, + 192, + 193, + 111, + 124, + 27, + 183, + 127, + 199, + 113, + 4, + 192, + 239, + 76, + 3, + 96, + 48, + 171, + 209, + 240, + 116, + 189, + 170, + 39, + 23, + 41, + 174, + 175, + 114, + 90, + 2, + 169, + 40, + 120, + 122, + 192, + 150, + 193, + 238, + 68, + 32, + 16, + 109, + 216, + 192, + 183, + 191, + 7, + 156, + 211, + 20, + 217, + 105, + 39, + 222, + 229, + 156, + 87, + 49, + 123, + 206, + 200, + 200, + 238, + 45, + 27, + 246, + 139, + 109, + 111, + 34, + 0, + 12, + 169, + 127, + 79, + 145, + 189, + 51, + 76, + 249, + 117, + 29, + 64, + 0, + 244, + 16, + 4, + 26, + 63, + 172, + 63, + 153, + 158, + 64, + 0, + 216, + 204, + 236, + 86, + 99, + 98, + 231, + 45, + 57, + 139, + 168, + 171, + 158, + 112, + 192, + 232, + 176, + 187, + 104, + 218, + 131, + 0, + 72, + 14, + 121, + 163, + 126, + 210, + 230, + 186, + 127, + 168, + 138, + 14, + 165, + 71, + 186, + 121, + 35, + 0, + 8, + 12, + 166, + 241, + 78, + 11, + 108, + 151, + 11, + 185, + 68, + 225, + 234, + 221, + 187, + 215, + 108, + 17, + 250, + 126, + 13, + 254, + 147, + 210, + 46, + 145, + 71, + 201, + 178, + 119, + 198, + 57, + 79, + 47, + 6, + 0, + 59, + 195, + 135, + 191, + 205, + 29, + 7, + 254, + 67, + 238, + 192, + 54, + 218, + 196, + 106, + 88, + 214, + 172, + 237, + 87, + 156, + 162, + 170, + 122, + 2, + 247, + 167, + 10, + 229, + 202, + 29, + 40, + 161, + 232, + 114, + 90, + 192, + 255, + 108, + 247, + 226, + 109, + 203, + 145, + 17, + 168, + 2, + 167, + 32, + 157, + 10, + 131, + 70, + 196, + 226, + 31, + 15, + 98, + 4, + 64, + 43, + 84, + 35, + 4, + 154, + 100, + 204, + 191, + 101, + 119, + 55, + 206, + 7, + 106, + 125, + 64, + 74, + 60, + 100, + 2, + 128, + 65, + 155, + 164, + 119, + 253, + 142, + 104, + 129, + 141, + 31, + 94, + 74, + 167, + 145, + 51, + 240, + 249, + 125, + 217, + 119, + 171, + 49, + 99, + 228, + 212, + 0, + 200, + 125, + 68, + 26, + 45, + 68, + 225, + 176, + 67, + 228, + 236, + 241, + 31, + 233, + 181, + 36, + 185, + 84, + 187, + 143, + 99, + 32, + 30, + 232, + 170, + 92, + 141, + 34, + 129, + 96, + 98, + 34, + 61, + 22, + 38, + 0, + 36, + 214, + 132, + 211, + 35, + 240, + 10, + 177, + 199, + 156, + 217, + 118, + 94, + 1, + 236, + 70, + 109, + 95, + 211, + 74, + 116, + 160, + 214, + 123, + 85, + 1, + 96, + 196, + 175, + 24, + 116, + 142, + 200, + 192, + 198, + 207, + 128, + 5, + 176, + 12, + 252, + 230, + 71, + 25, + 87, + 195, + 194, + 100, + 66, + 3, + 200, + 1, + 208, + 245, + 17, + 209, + 74, + 48, + 78, + 127, + 114, + 97, + 190, + 133, + 1, + 9, + 224, + 192, + 89, + 111, + 111, + 239, + 122, + 97, + 117, + 37, + 182, + 130, + 193, + 248, + 4, + 8, + 63, + 66, + 32, + 182, + 43, + 158, + 72, + 199, + 145, + 12, + 116, + 117, + 53, + 60, + 193, + 246, + 242, + 8, + 44, + 225, + 255, + 33, + 55, + 64, + 235, + 4, + 170, + 0, + 208, + 115, + 220, + 17, + 249, + 251, + 36, + 37, + 112, + 120, + 99, + 234, + 2, + 145, + 129, + 47, + 109, + 89, + 87, + 195, + 50, + 67, + 10, + 53, + 161, + 55, + 181, + 194, + 3, + 44, + 64, + 247, + 206, + 95, + 27, + 179, + 4, + 162, + 120, + 222, + 163, + 175, + 93, + 30, + 9, + 4, + 7, + 201, + 102, + 91, + 107, + 208, + 86, + 51, + 35, + 251, + 19, + 61, + 130, + 251, + 74, + 16, + 216, + 45, + 168, + 128, + 214, + 120, + 150, + 4, + 142, + 151, + 223, + 253, + 64, + 143, + 2, + 92, + 31, + 247, + 197, + 239, + 136, + 55, + 188, + 177, + 231, + 195, + 244, + 5, + 108, + 8, + 191, + 188, + 207, + 112, + 53, + 172, + 28, + 168, + 22, + 135, + 196, + 194, + 174, + 35, + 148, + 102, + 76, + 193, + 133, + 203, + 112, + 184, + 233, + 225, + 55, + 44, + 124, + 186, + 226, + 28, + 116, + 178, + 60, + 18, + 8, + 38, + 208, + 62, + 83, + 137, + 48, + 50, + 134, + 187, + 18, + 49, + 49, + 64, + 80, + 217, + 193, + 96, + 150, + 7, + 241, + 234, + 237, + 125, + 34, + 144, + 47, + 0, + 63, + 204, + 179, + 192, + 202, + 174, + 227, + 233, + 48, + 14, + 9, + 126, + 243, + 215, + 70, + 171, + 97, + 137, + 100, + 26, + 27, + 158, + 17, + 40, + 241, + 192, + 19, + 180, + 31, + 217, + 130, + 238, + 59, + 119, + 199, + 5, + 0, + 82, + 44, + 68, + 2, + 207, + 73, + 0, + 0, + 4, + 168, + 82, + 34, + 157, + 128, + 144, + 232, + 74, + 88, + 66, + 0, + 152, + 64, + 6, + 65, + 171, + 241, + 16, + 50, + 33, + 164, + 4, + 141, + 141, + 22, + 2, + 128, + 247, + 133, + 54, + 190, + 255, + 254, + 213, + 120, + 250, + 99, + 60, + 134, + 80, + 201, + 100, + 1, + 192, + 154, + 229, + 87, + 17, + 225, + 81, + 3, + 126, + 238, + 48, + 37, + 30, + 48, + 33, + 13, + 136, + 49, + 108, + 8, + 231, + 231, + 139, + 0, + 112, + 202, + 72, + 128, + 80, + 119, + 10, + 52, + 0, + 138, + 137, + 18, + 113, + 65, + 10, + 186, + 88, + 25, + 23, + 180, + 102, + 11, + 224, + 51, + 15, + 253, + 33, + 233, + 251, + 140, + 71, + 160, + 172, + 235, + 248, + 96, + 122, + 43, + 78, + 152, + 217, + 104, + 198, + 71, + 101, + 186, + 206, + 154, + 229, + 87, + 17, + 97, + 205, + 99, + 37, + 107, + 88, + 83, + 226, + 1, + 147, + 84, + 160, + 203, + 38, + 147, + 34, + 0, + 169, + 158, + 85, + 90, + 0, + 186, + 194, + 0, + 0, + 222, + 112, + 176, + 91, + 206, + 4, + 92, + 24, + 143, + 13, + 119, + 183, + 102, + 147, + 128, + 204, + 0, + 120, + 147, + 169, + 145, + 84, + 138, + 15, + 8, + 190, + 221, + 213, + 146, + 26, + 27, + 33, + 195, + 40, + 247, + 249, + 82, + 209, + 118, + 227, + 75, + 77, + 5, + 123, + 232, + 156, + 74, + 155, + 77, + 171, + 130, + 93, + 146, + 8, + 249, + 185, + 222, + 106, + 9, + 128, + 212, + 166, + 213, + 242, + 124, + 24, + 239, + 15, + 167, + 227, + 131, + 97, + 180, + 239, + 220, + 88, + 56, + 28, + 150, + 62, + 102, + 123, + 82, + 231, + 226, + 225, + 238, + 112, + 214, + 12, + 78, + 102, + 14, + 104, + 239, + 29, + 140, + 167, + 142, + 19, + 22, + 216, + 177, + 43, + 152, + 30, + 57, + 182, + 163, + 28, + 143, + 26, + 44, + 227, + 82, + 169, + 33, + 86, + 39, + 203, + 192, + 111, + 65, + 106, + 98, + 74, + 149, + 85, + 247, + 83, + 112, + 254, + 236, + 210, + 120, + 97, + 32, + 56, + 255, + 113, + 25, + 0, + 254, + 23, + 86, + 85, + 170, + 218, + 31, + 156, + 72, + 133, + 227, + 169, + 116, + 56, + 30, + 79, + 143, + 77, + 140, + 176, + 50, + 4, + 128, + 123, + 163, + 108, + 32, + 107, + 246, + 5, + 1, + 144, + 193, + 108, + 31, + 73, + 199, + 69, + 53, + 248, + 109, + 224, + 182, + 250, + 250, + 50, + 60, + 138, + 242, + 203, + 82, + 36, + 148, + 58, + 23, + 32, + 251, + 248, + 35, + 235, + 143, + 76, + 76, + 169, + 210, + 55, + 190, + 16, + 0, + 202, + 135, + 17, + 217, + 40, + 43, + 1, + 112, + 206, + 231, + 251, + 145, + 91, + 5, + 64, + 24, + 111, + 60, + 136, + 66, + 34, + 120, + 21, + 23, + 210, + 89, + 93, + 61, + 236, + 122, + 159, + 239, + 135, + 102, + 22, + 65, + 69, + 0, + 24, + 57, + 2, + 64, + 135, + 32, + 12, + 15, + 127, + 192, + 123, + 131, + 35, + 233, + 32, + 42, + 51, + 197, + 9, + 227, + 109, + 117, + 40, + 149, + 24, + 208, + 48, + 16, + 118, + 17, + 107, + 205, + 76, + 169, + 178, + 89, + 149, + 203, + 135, + 96, + 114, + 105, + 135, + 211, + 37, + 0, + 146, + 62, + 127, + 109, + 173, + 10, + 128, + 4, + 248, + 129, + 241, + 48, + 7, + 93, + 95, + 23, + 12, + 174, + 17, + 0, + 240, + 250, + 252, + 248, + 1, + 168, + 172, + 79, + 65, + 56, + 192, + 184, + 158, + 105, + 207, + 8, + 90, + 142, + 227, + 42, + 41, + 24, + 74, + 39, + 186, + 126, + 94, + 127, + 244, + 18, + 241, + 6, + 182, + 85, + 202, + 74, + 10, + 36, + 2, + 0, + 106, + 77, + 166, + 62, + 43, + 181, + 155, + 129, + 121, + 116, + 214, + 225, + 146, + 0, + 136, + 85, + 85, + 53, + 28, + 137, + 199, + 130, + 114, + 9, + 72, + 143, + 12, + 6, + 24, + 175, + 15, + 13, + 21, + 212, + 185, + 249, + 246, + 31, + 192, + 15, + 34, + 30, + 50, + 19, + 238, + 66, + 171, + 234, + 67, + 89, + 124, + 218, + 16, + 31, + 73, + 196, + 70, + 82, + 120, + 140, + 44, + 190, + 129, + 97, + 142, + 130, + 71, + 76, + 20, + 225, + 187, + 184, + 170, + 66, + 229, + 103, + 209, + 15, + 204, + 181, + 62, + 12, + 110, + 45, + 157, + 53, + 245, + 169, + 55, + 147, + 156, + 118, + 232, + 204, + 207, + 32, + 0, + 64, + 63, + 199, + 143, + 4, + 162, + 201, + 81, + 188, + 215, + 172, + 136, + 65, + 56, + 29, + 110, + 199, + 131, + 215, + 93, + 7, + 188, + 13, + 110, + 55, + 242, + 19, + 249, + 1, + 144, + 204, + 0, + 176, + 177, + 211, + 3, + 177, + 24, + 43, + 0, + 160, + 126, + 22, + 121, + 124, + 138, + 166, + 96, + 52, + 124, + 248, + 251, + 223, + 66, + 76, + 124, + 234, + 4, + 195, + 180, + 92, + 73, + 167, + 199, + 203, + 73, + 142, + 184, + 4, + 35, + 192, + 75, + 129, + 251, + 222, + 90, + 212, + 169, + 149, + 235, + 169, + 127, + 180, + 206, + 181, + 90, + 43, + 179, + 201, + 128, + 222, + 234, + 33, + 78, + 189, + 249, + 41, + 150, + 64, + 87, + 207, + 57, + 188, + 201, + 104, + 116, + 207, + 158, + 61, + 135, + 130, + 225, + 4, + 216, + 188, + 17, + 164, + 225, + 99, + 225, + 32, + 0, + 192, + 111, + 13, + 226, + 99, + 200, + 128, + 17, + 107, + 98, + 236, + 146, + 59, + 43, + 80, + 148, + 145, + 206, + 151, + 213, + 243, + 104, + 103, + 229, + 108, + 40, + 43, + 251, + 244, + 212, + 169, + 83, + 63, + 223, + 202, + 124, + 8, + 79, + 114, + 154, + 32, + 240, + 121, + 81, + 15, + 146, + 76, + 22, + 155, + 154, + 82, + 193, + 157, + 162, + 144, + 91, + 251, + 240, + 3, + 84, + 150, + 135, + 208, + 42, + 30, + 143, + 110, + 251, + 25, + 11, + 3, + 156, + 62, + 6, + 162, + 30, + 254, + 225, + 63, + 8, + 161, + 80, + 42, + 45, + 236, + 189, + 155, + 78, + 200, + 212, + 144, + 159, + 5, + 202, + 58, + 134, + 31, + 19, + 155, + 127, + 118, + 0, + 129, + 17, + 31, + 32, + 218, + 156, + 150, + 126, + 93, + 103, + 12, + 99, + 37, + 106, + 255, + 169, + 83, + 27, + 183, + 50, + 39, + 225, + 119, + 143, + 23, + 144, + 52, + 249, + 47, + 183, + 158, + 59, + 183, + 189, + 188, + 92, + 33, + 203, + 184, + 229, + 84, + 214, + 113, + 116, + 13, + 7, + 160, + 74, + 124, + 189, + 19, + 1, + 128, + 224, + 68, + 26, + 153, + 248, + 213, + 210, + 160, + 72, + 176, + 59, + 1, + 50, + 1, + 129, + 96, + 122, + 196, + 228, + 88, + 141, + 64, + 1, + 212, + 254, + 56, + 252, + 119, + 54, + 134, + 218, + 207, + 68, + 79, + 35, + 36, + 122, + 149, + 39, + 233, + 196, + 167, + 181, + 239, + 96, + 0, + 78, + 173, + 116, + 215, + 165, + 78, + 28, + 171, + 175, + 183, + 146, + 49, + 212, + 207, + 203, + 17, + 51, + 124, + 110, + 180, + 26, + 86, + 6, + 162, + 213, + 17, + 136, + 126, + 255, + 99, + 0, + 70, + 112, + 251, + 15, + 172, + 126, + 90, + 30, + 9, + 160, + 138, + 149, + 20, + 88, + 0, + 214, + 159, + 203, + 202, + 135, + 66, + 247, + 15, + 240, + 12, + 192, + 120, + 79, + 35, + 4, + 98, + 140, + 194, + 1, + 211, + 141, + 79, + 223, + 39, + 8, + 84, + 51, + 107, + 208, + 130, + 5, + 141, + 203, + 126, + 41, + 27, + 71, + 255, + 101, + 165, + 116, + 49, + 37, + 30, + 50, + 147, + 10, + 0, + 163, + 217, + 196, + 180, + 101, + 79, + 55, + 25, + 23, + 66, + 145, + 64, + 157, + 4, + 192, + 1, + 47, + 14, + 147, + 123, + 15, + 29, + 49, + 207, + 3, + 129, + 129, + 179, + 137, + 211, + 146, + 4, + 32, + 37, + 200, + 13, + 182, + 54, + 4, + 17, + 0, + 180, + 195, + 197, + 216, + 93, + 178, + 86, + 168, + 107, + 222, + 214, + 236, + 32, + 8, + 108, + 45, + 248, + 41, + 74, + 145, + 126, + 80, + 90, + 36, + 13, + 164, + 127, + 190, + 74, + 93, + 17, + 146, + 107, + 250, + 156, + 54, + 154, + 152, + 84, + 101, + 129, + 136, + 15, + 107, + 125, + 247, + 234, + 23, + 54, + 211, + 50, + 19, + 216, + 14, + 161, + 26, + 23, + 237, + 61, + 114, + 196, + 116, + 229, + 78, + 44, + 142, + 26, + 61, + 16, + 71, + 93, + 94, + 211, + 20, + 59, + 132, + 74, + 144, + 26, + 236, + 118, + 135, + 131, + 127, + 4, + 84, + 181, + 42, + 172, + 0, + 45, + 30, + 36, + 90, + 115, + 24, + 3, + 176, + 235, + 175, + 152, + 70, + 240, + 6, + 210, + 31, + 210, + 18, + 19, + 124, + 190, + 76, + 29, + 0, + 100, + 94, + 34, + 74, + 67, + 198, + 115, + 211, + 156, + 150, + 9, + 52, + 36, + 2, + 202, + 239, + 149, + 7, + 159, + 126, + 238, + 53, + 121, + 40, + 224, + 227, + 2, + 94, + 63, + 123, + 164, + 215, + 100, + 205, + 74, + 59, + 102, + 123, + 6, + 47, + 179, + 222, + 78, + 87, + 57, + 92, + 168, + 229, + 180, + 211, + 30, + 166, + 101, + 206, + 135, + 176, + 6, + 28, + 37, + 30, + 100, + 180, + 242, + 247, + 208, + 254, + 79, + 127, + 187, + 113, + 141, + 247, + 232, + 85, + 240, + 142, + 90, + 192, + 131, + 218, + 203, + 215, + 83, + 124, + 188, + 140, + 86, + 114, + 128, + 113, + 146, + 65, + 143, + 104, + 131, + 73, + 186, + 232, + 43, + 75, + 16, + 187, + 54, + 93, + 7, + 190, + 79, + 202, + 163, + 160, + 235, + 5, + 0, + 114, + 210, + 127, + 188, + 244, + 15, + 28, + 114, + 224, + 181, + 199, + 241, + 242, + 175, + 85, + 46, + 123, + 69, + 171, + 130, + 129, + 92, + 120, + 230, + 190, + 195, + 249, + 4, + 122, + 67, + 169, + 239, + 113, + 224, + 212, + 169, + 223, + 131, + 71, + 116, + 247, + 93, + 117, + 187, + 222, + 79, + 167, + 175, + 180, + 48, + 237, + 7, + 86, + 45, + 187, + 143, + 48, + 65, + 193, + 92, + 101, + 81, + 152, + 53, + 203, + 243, + 200, + 57, + 68, + 91, + 47, + 40, + 35, + 75, + 23, + 121, + 66, + 250, + 193, + 7, + 159, + 123, + 142, + 127, + 14, + 30, + 129, + 44, + 63, + 33, + 167, + 118, + 196, + 248, + 136, + 5, + 154, + 28, + 221, + 241, + 24, + 230, + 25, + 36, + 115, + 180, + 189, + 53, + 22, + 67, + 85, + 70, + 78, + 178, + 16, + 115, + 21, + 89, + 146, + 27, + 254, + 232, + 175, + 127, + 183, + 235, + 20, + 118, + 138, + 255, + 226, + 174, + 234, + 75, + 96, + 12, + 47, + 181, + 128, + 137, + 102, + 126, + 244, + 49, + 65, + 224, + 147, + 162, + 101, + 114, + 8, + 172, + 70, + 143, + 66, + 134, + 21, + 30, + 120, + 88, + 58, + 217, + 227, + 200, + 180, + 160, + 132, + 80, + 35, + 132, + 106, + 68, + 127, + 204, + 191, + 230, + 57, + 32, + 91, + 171, + 37, + 130, + 238, + 7, + 229, + 151, + 56, + 27, + 174, + 177, + 215, + 156, + 29, + 136, + 113, + 14, + 180, + 208, + 151, + 179, + 102, + 61, + 21, + 59, + 27, + 179, + 87, + 52, + 245, + 186, + 28, + 104, + 137, + 89, + 135, + 11, + 55, + 27, + 61, + 153, + 83, + 71, + 38, + 225, + 247, + 220, + 124, + 110, + 224, + 235, + 247, + 128, + 71, + 56, + 241, + 105, + 253, + 110, + 232, + 11, + 102, + 153, + 80, + 101, + 247, + 121, + 145, + 172, + 91, + 75, + 141, + 100, + 128, + 12, + 43, + 216, + 100, + 57, + 85, + 35, + 3, + 72, + 72, + 0, + 224, + 21, + 0, + 224, + 21, + 254, + 245, + 1, + 148, + 140, + 48, + 191, + 249, + 17, + 145, + 254, + 112, + 83, + 141, + 179, + 2, + 248, + 31, + 58, + 60, + 214, + 228, + 168, + 169, + 168, + 176, + 55, + 57, + 192, + 44, + 0, + 48, + 77, + 200, + 9, + 116, + 56, + 232, + 42, + 162, + 138, + 112, + 17, + 23, + 216, + 3, + 81, + 49, + 181, + 15, + 12, + 4, + 24, + 118, + 0, + 213, + 166, + 59, + 151, + 16, + 4, + 254, + 252, + 59, + 45, + 87, + 143, + 173, + 92, + 185, + 178, + 112, + 59, + 200, + 103, + 229, + 125, + 66, + 129, + 217, + 54, + 19, + 202, + 95, + 51, + 172, + 224, + 201, + 92, + 80, + 45, + 0, + 240, + 28, + 0, + 240, + 63, + 228, + 95, + 152, + 222, + 252, + 168, + 247, + 244, + 217, + 193, + 214, + 38, + 123, + 205, + 238, + 134, + 216, + 233, + 32, + 64, + 16, + 27, + 136, + 67, + 147, + 155, + 226, + 206, + 138, + 110, + 2, + 13, + 118, + 8, + 92, + 18, + 211, + 123, + 156, + 104, + 114, + 164, + 244, + 137, + 255, + 44, + 98, + 160, + 1, + 228, + 49, + 58, + 239, + 249, + 58, + 143, + 192, + 83, + 251, + 241, + 228, + 194, + 93, + 93, + 72, + 68, + 87, + 149, + 243, + 38, + 81, + 25, + 223, + 233, + 5, + 60, + 218, + 77, + 54, + 140, + 21, + 160, + 2, + 128, + 213, + 200, + 8, + 200, + 191, + 48, + 187, + 208, + 52, + 210, + 126, + 173, + 53, + 173, + 113, + 104, + 193, + 64, + 47, + 93, + 21, + 24, + 136, + 215, + 216, + 29, + 85, + 235, + 99, + 3, + 53, + 208, + 114, + 248, + 50, + 129, + 151, + 109, + 166, + 21, + 15, + 235, + 66, + 171, + 29, + 161, + 79, + 208, + 204, + 64, + 28, + 59, + 116, + 55, + 57, + 223, + 232, + 6, + 165, + 233, + 185, + 231, + 47, + 4, + 4, + 156, + 208, + 254, + 75, + 151, + 90, + 176, + 142, + 166, + 151, + 217, + 176, + 42, + 216, + 171, + 112, + 129, + 221, + 58, + 131, + 164, + 224, + 93, + 254, + 200, + 42, + 31, + 86, + 112, + 100, + 169, + 168, + 23, + 0, + 32, + 70, + 64, + 5, + 128, + 137, + 200, + 155, + 3, + 237, + 23, + 182, + 199, + 207, + 246, + 114, + 16, + 247, + 208, + 85, + 206, + 67, + 65, + 59, + 252, + 228, + 166, + 205, + 76, + 140, + 183, + 11, + 113, + 189, + 171, + 104, + 59, + 222, + 187, + 3, + 44, + 165, + 195, + 225, + 26, + 168, + 169, + 176, + 87, + 212, + 160, + 119, + 40, + 87, + 123, + 23, + 153, + 76, + 115, + 248, + 166, + 197, + 246, + 122, + 80, + 133, + 87, + 63, + 228, + 47, + 41, + 221, + 198, + 123, + 68, + 242, + 219, + 104, + 215, + 166, + 5, + 7, + 211, + 74, + 201, + 135, + 21, + 208, + 100, + 220, + 76, + 105, + 116, + 11, + 17, + 114, + 153, + 17, + 224, + 201, + 76, + 209, + 81, + 128, + 67, + 142, + 127, + 83, + 69, + 24, + 115, + 57, + 131, + 54, + 31, + 112, + 56, + 60, + 85, + 78, + 215, + 179, + 224, + 231, + 6, + 101, + 14, + 161, + 154, + 208, + 98, + 128, + 100, + 233, + 117, + 52, + 70, + 229, + 168, + 105, + 170, + 113, + 160, + 5, + 250, + 113, + 137, + 231, + 44, + 1, + 129, + 77, + 40, + 54, + 78, + 167, + 5, + 4, + 108, + 122, + 195, + 231, + 154, + 122, + 65, + 136, + 20, + 209, + 226, + 27, + 148, + 248, + 1, + 174, + 158, + 213, + 123, + 120, + 94, + 201, + 89, + 214, + 111, + 238, + 5, + 187, + 37, + 55, + 2, + 60, + 101, + 217, + 252, + 168, + 61, + 24, + 27, + 72, + 160, + 6, + 182, + 58, + 226, + 103, + 99, + 138, + 86, + 122, + 92, + 216, + 207, + 133, + 72, + 48, + 62, + 32, + 64, + 163, + 36, + 143, + 67, + 62, + 37, + 216, + 131, + 108, + 132, + 211, + 89, + 133, + 192, + 112, + 185, + 28, + 51, + 239, + 230, + 17, + 216, + 220, + 130, + 172, + 97, + 250, + 40, + 169, + 238, + 94, + 166, + 85, + 2, + 186, + 219, + 10, + 96, + 135, + 129, + 146, + 127, + 162, + 207, + 2, + 188, + 146, + 179, + 4, + 27, + 80, + 43, + 218, + 101, + 70, + 64, + 32, + 74, + 103, + 173, + 46, + 145, + 216, + 10, + 187, + 189, + 6, + 91, + 126, + 187, + 189, + 233, + 172, + 238, + 24, + 57, + 195, + 190, + 17, + 12, + 170, + 35, + 65, + 66, + 180, + 203, + 35, + 215, + 76, + 85, + 14, + 59, + 63, + 5, + 202, + 227, + 116, + 84, + 85, + 221, + 38, + 32, + 240, + 90, + 203, + 133, + 116, + 250, + 100, + 125, + 61, + 70, + 64, + 23, + 0, + 157, + 220, + 231, + 92, + 225, + 231, + 179, + 16, + 175, + 228, + 44, + 103, + 131, + 3, + 241, + 193, + 179, + 175, + 169, + 141, + 0, + 147, + 57, + 238, + 10, + 84, + 216, + 91, + 227, + 21, + 224, + 243, + 87, + 84, + 56, + 237, + 78, + 157, + 225, + 1, + 116, + 29, + 253, + 132, + 179, + 166, + 187, + 23, + 47, + 93, + 154, + 153, + 104, + 112, + 20, + 248, + 147, + 122, + 89, + 127, + 59, + 115, + 27, + 239, + 15, + 220, + 180, + 179, + 225, + 12, + 94, + 189, + 109, + 63, + 124, + 243, + 194, + 39, + 230, + 170, + 168, + 180, + 143, + 237, + 210, + 76, + 203, + 198, + 167, + 16, + 37, + 103, + 57, + 27, + 235, + 61, + 205, + 177, + 26, + 35, + 160, + 123, + 39, + 145, + 2, + 187, + 155, + 90, + 43, + 104, + 187, + 19, + 148, + 57, + 27, + 220, + 173, + 103, + 103, + 208, + 117, + 244, + 19, + 246, + 138, + 112, + 76, + 59, + 43, + 92, + 75, + 158, + 42, + 212, + 253, + 222, + 134, + 134, + 166, + 166, + 26, + 59, + 136, + 211, + 109, + 164, + 112, + 98, + 199, + 77, + 180, + 23, + 215, + 15, + 93, + 58, + 238, + 101, + 106, + 145, + 67, + 180, + 45, + 235, + 173, + 244, + 238, + 174, + 107, + 8, + 121, + 37, + 103, + 65, + 66, + 238, + 213, + 26, + 1, + 38, + 35, + 0, + 193, + 138, + 65, + 224, + 237, + 42, + 82, + 150, + 109, + 188, + 146, + 37, + 112, + 118, + 171, + 62, + 252, + 122, + 228, + 173, + 169, + 104, + 106, + 109, + 114, + 217, + 145, + 47, + 59, + 133, + 32, + 80, + 182, + 146, + 102, + 32, + 58, + 6, + 65, + 56, + 211, + 194, + 32, + 51, + 240, + 75, + 237, + 64, + 99, + 109, + 182, + 73, + 180, + 180, + 193, + 96, + 42, + 81, + 114, + 22, + 16, + 114, + 118, + 192, + 167, + 49, + 2, + 25, + 137, + 181, + 135, + 73, + 150, + 195, + 248, + 71, + 105, + 228, + 242, + 33, + 59, + 103, + 188, + 20, + 190, + 154, + 2, + 103, + 207, + 114, + 3, + 12, + 253, + 162, + 29, + 124, + 217, + 245, + 127, + 86, + 198, + 15, + 151, + 212, + 50, + 251, + 143, + 33, + 85, + 120, + 229, + 67, + 148, + 28, + 250, + 132, + 216, + 65, + 239, + 59, + 89, + 72, + 118, + 91, + 227, + 133, + 213, + 40, + 164, + 228, + 44, + 20, + 218, + 27, + 253, + 53, + 0, + 224, + 199, + 6, + 167, + 105, + 137, + 109, + 106, + 61, + 155, + 25, + 0, + 180, + 207, + 136, + 211, + 229, + 82, + 237, + 198, + 147, + 141, + 218, + 113, + 252, + 249, + 140, + 235, + 153, + 222, + 96, + 128, + 190, + 137, + 47, + 28, + 248, + 182, + 155, + 105, + 56, + 137, + 51, + 148, + 133, + 146, + 29, + 124, + 167, + 231, + 252, + 197, + 12, + 116, + 190, + 71, + 66, + 128, + 54, + 14, + 133, + 40, + 116, + 176, + 80, + 49, + 191, + 50, + 18, + 200, + 78, + 193, + 10, + 100, + 56, + 178, + 229, + 9, + 240, + 6, + 76, + 224, + 220, + 184, + 170, + 114, + 42, + 117, + 0, + 223, + 171, + 97, + 224, + 244, + 0, + 237, + 184, + 137, + 175, + 162, + 252, + 246, + 90, + 134, + 249, + 16, + 60, + 130, + 147, + 24, + 128, + 251, + 150, + 189, + 0, + 183, + 123, + 39, + 99, + 251, + 1, + 1, + 9, + 128, + 12, + 43, + 235, + 81, + 232, + 96, + 193, + 71, + 77, + 36, + 144, + 137, + 2, + 224, + 248, + 36, + 194, + 167, + 201, + 27, + 65, + 1, + 232, + 55, + 146, + 246, + 120, + 80, + 8, + 12, + 114, + 128, + 215, + 62, + 174, + 50, + 49, + 211, + 21, + 28, + 47, + 79, + 239, + 217, + 196, + 233, + 88, + 195, + 15, + 26, + 249, + 210, + 129, + 53, + 12, + 211, + 114, + 230, + 82, + 253, + 18, + 62, + 46, + 222, + 182, + 140, + 126, + 39, + 115, + 251, + 47, + 94, + 124, + 199, + 45, + 60, + 144, + 195, + 120, + 189, + 97, + 220, + 116, + 226, + 10, + 63, + 173, + 99, + 4, + 228, + 212, + 219, + 59, + 112, + 182, + 162, + 162, + 34, + 140, + 123, + 189, + 117, + 119, + 156, + 56, + 120, + 52, + 74, + 181, + 224, + 77, + 79, + 156, + 153, + 153, + 157, + 44, + 0, + 2, + 241, + 95, + 70, + 167, + 148, + 16, + 242, + 189, + 60, + 103, + 7, + 226, + 30, + 186, + 106, + 169, + 80, + 60, + 1, + 170, + 144, + 249, + 121, + 125, + 153, + 56, + 235, + 104, + 89, + 118, + 0, + 74, + 121, + 39, + 57, + 203, + 210, + 138, + 2, + 0, + 122, + 70, + 64, + 70, + 120, + 168, + 35, + 30, + 107, + 173, + 104, + 5, + 171, + 30, + 176, + 199, + 79, + 227, + 8, + 15, + 140, + 128, + 48, + 95, + 17, + 26, + 150, + 69, + 215, + 129, + 135, + 227, + 180, + 103, + 123, + 24, + 68, + 216, + 247, + 98, + 79, + 163, + 210, + 19, + 215, + 221, + 59, + 4, + 49, + 0, + 229, + 95, + 215, + 88, + 40, + 0, + 240, + 46, + 15, + 64, + 79, + 79, + 207, + 137, + 139, + 23, + 251, + 91, + 250, + 209, + 155, + 225, + 174, + 139, + 253, + 240, + 65, + 79, + 63, + 225, + 128, + 130, + 101, + 182, + 242, + 109, + 69, + 203, + 150, + 102, + 91, + 112, + 26, + 3, + 160, + 141, + 4, + 20, + 196, + 138, + 35, + 29, + 142, + 214, + 1, + 174, + 169, + 105, + 16, + 15, + 250, + 32, + 35, + 151, + 109, + 237, + 78, + 57, + 33, + 119, + 207, + 148, + 69, + 160, + 196, + 131, + 107, + 165, + 192, + 3, + 72, + 12, + 152, + 159, + 22, + 9, + 217, + 33, + 2, + 192, + 240, + 87, + 110, + 191, + 253, + 241, + 139, + 61, + 55, + 255, + 229, + 205, + 61, + 240, + 238, + 111, + 190, + 114, + 177, + 238, + 246, + 219, + 111, + 255, + 74, + 53, + 6, + 192, + 182, + 23, + 159, + 249, + 193, + 134, + 108, + 187, + 153, + 96, + 0, + 50, + 27, + 1, + 60, + 214, + 49, + 120, + 54, + 145, + 56, + 59, + 208, + 10, + 109, + 174, + 24, + 224, + 67, + 252, + 172, + 145, + 166, + 154, + 170, + 28, + 14, + 51, + 128, + 81, + 226, + 129, + 97, + 214, + 240, + 67, + 231, + 135, + 113, + 64, + 224, + 221, + 255, + 206, + 55, + 113, + 142, + 148, + 0, + 112, + 226, + 86, + 116, + 252, + 203, + 234, + 139, + 239, + 220, + 126, + 241, + 98, + 203, + 237, + 55, + 163, + 119, + 253, + 55, + 15, + 99, + 0, + 8, + 82, + 61, + 63, + 253, + 224, + 203, + 119, + 51, + 215, + 31, + 99, + 0, + 50, + 27, + 1, + 212, + 96, + 28, + 246, + 12, + 180, + 54, + 57, + 80, + 206, + 11, + 236, + 53, + 142, + 165, + 60, + 246, + 170, + 44, + 9, + 35, + 245, + 160, + 138, + 203, + 97, + 98, + 198, + 63, + 37, + 30, + 128, + 182, + 226, + 242, + 137, + 141, + 63, + 61, + 117, + 170, + 5, + 49, + 65, + 3, + 25, + 58, + 38, + 0, + 116, + 221, + 186, + 21, + 204, + 1, + 116, + 255, + 240, + 205, + 23, + 251, + 111, + 237, + 255, + 10, + 250, + 236, + 241, + 133, + 23, + 37, + 0, + 78, + 252, + 116, + 255, + 255, + 70, + 213, + 167, + 25, + 195, + 97, + 116, + 200, + 108, + 4, + 184, + 179, + 97, + 62, + 180, + 63, + 221, + 234, + 116, + 116, + 3, + 16, + 113, + 228, + 72, + 2, + 75, + 59, + 179, + 37, + 140, + 180, + 163, + 74, + 154, + 125, + 234, + 50, + 16, + 126, + 110, + 247, + 183, + 55, + 238, + 216, + 88, + 246, + 193, + 169, + 83, + 167, + 14, + 175, + 92, + 195, + 7, + 197, + 60, + 0, + 253, + 143, + 175, + 189, + 253, + 214, + 139, + 55, + 131, + 208, + 127, + 229, + 226, + 237, + 45, + 23, + 17, + 0, + 231, + 111, + 238, + 151, + 0, + 248, + 205, + 150, + 159, + 158, + 192, + 167, + 91, + 51, + 252, + 8, + 6, + 32, + 139, + 17, + 128, + 46, + 63, + 61, + 128, + 178, + 59, + 103, + 195, + 221, + 21, + 177, + 248, + 217, + 112, + 47, + 10, + 37, + 160, + 251, + 179, + 38, + 140, + 180, + 0, + 208, + 235, + 209, + 142, + 100, + 56, + 117, + 235, + 201, + 166, + 56, + 121, + 90, + 178, + 114, + 139, + 189, + 6, + 15, + 154, + 28, + 61, + 192, + 44, + 251, + 92, + 2, + 0, + 209, + 87, + 250, + 111, + 237, + 1, + 190, + 127, + 231, + 230, + 199, + 31, + 255, + 202, + 218, + 139, + 23, + 215, + 254, + 37, + 111, + 5, + 190, + 252, + 188, + 124, + 217, + 11, + 149, + 15, + 148, + 139, + 3, + 43, + 25, + 1, + 200, + 98, + 4, + 8, + 129, + 167, + 26, + 70, + 201, + 187, + 4, + 86, + 0, + 79, + 129, + 73, + 203, + 158, + 48, + 210, + 2, + 128, + 66, + 144, + 103, + 237, + 142, + 23, + 55, + 87, + 57, + 157, + 230, + 182, + 156, + 64, + 171, + 101, + 219, + 143, + 147, + 113, + 179, + 83, + 191, + 91, + 137, + 151, + 229, + 224, + 149, + 32, + 48, + 193, + 87, + 206, + 255, + 205, + 227, + 23, + 171, + 111, + 239, + 217, + 186, + 117, + 43, + 82, + 127, + 88, + 29, + 98, + 37, + 184, + 138, + 70, + 243, + 135, + 233, + 101, + 216, + 118, + 22, + 105, + 238, + 138, + 22, + 181, + 5, + 238, + 253, + 206, + 83, + 24, + 128, + 7, + 201, + 148, + 233, + 44, + 20, + 59, + 11, + 234, + 159, + 29, + 136, + 159, + 109, + 71, + 254, + 165, + 195, + 204, + 44, + 53, + 45, + 0, + 56, + 10, + 135, + 48, + 17, + 173, + 1, + 110, + 206, + 67, + 68, + 190, + 134, + 235, + 196, + 41, + 129, + 14, + 223, + 107, + 43, + 47, + 34, + 0, + 84, + 223, + 124, + 235, + 205, + 213, + 32, + 253, + 183, + 146, + 102, + 131, + 8, + 84, + 223, + 46, + 248, + 1, + 140, + 144, + 13, + 197, + 44, + 163, + 137, + 162, + 239, + 249, + 90, + 93, + 221, + 254, + 27, + 143, + 31, + 255, + 238, + 119, + 17, + 0, + 166, + 35, + 129, + 40, + 10, + 0, + 118, + 7, + 161, + 83, + 92, + 230, + 118, + 203, + 214, + 7, + 0, + 56, + 135, + 118, + 152, + 114, + 10, + 48, + 85, + 65, + 75, + 182, + 226, + 113, + 51, + 66, + 159, + 117, + 173, + 228, + 69, + 224, + 124, + 191, + 192, + 9, + 26, + 71, + 8, + 93, + 136, + 245, + 45, + 206, + 163, + 148, + 51, + 175, + 241, + 104, + 215, + 181, + 180, + 124, + 120, + 252, + 248, + 153, + 178, + 63, + 135, + 24, + 243, + 198, + 116, + 186, + 123, + 158, + 133, + 9, + 133, + 94, + 121, + 240, + 191, + 239, + 219, + 103, + 106, + 183, + 242, + 129, + 179, + 3, + 177, + 128, + 131, + 232, + 49, + 42, + 83, + 194, + 200, + 16, + 0, + 49, + 213, + 248, + 162, + 9, + 109, + 200, + 243, + 8, + 90, + 15, + 140, + 94, + 179, + 81, + 130, + 224, + 84, + 118, + 79, + 16, + 93, + 140, + 183, + 123, + 124, + 247, + 203, + 47, + 255, + 247, + 159, + 89, + 44, + 55, + 237, + 255, + 240, + 248, + 201, + 163, + 101, + 101, + 95, + 175, + 72, + 143, + 204, + 156, + 233, + 56, + 250, + 39, + 245, + 245, + 103, + 0, + 128, + 196, + 76, + 75, + 36, + 18, + 217, + 247, + 252, + 235, + 251, + 126, + 33, + 174, + 251, + 153, + 137, + 144, + 75, + 24, + 115, + 100, + 24, + 224, + 204, + 10, + 128, + 140, + 115, + 64, + 184, + 179, + 4, + 203, + 188, + 201, + 228, + 211, + 14, + 91, + 187, + 62, + 21, + 1, + 200, + 26, + 12, + 157, + 57, + 246, + 210, + 99, + 141, + 199, + 206, + 156, + 193, + 102, + 115, + 239, + 183, + 230, + 164, + 107, + 194, + 233, + 121, + 63, + 249, + 250, + 159, + 60, + 118, + 227, + 200, + 188, + 238, + 176, + 229, + 83, + 0, + 224, + 228, + 148, + 137, + 244, + 200, + 84, + 4, + 192, + 235, + 0, + 0, + 90, + 1, + 48, + 251, + 46, + 136, + 200, + 18, + 8, + 249, + 191, + 73, + 2, + 32, + 231, + 28, + 136, + 148, + 204, + 56, + 135, + 98, + 2, + 85, + 228, + 130, + 143, + 178, + 133, + 195, + 61, + 233, + 227, + 143, + 253, + 164, + 254, + 100, + 186, + 7, + 87, + 29, + 46, + 121, + 236, + 63, + 167, + 29, + 21, + 233, + 165, + 63, + 248, + 47, + 255, + 165, + 254, + 91, + 77, + 179, + 103, + 87, + 180, + 158, + 252, + 147, + 250, + 198, + 15, + 230, + 125, + 109, + 233, + 148, + 187, + 16, + 0, + 175, + 2, + 0, + 230, + 54, + 54, + 64, + 67, + 61, + 66, + 150, + 243, + 26, + 0, + 144, + 93, + 87, + 133, + 85, + 129, + 65, + 82, + 137, + 111, + 184, + 228, + 206, + 211, + 75, + 86, + 254, + 142, + 32, + 144, + 57, + 31, + 210, + 51, + 145, + 254, + 159, + 24, + 0, + 82, + 121, + 107, + 253, + 171, + 155, + 38, + 154, + 22, + 164, + 43, + 190, + 123, + 247, + 215, + 235, + 191, + 53, + 239, + 7, + 55, + 253, + 96, + 118, + 221, + 13, + 232, + 158, + 247, + 124, + 237, + 59, + 12, + 2, + 224, + 249, + 231, + 247, + 237, + 51, + 185, + 19, + 238, + 64, + 123, + 76, + 55, + 205, + 107, + 64, + 217, + 1, + 32, + 123, + 197, + 85, + 233, + 234, + 3, + 126, + 80, + 75, + 53, + 146, + 122, + 96, + 199, + 41, + 57, + 253, + 254, + 112, + 99, + 215, + 174, + 149, + 43, + 215, + 108, + 5, + 203, + 86, + 215, + 178, + 255, + 167, + 141, + 141, + 199, + 46, + 92, + 186, + 116, + 229, + 202, + 79, + 30, + 171, + 175, + 255, + 244, + 106, + 57, + 63, + 174, + 250, + 159, + 246, + 206, + 189, + 97, + 203, + 77, + 95, + 123, + 118, + 10, + 243, + 212, + 93, + 204, + 119, + 190, + 118, + 143, + 20, + 190, + 3, + 0, + 161, + 231, + 65, + 7, + 202, + 23, + 250, + 98, + 154, + 59, + 141, + 164, + 33, + 199, + 21, + 62, + 76, + 0, + 128, + 138, + 103, + 92, + 14, + 93, + 73, + 112, + 160, + 202, + 86, + 90, + 179, + 109, + 138, + 123, + 229, + 198, + 223, + 158, + 82, + 211, + 239, + 127, + 119, + 226, + 231, + 27, + 87, + 174, + 220, + 234, + 93, + 179, + 102, + 205, + 214, + 58, + 111, + 93, + 245, + 15, + 182, + 212, + 1, + 217, + 248, + 248, + 241, + 107, + 95, + 126, + 121, + 235, + 159, + 81, + 79, + 209, + 207, + 104, + 166, + 227, + 1, + 0, + 191, + 120, + 254, + 213, + 125, + 191, + 16, + 90, + 220, + 44, + 44, + 158, + 79, + 86, + 13, + 15, + 25, + 66, + 97, + 138, + 76, + 21, + 88, + 225, + 5, + 239, + 116, + 85, + 1, + 10, + 230, + 117, + 22, + 85, + 5, + 125, + 120, + 224, + 215, + 26, + 8, + 68, + 40, + 142, + 30, + 254, + 117, + 207, + 174, + 149, + 119, + 175, + 172, + 6, + 48, + 10, + 164, + 21, + 108, + 222, + 165, + 116, + 167, + 227, + 89, + 4, + 35, + 128, + 85, + 64, + 115, + 91, + 40, + 212, + 214, + 204, + 239, + 140, + 237, + 107, + 238, + 232, + 8, + 73, + 203, + 130, + 162, + 253, + 35, + 180, + 235, + 226, + 93, + 7, + 0, + 112, + 164, + 236, + 241, + 232, + 68, + 73, + 168, + 200, + 198, + 32, + 158, + 119, + 175, + 92, + 121, + 216, + 16, + 131, + 83, + 167, + 62, + 253, + 201, + 63, + 17, + 48, + 94, + 46, + 218, + 198, + 231, + 81, + 62, + 182, + 46, + 169, + 213, + 153, + 142, + 103, + 17, + 140, + 64, + 51, + 191, + 67, + 54, + 179, + 89, + 177, + 51, + 184, + 79, + 185, + 52, + 50, + 15, + 133, + 217, + 157, + 96, + 76, + 151, + 216, + 185, + 132, + 90, + 42, + 5, + 217, + 237, + 85, + 25, + 246, + 75, + 217, + 186, + 181, + 235, + 119, + 191, + 55, + 0, + 224, + 131, + 159, + 124, + 32, + 190, + 254, + 245, + 46, + 108, + 11, + 203, + 127, + 255, + 219, + 223, + 125, + 216, + 184, + 113, + 227, + 198, + 158, + 30, + 56, + 108, + 252, + 136, + 63, + 90, + 120, + 35, + 16, + 226, + 55, + 8, + 71, + 132, + 23, + 196, + 247, + 117, + 192, + 31, + 111, + 91, + 73, + 200, + 108, + 111, + 235, + 81, + 46, + 53, + 134, + 58, + 188, + 142, + 246, + 140, + 201, + 104, + 38, + 107, + 215, + 172, + 92, + 89, + 214, + 115, + 226, + 183, + 159, + 170, + 1, + 248, + 167, + 159, + 200, + 63, + 194, + 170, + 112, + 111, + 201, + 191, + 232, + 65, + 101, + 33, + 70, + 32, + 36, + 91, + 30, + 91, + 100, + 246, + 80, + 91, + 40, + 178, + 175, + 252, + 143, + 5, + 192, + 100, + 182, + 139, + 227, + 175, + 92, + 179, + 100, + 229, + 202, + 246, + 234, + 141, + 59, + 14, + 11, + 202, + 241, + 184, + 140, + 1, + 128, + 248, + 245, + 204, + 182, + 149, + 232, + 2, + 128, + 141, + 0, + 209, + 125, + 161, + 230, + 54, + 53, + 179, + 231, + 248, + 40, + 170, + 186, + 154, + 140, + 0, + 52, + 135, + 58, + 178, + 251, + 158, + 230, + 137, + 162, + 107, + 183, + 62, + 190, + 243, + 31, + 87, + 2, + 117, + 237, + 127, + 172, + 241, + 176, + 76, + 58, + 118, + 9, + 203, + 153, + 149, + 107, + 33, + 176, + 96, + 35, + 176, + 19, + 115, + 62, + 218, + 222, + 69, + 209, + 126, + 153, + 168, + 155, + 91, + 149, + 67, + 93, + 87, + 147, + 17, + 0, + 159, + 108, + 247, + 137, + 235, + 64, + 138, + 130, + 134, + 218, + 173, + 91, + 215, + 48, + 117, + 24, + 140, + 247, + 55, + 30, + 62, + 92, + 184, + 87, + 40, + 180, + 42, + 223, + 165, + 1, + 96, + 223, + 243, + 175, + 50, + 191, + 0, + 77, + 23, + 242, + 169, + 181, + 157, + 212, + 126, + 183, + 201, + 109, + 130, + 213, + 117, + 53, + 153, + 69, + 32, + 148, + 211, + 14, + 236, + 89, + 73, + 138, + 50, + 92, + 138, + 196, + 27, + 237, + 222, + 186, + 213, + 109, + 21, + 107, + 205, + 202, + 75, + 176, + 250, + 67, + 170, + 112, + 199, + 142, + 30, + 80, + 130, + 175, + 3, + 7, + 116, + 134, + 152, + 72, + 71, + 179, + 74, + 6, + 38, + 241, + 12, + 234, + 186, + 154, + 108, + 0, + 248, + 24, + 166, + 35, + 196, + 215, + 227, + 153, + 47, + 75, + 51, + 252, + 117, + 33, + 202, + 208, + 171, + 139, + 170, + 20, + 43, + 111, + 63, + 47, + 95, + 134, + 75, + 150, + 200, + 122, + 213, + 150, + 200, + 171, + 171, + 193, + 10, + 242, + 58, + 48, + 212, + 38, + 110, + 15, + 147, + 195, + 174, + 103, + 18, + 169, + 235, + 106, + 178, + 139, + 64, + 165, + 184, + 216, + 172, + 233, + 178, + 52, + 67, + 162, + 196, + 131, + 254, + 94, + 110, + 54, + 17, + 130, + 34, + 41, + 71, + 102, + 193, + 70, + 128, + 102, + 120, + 37, + 8, + 215, + 19, + 125, + 216, + 1, + 93, + 51, + 137, + 103, + 80, + 165, + 73, + 50, + 2, + 128, + 182, + 168, + 177, + 189, + 37, + 178, + 154, + 217, + 178, + 52, + 99, + 162, + 248, + 131, + 203, + 104, + 189, + 221, + 23, + 108, + 31, + 139, + 16, + 8, + 229, + 86, + 150, + 16, + 0, + 240, + 58, + 136, + 64, + 167, + 248, + 36, + 33, + 193, + 22, + 78, + 242, + 33, + 100, + 105, + 146, + 44, + 50, + 30, + 10, + 213, + 122, + 69, + 99, + 51, + 137, + 181, + 176, + 180, + 191, + 141, + 15, + 25, + 134, + 3, + 37, + 8, + 126, + 115, + 31, + 249, + 29, + 203, + 62, + 20, + 9, + 244, + 97, + 119, + 143, + 180, + 184, + 243, + 26, + 116, + 128, + 58, + 212, + 201, + 2, + 0, + 90, + 140, + 215, + 139, + 119, + 37, + 107, + 158, + 212, + 90, + 88, + 250, + 148, + 185, + 52, + 244, + 71, + 54, + 97, + 136, + 177, + 8, + 143, + 25, + 89, + 94, + 121, + 254, + 213, + 215, + 127, + 1, + 26, + 95, + 210, + 250, + 161, + 63, + 26, + 0, + 60, + 193, + 111, + 33, + 69, + 104, + 42, + 203, + 104, + 130, + 178, + 148, + 134, + 74, + 16, + 148, + 35, + 4, + 44, + 96, + 4, + 94, + 111, + 131, + 30, + 144, + 56, + 158, + 168, + 131, + 206, + 146, + 131, + 154, + 75, + 179, + 47, + 240, + 50, + 41, + 0, + 218, + 120, + 222, + 163, + 76, + 100, + 25, + 179, + 147, + 137, + 100, + 251, + 42, + 178, + 224, + 51, + 154, + 166, + 206, + 88, + 94, + 125, + 254, + 199, + 208, + 100, + 89, + 62, + 144, + 55, + 132, + 109, + 109, + 90, + 0, + 178, + 47, + 240, + 162, + 34, + 147, + 118, + 190, + 141, + 88, + 66, + 74, + 60, + 92, + 11, + 101, + 29, + 16, + 71, + 180, + 108, + 175, + 48, + 96, + 98, + 121, + 254, + 249, + 231, + 94, + 87, + 4, + 184, + 136, + 1, + 246, + 234, + 43, + 64, + 177, + 18, + 219, + 108, + 205, + 71, + 70, + 0, + 52, + 27, + 132, + 83, + 226, + 225, + 90, + 72, + 103, + 122, + 172, + 154, + 94, + 176, + 149, + 111, + 251, + 146, + 31, + 48, + 65, + 0, + 236, + 67, + 42, + 144, + 79, + 124, + 144, + 238, + 47, + 210, + 245, + 1, + 118, + 174, + 167, + 231, + 22, + 96, + 101, + 237, + 182, + 154, + 91, + 250, + 47, + 19, + 0, + 252, + 196, + 54, + 90, + 194, + 129, + 18, + 15, + 58, + 103, + 155, + 117, + 147, + 76, + 48, + 128, + 52, + 25, + 105, + 155, + 8, + 0, + 67, + 218, + 223, + 204, + 155, + 128, + 131, + 50, + 14, + 160, + 221, + 5, + 86, + 235, + 189, + 232, + 105, + 55, + 231, + 188, + 192, + 139, + 9, + 17, + 40, + 54, + 55, + 1, + 204, + 180, + 155, + 148, + 189, + 46, + 107, + 149, + 180, + 210, + 51, + 230, + 128, + 213, + 178, + 145, + 113, + 201, + 17, + 38, + 44, + 64, + 163, + 77, + 220, + 11, + 138, + 209, + 42, + 166, + 52, + 169, + 94, + 201, + 109, + 129, + 151, + 235, + 231, + 235, + 155, + 117, + 147, + 244, + 118, + 111, + 83, + 81, + 185, + 216, + 254, + 79, + 176, + 14, + 80, + 140, + 140, + 119, + 10, + 94, + 128, + 94, + 42, + 144, + 87, + 209, + 148, + 249, + 199, + 54, + 6, + 192, + 6, + 200, + 154, + 191, + 143, + 105, + 55, + 41, + 203, + 244, + 16, + 68, + 149, + 216, + 21, + 122, + 247, + 221, + 143, + 127, + 89, + 142, + 28, + 98, + 196, + 1, + 138, + 145, + 241, + 80, + 38, + 39, + 128, + 18, + 15, + 230, + 232, + 250, + 113, + 128, + 89, + 55, + 41, + 251, + 86, + 76, + 171, + 144, + 9, + 44, + 170, + 173, + 37, + 235, + 171, + 0, + 0, + 170, + 145, + 113, + 25, + 0, + 248, + 148, + 74, + 177, + 30, + 187, + 178, + 88, + 9, + 128, + 137, + 133, + 192, + 175, + 99, + 184, + 107, + 202, + 77, + 210, + 203, + 172, + 50, + 170, + 217, + 95, + 88, + 5, + 72, + 227, + 229, + 8, + 0, + 61, + 14, + 232, + 68, + 73, + 128, + 218, + 226, + 2, + 171, + 108, + 117, + 74, + 155, + 141, + 66, + 127, + 40, + 242, + 206, + 204, + 220, + 69, + 93, + 0, + 116, + 106, + 252, + 205, + 16, + 101, + 194, + 77, + 210, + 115, + 2, + 155, + 85, + 226, + 236, + 70, + 54, + 96, + 175, + 216, + 233, + 42, + 29, + 192, + 111, + 149, + 25, + 41, + 57, + 88, + 104, + 45, + 40, + 45, + 80, + 237, + 247, + 89, + 44, + 111, + 179, + 219, + 132, + 37, + 212, + 5, + 32, + 151, + 53, + 228, + 101, + 68, + 137, + 7, + 67, + 210, + 157, + 33, + 220, + 172, + 206, + 59, + 225, + 9, + 233, + 226, + 220, + 27, + 4, + 192, + 43, + 242, + 179, + 121, + 59, + 248, + 205, + 125, + 122, + 191, + 80, + 169, + 93, + 151, + 36, + 35, + 233, + 1, + 96, + 184, + 182, + 88, + 22, + 162, + 196, + 131, + 33, + 121, + 244, + 18, + 1, + 94, + 245, + 168, + 183, + 114, + 238, + 141, + 229, + 121, + 101, + 137, + 32, + 223, + 254, + 66, + 131, + 124, + 72, + 165, + 233, + 45, + 47, + 201, + 143, + 235, + 124, + 54, + 217, + 69, + 210, + 40, + 241, + 96, + 68, + 166, + 156, + 96, + 177, + 100, + 130, + 39, + 203, + 171, + 223, + 87, + 104, + 65, + 180, + 77, + 108, + 251, + 222, + 162, + 156, + 243, + 193, + 250, + 116, + 93, + 115, + 126, + 89, + 41, + 67, + 30, + 64, + 78, + 181, + 123, + 229, + 50, + 96, + 121, + 125, + 245, + 106, + 69, + 137, + 28, + 234, + 255, + 162, + 76, + 99, + 95, + 250, + 171, + 164, + 234, + 147, + 2, + 0, + 19, + 177, + 228, + 53, + 145, + 73, + 6, + 224, + 101, + 64, + 176, + 3, + 150, + 125, + 207, + 43, + 230, + 139, + 33, + 37, + 176, + 173, + 232, + 96, + 196, + 56, + 99, + 15, + 158, + 241, + 228, + 60, + 65, + 228, + 199, + 254, + 227, + 195, + 215, + 184, + 82, + 162, + 49, + 209, + 38, + 162, + 32, + 66, + 216, + 16, + 10, + 165, + 115, + 150, + 78, + 0, + 64, + 46, + 3, + 33, + 99, + 79, + 80, + 32, + 243, + 170, + 80, + 1, + 0, + 242, + 99, + 31, + 40, + 205, + 176, + 88, + 232, + 181, + 145, + 105, + 6, + 224, + 89, + 0, + 47, + 100, + 238, + 101, + 45, + 12, + 146, + 1, + 153, + 39, + 112, + 240, + 160, + 106, + 72, + 68, + 135, + 240, + 162, + 62, + 102, + 72, + 14, + 0, + 246, + 99, + 231, + 22, + 23, + 103, + 88, + 44, + 244, + 90, + 40, + 235, + 166, + 83, + 50, + 90, + 69, + 138, + 7, + 107, + 233, + 202, + 150, + 207, + 44, + 204, + 62, + 185, + 33, + 116, + 91, + 75, + 66, + 38, + 210, + 97, + 86, + 164, + 7, + 10, + 138, + 179, + 106, + 3, + 57, + 0, + 216, + 143, + 253, + 230, + 3, + 59, + 65, + 4, + 220, + 185, + 45, + 128, + 97, + 134, + 52, + 237, + 207, + 24, + 63, + 147, + 85, + 25, + 10, + 42, + 183, + 191, + 191, + 213, + 194, + 180, + 65, + 52, + 32, + 148, + 73, + 66, + 208, + 151, + 93, + 2, + 152, + 98, + 50, + 105, + 153, + 174, + 204, + 26, + 209, + 200, + 1, + 192, + 126, + 172, + 21, + 199, + 146, + 116, + 49, + 127, + 93, + 78, + 91, + 232, + 102, + 34, + 151, + 176, + 165, + 154, + 72, + 25, + 227, + 231, + 157, + 100, + 184, + 180, + 232, + 179, + 0, + 42, + 149, + 125, + 85, + 244, + 134, + 43, + 11, + 219, + 120, + 71, + 96, + 115, + 46, + 131, + 52, + 165, + 214, + 98, + 3, + 165, + 32, + 0, + 80, + 90, + 92, + 74, + 252, + 88, + 171, + 232, + 72, + 163, + 67, + 78, + 27, + 11, + 101, + 34, + 157, + 85, + 226, + 50, + 199, + 207, + 165, + 100, + 164, + 108, + 219, + 42, + 4, + 192, + 235, + 0, + 192, + 43, + 248, + 227, + 131, + 17, + 33, + 35, + 16, + 50, + 64, + 79, + 127, + 89, + 115, + 128, + 64, + 127, + 205, + 60, + 2, + 192, + 214, + 194, + 173, + 216, + 129, + 122, + 192, + 10, + 29, + 78, + 201, + 239, + 150, + 161, + 77, + 185, + 145, + 182, + 236, + 54, + 75, + 252, + 188, + 138, + 100, + 133, + 62, + 94, + 6, + 0, + 52, + 63, + 255, + 60, + 177, + 3, + 72, + 246, + 249, + 124, + 64, + 200, + 0, + 61, + 155, + 17, + 203, + 10, + 161, + 145, + 102, + 127, + 5, + 166, + 13, + 109, + 69, + 206, + 63, + 19, + 125, + 29, + 114, + 126, + 90, + 210, + 157, + 154, + 153, + 45, + 126, + 94, + 69, + 6, + 140, + 63, + 71, + 181, + 194, + 175, + 255, + 253, + 243, + 207, + 253, + 143, + 182, + 237, + 88, + 249, + 9, + 99, + 2, + 6, + 232, + 217, + 178, + 233, + 61, + 205, + 254, + 10, + 29, + 82, + 122, + 129, + 18, + 15, + 215, + 151, + 156, + 106, + 249, + 199, + 148, + 45, + 126, + 126, + 129, + 232, + 1, + 0, + 32, + 100, + 253, + 230, + 125, + 223, + 188, + 175, + 112, + 47, + 30, + 28, + 108, + 70, + 142, + 0, + 99, + 136, + 94, + 105, + 54, + 165, + 165, + 218, + 95, + 193, + 237, + 38, + 128, + 254, + 1, + 125, + 98, + 151, + 145, + 40, + 81, + 153, + 226, + 103, + 224, + 209, + 90, + 188, + 96, + 153, + 5, + 60, + 223, + 123, + 161, + 253, + 184, + 86, + 54, + 20, + 226, + 213, + 255, + 228, + 6, + 105, + 80, + 180, + 172, + 20, + 61, + 183, + 149, + 38, + 28, + 48, + 217, + 253, + 163, + 178, + 147, + 177, + 253, + 167, + 196, + 131, + 14, + 225, + 33, + 14, + 180, + 58, + 11, + 0, + 240, + 86, + 209, + 235, + 247, + 61, + 143, + 170, + 165, + 101, + 37, + 49, + 212, + 164, + 6, + 105, + 74, + 173, + 74, + 209, + 195, + 46, + 99, + 232, + 15, + 200, + 1, + 59, + 41, + 104, + 191, + 81, + 120, + 65, + 137, + 7, + 29, + 34, + 67, + 28, + 171, + 182, + 33, + 0, + 58, + 75, + 66, + 115, + 81, + 157, + 20, + 174, + 143, + 48, + 115, + 177, + 49, + 65, + 139, + 101, + 204, + 195, + 111, + 8, + 229, + 109, + 235, + 184, + 166, + 106, + 203, + 12, + 180, + 249, + 217, + 165, + 78, + 195, + 60, + 61, + 37, + 30, + 180, + 196, + 47, + 54, + 211, + 118, + 208, + 6, + 0, + 20, + 118, + 70, + 150, + 253, + 61, + 174, + 23, + 55, + 119, + 113, + 38, + 170, + 180, + 74, + 204, + 67, + 91, + 105, + 157, + 142, + 247, + 50, + 62, + 115, + 165, + 81, + 102, + 130, + 199, + 103, + 170, + 190, + 107, + 207, + 184, + 23, + 129, + 17, + 145, + 33, + 142, + 205, + 145, + 182, + 77, + 150, + 208, + 189, + 224, + 252, + 239, + 251, + 251, + 87, + 65, + 6, + 66, + 146, + 164, + 82, + 226, + 65, + 73, + 181, + 217, + 226, + 160, + 90, + 197, + 165, + 106, + 0, + 124, + 29, + 109, + 145, + 80, + 134, + 72, + 83, + 241, + 140, + 217, + 7, + 34, + 161, + 31, + 159, + 181, + 47, + 205, + 184, + 23, + 129, + 1, + 97, + 30, + 245, + 118, + 118, + 108, + 166, + 45, + 86, + 84, + 163, + 17, + 34, + 181, + 130, + 38, + 200, + 109, + 205, + 118, + 6, + 37, + 30, + 20, + 0, + 248, + 80, + 69, + 16, + 95, + 124, + 98, + 242, + 25, + 179, + 236, + 52, + 65, + 250, + 241, + 217, + 165, + 246, + 73, + 68, + 216, + 152, + 71, + 193, + 221, + 67, + 21, + 34, + 88, + 75, + 163, + 106, + 81, + 115, + 53, + 65, + 217, + 13, + 97, + 173, + 21, + 252, + 94, + 178, + 176, + 189, + 0, + 0, + 95, + 130, + 141, + 142, + 222, + 144, + 57, + 0, + 228, + 75, + 66, + 129, + 163, + 137, + 126, + 149, + 86, + 255, + 52, + 238, + 71, + 231, + 210, + 73, + 237, + 69, + 64, + 161, + 26, + 45, + 10, + 215, + 10, + 35, + 66, + 117, + 34, + 160, + 5, + 38, + 81, + 23, + 165, + 67, + 16, + 235, + 240, + 65, + 142, + 8, + 64, + 68, + 70, + 230, + 76, + 162, + 114, + 32, + 146, + 164, + 160, + 11, + 84, + 142, + 56, + 238, + 71, + 143, + 157, + 154, + 204, + 67, + 82, + 72, + 20, + 41, + 17, + 128, + 208, + 125, + 196, + 14, + 92, + 31, + 4, + 68, + 82, + 3, + 224, + 99, + 154, + 59, + 76, + 90, + 68, + 221, + 129, + 200, + 98, + 117, + 240, + 73, + 161, + 209, + 176, + 39, + 38, + 243, + 104, + 181, + 208, + 126, + 247, + 125, + 229, + 229, + 60, + 0, + 145, + 85, + 127, + 253, + 250, + 206, + 95, + 252, + 193, + 0, + 16, + 38, + 33, + 228, + 112, + 169, + 56, + 16, + 153, + 49, + 235, + 64, + 161, + 69, + 26, + 168, + 73, + 60, + 89, + 40, + 178, + 189, + 152, + 46, + 32, + 158, + 32, + 166, + 95, + 204, + 253, + 239, + 175, + 239, + 51, + 249, + 132, + 58, + 27, + 94, + 26, + 144, + 0, + 64, + 155, + 207, + 155, + 43, + 0, + 162, + 54, + 205, + 104, + 119, + 40, + 241, + 144, + 27, + 117, + 130, + 203, + 91, + 252, + 77, + 228, + 10, + 255, + 63, + 107, + 43, + 43, + 245, + 20, + 183, + 135, + 247, + 0, + 0, + 0, + 0, + 73, + 69, + 78, + 68, + 174, + 66, + 96, + 130 +]; diff --git a/test/tools/tile_server/static/generated/sea.dart b/test/tools/tile_server/static/generated/sea.dart index f3707c9e..5b3fad18 100644 --- a/test/tools/tile_server/static/generated/sea.dart +++ b/test/tools/tile_server/static/generated/sea.dart @@ -1 +1,105 @@ -final seaTileBytes = [137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 1, 0, 0, 0, 1, 0, 1, 3, 0, 0, 0, 102, 188, 58, 37, 0, 0, 0, 3, 80, 76, 84, 69, 170, 211, 223, 207, 236, 188, 245, 0, 0, 0, 31, 73, 68, 65, 84, 104, 129, 237, 193, 1, 13, 0, 0, 0, 194, 160, 247, 79, 109, 14, 55, 160, 0, 0, 0, 0, 0, 0, 0, 0, 190, 13, 33, 0, 0, 1, 154, 96, 225, 213, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130]; +final seaTileBytes = [ + 137, + 80, + 78, + 71, + 13, + 10, + 26, + 10, + 0, + 0, + 0, + 13, + 73, + 72, + 68, + 82, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 1, + 3, + 0, + 0, + 0, + 102, + 188, + 58, + 37, + 0, + 0, + 0, + 3, + 80, + 76, + 84, + 69, + 170, + 211, + 223, + 207, + 236, + 188, + 245, + 0, + 0, + 0, + 31, + 73, + 68, + 65, + 84, + 104, + 129, + 237, + 193, + 1, + 13, + 0, + 0, + 0, + 194, + 160, + 247, + 79, + 109, + 14, + 55, + 160, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 190, + 13, + 33, + 0, + 0, + 1, + 154, + 96, + 225, + 213, + 0, + 0, + 0, + 0, + 73, + 69, + 78, + 68, + 174, + 66, + 96, + 130 +]; From 2239b43a238dd3dd43e052da7e2eadb4d967adb5 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Fri, 28 Jul 2023 13:09:01 +0100 Subject: [PATCH 046/168] Moved tile server directory Updated CHANGELOG Former-commit-id: 6eba324135561e1575514c1e090a2e1f403f6311 [formerly b9e969691b53b8b26326f604ac7295a1703400a6] Former-commit-id: 11d0b0cdc5f5810c5359171d271a39abbd6e91c1 --- CHANGELOG.md | 12 +++++++++++- {test/tools/tile_server => tile_server}/.gitignore | 0 .../bin/generate_dart_images.dart | 0 .../bin/tile_server.dart | 0 .../tools/tile_server => tile_server}/pubspec.yaml | 0 .../start_dev.bat | 0 .../static/generated/favicon.dart | 0 .../static/generated/land.dart | 0 .../static/generated/sea.dart | 0 .../static/source/favicon.ico | Bin .../static/source/land.png | Bin .../static/source/sea.png | Bin 12 files changed, 11 insertions(+), 1 deletion(-) rename {test/tools/tile_server => tile_server}/.gitignore (100%) rename {test/tools/tile_server => tile_server}/bin/generate_dart_images.dart (100%) rename {test/tools/tile_server => tile_server}/bin/tile_server.dart (100%) rename {test/tools/tile_server => tile_server}/pubspec.yaml (100%) rename test/tools/fmtc_tile_server.bat => tile_server/start_dev.bat (100%) rename {test/tools/tile_server => tile_server}/static/generated/favicon.dart (100%) rename {test/tools/tile_server => tile_server}/static/generated/land.dart (100%) rename {test/tools/tile_server => tile_server}/static/generated/sea.dart (100%) rename {test/tools/tile_server => tile_server}/static/source/favicon.ico (100%) rename {test/tools/tile_server => tile_server}/static/source/land.png (100%) rename {test/tools/tile_server => tile_server}/static/source/sea.png (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f51f388..e976b80a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,9 +20,10 @@ Many thanks to my sponsors, no matter how much or how little they donated. Spons * Migrated to flutter_map v5 * Added support for Isar v4 (bug fixes & stability improvements) * Bulk downloading reimplementation + * Improved developer experience by re-organizing scope of `DownloadableRegion`s and `startForeground` * Improved download speed significantly * Added pause and resume functionality (to accompany existing cancel functionality) - * Added rate limiting support + * Added support for rate limiting * Added support for multiple simultaneous downloads * Fixed instability and bugs when cancelling buffering downloads * Fixed generation of `LineRegion` tiles by reducing number of redundant duplicate tiles @@ -31,9 +32,18 @@ Many thanks to my sponsors, no matter how much or how little they donated. Spons * Removed support for custom `HttpClient`s * Added secondary check to `FMTCImageProvider` to ensure responses are valid images * Added `StoreManagement.pruneTilesOlderThan` method +* Improved performance of `tileImage` methods +* Improved error objects * Replaced public facing `RegionType`/`type` with Dart 3 exhaustive switch statements through `BaseRegion/DownloadableRegion.when` & `RecoverableRegion.toRegion` * Removed HTTP/2 support + +Also: + +* Created a miniature testing tile server +* Created automated tests for tile generation * Improved & simplified example application + * Removed update mechanism + * Added user location tracking ## [8.0.0] - 2023/05/05 diff --git a/test/tools/tile_server/.gitignore b/tile_server/.gitignore similarity index 100% rename from test/tools/tile_server/.gitignore rename to tile_server/.gitignore diff --git a/test/tools/tile_server/bin/generate_dart_images.dart b/tile_server/bin/generate_dart_images.dart similarity index 100% rename from test/tools/tile_server/bin/generate_dart_images.dart rename to tile_server/bin/generate_dart_images.dart diff --git a/test/tools/tile_server/bin/tile_server.dart b/tile_server/bin/tile_server.dart similarity index 100% rename from test/tools/tile_server/bin/tile_server.dart rename to tile_server/bin/tile_server.dart diff --git a/test/tools/tile_server/pubspec.yaml b/tile_server/pubspec.yaml similarity index 100% rename from test/tools/tile_server/pubspec.yaml rename to tile_server/pubspec.yaml diff --git a/test/tools/fmtc_tile_server.bat b/tile_server/start_dev.bat similarity index 100% rename from test/tools/fmtc_tile_server.bat rename to tile_server/start_dev.bat diff --git a/test/tools/tile_server/static/generated/favicon.dart b/tile_server/static/generated/favicon.dart similarity index 100% rename from test/tools/tile_server/static/generated/favicon.dart rename to tile_server/static/generated/favicon.dart diff --git a/test/tools/tile_server/static/generated/land.dart b/tile_server/static/generated/land.dart similarity index 100% rename from test/tools/tile_server/static/generated/land.dart rename to tile_server/static/generated/land.dart diff --git a/test/tools/tile_server/static/generated/sea.dart b/tile_server/static/generated/sea.dart similarity index 100% rename from test/tools/tile_server/static/generated/sea.dart rename to tile_server/static/generated/sea.dart diff --git a/test/tools/tile_server/static/source/favicon.ico b/tile_server/static/source/favicon.ico similarity index 100% rename from test/tools/tile_server/static/source/favicon.ico rename to tile_server/static/source/favicon.ico diff --git a/test/tools/tile_server/static/source/land.png b/tile_server/static/source/land.png similarity index 100% rename from test/tools/tile_server/static/source/land.png rename to tile_server/static/source/land.png diff --git a/test/tools/tile_server/static/source/sea.png b/tile_server/static/source/sea.png similarity index 100% rename from test/tools/tile_server/static/source/sea.png rename to tile_server/static/source/sea.png From 03d12dee2951a701739e0809e49a7838bdc9a0a8 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Fri, 28 Jul 2023 13:20:44 +0100 Subject: [PATCH 047/168] Generate tile server images with GitHub Actions Former-commit-id: 9c48441e90daf6b4865aa6ca1e50eafb0234c4b7 [formerly fb052476b15b32e9282edfbe185c7c5b8742040d] Former-commit-id: 5293da18a5cf90ea7dde269b2ee5f3d89cd6d114 --- .github/workflows/main.yml | 14 +- tile_server/bin/generate_dart_images.dart | 3 +- tile_server/static/generated/favicon.dart | 11747 +----------- tile_server/static/generated/land.dart | 18693 +------------------- tile_server/static/generated/sea.dart | 107 +- 5 files changed, 16 insertions(+), 30548 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 02b8e39c..5c248d6e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -43,7 +43,7 @@ jobs: - name: Get Package & Example Dependencies run: flutter pub get - name: Get Test Tile Server Dependencies - run: dart pub get -C test/tools/tile_server + run: dart pub get -C tile_server - name: Check Formatting run: dart format --output=none --set-exit-if-changed . - name: Check Lints @@ -122,7 +122,7 @@ jobs: #needs: [analyse-code, run-tests, score-package] defaults: run: - working-directory: ./test/tools/tile_server + working-directory: ./tile_server steps: - name: Checkout Repository uses: actions/checkout@v3 @@ -130,13 +130,15 @@ jobs: uses: dart-lang/setup-dart@v1.5.0 - name: Get Dependencies run: dart pub get + - name: Run Pre-Compile Generator + run: dart run bin/generate_dart_images.dart - name: Compile run: dart compile exe bin/tile_server.dart - name: Upload Artifact uses: actions/upload-artifact@v3.1.2 with: name: windows-ts - path: test/tools/tile_server/bin/tile_server.exe + path: tile_server/bin/tile_server.exe if-no-files-found: error build-tile-server-linux: @@ -145,7 +147,7 @@ jobs: #needs: [analyse-code, run-tests, score-package] defaults: run: - working-directory: ./test/tools/tile_server + working-directory: ./tile_server steps: - name: Checkout Repository uses: actions/checkout@v3 @@ -153,11 +155,13 @@ jobs: uses: dart-lang/setup-dart@v1.5.0 - name: Get Dependencies run: dart pub get + - name: Run Pre-Compile Generator + run: dart run bin/generate_dart_images.dart - name: Compile run: dart compile exe bin/tile_server.dart - name: Upload Artifact uses: actions/upload-artifact@v3.1.2 with: name: linux-ts - path: test/tools/tile_server/bin/tile_server.exe + path: tile_server/bin/tile_server.exe if-no-files-found: error diff --git a/tile_server/bin/generate_dart_images.dart b/tile_server/bin/generate_dart_images.dart index 37729940..ea517673 100644 --- a/tile_server/bin/generate_dart_images.dart +++ b/tile_server/bin/generate_dart_images.dart @@ -1,4 +1,3 @@ -import 'dart:async'; import 'dart:io'; import 'package:path/path.dart' as p; @@ -9,7 +8,7 @@ const staticFilesInfo = [ (name: 'favicon', extension: 'ico'), ]; -Future main(List _) async { +void main(List _) { final execPath = p.split(Platform.script.toFilePath()); final staticPath = p.joinAll([...execPath.getRange(0, execPath.length - 2), 'static']); diff --git a/tile_server/static/generated/favicon.dart b/tile_server/static/generated/favicon.dart index 537d61a9..7dd43c1b 100644 --- a/tile_server/static/generated/favicon.dart +++ b/tile_server/static/generated/favicon.dart @@ -1,11745 +1,2 @@ -final faviconTileBytes = [ - 0, - 0, - 1, - 0, - 1, - 0, - 0, - 0, - 0, - 0, - 1, - 0, - 32, - 0, - 201, - 45, - 0, - 0, - 22, - 0, - 0, - 0, - 137, - 80, - 78, - 71, - 13, - 10, - 26, - 10, - 0, - 0, - 0, - 13, - 73, - 72, - 68, - 82, - 0, - 0, - 1, - 0, - 0, - 0, - 1, - 0, - 8, - 6, - 0, - 0, - 0, - 92, - 114, - 168, - 102, - 0, - 0, - 45, - 144, - 73, - 68, - 65, - 84, - 120, - 218, - 237, - 157, - 123, - 124, - 27, - 229, - 153, - 239, - 127, - 26, - 141, - 70, - 23, - 91, - 146, - 109, - 249, - 126, - 139, - 147, - 216, - 9, - 185, - 185, - 9, - 16, - 72, - 40, - 183, - 64, - 129, - 246, - 208, - 238, - 129, - 237, - 210, - 82, - 122, - 202, - 161, - 91, - 2, - 11, - 45, - 44, - 108, - 161, - 13, - 112, - 246, - 0, - 165, - 52, - 180, - 205, - 46, - 109, - 225, - 244, - 190, - 103, - 217, - 148, - 101, - 89, - 216, - 93, - 232, - 133, - 179, - 101, - 11, - 9, - 105, - 32, - 9, - 73, - 73, - 76, - 156, - 224, - 196, - 151, - 92, - 124, - 209, - 197, - 182, - 44, - 75, - 178, - 46, - 51, - 26, - 73, - 231, - 15, - 89, - 178, - 36, - 75, - 214, - 109, - 164, - 153, - 145, - 222, - 239, - 231, - 147, - 79, - 28, - 105, - 52, - 126, - 53, - 153, - 231, - 55, - 207, - 251, - 188, - 207, - 243, - 188, - 10, - 254, - 163, - 23, - 195, - 40, - 0, - 191, - 215, - 9, - 243, - 88, - 63, - 204, - 163, - 199, - 97, - 29, - 59, - 1, - 149, - 190, - 25, - 205, - 61, - 215, - 160, - 121, - 213, - 39, - 80, - 219, - 177, - 9, - 52, - 83, - 93, - 200, - 233, - 9, - 4, - 66, - 17, - 81, - 20, - 42, - 0, - 201, - 204, - 76, - 157, - 195, - 196, - 249, - 62, - 88, - 198, - 250, - 225, - 152, - 49, - 163, - 174, - 227, - 34, - 52, - 245, - 92, - 131, - 166, - 238, - 109, - 208, - 55, - 174, - 18, - 251, - 251, - 18, - 8, - 132, - 56, - 4, - 23, - 128, - 120, - 120, - 158, - 133, - 101, - 52, - 226, - 29, - 152, - 199, - 250, - 17, - 166, - 52, - 104, - 90, - 181, - 13, - 77, - 221, - 219, - 208, - 216, - 125, - 37, - 241, - 14, - 8, - 4, - 145, - 201, - 73, - 0, - 38, - 236, - 62, - 184, - 61, - 172, - 216, - 99, - 134, - 182, - 190, - 153, - 76, - 47, - 8, - 4, - 1, - 160, - 179, - 61, - 112, - 194, - 238, - 131, - 66, - 93, - 139, - 21, - 203, - 87, - 138, - 61, - 102, - 76, - 155, - 71, - 224, - 24, - 59, - 134, - 134, - 149, - 87, - 136, - 61, - 20, - 2, - 65, - 214, - 80, - 217, - 30, - 232, - 246, - 176, - 168, - 111, - 21, - 223, - 248, - 1, - 160, - 190, - 117, - 37, - 124, - 211, - 86, - 177, - 135, - 65, - 32, - 200, - 158, - 172, - 5, - 128, - 64, - 32, - 148, - 31, - 178, - 22, - 0, - 235, - 224, - 31, - 192, - 115, - 115, - 98, - 15, - 131, - 64, - 144, - 45, - 89, - 199, - 0, - 164, - 200, - 200, - 219, - 79, - 227, - 200, - 244, - 121, - 212, - 117, - 94, - 130, - 166, - 158, - 107, - 208, - 176, - 242, - 10, - 24, - 155, - 214, - 138, - 61, - 44, - 2, - 65, - 54, - 228, - 45, - 0, - 111, - 255, - 231, - 191, - 225, - 143, - 111, - 255, - 22, - 0, - 160, - 209, - 106, - 209, - 220, - 186, - 12, - 87, - 95, - 127, - 19, - 150, - 175, - 92, - 19, - 59, - 230, - 223, - 94, - 252, - 49, - 250, - 251, - 222, - 143, - 253, - 123, - 109, - 239, - 197, - 248, - 252, - 237, - 247, - 1, - 0, - 206, - 142, - 12, - 224, - 159, - 255, - 225, - 239, - 97, - 53, - 143, - 97, - 237, - 134, - 139, - 241, - 63, - 255, - 234, - 155, - 168, - 170, - 210, - 3, - 0, - 142, - 31, - 61, - 128, - 87, - 95, - 252, - 9, - 156, - 179, - 51, - 184, - 120, - 203, - 85, - 184, - 253, - 174, - 111, - 164, - 28, - 195, - 117, - 55, - 61, - 6, - 158, - 103, - 97, - 29, - 63, - 9, - 203, - 232, - 126, - 28, - 57, - 240, - 99, - 4, - 194, - 10, - 52, - 245, - 92, - 131, - 198, - 149, - 87, - 162, - 177, - 231, - 106, - 48, - 154, - 26, - 177, - 175, - 49, - 129, - 32, - 89, - 242, - 22, - 0, - 135, - 125, - 18, - 77, - 45, - 29, - 248, - 243, - 47, - 109, - 199, - 156, - 195, - 137, - 211, - 39, - 251, - 240, - 204, - 223, - 222, - 139, - 47, - 221, - 245, - 16, - 46, - 191, - 250, - 70, - 0, - 192, - 208, - 233, - 227, - 184, - 230, - 147, - 159, - 197, - 138, - 158, - 200, - 83, - 89, - 171, - 173, - 2, - 0, - 112, - 156, - 31, - 127, - 247, - 212, - 131, - 248, - 226, - 95, - 62, - 136, - 222, - 139, - 46, - 195, - 203, - 255, - 247, - 7, - 120, - 101, - 247, - 243, - 248, - 242, - 61, - 143, - 192, - 238, - 180, - 227, - 255, - 236, - 122, - 12, - 247, - 239, - 248, - 30, - 58, - 186, - 186, - 241, - 179, - 103, - 255, - 55, - 222, - 248, - 143, - 221, - 184, - 241, - 207, - 111, - 79, - 253, - 5, - 104, - 53, - 218, - 187, - 46, - 68, - 123, - 215, - 133, - 0, - 0, - 183, - 203, - 6, - 243, - 249, - 15, - 49, - 241, - 254, - 79, - 113, - 236, - 245, - 7, - 161, - 111, - 90, - 131, - 166, - 149, - 87, - 161, - 185, - 231, - 19, - 168, - 237, - 188, - 72, - 236, - 235, - 77, - 32, - 72, - 138, - 130, - 166, - 0, - 90, - 93, - 21, - 154, - 27, - 151, - 1, - 141, - 64, - 247, - 234, - 94, - 116, - 116, - 117, - 227, - 185, - 239, - 237, - 192, - 37, - 151, - 93, - 11, - 134, - 209, - 192, - 53, - 235, - 192, - 138, - 158, - 181, - 232, - 88, - 214, - 157, - 240, - 185, - 161, - 83, - 253, - 48, - 24, - 106, - 176, - 245, - 202, - 27, - 0, - 0, - 55, - 221, - 126, - 47, - 118, - 220, - 125, - 51, - 190, - 248, - 149, - 7, - 209, - 119, - 96, - 47, - 214, - 125, - 108, - 51, - 214, - 245, - 110, - 6, - 0, - 252, - 217, - 95, - 124, - 25, - 191, - 124, - 254, - 219, - 105, - 5, - 32, - 25, - 189, - 161, - 9, - 171, - 55, - 92, - 143, - 213, - 27, - 174, - 7, - 207, - 179, - 176, - 219, - 206, - 96, - 226, - 124, - 31, - 250, - 254, - 253, - 85, - 120, - 125, - 30, - 52, - 173, - 186, - 38, - 226, - 33, - 116, - 95, - 5, - 117, - 85, - 189, - 216, - 215, - 159, - 64, - 16, - 21, - 65, - 99, - 0, - 189, - 23, - 94, - 6, - 138, - 82, - 98, - 232, - 84, - 63, - 214, - 245, - 110, - 134, - 219, - 237, - 68, - 223, - 159, - 246, - 227, - 248, - 7, - 7, - 208, - 209, - 213, - 141, - 222, - 11, - 47, - 3, - 0, - 88, - 39, - 206, - 163, - 61, - 78, - 20, - 76, - 70, - 19, - 0, - 192, - 53, - 235, - 128, - 213, - 60, - 138, - 214, - 182, - 174, - 216, - 123, - 237, - 93, - 61, - 152, - 180, - 78, - 228, - 247, - 229, - 104, - 53, - 154, - 218, - 214, - 160, - 169, - 109, - 13, - 112, - 217, - 23, - 224, - 247, - 58, - 49, - 113, - 190, - 15, - 230, - 99, - 187, - 113, - 252, - 119, - 223, - 132, - 198, - 216, - 129, - 166, - 85, - 215, - 162, - 105, - 229, - 85, - 36, - 177, - 136, - 80, - 145, - 8, - 30, - 4, - 172, - 51, - 53, - 194, - 237, - 114, - 0, - 0, - 62, - 245, - 103, - 183, - 197, - 94, - 255, - 247, - 151, - 126, - 134, - 55, - 127, - 251, - 47, - 120, - 248, - 241, - 231, - 224, - 247, - 121, - 161, - 102, - 212, - 9, - 159, - 211, - 234, - 244, - 112, - 187, - 103, - 193, - 113, - 44, - 106, - 106, - 23, - 158, - 204, - 85, - 85, - 122, - 4, - 2, - 28, - 60, - 30, - 119, - 44, - 70, - 144, - 47, - 26, - 157, - 17, - 43, - 215, - 92, - 133, - 149, - 107, - 174, - 2, - 0, - 76, - 217, - 134, - 97, - 29, - 59, - 129, - 83, - 255, - 185, - 3, - 179, - 179, - 54, - 152, - 150, - 109, - 65, - 211, - 170, - 107, - 208, - 188, - 234, - 90, - 84, - 213, - 46, - 43, - 222, - 85, - 39, - 16, - 36, - 130, - 224, - 2, - 48, - 61, - 101, - 129, - 222, - 80, - 11, - 0, - 9, - 110, - 251, - 117, - 159, - 254, - 28, - 190, - 122, - 251, - 245, - 24, - 59, - 63, - 140, - 106, - 67, - 13, - 216, - 179, - 131, - 9, - 159, - 243, - 121, - 221, - 208, - 84, - 49, - 208, - 235, - 141, - 240, - 121, - 23, - 150, - 246, - 60, - 30, - 55, - 0, - 20, - 108, - 252, - 169, - 104, - 104, - 234, - 70, - 67, - 83, - 55, - 54, - 92, - 124, - 19, - 252, - 94, - 39, - 108, - 230, - 83, - 176, - 12, - 255, - 30, - 251, - 247, - 124, - 15, - 148, - 218, - 128, - 166, - 85, - 159, - 64, - 211, - 170, - 109, - 168, - 239, - 218, - 74, - 188, - 3, - 66, - 89, - 66, - 191, - 251, - 135, - 31, - 163, - 181, - 179, - 23, - 173, - 29, - 27, - 160, - 209, - 25, - 11, - 58, - 217, - 161, - 119, - 255, - 11, - 20, - 165, - 68, - 207, - 5, - 27, - 22, - 189, - 199, - 48, - 26, - 104, - 117, - 122, - 240, - 124, - 0, - 109, - 29, - 93, - 120, - 243, - 55, - 47, - 197, - 222, - 179, - 59, - 237, - 224, - 88, - 22, - 166, - 186, - 54, - 52, - 183, - 47, - 195, - 209, - 247, - 247, - 197, - 222, - 27, - 63, - 55, - 132, - 150, - 214, - 206, - 162, - 95, - 8, - 141, - 206, - 136, - 101, - 221, - 151, - 98, - 89, - 247, - 165, - 0, - 0, - 215, - 172, - 5, - 227, - 231, - 142, - 225, - 204, - 158, - 157, - 56, - 50, - 117, - 22, - 198, - 214, - 141, - 104, - 238, - 185, - 6, - 77, - 171, - 175, - 37, - 75, - 141, - 132, - 178, - 129, - 174, - 93, - 115, - 51, - 206, - 13, - 239, - 197, - 7, - 7, - 94, - 65, - 85, - 149, - 30, - 45, - 29, - 189, - 104, - 237, - 236, - 141, - 204, - 155, - 51, - 224, - 243, - 122, - 96, - 157, - 60, - 15, - 231, - 148, - 29, - 253, - 199, - 14, - 225, - 205, - 223, - 189, - 140, - 237, - 247, - 63, - 30, - 9, - 0, - 186, - 28, - 24, - 57, - 221, - 143, - 85, - 107, - 55, - 1, - 0, - 222, - 121, - 243, - 53, - 168, - 213, - 106, - 180, - 117, - 44, - 7, - 195, - 104, - 16, - 8, - 112, - 120, - 247, - 157, - 55, - 176, - 105, - 243, - 149, - 120, - 125, - 247, - 143, - 113, - 233, - 229, - 215, - 129, - 97, - 52, - 216, - 180, - 249, - 74, - 188, - 244, - 15, - 207, - 226, - 228, - 241, - 35, - 232, - 232, - 234, - 198, - 111, - 254, - 237, - 31, - 99, - 193, - 194, - 82, - 98, - 168, - 105, - 193, - 218, - 141, - 45, - 88, - 187, - 241, - 191, - 197, - 150, - 26, - 173, - 227, - 135, - 113, - 228, - 240, - 47, - 17, - 8, - 43, - 208, - 184, - 242, - 42, - 52, - 245, - 108, - 67, - 195, - 138, - 203, - 73, - 48, - 145, - 32, - 91, - 20, - 46, - 135, - 45, - 86, - 13, - 232, - 24, - 253, - 0, - 214, - 161, - 183, - 96, - 27, - 217, - 7, - 183, - 109, - 0, - 77, - 173, - 23, - 160, - 181, - 243, - 99, - 104, - 237, - 236, - 197, - 248, - 172, - 10, - 43, - 214, - 127, - 60, - 246, - 193, - 228, - 60, - 128, - 182, - 142, - 21, - 216, - 118, - 195, - 159, - 199, - 34, - 254, - 30, - 143, - 27, - 255, - 240, - 252, - 83, - 56, - 63, - 114, - 26, - 148, - 82, - 137, - 229, - 221, - 107, - 241, - 185, - 47, - 125, - 21, - 245, - 141, - 45, - 0, - 128, - 177, - 243, - 195, - 248, - 167, - 159, - 125, - 23, - 147, - 86, - 51, - 46, - 88, - 183, - 41, - 33, - 15, - 224, - 228, - 241, - 35, - 120, - 249, - 133, - 31, - 97, - 110, - 206, - 137, - 141, - 23, - 95, - 142, - 47, - 220, - 113, - 63, - 24, - 70, - 147, - 48, - 240, - 51, - 39, - 222, - 195, - 5, - 157, - 53, - 162, - 92, - 180, - 57, - 215, - 84, - 164, - 196, - 121, - 244, - 67, - 216, - 204, - 167, - 200, - 82, - 35, - 65, - 182, - 36, - 8, - 64, - 60, - 172, - 103, - 26, - 83, - 103, - 222, - 133, - 109, - 104, - 47, - 172, - 131, - 111, - 99, - 195, - 182, - 199, - 19, - 4, - 64, - 108, - 196, - 20, - 128, - 100, - 108, - 19, - 3, - 176, - 140, - 245, - 195, - 60, - 250, - 33, - 60, - 30, - 55, - 76, - 203, - 183, - 160, - 169, - 123, - 27, - 154, - 87, - 93, - 11, - 173, - 177, - 77, - 236, - 225, - 17, - 8, - 105, - 73, - 43, - 0, - 201, - 140, - 190, - 255, - 42, - 17, - 128, - 44, - 32, - 45, - 210, - 8, - 114, - 66, - 214, - 181, - 0, - 82, - 68, - 163, - 51, - 98, - 197, - 234, - 203, - 177, - 98, - 245, - 229, - 0, - 22, - 90, - 164, - 157, - 126, - 243, - 49, - 56, - 102, - 204, - 48, - 45, - 219, - 130, - 198, - 149, - 87, - 144, - 22, - 105, - 4, - 73, - 64, - 4, - 160, - 200, - 212, - 53, - 116, - 161, - 174, - 161, - 11, - 27, - 46, - 190, - 105, - 161, - 69, - 218, - 249, - 119, - 112, - 224, - 221, - 231, - 72, - 139, - 52, - 130, - 232, - 100, - 61, - 5, - 152, - 26, - 217, - 15, - 85, - 8, - 146, - 104, - 10, - 50, - 109, - 30, - 65, - 152, - 117, - 160, - 205, - 164, - 21, - 123, - 40, - 5, - 17, - 93, - 106, - 180, - 140, - 245, - 99, - 218, - 54, - 2, - 99, - 75, - 47, - 154, - 87, - 125, - 2, - 77, - 221, - 87, - 195, - 216, - 186, - 161, - 240, - 95, - 64, - 32, - 100, - 32, - 107, - 1, - 224, - 185, - 57, - 56, - 198, - 142, - 73, - 162, - 19, - 143, - 190, - 74, - 141, - 38, - 35, - 5, - 154, - 86, - 23, - 126, - 50, - 137, - 192, - 243, - 44, - 166, - 204, - 167, - 99, - 241, - 3, - 63, - 199, - 161, - 169, - 251, - 42, - 82, - 183, - 64, - 40, - 42, - 89, - 11, - 128, - 80, - 36, - 47, - 53, - 54, - 182, - 172, - 70, - 107, - 231, - 6, - 180, - 116, - 108, - 128, - 161, - 166, - 69, - 236, - 235, - 33, - 25, - 230, - 92, - 83, - 176, - 77, - 124, - 20, - 105, - 177, - 62, - 126, - 18, - 213, - 166, - 110, - 52, - 175, - 254, - 4, - 26, - 86, - 92, - 137, - 250, - 229, - 91, - 197, - 30, - 30, - 161, - 76, - 40, - 185, - 0, - 196, - 195, - 249, - 103, - 49, - 125, - 230, - 0, - 172, - 131, - 111, - 193, - 54, - 180, - 7, - 84, - 152, - 71, - 107, - 199, - 6, - 180, - 117, - 109, - 68, - 115, - 251, - 186, - 178, - 122, - 194, - 23, - 202, - 148, - 109, - 24, - 230, - 115, - 125, - 48, - 143, - 126, - 136, - 57, - 247, - 52, - 76, - 43, - 34, - 129, - 196, - 166, - 158, - 109, - 208, - 213, - 116, - 136, - 61, - 60, - 130, - 76, - 17, - 85, - 0, - 146, - 113, - 218, - 62, - 194, - 212, - 200, - 126, - 216, - 134, - 246, - 96, - 102, - 244, - 48, - 234, - 234, - 151, - 161, - 165, - 115, - 3, - 90, - 59, - 122, - 81, - 215, - 208, - 37, - 246, - 240, - 36, - 67, - 116, - 169, - 209, - 58, - 118, - 2, - 214, - 241, - 147, - 80, - 86, - 53, - 160, - 113, - 197, - 21, - 164, - 110, - 129, - 144, - 51, - 146, - 18, - 128, - 120, - 120, - 110, - 14, - 211, - 231, - 14, - 98, - 106, - 228, - 93, - 88, - 135, - 246, - 32, - 224, - 182, - 162, - 181, - 179, - 23, - 205, - 29, - 235, - 5, - 169, - 91, - 40, - 39, - 102, - 166, - 206, - 193, - 60, - 118, - 28, - 150, - 209, - 126, - 204, - 144, - 22, - 105, - 132, - 28, - 144, - 172, - 0, - 36, - 227, - 157, - 29, - 131, - 109, - 104, - 47, - 108, - 195, - 123, - 97, - 63, - 179, - 31, - 213, - 250, - 250, - 72, - 154, - 114, - 215, - 70, - 52, - 52, - 117, - 23, - 254, - 11, - 202, - 132, - 133, - 22, - 105, - 253, - 176, - 140, - 245, - 147, - 22, - 105, - 132, - 37, - 145, - 141, - 0, - 36, - 51, - 125, - 246, - 32, - 166, - 206, - 252, - 17, - 214, - 211, - 111, - 97, - 206, - 62, - 140, - 150, - 246, - 117, - 104, - 110, - 95, - 143, - 214, - 206, - 94, - 84, - 27, - 26, - 196, - 30, - 158, - 100, - 136, - 182, - 72, - 51, - 143, - 246, - 99, - 210, - 114, - 154, - 212, - 45, - 16, - 18, - 144, - 173, - 0, - 196, - 195, - 122, - 166, - 49, - 57, - 188, - 15, - 182, - 161, - 61, - 176, - 13, - 239, - 131, - 134, - 97, - 98, - 37, - 206, - 13, - 173, - 171, - 73, - 48, - 113, - 158, - 248, - 22, - 105, - 150, - 209, - 227, - 164, - 69, - 26, - 161, - 60, - 4, - 32, - 25, - 167, - 185, - 31, - 182, - 225, - 119, - 96, - 29, - 124, - 11, - 78, - 203, - 113, - 212, - 55, - 173, - 68, - 75, - 199, - 6, - 180, - 119, - 109, - 34, - 75, - 141, - 113, - 196, - 90, - 164, - 141, - 246, - 195, - 58, - 113, - 146, - 180, - 72, - 171, - 64, - 202, - 82, - 0, - 226, - 225, - 185, - 57, - 76, - 14, - 255, - 17, - 182, - 225, - 189, - 176, - 13, - 238, - 133, - 34, - 228, - 71, - 107, - 199, - 6, - 180, - 118, - 246, - 162, - 165, - 115, - 3, - 241, - 14, - 226, - 136, - 182, - 72, - 51, - 159, - 239, - 35, - 45, - 210, - 42, - 132, - 178, - 23, - 128, - 100, - 220, - 147, - 131, - 17, - 49, - 152, - 95, - 106, - 172, - 53, - 117, - 160, - 165, - 99, - 3, - 218, - 150, - 109, - 36, - 75, - 141, - 113, - 68, - 91, - 164, - 153, - 199, - 142, - 195, - 114, - 254, - 120, - 66, - 139, - 52, - 125, - 195, - 42, - 208, - 76, - 21, - 153, - 50, - 148, - 1, - 21, - 39, - 0, - 241, - 68, - 211, - 155, - 173, - 131, - 111, - 193, - 58, - 180, - 7, - 156, - 219, - 130, - 150, - 121, - 239, - 128, - 44, - 53, - 38, - 50, - 51, - 117, - 14, - 214, - 137, - 143, - 96, - 62, - 255, - 33, - 188, - 115, - 118, - 176, - 172, - 7, - 1, - 206, - 15, - 5, - 165, - 4, - 173, - 214, - 67, - 165, - 174, - 134, - 74, - 173, - 135, - 82, - 93, - 13, - 70, - 87, - 3, - 90, - 165, - 131, - 74, - 99, - 4, - 163, - 53, - 66, - 169, - 210, - 65, - 165, - 53, - 130, - 102, - 170, - 160, - 210, - 26, - 64, - 171, - 170, - 161, - 82, - 87, - 131, - 214, - 26, - 136, - 144, - 136, - 76, - 69, - 11, - 64, - 50, - 62, - 231, - 4, - 172, - 131, - 111, - 71, - 150, - 26, - 207, - 30, - 202, - 185, - 69, - 90, - 37, - 194, - 243, - 44, - 120, - 206, - 143, - 0, - 239, - 71, - 128, - 245, - 33, - 192, - 249, - 16, - 8, - 248, - 192, - 177, - 94, - 4, - 56, - 47, - 2, - 129, - 200, - 235, - 28, - 235, - 137, - 252, - 204, - 249, - 16, - 224, - 188, - 224, - 3, - 126, - 176, - 108, - 228, - 239, - 69, - 66, - 162, - 49, - 66, - 165, - 49, - 128, - 214, - 84, - 167, - 20, - 18, - 149, - 198, - 0, - 90, - 93, - 5, - 149, - 218, - 0, - 90, - 163, - 135, - 74, - 173, - 7, - 173, - 209, - 147, - 37, - 206, - 60, - 32, - 2, - 176, - 4, - 75, - 181, - 72, - 35, - 75, - 141, - 194, - 146, - 74, - 72, - 22, - 68, - 195, - 11, - 142, - 245, - 130, - 15, - 176, - 139, - 132, - 36, - 192, - 122, - 193, - 5, - 252, - 224, - 3, - 126, - 112, - 172, - 7, - 180, - 74, - 3, - 74, - 85, - 181, - 72, - 72, - 84, - 234, - 136, - 183, - 65, - 132, - 36, - 17, - 34, - 0, - 89, - 18, - 223, - 34, - 109, - 114, - 100, - 31, - 84, - 138, - 48, - 90, - 58, - 54, - 160, - 165, - 115, - 3, - 169, - 91, - 144, - 16, - 81, - 33, - 225, - 184, - 136, - 96, - 68, - 133, - 132, - 99, - 61, - 224, - 121, - 118, - 222, - 51, - 137, - 122, - 42, - 139, - 133, - 132, - 99, - 61, - 224, - 3, - 126, - 208, - 42, - 13, - 104, - 141, - 17, - 180, - 90, - 63, - 239, - 133, - 44, - 8, - 9, - 163, - 173, - 1, - 205, - 232, - 202, - 66, - 72, - 136, - 0, - 228, - 137, - 211, - 246, - 17, - 108, - 167, - 223, - 134, - 117, - 104, - 15, - 156, - 230, - 62, - 152, - 26, - 150, - 163, - 117, - 217, - 199, - 208, - 220, - 182, - 150, - 4, - 19, - 203, - 128, - 120, - 33, - 97, - 89, - 15, - 120, - 214, - 11, - 158, - 231, - 82, - 10, - 9, - 199, - 121, - 192, - 7, - 184, - 140, - 66, - 162, - 154, - 23, - 7, - 37, - 163, - 75, - 16, - 18, - 149, - 182, - 6, - 74, - 149, - 54, - 38, - 36, - 106, - 77, - 29, - 40, - 181, - 22, - 140, - 198, - 56, - 31, - 59, - 41, - 222, - 114, - 44, - 17, - 0, - 1, - 136, - 214, - 45, - 216, - 6, - 247, - 194, - 54, - 248, - 22, - 66, - 172, - 11, - 45, - 203, - 122, - 209, - 218, - 209, - 139, - 166, - 214, - 11, - 72, - 48, - 177, - 130, - 89, - 74, - 72, - 88, - 214, - 131, - 32, - 207, - 45, - 8, - 9, - 235, - 141, - 136, - 9, - 231, - 3, - 199, - 249, - 22, - 4, - 39, - 224, - 7, - 205, - 84, - 129, - 86, - 87, - 47, - 18, - 18, - 70, - 91, - 27, - 241, - 56, - 210, - 8, - 9, - 173, - 209, - 71, - 188, - 147, - 52, - 66, - 66, - 4, - 160, - 8, - 120, - 28, - 231, - 35, - 193, - 196, - 193, - 61, - 176, - 159, - 63, - 132, - 154, - 154, - 38, - 180, - 46, - 219, - 136, - 230, - 142, - 245, - 164, - 110, - 129, - 144, - 23, - 169, - 132, - 132, - 227, - 124, - 243, - 193, - 212, - 136, - 144, - 112, - 172, - 7, - 1, - 214, - 139, - 0, - 239, - 143, - 196, - 76, - 230, - 133, - 36, - 242, - 26, - 155, - 82, - 72, - 136, - 0, - 20, - 153, - 232, - 82, - 163, - 109, - 100, - 31, - 108, - 131, - 111, - 195, - 239, - 28, - 67, - 115, - 219, - 58, - 180, - 118, - 70, - 114, - 15, - 136, - 119, - 64, - 40, - 37, - 60, - 207, - 194, - 239, - 117, - 33, - 20, - 226, - 193, - 178, - 30, - 34, - 0, - 165, - 198, - 231, - 156, - 192, - 244, - 185, - 67, - 145, - 186, - 133, - 193, - 61, - 208, - 105, - 171, - 208, - 210, - 217, - 139, - 150, - 246, - 117, - 164, - 110, - 129, - 80, - 114, - 136, - 0, - 136, - 12, - 105, - 145, - 70, - 16, - 19, - 34, - 0, - 18, - 130, - 243, - 207, - 98, - 114, - 232, - 29, - 76, - 142, - 252, - 17, - 182, - 161, - 61, - 100, - 169, - 145, - 80, - 116, - 136, - 0, - 72, - 24, - 210, - 34, - 141, - 80, - 108, - 136, - 0, - 200, - 132, - 132, - 165, - 198, - 161, - 61, - 8, - 249, - 103, - 209, - 220, - 190, - 142, - 180, - 72, - 35, - 20, - 4, - 17, - 0, - 153, - 226, - 113, - 156, - 143, - 52, - 65, - 33, - 45, - 210, - 8, - 5, - 64, - 4, - 160, - 76, - 152, - 62, - 123, - 16, - 214, - 161, - 183, - 49, - 53, - 188, - 47, - 214, - 34, - 173, - 109, - 217, - 70, - 52, - 181, - 173, - 37, - 117, - 11, - 132, - 180, - 16, - 1, - 40, - 67, - 72, - 139, - 52, - 66, - 182, - 16, - 1, - 168, - 0, - 72, - 139, - 52, - 66, - 58, - 136, - 0, - 84, - 24, - 164, - 69, - 26, - 33, - 30, - 34, - 0, - 21, - 78, - 66, - 139, - 180, - 177, - 15, - 80, - 91, - 215, - 74, - 90, - 164, - 85, - 16, - 68, - 0, - 8, - 49, - 146, - 91, - 164, - 5, - 220, - 86, - 52, - 119, - 172, - 71, - 93, - 67, - 23, - 12, - 53, - 205, - 208, - 85, - 213, - 129, - 86, - 169, - 193, - 48, - 58, - 208, - 140, - 134, - 120, - 11, - 101, - 0, - 17, - 0, - 66, - 90, - 162, - 117, - 11, - 179, - 230, - 227, - 112, - 79, - 143, - 128, - 157, - 155, - 66, - 192, - 239, - 68, - 128, - 157, - 67, - 40, - 176, - 80, - 239, - 206, - 168, - 171, - 192, - 168, - 52, - 80, - 169, - 117, - 80, - 49, - 58, - 168, - 24, - 45, - 84, - 42, - 77, - 228, - 111, - 70, - 11, - 38, - 250, - 250, - 252, - 177, - 42, - 70, - 75, - 132, - 68, - 34, - 16, - 1, - 32, - 20, - 4, - 231, - 159, - 5, - 239, - 119, - 35, - 192, - 186, - 231, - 255, - 118, - 129, - 103, - 61, - 8, - 248, - 93, - 8, - 248, - 156, - 8, - 6, - 188, - 224, - 124, - 78, - 240, - 156, - 39, - 242, - 158, - 127, - 46, - 242, - 222, - 188, - 144, - 240, - 172, - 27, - 225, - 80, - 16, - 42, - 70, - 19, - 17, - 147, - 20, - 66, - 194, - 168, - 171, - 34, - 130, - 145, - 66, - 72, - 84, - 106, - 45, - 84, - 180, - 134, - 8, - 73, - 158, - 16, - 1, - 32, - 72, - 2, - 214, - 51, - 13, - 158, - 243, - 68, - 254, - 204, - 11, - 73, - 192, - 231, - 138, - 8, - 71, - 156, - 144, - 4, - 252, - 78, - 240, - 1, - 47, - 56, - 239, - 44, - 130, - 156, - 55, - 173, - 144, - 168, - 213, - 58, - 208, - 42, - 77, - 76, - 72, - 212, - 234, - 170, - 200, - 191, - 213, - 81, - 239, - 68, - 23, - 17, - 20, - 149, - 182, - 162, - 133, - 132, - 8, - 0, - 161, - 172, - 136, - 9, - 137, - 207, - 21, - 17, - 134, - 192, - 92, - 130, - 144, - 112, - 126, - 103, - 76, - 56, - 162, - 158, - 73, - 144, - 157, - 139, - 8, - 9, - 231, - 77, - 16, - 18, - 245, - 188, - 231, - 17, - 21, - 17, - 38, - 234, - 149, - 48, - 90, - 208, - 140, - 166, - 44, - 132, - 132, - 8, - 0, - 129, - 144, - 4, - 207, - 205, - 33, - 24, - 240, - 39, - 8, - 73, - 128, - 117, - 130, - 103, - 61, - 17, - 1, - 137, - 254, - 157, - 78, - 72, - 230, - 167, - 68, - 0, - 192, - 196, - 98, - 32, - 218, - 121, - 143, - 100, - 177, - 144, - 48, - 76, - 220, - 20, - 71, - 21, - 141, - 155, - 84, - 129, - 166, - 153, - 162, - 215, - 120, - 16, - 1, - 32, - 16, - 138, - 68, - 182, - 66, - 18, - 152, - 23, - 19, - 206, - 231, - 4, - 207, - 186, - 17, - 240, - 187, - 16, - 228, - 60, - 105, - 133, - 68, - 197, - 232, - 98, - 65, - 212, - 168, - 88, - 40, - 85, - 76, - 94, - 66, - 66, - 4, - 128, - 64, - 144, - 56, - 201, - 66, - 194, - 249, - 102, - 17, - 96, - 221, - 8, - 6, - 124, - 25, - 133, - 132, - 103, - 61, - 145, - 159, - 211, - 8, - 9, - 17, - 0, - 2, - 161, - 66, - 136, - 10, - 9, - 231, - 153, - 65, - 136, - 103, - 193, - 249, - 102, - 137, - 0, - 16, - 8, - 149, - 12, - 37, - 246, - 0, - 8, - 4, - 130, - 120, - 16, - 1, - 32, - 16, - 42, - 24, - 34, - 0, - 4, - 66, - 5, - 67, - 4, - 128, - 64, - 168, - 96, - 136, - 0, - 16, - 8, - 21, - 12, - 17, - 0, - 2, - 161, - 130, - 161, - 197, - 30, - 0, - 129, - 16, - 101, - 192, - 113, - 2, - 78, - 191, - 3, - 0, - 64, - 81, - 74, - 208, - 97, - 26, - 148, - 82, - 1, - 53, - 173, - 5, - 173, - 160, - 161, - 85, - 234, - 208, - 174, - 239, - 20, - 123, - 152, - 101, - 5, - 201, - 3, - 32, - 136, - 206, - 128, - 227, - 4, - 166, - 125, - 147, - 240, - 243, - 190, - 172, - 142, - 167, - 20, - 20, - 40, - 5, - 5, - 5, - 148, - 80, - 82, - 20, - 40, - 80, - 80, - 42, - 148, - 160, - 40, - 37, - 40, - 80, - 160, - 41, - 37, - 104, - 74, - 5, - 37, - 69, - 67, - 163, - 212, - 194, - 200, - 212, - 160, - 78, - 99, - 18, - 251, - 107, - 74, - 18, - 226, - 1, - 16, - 68, - 35, - 87, - 195, - 143, - 18, - 10, - 135, - 16, - 10, - 135, - 0, - 240, - 8, - 132, - 178, - 255, - 28, - 77, - 69, - 110, - 119, - 74, - 161, - 140, - 136, - 72, - 156, - 112, - 68, - 189, - 13, - 154, - 82, - 65, - 173, - 212, - 84, - 140, - 183, - 65, - 60, - 0, - 66, - 201, - 201, - 215, - 240, - 197, - 32, - 87, - 111, - 163, - 90, - 85, - 141, - 6, - 109, - 147, - 216, - 195, - 206, - 26, - 226, - 1, - 16, - 74, - 134, - 156, - 12, - 63, - 74, - 185, - 123, - 27, - 196, - 3, - 32, - 20, - 29, - 57, - 26, - 190, - 24, - 228, - 228, - 109, - 64, - 141, - 106, - 141, - 161, - 96, - 111, - 131, - 120, - 0, - 132, - 162, - 81, - 206, - 134, - 207, - 7, - 120, - 76, - 78, - 78, - 161, - 177, - 177, - 1, - 180, - 138, - 70, - 40, - 20, - 134, - 195, - 49, - 3, - 214, - 207, - 65, - 167, - 211, - 193, - 104, - 212, - 67, - 65, - 229, - 182, - 202, - 158, - 179, - 183, - 17, - 169, - 240, - 205, - 217, - 219, - 48, - 168, - 141, - 48, - 48, - 145, - 254, - 0, - 196, - 3, - 32, - 8, - 78, - 57, - 27, - 126, - 148, - 57, - 183, - 27, - 44, - 199, - 65, - 205, - 48, - 168, - 214, - 235, - 49, - 235, - 112, - 2, - 0, - 12, - 70, - 3, - 92, - 78, - 23, - 0, - 160, - 166, - 86, - 186, - 59, - 54, - 71, - 189, - 13, - 146, - 8, - 68, - 16, - 140, - 1, - 199, - 9, - 236, - 55, - 239, - 193, - 184, - 251, - 124, - 89, - 27, - 63, - 0, - 120, - 61, - 126, - 24, - 13, - 70, - 120, - 61, - 254, - 200, - 191, - 189, - 94, - 24, - 140, - 6, - 80, - 148, - 2, - 6, - 163, - 1, - 94, - 175, - 87, - 236, - 33, - 46, - 73, - 40, - 28, - 2, - 31, - 226, - 201, - 20, - 128, - 80, - 56, - 149, - 240, - 196, - 143, - 135, - 15, - 240, - 80, - 170, - 40, - 208, - 42, - 26, - 74, - 21, - 5, - 62, - 192, - 47, - 58, - 134, - 202, - 209, - 253, - 23, - 11, - 34, - 0, - 132, - 188, - 17, - 218, - 240, - 139, - 49, - 175, - 46, - 6, - 126, - 191, - 15, - 106, - 134, - 1, - 0, - 168, - 25, - 6, - 126, - 191, - 15, - 58, - 157, - 14, - 46, - 167, - 43, - 54, - 5, - 208, - 104, - 52, - 98, - 15, - 51, - 43, - 196, - 191, - 154, - 4, - 217, - 81, - 44, - 87, - 223, - 239, - 247, - 65, - 173, - 137, - 24, - 20, - 0, - 184, - 156, - 46, - 40, - 41, - 26, - 205, - 45, - 205, - 0, - 0, - 167, - 211, - 45, - 246, - 87, - 7, - 16, - 113, - 255, - 53, - 26, - 45, - 0, - 64, - 163, - 209, - 194, - 235, - 241, - 195, - 96, - 52, - 32, - 24, - 226, - 97, - 181, - 88, - 17, - 12, - 241, - 48, - 24, - 13, - 98, - 15, - 51, - 43, - 136, - 7, - 64, - 200, - 154, - 98, - 187, - 250, - 94, - 143, - 31, - 117, - 166, - 90, - 204, - 216, - 29, - 168, - 214, - 235, - 225, - 245, - 122, - 209, - 220, - 210, - 28, - 155, - 87, - 91, - 45, - 86, - 73, - 4, - 214, - 248, - 96, - 196, - 83, - 137, - 135, - 162, - 20, - 48, - 153, - 76, - 48, - 79, - 88, - 96, - 50, - 201, - 39, - 237, - 152, - 8, - 0, - 33, - 35, - 165, - 152, - 227, - 203, - 105, - 94, - 221, - 218, - 214, - 2, - 0, - 48, - 79, - 88, - 98, - 63, - 203, - 21, - 34, - 0, - 132, - 180, - 148, - 50, - 184, - 87, - 78, - 243, - 106, - 57, - 65, - 4, - 128, - 176, - 8, - 49, - 162, - 250, - 81, - 247, - 31, - 136, - 204, - 171, - 103, - 236, - 14, - 212, - 55, - 214, - 195, - 225, - 152, - 129, - 213, - 98, - 133, - 90, - 195, - 160, - 182, - 182, - 78, - 236, - 75, - 83, - 118, - 144, - 68, - 32, - 66, - 12, - 49, - 151, - 243, - 204, - 19, - 150, - 69, - 175, - 149, - 147, - 171, - 45, - 4, - 233, - 86, - 69, - 146, - 87, - 79, - 114, - 129, - 120, - 0, - 4, - 73, - 172, - 227, - 19, - 99, - 207, - 204, - 194, - 170, - 72, - 29, - 92, - 78, - 23, - 156, - 78, - 55, - 106, - 106, - 141, - 9, - 171, - 39, - 213, - 42, - 125, - 78, - 231, - 148, - 70, - 84, - 133, - 32, - 10, - 149, - 148, - 185, - 87, - 14, - 164, - 203, - 54, - 76, - 206, - 74, - 204, - 5, - 226, - 1, - 84, - 32, - 82, - 120, - 226, - 19, - 10, - 135, - 154, - 119, - 255, - 147, - 87, - 79, - 114, - 153, - 6, - 144, - 24, - 64, - 5, - 65, - 12, - 95, - 222, - 164, - 42, - 56, - 162, - 233, - 136, - 19, - 95, - 173, - 215, - 99, - 206, - 237, - 142, - 253, - 156, - 45, - 68, - 0, - 42, - 0, - 98, - 248, - 229, - 65, - 124, - 16, - 48, - 186, - 42, - 50, - 61, - 57, - 141, - 58, - 83, - 45, - 104, - 21, - 13, - 62, - 192, - 99, - 198, - 238, - 64, - 99, - 115, - 67, - 194, - 231, - 194, - 161, - 16, - 154, - 221, - 141, - 9, - 175, - 241, - 170, - 32, - 0, - 50, - 5, - 40, - 107, - 136, - 225, - 151, - 23, - 169, - 178, - 13, - 83, - 101, - 37, - 46, - 194, - 27, - 68, - 107, - 221, - 10, - 212, - 183, - 45, - 8, - 3, - 207, - 205, - 97, - 206, - 21, - 34, - 2, - 80, - 142, - 16, - 195, - 175, - 28, - 178, - 90, - 61, - 209, - 41, - 17, - 240, - 37, - 222, - 11, - 52, - 83, - 141, - 154, - 122, - 226, - 1, - 148, - 21, - 196, - 240, - 9, - 169, - 80, - 80, - 20, - 248, - 32, - 159, - 242, - 61, - 34, - 0, - 101, - 0, - 49, - 124, - 66, - 190, - 16, - 1, - 144, - 57, - 135, - 44, - 251, - 225, - 14, - 184, - 196, - 30, - 6, - 161, - 132, - 164, - 114, - 245, - 51, - 37, - 79, - 133, - 66, - 169, - 61, - 0, - 146, - 8, - 36, - 115, - 124, - 65, - 105, - 183, - 158, - 34, - 72, - 27, - 34, - 0, - 50, - 102, - 220, - 61, - 10, - 62, - 141, - 178, - 19, - 8, - 217, - 64, - 4, - 64, - 198, - 204, - 114, - 51, - 98, - 15, - 129, - 32, - 115, - 136, - 0, - 200, - 24, - 63, - 159, - 123, - 238, - 55, - 161, - 50, - 225, - 130, - 108, - 202, - 215, - 137, - 0, - 200, - 24, - 54, - 72, - 4, - 32, - 19, - 161, - 80, - 24, - 118, - 187, - 29, - 230, - 9, - 11, - 102, - 29, - 78, - 132, - 67, - 145, - 29, - 55, - 248, - 0, - 15, - 243, - 132, - 37, - 101, - 231, - 161, - 74, - 130, - 8, - 128, - 140, - 153, - 117, - 146, - 27, - 58, - 19, - 233, - 26, - 139, - 38, - 55, - 32, - 173, - 84, - 136, - 0, - 200, - 24, - 46, - 228, - 35, - 55, - 116, - 6, - 138, - 81, - 66, - 43, - 71, - 248, - 112, - 32, - 229, - 235, - 68, - 0, - 100, - 202, - 89, - 215, - 8, - 116, - 85, - 213, - 21, - 123, - 67, - 231, - 75, - 186, - 18, - 218, - 114, - 39, - 0, - 34, - 0, - 101, - 133, - 39, - 16, - 121, - 242, - 87, - 234, - 13, - 157, - 45, - 209, - 198, - 162, - 161, - 80, - 56, - 214, - 88, - 52, - 85, - 3, - 210, - 74, - 133, - 8, - 128, - 76, - 241, - 242, - 94, - 114, - 67, - 103, - 65, - 170, - 13, - 59, - 82, - 109, - 236, - 81, - 238, - 240, - 170, - 32, - 102, - 167, - 23, - 103, - 140, - 18, - 1, - 144, - 41, - 211, - 147, - 230, - 138, - 190, - 161, - 179, - 37, - 90, - 66, - 11, - 0, - 38, - 147, - 9, - 20, - 165, - 136, - 149, - 208, - 154, - 39, - 44, - 152, - 156, - 156, - 74, - 91, - 40, - 83, - 78, - 76, - 135, - 38, - 83, - 190, - 78, - 106, - 1, - 100, - 138, - 39, - 196, - 229, - 86, - 19, - 94, - 36, - 138, - 209, - 169, - 182, - 216, - 84, - 100, - 3, - 82, - 154, - 2, - 207, - 46, - 206, - 5, - 32, - 30, - 128, - 76, - 241, - 251, - 23, - 158, - 242, - 173, - 109, - 45, - 177, - 27, - 57, - 254, - 231, - 82, - 64, - 150, - 217, - 100, - 2, - 163, - 128, - 223, - 51, - 183, - 232, - 101, - 34, - 0, - 50, - 100, - 200, - 121, - 26, - 161, - 249, - 245, - 127, - 177, - 33, - 203, - 108, - 242, - 64, - 65, - 81, - 8, - 135, - 23, - 119, - 255, - 35, - 2, - 32, - 67, - 60, - 156, - 116, - 203, - 127, - 165, - 186, - 42, - 145, - 79, - 9, - 109, - 33, - 72, - 49, - 3, - 49, - 85, - 73, - 48, - 17, - 0, - 25, - 194, - 133, - 2, - 37, - 191, - 161, - 211, - 65, - 150, - 217, - 82, - 35, - 151, - 169, - 17, - 17, - 0, - 25, - 34, - 165, - 26, - 0, - 178, - 204, - 150, - 26, - 185, - 76, - 141, - 164, - 21, - 158, - 37, - 100, - 5, - 31, - 10, - 20, - 126, - 18, - 129, - 200, - 187, - 83, - 109, - 133, - 33, - 196, - 38, - 30, - 197, - 128, - 8, - 128, - 204, - 152, - 241, - 219, - 37, - 223, - 4, - 164, - 34, - 151, - 217, - 146, - 72, - 181, - 181, - 121, - 170, - 169, - 81, - 174, - 123, - 249, - 21, - 66, - 170, - 146, - 96, - 50, - 5, - 144, - 25, - 54, - 159, - 165, - 240, - 147, - 16, - 138, - 142, - 92, - 166, - 70, - 196, - 3, - 144, - 25, - 44, - 233, - 252, - 43, - 11, - 228, - 50, - 53, - 34, - 2, - 32, - 51, - 164, - 218, - 5, - 72, - 42, - 171, - 18, - 82, - 70, - 236, - 169, - 81, - 170, - 146, - 96, - 50, - 5, - 144, - 25, - 129, - 176, - 116, - 2, - 128, - 4, - 121, - 145, - 170, - 36, - 152, - 8, - 128, - 204, - 144, - 210, - 10, - 0, - 65, - 254, - 16, - 1, - 144, - 17, - 22, - 239, - 132, - 228, - 87, - 0, - 8, - 137, - 72, - 105, - 106, - 196, - 171, - 130, - 224, - 185, - 196, - 122, - 0, - 34, - 0, - 50, - 194, - 230, - 49, - 139, - 61, - 4, - 130, - 140, - 9, - 168, - 188, - 152, - 115, - 37, - 214, - 144, - 144, - 32, - 160, - 76, - 56, - 106, - 61, - 12, - 59, - 39, - 173, - 8, - 50, - 65, - 94, - 56, - 217, - 185, - 69, - 37, - 193, - 68, - 0, - 100, - 192, - 97, - 219, - 1, - 56, - 57, - 135, - 216, - 195, - 32, - 72, - 156, - 76, - 189, - 25, - 26, - 76, - 245, - 139, - 183, - 9, - 23, - 123, - 208, - 132, - 165, - 169, - 228, - 205, - 63, - 127, - 254, - 244, - 175, - 48, - 126, - 102, - 233, - 105, - 79, - 199, - 202, - 54, - 108, - 127, - 244, - 127, - 136, - 61, - 84, - 73, - 176, - 80, - 128, - 84, - 7, - 151, - 211, - 5, - 167, - 211, - 141, - 154, - 90, - 99, - 172, - 0, - 137, - 13, - 248, - 193, - 211, - 137, - 49, - 36, - 34, - 0, - 18, - 197, - 197, - 57, - 241, - 145, - 253, - 120, - 197, - 26, - 127, - 40, - 20, - 198, - 96, - 255, - 8, - 206, - 14, - 140, - 46, - 121, - 92, - 128, - 35, - 65, - 209, - 40, - 94, - 175, - 23, - 205, - 45, - 205, - 177, - 2, - 36, - 171, - 197, - 138, - 154, - 218, - 72, - 225, - 81, - 157, - 169, - 22, - 51, - 118, - 7, - 66, - 122, - 34, - 0, - 146, - 103, - 198, - 111, - 199, - 192, - 76, - 63, - 188, - 188, - 71, - 236, - 161, - 136, - 134, - 203, - 233, - 130, - 66, - 161, - 0, - 0, - 236, - 220, - 185, - 19, - 91, - 182, - 108, - 73, - 120, - 255, - 224, - 193, - 131, - 120, - 244, - 209, - 71, - 197, - 30, - 166, - 164, - 73, - 85, - 128, - 148, - 12, - 17, - 0, - 137, - 97, - 241, - 78, - 96, - 120, - 246, - 52, - 252, - 21, - 158, - 242, - 235, - 245, - 122, - 161, - 84, - 42, - 1, - 0, - 87, - 95, - 125, - 245, - 34, - 1, - 80, - 171, - 213, - 98, - 15, - 81, - 114, - 100, - 83, - 128, - 148, - 92, - 16, - 68, - 4, - 64, - 66, - 140, - 187, - 71, - 49, - 226, - 26, - 76, - 187, - 145, - 35, - 129, - 176, - 20, - 6, - 163, - 1, - 14, - 199, - 12, - 172, - 22, - 43, - 212, - 26, - 6, - 181, - 181, - 117, - 152, - 158, - 156, - 70, - 157, - 169, - 22, - 64, - 164, - 0, - 105, - 118, - 102, - 22, - 227, - 131, - 103, - 65, - 205, - 183, - 7, - 19, - 77, - 0, - 44, - 222, - 9, - 76, - 184, - 199, - 176, - 194, - 216, - 131, - 58, - 141, - 73, - 236, - 107, - 39, - 58, - 103, - 93, - 35, - 56, - 231, - 26, - 38, - 137, - 62, - 243, - 232, - 116, - 58, - 4, - 131, - 65, - 177, - 135, - 33, - 43, - 178, - 41, - 64, - 82, - 53, - 48, - 216, - 220, - 189, - 60, - 246, - 111, - 81, - 4, - 96, - 198, - 111, - 143, - 185, - 185, - 31, - 78, - 59, - 209, - 92, - 213, - 134, - 53, - 181, - 235, - 69, - 190, - 124, - 226, - 49, - 228, - 60, - 141, - 113, - 247, - 57, - 98, - 252, - 113, - 24, - 140, - 134, - 148, - 77, - 44, - 9, - 185, - 145, - 92, - 128, - 100, - 80, - 27, - 19, - 222, - 23, - 69, - 0, - 6, - 102, - 250, - 99, - 115, - 92, - 62, - 196, - 99, - 220, - 125, - 30, - 78, - 191, - 3, - 203, - 140, - 43, - 208, - 162, - 107, - 19, - 249, - 146, - 149, - 248, - 90, - 56, - 78, - 192, - 234, - 33, - 41, - 190, - 201, - 80, - 148, - 2, - 52, - 77, - 102, - 168, - 197, - 166, - 228, - 169, - 192, - 135, - 44, - 251, - 83, - 70, - 183, - 221, - 1, - 23, - 78, - 205, - 156, - 192, - 9, - 123, - 159, - 216, - 215, - 164, - 100, - 12, - 56, - 78, - 192, - 60, - 55, - 86, - 144, - 241, - 75, - 177, - 251, - 44, - 65, - 62, - 148, - 84, - 98, - 15, - 219, - 14, - 44, - 185, - 174, - 205, - 135, - 120, - 88, - 60, - 19, - 112, - 176, - 51, - 104, - 215, - 118, - 96, - 121, - 109, - 143, - 216, - 215, - 167, - 104, - 156, - 176, - 247, - 193, - 230, - 181, - 32, - 20, - 46, - 172, - 191, - 127, - 166, - 228, - 143, - 82, - 183, - 157, - 34, - 136, - 207, - 82, - 5, - 72, - 20, - 40, - 88, - 92, - 67, - 177, - 215, - 75, - 230, - 1, - 252, - 201, - 118, - 8, - 78, - 54, - 187, - 116, - 86, - 235, - 148, - 21, - 125, - 230, - 15, - 208, - 55, - 117, - 164, - 84, - 195, - 43, - 41, - 125, - 83, - 71, - 96, - 241, - 76, - 20, - 108, - 252, - 128, - 124, - 186, - 207, - 18, - 164, - 129, - 46, - 204, - 193, - 225, - 181, - 196, - 254, - 148, - 68, - 0, - 250, - 166, - 142, - 192, - 193, - 218, - 179, - 62, - 222, - 235, - 245, - 66, - 87, - 85, - 141, - 41, - 223, - 36, - 222, - 25, - 127, - 11, - 3, - 142, - 19, - 162, - 93, - 48, - 161, - 57, - 106, - 61, - 140, - 41, - 223, - 100, - 225, - 39, - 74, - 131, - 84, - 55, - 230, - 32, - 72, - 147, - 162, - 11, - 192, - 9, - 123, - 95, - 65, - 55, - 60, - 203, - 251, - 113, - 116, - 232, - 79, - 56, - 100, - 217, - 143, - 25, - 127, - 246, - 34, - 34, - 69, - 14, - 219, - 14, - 8, - 94, - 209, - 71, - 54, - 230, - 32, - 20, - 66, - 81, - 5, - 96, - 192, - 113, - 2, - 22, - 207, - 68, - 206, - 159, - 75, - 190, - 169, - 25, - 53, - 3, - 203, - 204, - 4, - 14, - 141, - 238, - 151, - 173, - 55, - 112, - 200, - 178, - 63, - 235, - 41, - 80, - 46, - 200, - 165, - 251, - 44, - 65, - 154, - 20, - 45, - 8, - 24, - 141, - 112, - 231, - 67, - 186, - 140, - 38, - 141, - 73, - 139, - 113, - 247, - 121, - 204, - 248, - 166, - 177, - 162, - 166, - 71, - 22, - 75, - 134, - 197, - 46, - 234, - 145, - 75, - 247, - 89, - 130, - 52, - 41, - 138, - 0, - 12, - 57, - 79, - 195, - 60, - 55, - 150, - 119, - 144, - 43, - 155, - 155, - 122, - 204, - 54, - 138, - 245, - 157, - 189, - 88, - 111, - 218, - 88, - 250, - 171, - 150, - 37, - 98, - 21, - 245, - 136, - 221, - 125, - 182, - 156, - 200, - 84, - 99, - 223, - 216, - 216, - 32, - 250, - 238, - 62, - 133, - 32, - 248, - 20, - 224, - 172, - 107, - 4, - 227, - 238, - 115, - 130, - 68, - 184, - 227, - 137, - 223, - 247, - 190, - 181, - 173, - 5, - 38, - 147, - 9, - 22, - 207, - 4, - 222, - 51, - 191, - 131, - 113, - 247, - 104, - 129, - 103, - 23, - 30, - 139, - 119, - 2, - 39, - 103, - 62, - 172, - 232, - 138, - 190, - 114, - 64, - 46, - 155, - 124, - 230, - 139, - 160, - 2, - 48, - 238, - 30, - 45, - 121, - 62, - 187, - 151, - 247, - 224, - 244, - 236, - 73, - 73, - 45, - 25, - 142, - 187, - 71, - 49, - 232, - 24, - 168, - 248, - 138, - 190, - 114, - 160, - 220, - 151, - 89, - 5, - 19, - 128, - 41, - 159, - 13, - 35, - 174, - 65, - 81, - 82, - 90, - 67, - 225, - 16, - 166, - 124, - 147, - 216, - 111, - 222, - 131, - 179, - 174, - 145, - 146, - 255, - 254, - 120, - 206, - 186, - 70, - 48, - 228, - 28, - 40, - 121, - 69, - 159, - 148, - 186, - 207, - 150, - 51, - 229, - 182, - 204, - 42, - 136, - 0, - 204, - 248, - 237, - 56, - 229, - 56, - 41, - 248, - 77, - 159, - 235, - 77, - 237, - 231, - 125, - 56, - 227, - 28, - 196, - 159, - 108, - 135, - 4, - 190, - 76, - 217, - 49, - 228, - 60, - 77, - 42, - 250, - 202, - 140, - 114, - 95, - 102, - 21, - 68, - 0, - 226, - 139, - 123, - 196, - 38, - 20, - 14, - 193, - 193, - 218, - 177, - 111, - 226, - 45, - 12, - 205, - 158, - 42, - 217, - 239, - 29, - 112, - 156, - 192, - 168, - 235, - 12, - 49, - 254, - 50, - 163, - 220, - 151, - 89, - 11, - 14, - 95, - 166, - 43, - 238, - 17, - 27, - 46, - 200, - 226, - 156, - 107, - 4, - 124, - 152, - 47, - 122, - 169, - 113, - 223, - 212, - 17, - 156, - 60, - 123, - 18, - 140, - 154, - 41, - 187, - 40, - 113, - 165, - 83, - 238, - 203, - 172, - 5, - 121, - 0, - 153, - 138, - 123, - 42, - 129, - 163, - 214, - 195, - 56, - 59, - 57, - 130, - 250, - 198, - 122, - 0, - 229, - 23, - 37, - 38, - 44, - 38, - 121, - 69, - 74, - 206, - 177, - 150, - 188, - 5, - 32, - 151, - 226, - 30, - 49, - 169, - 215, - 52, - 20, - 237, - 220, - 125, - 83, - 71, - 112, - 242, - 252, - 9, - 232, - 170, - 170, - 203, - 54, - 74, - 76, - 40, - 111, - 242, - 18, - 128, - 92, - 139, - 123, - 196, - 130, - 166, - 104, - 52, - 104, - 155, - 138, - 114, - 238, - 104, - 81, - 79, - 40, - 148, - 152, - 239, - 80, - 110, - 81, - 98, - 66, - 121, - 147, - 151, - 0, - 84, - 49, - 6, - 168, - 40, - 233, - 119, - 101, - 165, - 20, - 202, - 162, - 156, - 55, - 190, - 168, - 167, - 220, - 163, - 196, - 132, - 8, - 197, - 94, - 102, - 21, - 171, - 177, - 75, - 94, - 2, - 208, - 99, - 92, - 141, - 222, - 250, - 77, - 208, - 209, - 85, - 146, - 238, - 58, - 67, - 43, - 132, - 15, - 190, - 37, - 23, - 245, - 148, - 123, - 148, - 56, - 27, - 146, - 111, - 210, - 116, - 55, - 51, - 33, - 61, - 98, - 101, - 28, - 230, - 29, - 3, - 168, - 211, - 152, - 240, - 241, - 214, - 171, - 177, - 97, - 121, - 47, - 66, - 156, - 8, - 87, - 44, - 11, - 84, - 74, - 70, - 176, - 115, - 185, - 56, - 103, - 202, - 109, - 186, - 162, - 81, - 98, - 0, - 48, - 153, - 76, - 160, - 40, - 69, - 44, - 74, - 108, - 158, - 176, - 96, - 114, - 114, - 10, - 124, - 80, - 154, - 2, - 41, - 20, - 201, - 55, - 105, - 186, - 155, - 153, - 144, - 30, - 177, - 50, - 14, - 11, - 206, - 3, - 184, - 176, - 249, - 18, - 172, - 109, - 91, - 15, - 29, - 93, - 85, - 218, - 43, - 150, - 5, - 12, - 165, - 18, - 228, - 60, - 51, - 126, - 59, - 250, - 167, - 143, - 101, - 189, - 226, - 81, - 78, - 81, - 226, - 108, - 72, - 190, - 73, - 211, - 221, - 204, - 132, - 236, - 41, - 85, - 44, - 73, - 144, - 68, - 160, - 229, - 134, - 149, - 248, - 120, - 235, - 213, - 104, - 208, - 54, - 130, - 82, - 148, - 188, - 207, - 104, - 90, - 170, - 4, - 232, - 133, - 71, - 138, - 122, - 150, - 38, - 155, - 155, - 148, - 162, - 164, - 115, - 79, - 72, - 21, - 177, - 98, - 73, - 130, - 254, - 207, - 108, - 108, - 216, - 140, - 78, - 195, - 10, - 201, - 4, - 8, - 77, - 2, - 44, - 1, - 218, - 125, - 83, - 146, - 201, - 114, - 148, - 34, - 169, - 110, - 210, - 84, - 55, - 51, - 97, - 105, - 196, - 138, - 37, - 9, - 46, - 205, - 209, - 0, - 161, - 94, - 101, - 40, - 254, - 85, - 91, - 2, - 154, - 162, - 5, - 217, - 113, - 200, - 207, - 103, - 119, - 209, - 43, - 181, - 24, - 39, - 213, - 77, - 154, - 234, - 102, - 38, - 44, - 141, - 88, - 177, - 164, - 162, - 228, - 168, - 214, - 105, - 76, - 216, - 210, - 114, - 197, - 124, - 63, - 64, - 91, - 73, - 243, - 227, - 163, - 13, - 28, - 148, - 20, - 13, - 180, - 23, - 126, - 62, - 62, - 20, - 40, - 217, - 216, - 229, - 72, - 170, - 180, - 216, - 84, - 233, - 179, - 132, - 220, - 41, - 69, - 99, - 151, - 162, - 38, - 169, - 175, - 55, - 109, - 196, - 184, - 123, - 20, - 231, - 221, - 103, - 74, - 54, - 135, - 142, - 70, - 160, - 91, - 27, - 90, - 5, - 57, - 95, - 32, - 76, - 4, - 96, - 41, - 72, - 247, - 33, - 121, - 83, - 244, - 232, - 76, - 187, - 190, - 179, - 164, - 1, - 194, - 104, - 4, - 90, - 168, - 37, - 192, - 32, - 89, - 195, - 38, - 148, - 49, - 37, - 11, - 207, - 150, - 58, - 64, - 168, - 163, - 117, - 130, - 156, - 39, - 72, - 60, - 0, - 66, - 9, - 41, - 117, - 44, - 169, - 164, - 235, - 51, - 165, - 8, - 16, - 70, - 35, - 208, - 106, - 101, - 225, - 145, - 103, - 139, - 87, - 152, - 221, - 123, - 42, - 1, - 177, - 130, - 160, - 28, - 199, - 145, - 61, - 17, - 11, - 160, - 228, - 11, - 180, - 209, - 0, - 97, - 75, - 85, - 27, - 104, - 74, - 248, - 16, - 68, - 52, - 2, - 45, - 196, - 18, - 224, - 172, - 12, - 170, - 29, - 43, - 29, - 134, - 137, - 36, - 123, - 145, - 50, - 236, - 252, - 16, - 45, - 67, - 99, - 189, - 105, - 35, - 122, - 140, - 107, - 4, - 207, - 32, - 164, - 40, - 5, - 154, - 26, - 154, - 4, - 89, - 2, - 100, - 201, - 250, - 191, - 12, - 32, - 101, - 216, - 133, - 32, - 106, - 138, - 86, - 177, - 2, - 132, - 180, - 64, - 41, - 192, - 28, - 89, - 2, - 148, - 13, - 164, - 12, - 59, - 63, - 36, - 145, - 163, - 41, - 116, - 128, - 80, - 165, - 16, - 70, - 0, - 66, - 161, - 160, - 152, - 151, - 37, - 143, - 241, - 138, - 83, - 82, - 42, - 46, - 164, - 12, - 187, - 16, - 36, - 33, - 0, - 128, - 176, - 1, - 66, - 161, - 60, - 0, - 185, - 229, - 0, - 148, - 251, - 38, - 22, - 169, - 224, - 184, - 64, - 197, - 150, - 97, - 11, - 129, - 100, - 4, - 0, - 16, - 46, - 64, - 168, - 161, - 133, - 201, - 61, - 151, - 91, - 22, - 160, - 216, - 155, - 88, - 240, - 1, - 30, - 167, - 250, - 134, - 75, - 254, - 189, - 133, - 74, - 157, - 61, - 117, - 108, - 168, - 76, - 189, - 164, - 244, - 72, - 74, - 0, - 162, - 20, - 26, - 32, - 84, - 211, - 90, - 65, - 198, - 33, - 247, - 22, - 223, - 165, - 156, - 23, - 7, - 184, - 0, - 190, - 115, - 223, - 15, - 241, - 248, - 157, - 223, - 195, - 177, - 247, - 250, - 69, - 249, - 190, - 133, - 148, - 97, - 31, - 123, - 175, - 31, - 143, - 111, - 255, - 62, - 190, - 115, - 223, - 15, - 43, - 74, - 4, - 36, - 41, - 0, - 64, - 97, - 1, - 194, - 38, - 109, - 115, - 193, - 191, - 95, - 236, - 29, - 134, - 242, - 65, - 172, - 146, - 210, - 0, - 23, - 192, - 83, - 247, - 62, - 139, - 15, - 15, - 158, - 4, - 31, - 224, - 241, - 204, - 95, - 63, - 39, - 154, - 8, - 228, - 195, - 177, - 247, - 250, - 241, - 204, - 95, - 63, - 7, - 62, - 192, - 227, - 195, - 131, - 39, - 241, - 157, - 251, - 126, - 136, - 0, - 39, - 47, - 239, - 47, - 95, - 36, - 43, - 0, - 81, - 114, - 13, - 16, - 210, - 20, - 13, - 3, - 99, - 44, - 248, - 247, - 122, - 2, - 242, - 235, - 98, - 35, - 70, - 73, - 41, - 235, - 231, - 240, - 212, - 189, - 207, - 226, - 228, - 145, - 83, - 88, - 182, - 108, - 25, - 190, - 254, - 245, - 175, - 203, - 74, - 4, - 226, - 141, - 223, - 100, - 50, - 129, - 97, - 24, - 124, - 120, - 240, - 36, - 158, - 186, - 247, - 217, - 138, - 16, - 1, - 201, - 11, - 0, - 144, - 91, - 128, - 80, - 168, - 0, - 160, - 220, - 230, - 255, - 64, - 233, - 75, - 74, - 89, - 63, - 135, - 39, - 239, - 222, - 133, - 147, - 71, - 78, - 161, - 189, - 189, - 29, - 239, - 190, - 251, - 46, - 118, - 237, - 218, - 133, - 103, - 159, - 125, - 54, - 38, - 2, - 253, - 135, - 7, - 138, - 250, - 157, - 25, - 102, - 113, - 205, - 71, - 182, - 174, - 127, - 188, - 241, - 223, - 124, - 243, - 205, - 176, - 217, - 108, - 216, - 183, - 111, - 31, - 170, - 170, - 170, - 112, - 242, - 200, - 41, - 60, - 117, - 239, - 179, - 96, - 253, - 18, - 237, - 119, - 39, - 16, - 178, - 16, - 0, - 32, - 251, - 0, - 161, - 80, - 75, - 128, - 229, - 146, - 3, - 80, - 172, - 246, - 100, - 62, - 175, - 31, - 79, - 222, - 189, - 11, - 167, - 251, - 134, - 209, - 222, - 222, - 142, - 253, - 251, - 247, - 163, - 189, - 61, - 82, - 127, - 253, - 192, - 3, - 15, - 196, - 68, - 224, - 233, - 175, - 254, - 160, - 232, - 34, - 144, - 15, - 241, - 198, - 127, - 235, - 173, - 183, - 226, - 213, - 87, - 95, - 133, - 82, - 169, - 196, - 150, - 45, - 91, - 176, - 111, - 223, - 62, - 24, - 12, - 6, - 156, - 60, - 114, - 10, - 79, - 222, - 189, - 171, - 172, - 69, - 64, - 54, - 2, - 16, - 37, - 83, - 128, - 80, - 176, - 37, - 192, - 96, - 249, - 254, - 167, - 23, - 138, - 207, - 235, - 199, - 19, - 219, - 191, - 159, - 96, - 252, - 93, - 93, - 93, - 9, - 199, - 68, - 69, - 32, - 192, - 5, - 36, - 39, - 2, - 253, - 135, - 7, - 18, - 140, - 255, - 165, - 151, - 94, - 130, - 82, - 185, - 208, - 66, - 254, - 162, - 139, - 46, - 194, - 254, - 253, - 251, - 81, - 83, - 83, - 131, - 211, - 125, - 195, - 120, - 242, - 238, - 93, - 240, - 121, - 203, - 115, - 73, - 81, - 118, - 2, - 0, - 44, - 29, - 32, - 20, - 108, - 9, - 48, - 92, - 57, - 145, - 224, - 92, - 57, - 221, - 55, - 140, - 225, - 19, - 103, - 1, - 0, - 59, - 118, - 236, - 88, - 100, - 252, - 81, - 164, - 40, - 2, - 253, - 135, - 7, - 240, - 244, - 87, - 127, - 144, - 96, - 252, - 10, - 133, - 98, - 209, - 113, - 189, - 189, - 189, - 120, - 244, - 209, - 71, - 99, - 223, - 119, - 232, - 248, - 25, - 177, - 135, - 94, - 20, - 100, - 41, - 0, - 81, - 162, - 1, - 66, - 70, - 185, - 16, - 32, - 20, - 162, - 17, - 40, - 0, - 132, - 194, - 242, - 202, - 2, - 140, - 167, - 216, - 149, - 121, - 27, - 47, - 91, - 143, - 135, - 118, - 221, - 11, - 74, - 73, - 225, - 129, - 7, - 30, - 192, - 27, - 111, - 188, - 145, - 246, - 88, - 41, - 137, - 64, - 212, - 248, - 3, - 92, - 96, - 73, - 227, - 7, - 128, - 215, - 94, - 123, - 13, - 143, - 62, - 250, - 40, - 40, - 37, - 133, - 135, - 118, - 221, - 139, - 222, - 45, - 107, - 83, - 30, - 39, - 247, - 61, - 17, - 100, - 45, - 0, - 64, - 36, - 64, - 184, - 193, - 20, - 9, - 16, - 82, - 10, - 10, - 203, - 13, - 43, - 11, - 62, - 167, - 139, - 115, - 22, - 156, - 3, - 32, - 247, - 27, - 35, - 19, - 91, - 175, - 187, - 24, - 127, - 243, - 221, - 191, - 66, - 40, - 28, - 194, - 77, - 55, - 221, - 36, - 121, - 17, - 200, - 213, - 248, - 111, - 185, - 229, - 22, - 132, - 194, - 33, - 252, - 205, - 119, - 255, - 10, - 91, - 175, - 187, - 56, - 237, - 121, - 229, - 190, - 39, - 130, - 236, - 5, - 0, - 88, - 8, - 16, - 118, - 234, - 151, - 11, - 114, - 62, - 155, - 207, - 154, - 242, - 117, - 69, - 128, - 135, - 225, - 131, - 97, - 180, - 253, - 234, - 109, - 172, - 248, - 238, - 43, - 184, - 224, - 161, - 95, - 96, - 245, - 55, - 127, - 137, - 21, - 207, - 188, - 130, - 182, - 95, - 189, - 13, - 195, - 7, - 195, - 80, - 204, - 27, - 188, - 220, - 111, - 140, - 108, - 136, - 138, - 64, - 48, - 24, - 196, - 77, - 55, - 221, - 132, - 215, - 94, - 123, - 45, - 237, - 177, - 15, - 60, - 240, - 0, - 118, - 238, - 220, - 41, - 138, - 8, - 196, - 27, - 255, - 237, - 183, - 223, - 158, - 149, - 241, - 135, - 17, - 206, - 104, - 252, - 128, - 252, - 247, - 68, - 40, - 11, - 1, - 136, - 210, - 83, - 115, - 129, - 32, - 231, - 73, - 85, - 6, - 92, - 253, - 209, - 40, - 86, - 62, - 253, - 47, - 104, - 255, - 167, - 63, - 192, - 120, - 100, - 16, - 154, - 9, - 59, - 40, - 142, - 135, - 210, - 23, - 128, - 198, - 108, - 135, - 241, - 200, - 32, - 218, - 255, - 233, - 15, - 88, - 249, - 244, - 191, - 160, - 250, - 163, - 81, - 217, - 223, - 24, - 217, - 178, - 245, - 186, - 139, - 113, - 255, - 211, - 119, - 34, - 24, - 12, - 226, - 150, - 91, - 110, - 89, - 82, - 4, - 118, - 236, - 216, - 145, - 32, - 2, - 3, - 199, - 6, - 139, - 62, - 190, - 100, - 227, - 127, - 225, - 133, - 23, - 4, - 51, - 254, - 114, - 216, - 19, - 65, - 218, - 163, - 19, - 137, - 228, - 86, - 224, - 245, - 111, - 126, - 128, - 206, - 159, - 190, - 1, - 102, - 102, - 46, - 227, - 103, - 153, - 153, - 57, - 116, - 254, - 244, - 13, - 44, - 63, - 120, - 74, - 214, - 55, - 70, - 46, - 92, - 121, - 227, - 86, - 220, - 255, - 244, - 157, - 8, - 133, - 66, - 57, - 137, - 192, - 83, - 247, - 60, - 91, - 84, - 17, - 200, - 197, - 248, - 95, - 126, - 249, - 229, - 156, - 140, - 31, - 40, - 143, - 61, - 17, - 202, - 231, - 46, - 20, - 144, - 248, - 36, - 160, - 250, - 55, - 63, - 64, - 227, - 27, - 135, - 115, - 62, - 199, - 178, - 189, - 39, - 80, - 255, - 230, - 7, - 178, - 189, - 49, - 114, - 37, - 31, - 17, - 96, - 125, - 108, - 209, - 68, - 96, - 224, - 216, - 96, - 78, - 198, - 127, - 219, - 109, - 183, - 229, - 100, - 252, - 64, - 121, - 236, - 137, - 64, - 191, - 49, - 240, - 107, - 180, - 212, - 182, - 160, - 86, - 103, - 18, - 36, - 128, - 38, - 22, - 238, - 57, - 15, - 94, - 248, - 231, - 215, - 240, - 238, - 193, - 163, - 176, - 216, - 166, - 10, - 63, - 33, - 128, - 13, - 138, - 0, - 30, - 81, - 185, - 129, - 52, - 55, - 78, - 38, - 26, - 223, - 56, - 140, - 185, - 118, - 19, - 70, - 77, - 85, - 168, - 111, - 172, - 135, - 195, - 49, - 3, - 171, - 197, - 10, - 181, - 134, - 65, - 109, - 109, - 157, - 216, - 151, - 76, - 112, - 174, - 188, - 113, - 43, - 0, - 224, - 71, - 143, - 253, - 18, - 183, - 220, - 114, - 11, - 94, - 124, - 241, - 69, - 220, - 122, - 235, - 173, - 41, - 143, - 221, - 177, - 99, - 7, - 0, - 224, - 145, - 71, - 30, - 193, - 83, - 247, - 60, - 139, - 191, - 253, - 201, - 131, - 88, - 179, - 105, - 149, - 32, - 227, - 24, - 56, - 54, - 136, - 167, - 238, - 137, - 4, - 29, - 183, - 111, - 223, - 142, - 159, - 253, - 236, - 103, - 25, - 141, - 95, - 65, - 41, - 114, - 50, - 126, - 64, - 62, - 123, - 34, - 68, - 247, - 202, - 96, - 253, - 28, - 140, - 29, - 106, - 84, - 211, - 11, - 217, - 147, - 148, - 219, - 239, - 196, - 121, - 251, - 25, - 12, - 207, - 158, - 194, - 222, - 241, - 55, - 241, - 158, - 249, - 29, - 28, - 181, - 30, - 198, - 144, - 243, - 180, - 216, - 227, - 206, - 137, - 127, - 124, - 241, - 63, - 240, - 234, - 235, - 111, - 46, - 105, - 252, - 52, - 157, - 125, - 137, - 49, - 131, - 48, - 238, - 86, - 121, - 210, - 222, - 56, - 217, - 210, - 250, - 210, - 59, - 8, - 249, - 217, - 148, - 105, - 186, - 229, - 72, - 188, - 39, - 112, - 219, - 109, - 183, - 225, - 229, - 151, - 95, - 78, - 123, - 108, - 49, - 60, - 129, - 168, - 241, - 179, - 62, - 22, - 219, - 183, - 111, - 199, - 207, - 127, - 254, - 243, - 140, - 198, - 15, - 32, - 103, - 227, - 7, - 228, - 179, - 9, - 108, - 114, - 0, - 58, - 30, - 218, - 104, - 48, - 98, - 198, - 238, - 64, - 181, - 94, - 15, - 62, - 196, - 131, - 15, - 241, - 240, - 194, - 3, - 59, - 55, - 133, - 113, - 247, - 57, - 208, - 148, - 10, - 90, - 165, - 14, - 85, - 76, - 53, - 214, - 212, - 174, - 23, - 251, - 187, - 164, - 229, - 205, - 183, - 222, - 77, - 255, - 166, - 174, - 26, - 45, - 219, - 191, - 129, - 150, - 143, - 93, - 136, - 41, - 127, - 16, - 142, - 131, - 123, - 225, - 126, - 233, - 121, - 40, - 150, - 200, - 139, - 191, - 68, - 193, - 193, - 164, - 8, - 167, - 125, - 127, - 142, - 15, - 194, - 50, - 95, - 44, - 210, - 194, - 168, - 80, - 77, - 43, - 83, - 30, - 167, - 113, - 251, - 176, - 122, - 210, - 11, - 103, - 151, - 216, - 87, - 168, - 116, - 196, - 123, - 2, - 81, - 3, - 91, - 202, - 19, - 96, - 89, - 22, - 79, - 60, - 241, - 68, - 193, - 158, - 64, - 42, - 227, - 79, - 199, - 238, - 221, - 187, - 113, - 199, - 29, - 119, - 0, - 0, - 238, - 127, - 250, - 206, - 156, - 141, - 95, - 78, - 120, - 189, - 94, - 52, - 183, - 52, - 131, - 162, - 20, - 9, - 79, - 127, - 0, - 160, - 227, - 3, - 85, - 180, - 42, - 241, - 9, - 25, - 21, - 4, - 63, - 239, - 131, - 131, - 181, - 195, - 60, - 55, - 6, - 70, - 169, - 134, - 90, - 169, - 129, - 158, - 49, - 160, - 73, - 219, - 34, - 72, - 243, - 77, - 33, - 112, - 123, - 210, - 68, - 213, - 181, - 85, - 104, - 186, - 243, - 27, - 168, - 89, - 191, - 9, - 238, - 64, - 16, - 246, - 183, - 126, - 13, - 239, - 235, - 47, - 64, - 145, - 97, - 29, - 126, - 147, - 50, - 125, - 45, - 192, - 144, - 215, - 143, - 253, - 206, - 196, - 157, - 142, - 174, - 48, - 86, - 161, - 71, - 151, - 122, - 94, - 95, - 125, - 242, - 28, - 156, - 151, - 8, - 227, - 222, - 202, - 133, - 92, - 68, - 224, - 241, - 199, - 31, - 135, - 223, - 239, - 199, - 51, - 207, - 60, - 147, - 183, - 8, - 20, - 98, - 252, - 209, - 177, - 86, - 34, - 52, - 176, - 16, - 193, - 172, - 206, - 144, - 69, - 23, - 10, - 135, - 224, - 231, - 125, - 240, - 243, - 62, - 56, - 89, - 7, - 198, - 221, - 231, - 35, - 130, - 64, - 169, - 81, - 205, - 232, - 97, - 210, - 54, - 160, - 69, - 215, - 38, - 246, - 119, - 138, - 161, - 168, - 50, - 96, - 217, - 87, - 31, - 5, - 213, - 189, - 1, - 254, - 96, - 16, - 83, - 7, - 246, - 194, - 243, - 250, - 238, - 140, - 198, - 15, - 0, - 203, - 21, - 169, - 51, - 1, - 231, - 248, - 224, - 34, - 227, - 7, - 128, - 253, - 78, - 79, - 90, - 79, - 64, - 59, - 110, - 23, - 251, - 82, - 136, - 66, - 178, - 8, - 112, - 28, - 135, - 219, - 111, - 191, - 61, - 229, - 177, - 59, - 119, - 238, - 4, - 128, - 152, - 8, - 60, - 241, - 139, - 135, - 179, - 254, - 61, - 241, - 198, - 255, - 181, - 175, - 125, - 13, - 207, - 61, - 247, - 92, - 218, - 99, - 139, - 97, - 252, - 82, - 223, - 24, - 54, - 26, - 128, - 54, - 24, - 13, - 152, - 227, - 185, - 196, - 24, - 0, - 80, - 88, - 157, - 56, - 23, - 100, - 225, - 14, - 184, - 96, - 241, - 76, - 224, - 196, - 116, - 31, - 222, - 25, - 127, - 11, - 135, - 44, - 251, - 209, - 55, - 117, - 4, - 227, - 238, - 81, - 209, - 190, - 180, - 66, - 163, - 67, - 199, - 87, - 30, - 132, - 113, - 213, - 90, - 240, - 60, - 15, - 219, - 127, - 189, - 14, - 247, - 139, - 207, - 67, - 145, - 101, - 134, - 95, - 157, - 34, - 181, - 72, - 156, - 91, - 162, - 50, - 204, - 146, - 166, - 126, - 156, - 158, - 93, - 72, - 250, - 145, - 210, - 141, - 33, - 20, - 75, - 101, - 61, - 246, - 94, - 182, - 22, - 247, - 125, - 251, - 43, - 0, - 128, - 59, - 238, - 184, - 3, - 187, - 119, - 239, - 78, - 123, - 158, - 157, - 59, - 119, - 70, - 166, - 4, - 62, - 22, - 223, - 186, - 107, - 23, - 134, - 250, - 51, - 231, - 223, - 15, - 30, - 31, - 17, - 213, - 248, - 229, - 64, - 252, - 202, - 68, - 50, - 148, - 208, - 117, - 226, - 129, - 80, - 68, - 16, - 166, - 124, - 147, - 24, - 112, - 244, - 199, - 2, - 139, - 125, - 83, - 71, - 74, - 215, - 101, - 167, - 74, - 143, - 186, - 175, - 125, - 11, - 117, - 31, - 219, - 12, - 132, - 21, - 152, - 57, - 184, - 23, - 222, - 223, - 254, - 10, - 10, - 74, - 1, - 208, - 42, - 132, - 149, - 153, - 43, - 6, - 133, - 44, - 5, - 10, - 169, - 138, - 186, - 7, - 171, - 232, - 100, - 202, - 122, - 252, - 216, - 199, - 215, - 225, - 158, - 199, - 239, - 0, - 144, - 189, - 8, - 228, - 82, - 125, - 151, - 141, - 241, - 255, - 226, - 23, - 191, - 168, - 72, - 227, - 7, - 18, - 251, - 68, - 44, - 138, - 1, - 0, - 197, - 125, - 42, - 197, - 2, - 139, - 188, - 7, - 83, - 190, - 73, - 156, - 115, - 13, - 131, - 161, - 34, - 113, - 4, - 163, - 186, - 70, - 176, - 236, - 189, - 120, - 212, - 23, - 94, - 14, - 69, - 215, - 42, - 216, - 217, - 0, - 102, - 222, - 250, - 29, - 60, - 191, - 255, - 87, - 104, - 175, - 255, - 28, - 148, - 109, - 93, - 64, - 56, - 4, - 238, - 232, - 1, - 176, - 71, - 247, - 67, - 177, - 196, - 182, - 95, - 147, - 97, - 10, - 203, - 83, - 120, - 1, - 93, - 26, - 6, - 135, - 221, - 139, - 227, - 13, - 12, - 20, - 104, - 97, - 82, - 11, - 11, - 215, - 80, - 120, - 135, - 34, - 41, - 227, - 245, - 248, - 81, - 103, - 170, - 141, - 5, - 147, - 227, - 131, - 78, - 6, - 163, - 1, - 86, - 139, - 21, - 215, - 222, - 124, - 5, - 0, - 224, - 39, - 79, - 190, - 16, - 51, - 196, - 108, - 166, - 3, - 217, - 144, - 141, - 241, - 223, - 117, - 215, - 93, - 80, - 40, - 20, - 21, - 103, - 252, - 153, - 40, - 249, - 163, - 41, - 94, - 16, - 28, - 172, - 29, - 163, - 238, - 179, - 9, - 129, - 197, - 182, - 170, - 142, - 130, - 91, - 122, - 113, - 71, - 246, - 33, - 112, - 225, - 229, - 240, - 217, - 38, - 48, - 247, - 155, - 221, - 80, - 223, - 248, - 37, - 168, - 55, - 109, - 5, - 20, - 0, - 123, - 244, - 61, - 176, - 199, - 150, - 54, - 126, - 0, - 24, - 11, - 41, - 177, - 156, - 90, - 124, - 76, - 53, - 173, - 196, - 21, - 198, - 170, - 69, - 113, - 128, - 75, - 141, - 186, - 180, - 43, - 1, - 190, - 174, - 194, - 123, - 20, - 74, - 149, - 92, - 210, - 97, - 139, - 33, - 2, - 196, - 248, - 11, - 67, - 116, - 223, - 52, - 57, - 176, - 104, - 158, - 27, - 139, - 44, - 61, - 210, - 58, - 232, - 104, - 29, - 154, - 116, - 45, - 104, - 208, - 54, - 229, - 116, - 206, - 176, - 223, - 11, - 231, - 79, - 191, - 5, - 168, - 212, - 96, - 62, - 243, - 151, - 160, - 47, - 222, - 6, - 62, - 200, - 130, - 63, - 250, - 14, - 216, - 255, - 122, - 37, - 171, - 32, - 224, - 31, - 130, - 106, - 92, - 73, - 167, - 158, - 211, - 247, - 232, - 52, - 104, - 97, - 84, - 89, - 45, - 3, - 2, - 128, - 227, - 202, - 117, - 98, - 95, - 230, - 162, - 177, - 84, - 58, - 172, - 193, - 104, - 88, - 148, - 245, - 152, - 44, - 2, - 44, - 27, - 137, - 218, - 167, - 34, - 42, - 2, - 233, - 120, - 232, - 161, - 135, - 240, - 253, - 239, - 127, - 63, - 237, - 251, - 196, - 248, - 51, - 67, - 75, - 45, - 40, - 21, - 10, - 135, - 192, - 5, - 89, - 112, - 65, - 22, - 78, - 214, - 1, - 139, - 103, - 34, - 97, - 165, - 161, - 134, - 169, - 67, - 187, - 190, - 51, - 243, - 137, - 88, - 22, - 225, - 48, - 64, - 85, - 233, - 17, - 6, - 48, - 113, - 98, - 20, - 179, - 239, - 157, - 70, - 135, - 207, - 151, - 85, - 254, - 243, - 48, - 84, - 24, - 8, - 41, - 177, - 134, - 74, - 189, - 26, - 80, - 77, - 43, - 209, - 179, - 132, - 209, - 71, - 241, - 244, - 180, - 130, - 107, - 168, - 17, - 251, - 178, - 22, - 141, - 168, - 251, - 15, - 68, - 130, - 201, - 51, - 118, - 71, - 198, - 172, - 199, - 120, - 17, - 184, - 235, - 174, - 187, - 0, - 96, - 73, - 17, - 240, - 249, - 22, - 23, - 103, - 109, - 218, - 180, - 9, - 91, - 183, - 166, - 55, - 232, - 231, - 159, - 127, - 30, - 247, - 221, - 119, - 31, - 49, - 254, - 56, - 82, - 217, - 186, - 232, - 30, - 64, - 54, - 68, - 5, - 33, - 186, - 218, - 48, - 236, - 28, - 132, - 70, - 169, - 134, - 134, - 214, - 192, - 168, - 174, - 75, - 155, - 194, - 172, - 224, - 88, - 176, - 175, - 60, - 15, - 254, - 234, - 219, - 48, - 49, - 228, - 65, - 64, - 213, - 12, - 218, - 176, - 1, - 45, - 174, - 126, - 80, - 8, - 103, - 252, - 189, - 154, - 155, - 84, - 192, - 27, - 65, - 32, - 207, - 222, - 32, - 97, - 37, - 5, - 243, - 231, - 175, - 18, - 251, - 242, - 21, - 149, - 124, - 211, - 97, - 115, - 17, - 1, - 173, - 118, - 241, - 62, - 15, - 75, - 213, - 82, - 16, - 227, - 207, - 30, - 89, - 8, - 64, - 50, - 129, - 16, - 155, - 176, - 218, - 112, - 206, - 181, - 196, - 110, - 52, - 156, - 31, - 138, - 63, - 236, - 134, - 174, - 230, - 34, - 184, - 116, - 29, - 176, - 87, - 119, - 131, - 87, - 170, - 209, - 225, - 248, - 0, - 20, - 210, - 79, - 5, - 158, - 252, - 10, - 135, - 79, - 93, - 14, - 216, - 41, - 26, - 83, - 175, - 231, - 183, - 38, - 48, - 245, - 169, - 139, - 17, - 104, - 172, - 17, - 251, - 114, - 21, - 149, - 232, - 83, - 197, - 60, - 97, - 201, - 57, - 152, - 156, - 139, - 8, - 100, - 75, - 188, - 241, - 255, - 229, - 35, - 183, - 226, - 178, - 235, - 55, - 139, - 125, - 137, - 36, - 141, - 44, - 5, - 32, - 153, - 76, - 221, - 123, - 148, - 97, - 30, - 93, - 142, - 247, - 1, - 199, - 251, - 89, - 157, - 239, - 201, - 175, - 112, - 248, - 179, - 203, - 35, - 143, - 125, - 211, - 13, - 74, - 120, - 6, - 130, - 240, - 158, - 206, - 236, - 49, - 196, - 227, - 233, - 105, - 197, - 244, - 117, - 23, - 138, - 125, - 105, - 36, - 79, - 178, - 8, - 176, - 108, - 100, - 73, - 47, - 31, - 226, - 141, - 255, - 158, - 199, - 239, - 192, - 165, - 159, - 216, - 152, - 85, - 130, - 91, - 37, - 67, - 202, - 129, - 147, - 136, - 55, - 126, - 0, - 128, - 66, - 129, - 182, - 237, - 12, - 168, - 26, - 38, - 235, - 115, - 132, - 141, - 70, - 140, - 127, - 249, - 134, - 188, - 171, - 8, - 43, - 141, - 107, - 111, - 190, - 34, - 150, - 39, - 112, - 223, - 125, - 247, - 225, - 249, - 231, - 159, - 207, - 249, - 28, - 187, - 118, - 237, - 138, - 25, - 255, - 221, - 255, - 235, - 118, - 92, - 123, - 243, - 21, - 100, - 131, - 208, - 44, - 32, - 2, - 16, - 199, - 34, - 227, - 159, - 199, - 161, - 217, - 12, - 207, - 23, - 238, - 201, - 250, - 60, - 190, - 47, - 126, - 25, - 193, - 234, - 242, - 170, - 247, - 207, - 68, - 161, - 233, - 176, - 215, - 222, - 124, - 5, - 238, - 122, - 236, - 75, - 0, - 114, - 23, - 129, - 93, - 187, - 118, - 225, - 225, - 135, - 31, - 134, - 66, - 161, - 192, - 23, - 254, - 250, - 102, - 172, - 219, - 186, - 170, - 40, - 27, - 161, - 148, - 35, - 68, - 0, - 230, - 73, - 103, - 252, - 211, - 252, - 197, - 56, - 199, - 222, - 138, - 96, - 119, - 15, - 184, - 109, - 215, - 102, - 60, - 15, - 119, - 245, - 53, - 8, - 173, - 236, - 46, - 201, - 152, - 211, - 53, - 26, - 77, - 78, - 205, - 149, - 11, - 55, - 124, - 110, - 91, - 206, - 34, - 16, - 111, - 252, - 247, - 60, - 126, - 7, - 62, - 251, - 229, - 79, - 203, - 162, - 68, - 87, - 42, - 16, - 1, - 64, - 102, - 227, - 143, - 194, - 221, - 240, - 105, - 132, - 140, - 53, - 105, - 207, - 19, - 170, - 51, - 129, - 251, - 228, - 103, - 74, - 54, - 238, - 116, - 141, - 70, - 147, - 83, - 115, - 229, - 68, - 178, - 8, - 236, - 218, - 181, - 43, - 237, - 177, - 201, - 198, - 31, - 141, - 39, - 16, - 178, - 167, - 226, - 5, - 32, - 157, - 241, - 115, - 218, - 107, - 224, - 53, - 62, - 144, - 248, - 162, - 74, - 5, - 246, - 150, - 91, - 211, - 158, - 139, - 253, - 252, - 109, - 128, - 74, - 152, - 157, - 137, - 178, - 33, - 93, - 163, - 209, - 228, - 134, - 164, - 114, - 35, - 94, - 4, - 30, - 126, - 248, - 225, - 148, - 34, - 240, - 228, - 147, - 79, - 38, - 184, - 253, - 196, - 248, - 243, - 163, - 162, - 5, - 32, - 147, - 241, - 27, - 244, - 6, - 52, - 54, - 38, - 166, - 241, - 6, - 215, - 172, - 71, - 112, - 213, - 234, - 69, - 159, - 9, - 174, - 90, - 141, - 96, - 247, - 106, - 136, - 9, - 69, - 81, - 89, - 165, - 230, - 202, - 129, - 100, - 17, - 248, - 193, - 15, - 126, - 16, - 123, - 239, - 153, - 103, - 158, - 193, - 19, - 79, - 60, - 145, - 224, - 246, - 39, - 67, - 92, - 255, - 236, - 40, - 139, - 101, - 192, - 124, - 200, - 246, - 201, - 111, - 208, - 71, - 154, - 58, - 78, - 78, - 46, - 148, - 82, - 114, - 87, - 108, - 131, - 118, - 48, - 177, - 101, - 26, - 119, - 229, - 182, - 146, - 127, - 135, - 84, - 41, - 183, - 169, - 82, - 115, - 229, - 186, - 12, - 118, - 195, - 231, - 34, - 215, - 244, - 23, - 223, - 121, - 17, - 15, - 62, - 248, - 32, - 212, - 106, - 53, - 88, - 150, - 197, - 35, - 143, - 60, - 146, - 16, - 237, - 39, - 100, - 134, - 166, - 104, - 52, - 209, - 139, - 19, - 170, - 42, - 82, - 0, - 210, - 25, - 255, - 107, - 254, - 110, - 120, - 212, - 159, - 69, - 242, - 243, - 36, - 89, - 4, - 130, - 107, - 214, - 33, - 100, - 170, - 7, - 101, - 159, - 6, - 16, - 153, - 251, - 7, - 47, - 40, - 125, - 190, - 191, - 193, - 104, - 88, - 148, - 114, - 59, - 61, - 57, - 189, - 40, - 53, - 183, - 90, - 47, - 79, - 1, - 0, - 34, - 34, - 160, - 173, - 210, - 224, - 71, - 143, - 253, - 18, - 247, - 222, - 123, - 47, - 128, - 72, - 166, - 225, - 125, - 223, - 38, - 25, - 126, - 217, - 98, - 164, - 212, - 139, - 202, - 128, - 163, - 84, - 156, - 0, - 44, - 101, - 252, - 223, - 116, - 95, - 5, - 184, - 143, - 2, - 0, - 62, - 93, - 219, - 145, - 240, - 126, - 130, - 8, - 40, - 20, - 8, - 108, - 190, - 20, - 234, - 223, - 71, - 182, - 195, - 10, - 92, - 178, - 69, - 148, - 53, - 255, - 84, - 41, - 183, - 169, - 82, - 115, - 229, - 206, - 149, - 55, - 110, - 5, - 173, - 162, - 241, - 119, - 15, - 255, - 4, - 0, - 240, - 240, - 223, - 127, - 13, - 151, - 108, - 219, - 36, - 246, - 176, - 100, - 65, - 27, - 179, - 180, - 248, - 87, - 148, - 0, - 100, - 52, - 254, - 121, - 190, - 53, - 158, - 89, - 4, - 66, - 221, - 61, - 177, - 215, - 67, - 43, - 165, - 211, - 239, - 175, - 144, - 212, - 92, - 41, - 115, - 217, - 245, - 155, - 161, - 98, - 104, - 168, - 181, - 106, - 244, - 94, - 186, - 182, - 240, - 19, - 150, - 57, - 26, - 138, - 134, - 41, - 133, - 203, - 159, - 76, - 197, - 8, - 64, - 182, - 198, - 31, - 37, - 163, - 8, - 4, - 131, - 192, - 124, - 155, - 241, - 224, - 178, - 46, - 177, - 191, - 94, - 69, - 176, - 249, - 106, - 242, - 212, - 207, - 134, - 165, - 92, - 254, - 100, - 42, - 66, - 0, - 114, - 53, - 254, - 40, - 153, - 68, - 192, - 181, - 124, - 190, - 10, - 81, - 153, - 185, - 44, - 152, - 64, - 200, - 23, - 62, - 16, - 153, - 214, - 53, - 54, - 54, - 128, - 86, - 209, - 9, - 27, - 125, - 232, - 116, - 58, - 24, - 141, - 122, - 40, - 230, - 155, - 174, - 100, - 114, - 249, - 147, - 41, - 251, - 101, - 192, - 124, - 141, - 63, - 202, - 183, - 198, - 143, - 226, - 119, - 142, - 177, - 69, - 175, - 27, - 244, - 6, - 232, - 46, - 189, - 12, - 225, - 246, - 44, - 122, - 19, - 20, - 25, - 169, - 119, - 165, - 37, - 20, - 70, - 54, - 59, - 77, - 27, - 41, - 117, - 206, - 198, - 15, - 228, - 233, - 1, - 164, - 83, - 160, - 100, - 165, - 18, - 155, - 66, - 141, - 63, - 74, - 58, - 79, - 160, - 238, - 47, - 190, - 0, - 218, - 237, - 74, - 88, - 34, - 44, - 71, - 254, - 245, - 39, - 191, - 22, - 123, - 8, - 146, - 228, - 243, - 247, - 252, - 247, - 146, - 252, - 158, - 76, - 61, - 23, - 89, - 199, - 92, - 214, - 46, - 127, - 50, - 121, - 89, - 233, - 130, - 2, - 213, - 193, - 229, - 116, - 193, - 233, - 116, - 163, - 166, - 214, - 152, - 160, - 84, - 98, - 175, - 61, - 11, - 101, - 252, - 81, - 178, - 9, - 12, - 150, - 43, - 175, - 252, - 148, - 8, - 64, - 42, - 62, - 123, - 231, - 141, - 69, - 127, - 224, - 101, - 74, - 236, - 234, - 208, - 24, - 128, - 150, - 252, - 55, - 32, - 205, - 107, - 212, - 169, - 186, - 190, - 214, - 212, - 26, - 23, - 41, - 149, - 88, - 8, - 109, - 252, - 81, - 42, - 89, - 4, - 128, - 200, - 14, - 62, - 132, - 72, - 26, - 50, - 128, - 146, - 60, - 240, - 210, - 245, - 92, - 12, - 186, - 253, - 232, - 104, - 104, - 44, - 248, - 252, - 130, - 200, - 86, - 186, - 20, - 84, - 49, - 166, - 1, - 197, - 50, - 254, - 40, - 149, - 44, - 2, - 79, - 60, - 241, - 132, - 216, - 67, - 144, - 4, - 81, - 1, - 40, - 197, - 3, - 47, - 85, - 207, - 197, - 158, - 214, - 246, - 188, - 93, - 254, - 100, - 242, - 178, - 80, - 169, - 166, - 160, - 22, - 219, - 248, - 163, - 84, - 178, - 8, - 16, - 22, - 40, - 197, - 3, - 47, - 62, - 177, - 139, - 86, - 40, - 209, - 94, - 215, - 32, - 152, - 241, - 3, - 121, - 174, - 2, - 196, - 111, - 53, - 20, - 12, - 241, - 145, - 74, - 52, - 143, - 31, - 26, - 77, - 36, - 241, - 64, - 140, - 78, - 44, - 165, - 50, - 254, - 40, - 75, - 173, - 14, - 24, - 141, - 181, - 37, - 253, - 238, - 4, - 113, - 72, - 126, - 224, - 21, - 131, - 104, - 79, - 3, - 131, - 90, - 139, - 77, - 93, - 43, - 209, - 96, - 200, - 127, - 190, - 159, - 138, - 188, - 36, - 75, - 138, - 41, - 168, - 215, - 94, - 180, - 184, - 193, - 103, - 177, - 140, - 63, - 74, - 58, - 79, - 32, - 28, - 38, - 173, - 192, - 42, - 129, - 248, - 7, - 94, - 49, - 167, - 1, - 109, - 140, - 30, - 104, - 45, - 206, - 185, - 5, - 243, - 89, - 196, - 78, - 65, - 221, - 127, - 174, - 25, - 159, - 92, - 99, - 137, - 253, - 187, - 216, - 198, - 31, - 37, - 149, - 8, - 124, - 56, - 212, - 135, - 112, - 147, - 56, - 49, - 16, - 66, - 233, - 40, - 246, - 3, - 47, - 219, - 116, - 222, - 66, - 40, - 155, - 59, - 212, - 187, - 242, - 147, - 216, - 51, - 240, - 91, - 172, - 107, - 158, - 193, - 1, - 170, - 19, - 223, - 228, - 74, - 215, - 143, - 255, - 91, - 227, - 71, - 209, - 74, - 169, - 209, - 30, - 160, - 240, - 209, - 217, - 147, - 240, - 25, - 205, - 128, - 31, - 162, - 47, - 133, - 10, - 141, - 130, - 52, - 57, - 77, - 160, - 181, - 173, - 165, - 104, - 15, - 188, - 92, - 210, - 121, - 11, - 161, - 108, - 4, - 0, - 0, - 102, - 87, - 126, - 6, - 239, - 205, - 255, - 252, - 140, - 54, - 243, - 246, - 95, - 249, - 18, - 159, - 8, - 21, - 45, - 195, - 29, - 60, - 253, - 58, - 166, - 77, - 181, - 160, - 155, - 104, - 104, - 2, - 242, - 47, - 195, - 37, - 136, - 71, - 62, - 25, - 125, - 249, - 82, - 144, - 0, - 84, - 106, - 10, - 170, - 20, - 99, - 32, - 197, - 228, - 185, - 255, - 247, - 116, - 44, - 239, - 35, - 20, - 10, - 99, - 210, - 54, - 137, - 250, - 122, - 19, - 156, - 46, - 39, - 76, - 38, - 19, - 236, - 118, - 59, - 140, - 6, - 99, - 217, - 79, - 121, - 138, - 61, - 189, - 213, - 64, - 13, - 19, - 83, - 252, - 167, - 126, - 60, - 229, - 253, - 63, - 86, - 66, - 196, - 142, - 129, - 20, - 19, - 169, - 46, - 251, - 150, - 154, - 98, - 62, - 240, - 244, - 42, - 3, - 12, - 138, - 220, - 54, - 159, - 17, - 130, - 178, - 47, - 6, - 34, - 20, - 142, - 20, - 151, - 125, - 203, - 137, - 54, - 70, - 47, - 138, - 241, - 3, - 50, - 244, - 0, - 114, - 41, - 141, - 36, - 8, - 67, - 165, - 77, - 121, - 74, - 69, - 169, - 2, - 125, - 75, - 33, - 59, - 75, - 201, - 166, - 52, - 178, - 84, - 84, - 106, - 12, - 36, - 250, - 61, - 201, - 6, - 28, - 249, - 19, - 53, - 254, - 169, - 201, - 105, - 81, - 199, - 33, - 59, - 1, - 72, - 238, - 121, - 159, - 174, - 55, - 62, - 129, - 32, - 69, - 52, - 20, - 13, - 35, - 165, - 198, - 220, - 212, - 12, - 92, - 46, - 23, - 212, - 26, - 226, - 1, - 100, - 77, - 54, - 61, - 239, - 41, - 226, - 254, - 151, - 13, - 229, - 182, - 245, - 153, - 145, - 82, - 195, - 68, - 107, - 225, - 155, - 113, - 1, - 0, - 88, - 63, - 7, - 131, - 192, - 169, - 189, - 185, - 34, - 43, - 107, - 73, - 87, - 26, - 233, - 114, - 186, - 16, - 10, - 133, - 99, - 17, - 106, - 66, - 113, - 40, - 245, - 148, - 167, - 156, - 182, - 62, - 107, - 99, - 244, - 240, - 205, - 184, - 34, - 129, - 212, - 249, - 13, - 75, - 131, - 193, - 72, - 96, - 213, - 106, - 17, - 174, - 120, - 44, - 215, - 115, - 201, - 42, - 8, - 152, - 170, - 52, - 178, - 190, - 177, - 126, - 81, - 111, - 124, - 66, - 121, - 32, - 245, - 190, - 19, - 217, - 16, - 159, - 206, - 219, - 208, - 88, - 47, - 246, - 112, - 22, - 33, - 43, - 15, - 32, - 26, - 121, - 142, - 223, - 250, - 57, - 26, - 161, - 6, - 0, - 147, - 201, - 4, - 138, - 34, - 233, - 170, - 229, - 138, - 220, - 182, - 62, - 139, - 186, - 252, - 165, - 32, - 57, - 152, - 152, - 109, - 112, - 81, - 86, - 30, - 64, - 57, - 39, - 219, - 16, - 22, - 35, - 231, - 4, - 164, - 82, - 166, - 243, - 2, - 128, - 90, - 195, - 192, - 229, - 138, - 196, - 22, - 114, - 9, - 46, - 202, - 202, - 3, - 32, - 84, - 22, - 114, - 76, - 64, - 202, - 183, - 59, - 111, - 193, - 215, - 202, - 96, - 0, - 235, - 231, - 0, - 228, - 22, - 92, - 148, - 149, - 7, - 64, - 168, - 44, - 228, - 150, - 128, - 36, - 86, - 98, - 79, - 114, - 224, - 47, - 26, - 92, - 4, - 16, - 11, - 160, - 166, - 67, - 150, - 2, - 80, - 201, - 9, - 56, - 149, - 142, - 20, - 167, - 129, - 233, - 118, - 222, - 45, - 21, - 153, - 140, - 124, - 201, - 177, - 139, - 54, - 106, - 2, - 161, - 12, - 144, - 66, - 58, - 111, - 33, - 16, - 1, - 32, - 16, - 242, - 68, - 140, - 185, - 190, - 208, - 16, - 1, - 32, - 72, - 30, - 169, - 77, - 249, - 74, - 209, - 170, - 171, - 84, - 144, - 85, - 0, - 153, - 144, - 156, - 254, - 154, - 46, - 77, - 150, - 80, - 92, - 74, - 185, - 182, - 95, - 10, - 136, - 0, - 200, - 4, - 41, - 85, - 65, - 86, - 42, - 26, - 138, - 150, - 245, - 124, - 63, - 21, - 68, - 0, - 100, - 2, - 169, - 130, - 20, - 31, - 127, - 136, - 199, - 4, - 231, - 134, - 157, - 151, - 79, - 13, - 66, - 38, - 72, - 12, - 64, - 6, - 144, - 42, - 72, - 105, - 17, - 21, - 2, - 64, - 254, - 241, - 0, - 34, - 0, - 50, - 96, - 169, - 42, - 200, - 248, - 52, - 89, - 66, - 233, - 137, - 23, - 3, - 154, - 162, - 81, - 5, - 165, - 172, - 166, - 9, - 228, - 177, - 33, - 3, - 82, - 165, - 191, - 166, - 74, - 147, - 37, - 136, - 11, - 31, - 226, - 225, - 12, - 177, - 152, - 224, - 220, - 152, - 224, - 220, - 152, - 227, - 57, - 184, - 61, - 156, - 216, - 195, - 90, - 18, - 193, - 61, - 128, - 116, - 61, - 250, - 146, - 123, - 249, - 17, - 178, - 39, - 85, - 250, - 107, - 170, - 52, - 89, - 130, - 180, - 112, - 134, - 88, - 152, - 39, - 45, - 104, - 110, - 105, - 70, - 45, - 173, - 65, - 53, - 205, - 96, - 142, - 231, - 36, - 229, - 33, - 8, - 238, - 1, - 148, - 83, - 19, - 7, - 169, - 64, - 250, - 239, - 201, - 31, - 103, - 136, - 197, - 152, - 223, - 133, - 211, - 19, - 163, - 176, - 115, - 28, - 166, - 230, - 43, - 247, - 196, - 70, - 112, - 1, - 72, - 23, - 157, - 78, - 142, - 98, - 151, - 3, - 229, - 214, - 178, - 138, - 32, - 60, - 169, - 58, - 86, - 77, - 187, - 167, - 49, - 19, - 152, - 195, - 4, - 231, - 198, - 148, - 203, - 37, - 234, - 52, - 161, - 232, - 49, - 0, - 185, - 53, - 113, - 200, - 5, - 226, - 237, - 16, - 50, - 145, - 169, - 164, - 217, - 171, - 12, - 98, - 216, - 62, - 30, - 139, - 25, - 148, - 26, - 193, - 5, - 32, - 149, - 226, - 165, - 138, - 98, - 151, - 3, - 165, - 246, - 118, - 164, - 150, - 18, - 75, - 200, - 76, - 170, - 142, - 85, - 169, - 58, - 91, - 1, - 88, - 20, - 64, - 44, - 201, - 248, - 132, - 62, - 161, - 28, - 155, - 56, - 8, - 69, - 57, - 123, - 59, - 4, - 225, - 200, - 38, - 166, - 147, - 73, - 12, - 132, - 106, - 36, - 42, - 120, - 56, - 94, - 110, - 77, - 28, - 10, - 65, - 206, - 45, - 171, - 42, - 25, - 185, - 237, - 46, - 229, - 12, - 177, - 112, - 114, - 44, - 128, - 133, - 196, - 35, - 93, - 149, - 78, - 144, - 115, - 151, - 228, - 91, - 150, - 107, - 20, - 187, - 146, - 189, - 29, - 57, - 35, - 231, - 186, - 138, - 104, - 226, - 145, - 91, - 163, - 128, - 157, - 247, - 21, - 60, - 85, - 144, - 142, - 204, - 201, - 144, - 92, - 230, - 119, - 149, - 140, - 212, - 42, - 25, - 197, - 168, - 171, - 40, - 70, - 252, - 198, - 31, - 151, - 120, - 100, - 203, - 83, - 12, - 72, - 70, - 142, - 192, - 72, - 177, - 101, - 149, - 216, - 196, - 63, - 113, - 171, - 85, - 250, - 184, - 39, - 110, - 29, - 92, - 78, - 23, - 156, - 78, - 55, - 106, - 106, - 141, - 37, - 25, - 75, - 185, - 214, - 85, - 240, - 33, - 30, - 78, - 240, - 177, - 169, - 66, - 192, - 233, - 67, - 87, - 67, - 99, - 198, - 207, - 21, - 237, - 155, - 146, - 136, - 53, - 33, - 138, - 148, - 42, - 25, - 43, - 101, - 119, - 169, - 169, - 185, - 89, - 28, - 57, - 59, - 136, - 35, - 103, - 7, - 49, - 108, - 49, - 167, - 77, - 60, - 146, - 159, - 212, - 17, - 100, - 133, - 212, - 158, - 184, - 149, - 82, - 87, - 17, - 31, - 107, - 211, - 154, - 244, - 224, - 52, - 10, - 216, - 82, - 148, - 49, - 147, - 41, - 128, - 0, - 16, - 111, - 39, - 61, - 82, - 171, - 100, - 172, - 228, - 186, - 10, - 62, - 196, - 199, - 226, - 4, - 209, - 122, - 4, - 226, - 1, - 16, - 138, - 138, - 212, - 158, - 184, - 229, - 186, - 34, - 149, - 45, - 206, - 16, - 11, - 235, - 212, - 194, - 182, - 97, - 196, - 3, - 32, - 20, - 149, - 74, - 126, - 226, - 74, - 129, - 84, - 2, - 167, - 53, - 45, - 228, - 165, - 16, - 1, - 32, - 20, - 21, - 178, - 42, - 34, - 77, - 38, - 56, - 55, - 218, - 24, - 61, - 254, - 63, - 91, - 213, - 67, - 145, - 233, - 13, - 169, - 197, - 0, - 0, - 0, - 0, - 73, - 69, - 78, - 68, - 174, - 66, - 96, - 130 -]; +// Will be replaced automatically by GitHub Actions +final faviconTileBytes = []; diff --git a/tile_server/static/generated/land.dart b/tile_server/static/generated/land.dart index 085ec8f8..1029ac5f 100644 --- a/tile_server/static/generated/land.dart +++ b/tile_server/static/generated/land.dart @@ -1,18691 +1,2 @@ -final landTileBytes = [ - 137, - 80, - 78, - 71, - 13, - 10, - 26, - 10, - 0, - 0, - 0, - 13, - 73, - 72, - 68, - 82, - 0, - 0, - 1, - 0, - 0, - 0, - 1, - 0, - 8, - 3, - 0, - 0, - 0, - 107, - 172, - 88, - 84, - 0, - 0, - 3, - 0, - 80, - 76, - 84, - 69, - 51, - 52, - 49, - 72, - 52, - 55, - 60, - 65, - 58, - 76, - 85, - 24, - 157, - 17, - 17, - 83, - 80, - 48, - 162, - 28, - 28, - 73, - 74, - 72, - 82, - 75, - 68, - 78, - 83, - 73, - 92, - 100, - 43, - 83, - 86, - 71, - 86, - 87, - 84, - 169, - 46, - 46, - 108, - 79, - 76, - 124, - 90, - 55, - 104, - 112, - 57, - 89, - 109, - 86, - 129, - 96, - 61, - 102, - 108, - 85, - 113, - 94, - 97, - 131, - 89, - 89, - 134, - 105, - 70, - 104, - 105, - 102, - 94, - 131, - 93, - 104, - 115, - 103, - 121, - 128, - 77, - 139, - 112, - 78, - 141, - 89, - 99, - 141, - 101, - 88, - 113, - 117, - 107, - 182, - 78, - 78, - 142, - 116, - 84, - 117, - 117, - 116, - 149, - 107, - 95, - 146, - 121, - 90, - 151, - 150, - 59, - 117, - 141, - 112, - 147, - 111, - 113, - 128, - 134, - 111, - 138, - 144, - 96, - 148, - 150, - 83, - 130, - 132, - 125, - 168, - 127, - 94, - 154, - 135, - 104, - 177, - 114, - 110, - 129, - 155, - 119, - 177, - 153, - 75, - 136, - 137, - 133, - 141, - 168, - 98, - 147, - 153, - 107, - 150, - 171, - 87, - 156, - 139, - 114, - 211, - 118, - 88, - 153, - 166, - 102, - 157, - 149, - 115, - 165, - 165, - 91, - 135, - 165, - 124, - 125, - 185, - 115, - 202, - 111, - 113, - 152, - 137, - 138, - 141, - 150, - 137, - 134, - 167, - 129, - 218, - 89, - 126, - 224, - 83, - 126, - 206, - 146, - 84, - 142, - 189, - 107, - 166, - 171, - 101, - 172, - 150, - 116, - 150, - 150, - 139, - 153, - 171, - 115, - 156, - 180, - 104, - 132, - 189, - 120, - 153, - 139, - 150, - 221, - 92, - 129, - 182, - 122, - 144, - 224, - 93, - 131, - 161, - 165, - 123, - 210, - 110, - 130, - 168, - 179, - 105, - 140, - 184, - 131, - 149, - 169, - 137, - 180, - 173, - 102, - 206, - 141, - 108, - 206, - 164, - 86, - 152, - 187, - 120, - 155, - 155, - 151, - 142, - 195, - 125, - 147, - 182, - 133, - 149, - 178, - 137, - 142, - 194, - 129, - 168, - 154, - 145, - 149, - 184, - 135, - 182, - 183, - 104, - 157, - 182, - 131, - 183, - 166, - 121, - 172, - 165, - 134, - 168, - 184, - 120, - 151, - 185, - 137, - 154, - 179, - 141, - 157, - 165, - 153, - 147, - 197, - 133, - 157, - 187, - 134, - 228, - 110, - 142, - 148, - 205, - 128, - 154, - 189, - 139, - 150, - 197, - 137, - 155, - 196, - 133, - 155, - 184, - 147, - 172, - 146, - 168, - 181, - 168, - 139, - 184, - 186, - 118, - 228, - 142, - 118, - 170, - 189, - 130, - 204, - 173, - 112, - 165, - 169, - 156, - 157, - 192, - 142, - 153, - 206, - 133, - 208, - 143, - 141, - 186, - 195, - 115, - 155, - 201, - 141, - 156, - 209, - 135, - 158, - 198, - 145, - 168, - 168, - 165, - 161, - 196, - 145, - 182, - 172, - 148, - 169, - 196, - 138, - 182, - 186, - 135, - 168, - 184, - 157, - 163, - 204, - 149, - 167, - 193, - 156, - 172, - 199, - 145, - 182, - 183, - 151, - 163, - 213, - 141, - 171, - 182, - 166, - 177, - 172, - 170, - 195, - 202, - 122, - 188, - 197, - 136, - 231, - 141, - 149, - 172, - 203, - 148, - 203, - 154, - 166, - 167, - 205, - 152, - 171, - 196, - 157, - 204, - 179, - 141, - 184, - 167, - 179, - 195, - 200, - 136, - 171, - 205, - 156, - 180, - 199, - 154, - 198, - 173, - 163, - 228, - 186, - 120, - 185, - 183, - 167, - 172, - 202, - 163, - 173, - 209, - 158, - 232, - 146, - 162, - 175, - 210, - 161, - 201, - 210, - 136, - 181, - 214, - 155, - 197, - 186, - 167, - 212, - 199, - 139, - 182, - 202, - 168, - 218, - 168, - 166, - 178, - 212, - 163, - 198, - 201, - 155, - 205, - 169, - 180, - 187, - 186, - 183, - 182, - 216, - 164, - 213, - 184, - 165, - 182, - 213, - 169, - 187, - 212, - 165, - 196, - 216, - 153, - 198, - 198, - 169, - 210, - 215, - 140, - 186, - 199, - 181, - 214, - 200, - 152, - 184, - 225, - 158, - 194, - 189, - 187, - 186, - 219, - 166, - 187, - 214, - 171, - 209, - 172, - 191, - 188, - 219, - 171, - 248, - 177, - 155, - 190, - 225, - 166, - 189, - 216, - 178, - 197, - 201, - 185, - 215, - 220, - 148, - 199, - 215, - 171, - 214, - 202, - 169, - 215, - 187, - 183, - 191, - 206, - 193, - 241, - 203, - 146, - 198, - 197, - 196, - 235, - 181, - 178, - 215, - 199, - 182, - 216, - 212, - 170, - 226, - 202, - 170, - 198, - 228, - 173, - 199, - 218, - 183, - 221, - 226, - 155, - 225, - 215, - 164, - 224, - 229, - 159, - 210, - 204, - 200, - 205, - 234, - 176, - 214, - 216, - 185, - 248, - 193, - 174, - 204, - 216, - 197, - 232, - 201, - 184, - 236, - 188, - 197, - 245, - 197, - 183, - 204, - 226, - 196, - 210, - 230, - 187, - 229, - 233, - 166, - 213, - 217, - 200, - 252, - 214, - 164, - 233, - 200, - 199, - 228, - 218, - 187, - 215, - 228, - 201, - 244, - 200, - 202, - 231, - 218, - 198, - 232, - 231, - 184, - 217, - 217, - 215, - 245, - 220, - 185, - 217, - 240, - 195, - 237, - 241, - 178, - 244, - 202, - 210, - 249, - 230, - 178, - 226, - 221, - 219, - 229, - 233, - 204, - 228, - 227, - 212, - 220, - 234, - 214, - 252, - 215, - 204, - 228, - 226, - 220, - 229, - 234, - 211, - 232, - 229, - 213, - 245, - 229, - 201, - 244, - 216, - 219, - 227, - 236, - 219, - 235, - 238, - 211, - 237, - 228, - 219, - 247, - 250, - 191, - 234, - 234, - 222, - 247, - 249, - 194, - 238, - 240, - 213, - 245, - 233, - 214, - 232, - 231, - 230, - 248, - 219, - 226, - 241, - 242, - 214, - 234, - 243, - 229, - 243, - 238, - 233, - 243, - 243, - 242, - 246, - 247, - 235, - 251, - 238, - 241, - 244, - 249, - 241, - 251, - 243, - 244, - 251, - 251, - 245, - 253, - 246, - 248, - 254, - 254, - 254, - 65, - 248, - 164, - 27, - 0, - 0, - 69, - 188, - 73, - 68, - 65, - 84, - 120, - 156, - 205, - 125, - 15, - 96, - 84, - 213, - 153, - 239, - 172, - 221, - 87, - 197, - 133, - 85, - 233, - 43, - 20, - 125, - 75, - 228, - 165, - 246, - 45, - 44, - 42, - 187, - 90, - 153, - 74, - 89, - 183, - 27, - 144, - 170, - 200, - 172, - 65, - 134, - 171, - 109, - 22, - 162, - 99, - 145, - 170, - 132, - 56, - 188, - 68, - 95, - 32, - 140, - 153, - 104, - 128, - 148, - 65, - 111, - 90, - 184, - 25, - 192, - 100, - 220, - 164, - 3, - 17, - 155, - 228, - 133, - 204, - 52, - 74, - 131, - 118, - 174, - 175, - 51, - 105, - 74, - 39, - 206, - 194, - 213, - 55, - 187, - 242, - 38, - 25, - 34, - 75, - 130, - 60, - 54, - 144, - 73, - 128, - 48, - 239, - 59, - 231, - 220, - 255, - 127, - 102, - 238, - 4, - 218, - 183, - 159, - 114, - 51, - 127, - 238, - 189, - 115, - 207, - 239, - 124, - 255, - 207, - 119, - 206, - 177, - 48, - 255, - 81, - 201, - 251, - 222, - 219, - 231, - 3, - 44, - 165, - 250, - 180, - 189, - 203, - 221, - 37, - 80, - 40, - 210, - 204, - 48, - 161, - 72, - 22, - 234, - 234, - 98, - 51, - 125, - 109, - 249, - 255, - 209, - 54, - 83, - 228, - 27, - 126, - 121, - 248, - 124, - 31, - 165, - 250, - 244, - 0, - 0, - 80, - 39, - 32, - 16, - 241, - 121, - 59, - 100, - 109, - 209, - 197, - 130, - 237, - 202, - 12, - 209, - 127, - 92, - 0, - 152, - 254, - 143, - 62, - 58, - 207, - 246, - 177, - 170, - 79, - 235, - 220, - 117, - 117, - 18, - 15, - 48, - 2, - 0, - 29, - 29, - 161, - 102, - 166, - 57, - 11, - 51, - 132, - 116, - 152, - 225, - 15, - 11, - 192, - 214, - 173, - 215, - 112, - 113, - 223, - 240, - 203, - 231, - 207, - 159, - 239, - 87, - 125, - 10, - 0, - 144, - 198, - 151, - 188, - 3, - 44, - 32, - 244, - 110, - 136, - 124, - 153, - 165, - 249, - 136, - 105, - 254, - 184, - 0, - 60, - 249, - 164, - 222, - 167, - 59, - 169, - 157, - 12, - 189, - 158, - 218, - 68, - 103, - 185, - 186, - 239, - 237, - 207, - 0, - 1, - 37, - 11, - 248, - 170, - 221, - 64, - 34, - 0, - 2, - 117, - 162, - 239, - 218, - 12, - 153, - 29, - 244, - 64, - 40, - 210, - 245, - 199, - 7, - 160, - 228, - 123, - 223, - 211, - 99, - 129, - 205, - 235, - 55, - 51, - 155, - 54, - 209, - 155, - 54, - 101, - 187, - 190, - 107, - 23, - 0, - 192, - 41, - 63, - 34, - 4, - 175, - 182, - 111, - 197, - 210, - 221, - 73, - 90, - 29, - 242, - 42, - 212, - 129, - 170, - 249, - 2, - 233, - 168, - 195, - 220, - 1, - 48, - 219, - 127, - 12, - 67, - 127, - 15, - 72, - 231, - 180, - 103, - 118, - 62, - 195, - 80, - 52, - 67, - 83, - 89, - 239, - 128, - 100, - 192, - 175, - 248, - 4, - 183, - 227, - 0, - 126, - 89, - 72, - 90, - 192, - 243, - 125, - 200, - 64, - 5, - 242, - 172, - 207, - 155, - 141, - 235, - 1, - 128, - 233, - 254, - 99, - 94, - 70, - 0, - 172, - 209, - 124, - 188, - 115, - 61, - 179, - 126, - 39, - 2, - 224, - 135, - 89, - 239, - 240, - 214, - 137, - 243, - 195, - 202, - 79, - 14, - 64, - 227, - 125, - 228, - 101, - 17, - 105, - 65, - 155, - 172, - 53, - 29, - 62, - 21, - 27, - 32, - 166, - 103, - 69, - 0, - 16, - 78, - 254, - 16, - 2, - 171, - 243, - 26, - 0, - 48, - 221, - 127, - 117, - 223, - 195, - 84, - 167, - 254, - 124, - 243, - 102, - 248, - 223, - 28, - 132, - 125, - 39, - 52, - 74, - 80, - 162, - 183, - 72, - 135, - 50, - 62, - 190, - 41, - 240, - 182, - 67, - 173, - 6, - 112, - 163, - 101, - 237, - 239, - 96, - 188, - 33, - 172, - 50, - 67, - 147, - 6, - 192, - 124, - 255, - 189, - 76, - 0, - 120, - 89, - 253, - 249, - 51, - 59, - 25, - 192, - 112, - 61, - 181, - 62, - 187, - 16, - 185, - 183, - 114, - 237, - 134, - 95, - 242, - 109, - 16, - 94, - 132, - 186, - 180, - 82, - 64, - 90, - 205, - 226, - 63, - 232, - 75, - 220, - 114, - 130, - 192, - 164, - 1, - 200, - 161, - 255, - 12, - 136, - 66, - 4, - 127, - 76, - 156, - 234, - 182, - 101, - 254, - 190, - 77, - 232, - 205, - 16, - 81, - 242, - 42, - 4, - 26, - 186, - 176, - 18, - 136, - 72, - 186, - 31, - 97, - 212, - 6, - 151, - 25, - 0, - 96, - 70, - 191, - 229, - 208, - 127, - 134, - 68, - 137, - 135, - 108, - 100, - 205, - 248, - 109, - 41, - 121, - 134, - 230, - 136, - 92, - 202, - 229, - 12, - 32, - 189, - 148, - 65, - 131, - 125, - 6, - 125, - 17, - 48, - 163, - 223, - 114, - 232, - 63, - 227, - 123, - 136, - 135, - 108, - 100, - 205, - 248, - 109, - 41, - 207, - 32, - 146, - 157, - 211, - 218, - 121, - 177, - 209, - 242, - 246, - 203, - 92, - 70, - 5, - 0, - 230, - 244, - 155, - 249, - 199, - 191, - 246, - 59, - 88, - 51, - 243, - 89, - 129, - 213, - 90, - 80, - 169, - 0, - 64, - 31, - 1, - 212, - 104, - 252, - 167, - 185, - 141, - 92, - 168, - 15, - 128, - 73, - 253, - 102, - 254, - 241, - 175, - 157, - 10, - 178, - 10, - 90, - 45, - 252, - 123, - 167, - 48, - 147, - 169, - 15, - 225, - 70, - 135, - 34, - 157, - 240, - 183, - 51, - 4, - 212, - 44, - 247, - 25, - 45, - 178, - 95, - 48, - 169, - 223, - 40, - 241, - 240, - 135, - 39, - 171, - 153, - 147, - 192, - 45, - 222, - 110, - 196, - 0, - 157, - 200, - 76, - 32, - 10, - 33, - 46, - 208, - 129, - 199, - 82, - 108, - 173, - 21, - 110, - 116, - 61, - 244, - 219, - 117, - 38, - 107, - 246, - 83, - 218, - 161, - 217, - 219, - 183, - 122, - 15, - 160, - 238, - 15, - 225, - 22, - 203, - 136, - 102, - 58, - 59, - 165, - 83, - 51, - 122, - 130, - 182, - 210, - 235, - 162, - 223, - 152, - 118, - 150, - 5, - 211, - 237, - 13, - 4, - 192, - 99, - 99, - 217, - 128, - 247, - 90, - 238, - 85, - 106, - 45, - 206, - 126, - 18, - 106, - 121, - 73, - 29, - 227, - 37, - 221, - 175, - 108, - 101, - 243, - 51, - 242, - 51, - 59, - 117, - 218, - 47, - 1, - 64, - 151, - 50, - 215, - 131, - 187, - 163, - 189, - 107, - 31, - 239, - 101, - 253, - 201, - 234, - 96, - 192, - 159, - 220, - 178, - 176, - 58, - 57, - 105, - 4, - 104, - 155, - 181, - 88, - 135, - 21, - 137, - 153, - 70, - 6, - 91, - 32, - 64, - 160, - 208, - 203, - 248, - 48, - 7, - 224, - 238, - 150, - 26, - 186, - 158, - 146, - 95, - 170, - 27, - 45, - 40, - 29, - 33, - 138, - 161, - 233, - 201, - 3, - 0, - 79, - 229, - 227, - 166, - 239, - 222, - 61, - 125, - 40, - 57, - 127, - 250, - 150, - 64, - 244, - 254, - 37, - 193, - 252, - 106, - 150, - 241, - 70, - 147, - 28, - 23, - 141, - 114, - 81, - 127, - 246, - 91, - 200, - 168, - 184, - 82, - 247, - 99, - 162, - 163, - 144, - 193, - 22, - 9, - 68, - 192, - 139, - 25, - 1, - 71, - 137, - 216, - 61, - 2, - 213, - 215, - 129, - 253, - 35, - 249, - 165, - 122, - 237, - 215, - 0, - 224, - 182, - 150, - 82, - 57, - 61, - 167, - 140, - 224, - 169, - 252, - 189, - 51, - 146, - 220, - 244, - 36, - 87, - 189, - 4, - 0, - 200, - 111, - 73, - 174, - 93, - 203, - 50, - 129, - 150, - 45, - 243, - 243, - 91, - 214, - 222, - 185, - 112, - 18, - 220, - 80, - 170, - 249, - 132, - 152, - 105, - 100, - 176, - 5, - 106, - 199, - 1, - 162, - 20, - 38, - 162, - 216, - 32, - 180, - 30, - 196, - 48, - 18, - 17, - 52, - 32, - 33, - 125, - 0, - 100, - 249, - 6, - 110, - 120, - 184, - 159, - 251, - 66, - 157, - 131, - 50, - 79, - 232, - 169, - 184, - 181, - 119, - 222, - 185, - 133, - 11, - 36, - 1, - 128, - 0, - 59, - 99, - 126, - 126, - 210, - 207, - 4, - 170, - 111, - 225, - 122, - 111, - 169, - 78, - 230, - 239, - 15, - 152, - 189, - 147, - 104, - 253, - 172, - 154, - 175, - 176, - 153, - 198, - 6, - 155, - 188, - 63, - 32, - 185, - 0, - 40, - 108, - 240, - 117, - 18, - 86, - 223, - 236, - 37, - 127, - 101, - 87, - 234, - 39, - 204, - 44, - 77, - 193, - 6, - 254, - 215, - 56, - 8, - 190, - 207, - 15, - 247, - 247, - 225, - 12, - 68, - 45, - 147, - 51, - 161, - 167, - 98, - 98, - 249, - 107, - 231, - 207, - 79, - 122, - 89, - 196, - 1, - 213, - 249, - 107, - 103, - 176, - 1, - 0, - 96, - 126, - 52, - 121, - 75, - 18, - 125, - 100, - 124, - 177, - 66, - 180, - 109, - 98, - 199, - 23, - 184, - 213, - 39, - 98, - 17, - 192, - 6, - 91, - 209, - 126, - 146, - 34, - 32, - 226, - 143, - 101, - 61, - 68, - 108, - 189, - 236, - 74, - 221, - 246, - 71, - 44, - 233, - 177, - 193, - 112, - 119, - 211, - 27, - 78, - 87, - 67, - 79, - 63, - 70, - 0, - 254, - 231, - 250, - 251, - 75, - 144, - 4, - 114, - 95, - 244, - 247, - 153, - 7, - 0, - 61, - 149, - 127, - 203, - 66, - 142, - 203, - 111, - 105, - 71, - 173, - 77, - 78, - 79, - 70, - 91, - 242, - 163, - 0, - 192, - 253, - 4, - 128, - 13, - 122, - 0, - 180, - 17, - 63, - 69, - 71, - 180, - 17, - 213, - 22, - 104, - 46, - 64, - 102, - 154, - 24, - 108, - 68, - 72, - 247, - 117, - 109, - 23, - 66, - 238, - 16, - 31, - 0, - 71, - 36, - 231, - 223, - 199, - 72, - 223, - 105, - 41, - 196, - 88, - 210, - 60, - 141, - 141, - 196, - 187, - 91, - 155, - 90, - 99, - 95, - 16, - 20, - 254, - 237, - 215, - 61, - 93, - 125, - 232, - 149, - 121, - 0, - 208, - 83, - 121, - 246, - 223, - 25, - 139, - 205, - 232, - 37, - 0, - 204, - 8, - 38, - 55, - 220, - 159, - 17, - 128, - 102, - 146, - 209, - 234, - 208, - 136, - 182, - 91, - 96, - 95, - 29, - 95, - 152, - 18, - 3, - 18, - 76, - 7, - 186, - 14, - 128, - 190, - 236, - 192, - 74, - 47, - 34, - 37, - 62, - 24, - 62, - 81, - 210, - 145, - 9, - 0, - 144, - 23, - 17, - 0, - 158, - 70, - 194, - 173, - 77, - 65, - 238, - 95, - 48, - 253, - 27, - 206, - 200, - 177, - 156, - 73, - 237, - 141, - 30, - 234, - 135, - 220, - 134, - 59, - 243, - 119, - 195, - 69, - 213, - 45, - 1, - 182, - 119, - 126, - 254, - 66, - 136, - 231, - 3, - 193, - 45, - 209, - 228, - 253, - 73, - 244, - 145, - 230, - 26, - 145, - 83, - 149, - 162, - 29, - 217, - 94, - 216, - 73, - 60, - 248, - 74, - 45, - 11, - 80, - 226, - 65, - 160, - 210, - 82, - 194, - 223, - 66, - 242, - 11, - 249, - 67, - 56, - 72, - 6, - 125, - 216, - 44, - 156, - 228, - 211, - 34, - 128, - 140, - 166, - 165, - 169, - 169, - 181, - 59, - 62, - 56, - 38, - 7, - 97, - 44, - 222, - 221, - 180, - 255, - 67, - 64, - 96, - 24, - 84, - 2, - 128, - 96, - 90, - 12, - 224, - 169, - 188, - 44, - 27, - 101, - 193, - 42, - 71, - 163, - 94, - 38, - 64, - 76, - 159, - 55, - 26, - 245, - 7, - 162, - 1, - 252, - 145, - 154, - 80, - 122, - 2, - 39, - 40, - 68, - 209, - 166, - 193, - 105, - 7, - 117, - 21, - 42, - 122, - 139, - 176, - 111, - 177, - 198, - 23, - 210, - 2, - 80, - 89, - 204, - 200, - 219, - 143, - 88, - 0, - 161, - 8, - 183, - 13, - 69, - 100, - 134, - 48, - 18, - 217, - 91, - 94, - 4, - 196, - 39, - 80, - 246, - 21, - 35, - 85, - 99, - 65, - 28, - 223, - 31, - 11, - 54, - 53, - 53, - 117, - 135, - 19, - 50, - 28, - 6, - 187, - 155, - 130, - 88, - 41, - 156, - 83, - 229, - 101, - 51, - 3, - 144, - 163, - 31, - 229, - 139, - 132, - 182, - 17, - 65, - 229, - 69, - 27, - 245, - 19, - 60, - 35, - 124, - 246, - 86, - 97, - 209, - 65, - 116, - 74, - 129, - 218, - 31, - 208, - 254, - 74, - 109, - 1, - 86, - 126, - 56, - 249, - 215, - 215, - 39, - 1, - 0, - 93, - 28, - 145, - 123, - 194, - 161, - 131, - 251, - 246, - 49, - 109, - 40, - 32, - 58, - 88, - 96, - 45, - 176, - 149, - 214, - 10, - 0, - 240, - 244, - 5, - 224, - 208, - 26, - 30, - 145, - 56, - 33, - 188, - 27, - 84, - 226, - 224, - 224, - 192, - 31, - 14, - 0, - 232, - 161, - 78, - 94, - 99, - 53, - 135, - 58, - 55, - 131, - 96, - 163, - 134, - 176, - 7, - 139, - 10, - 139, - 138, - 238, - 141, - 188, - 5, - 158, - 76, - 136, - 214, - 250, - 2, - 26, - 178, - 34, - 65, - 98, - 187, - 160, - 237, - 125, - 8, - 6, - 22, - 0, - 232, - 68, - 247, - 109, - 150, - 78, - 169, - 180, - 226, - 63, - 26, - 71, - 82, - 14, - 0, - 143, - 66, - 107, - 83, - 119, - 156, - 103, - 133, - 177, - 166, - 216, - 96, - 34, - 145, - 200, - 165, - 73, - 57, - 145, - 32, - 140, - 44, - 159, - 176, - 15, - 81, - 50, - 99, - 181, - 119, - 27, - 249, - 107, - 226, - 62, - 52, - 186, - 21, - 106, - 124, - 159, - 144, - 7, - 12, - 225, - 192, - 72, - 58, - 195, - 205, - 107, - 83, - 141, - 181, - 209, - 0, - 128, - 137, - 3, - 86, - 136, - 99, - 8, - 226, - 77, - 241, - 68, - 162, - 183, - 183, - 247, - 122, - 55, - 157, - 144, - 198, - 42, - 237, - 227, - 95, - 224, - 70, - 188, - 69, - 94, - 155, - 138, - 77, - 155, - 241, - 37, - 125, - 17, - 1, - 0, - 232, - 124, - 159, - 252, - 251, - 2, - 222, - 177, - 209, - 56, - 146, - 250, - 0, - 96, - 16, - 118, - 199, - 137, - 28, - 116, - 55, - 133, - 19, - 137, - 201, - 251, - 135, - 153, - 40, - 20, - 209, - 27, - 211, - 69, - 236, - 12, - 13, - 225, - 185, - 162, - 211, - 106, - 230, - 78, - 116, - 9, - 18, - 127, - 116, - 241, - 129, - 3, - 237, - 10, - 213, - 71, - 168, - 152, - 23, - 36, - 181, - 35, - 153, - 1, - 0, - 208, - 141, - 77, - 9, - 34, - 9, - 97, - 64, - 32, - 102, - 156, - 159, - 158, - 52, - 225, - 182, - 31, - 44, - 58, - 168, - 104, - 126, - 51, - 207, - 24, - 88, - 44, - 74, - 66, - 33, - 171, - 57, - 167, - 180, - 212, - 250, - 14, - 200, - 128, - 156, - 235, - 49, - 85, - 90, - 201, - 245, - 2, - 0, - 42, - 71, - 18, - 0, - 248, - 232, - 68, - 191, - 49, - 4, - 177, - 38, - 162, - 12, - 154, - 64, - 17, - 196, - 175, - 83, - 171, - 101, - 132, - 1, - 104, - 59, - 136, - 212, - 85, - 73, - 209, - 54, - 192, - 1, - 84, - 116, - 179, - 194, - 99, - 57, - 88, - 104, - 178, - 253, - 64, - 37, - 96, - 54, - 66, - 29, - 170, - 15, - 75, - 233, - 90, - 171, - 27, - 132, - 136, - 22, - 4, - 73, - 233, - 72, - 34, - 0, - 222, - 219, - 245, - 182, - 49, - 2, - 195, - 193, - 166, - 110, - 48, - 11, - 35, - 131, - 35, - 32, - 5, - 62, - 36, - 85, - 62, - 63, - 24, - 116, - 54, - 10, - 1, - 174, - 233, - 208, - 198, - 144, - 58, - 121, - 111, - 132, - 112, - 194, - 182, - 194, - 18, - 162, - 181, - 197, - 140, - 93, - 91, - 199, - 190, - 44, - 89, - 81, - 37, - 161, - 203, - 107, - 53, - 81, - 180, - 91, - 25, - 80, - 80, - 74, - 71, - 82, - 16, - 129, - 247, - 79, - 160, - 227, - 197, - 241, - 139, - 106, - 8, - 6, - 19, - 77, - 233, - 244, - 68, - 247, - 216, - 96, - 34, - 57, - 62, - 26, - 101, - 216, - 36, - 208, - 145, - 36, - 215, - 59, - 122, - 36, - 153, - 91, - 124, - 175, - 247, - 192, - 168, - 149, - 168, - 207, - 218, - 58, - 145, - 75, - 28, - 58, - 88, - 200, - 251, - 125, - 68, - 248, - 153, - 230, - 182, - 156, - 218, - 143, - 137, - 182, - 89, - 69, - 187, - 169, - 137, - 164, - 16, - 81, - 226, - 1, - 147, - 229, - 226, - 197, - 243, - 23, - 225, - 95, - 255, - 123, - 39, - 224, - 207, - 120, - 10, - 35, - 112, - 17, - 17, - 249, - 11, - 0, - 0, - 11, - 196, - 111, - 28, - 27, - 25, - 28, - 111, - 170, - 72, - 122, - 147, - 111, - 44, - 29, - 29, - 154, - 51, - 111, - 74, - 172, - 247, - 198, - 100, - 192, - 239, - 103, - 188, - 237, - 237, - 192, - 23, - 126, - 95, - 123, - 187, - 215, - 215, - 158, - 43, - 34, - 161, - 136, - 74, - 91, - 249, - 112, - 123, - 139, - 109, - 194, - 128, - 103, - 200, - 154, - 227, - 29, - 17, - 209, - 197, - 0, - 65, - 177, - 21, - 72, - 215, - 131, - 160, - 196, - 3, - 38, - 203, - 248, - 229, - 203, - 227, - 64, - 163, - 240, - 239, - 242, - 68, - 236, - 107, - 227, - 227, - 169, - 243, - 227, - 132, - 46, - 226, - 227, - 185, - 145, - 49, - 0, - 96, - 10, - 2, - 192, - 62, - 111, - 212, - 155, - 180, - 207, - 74, - 14, - 37, - 23, - 28, - 26, - 5, - 0, - 4, - 138, - 114, - 226, - 203, - 220, - 52, - 101, - 155, - 86, - 91, - 99, - 170, - 180, - 150, - 16, - 41, - 216, - 167, - 141, - 4, - 76, - 81, - 113, - 109, - 169, - 33, - 235, - 80, - 226, - 1, - 147, - 101, - 106, - 124, - 100, - 206, - 212, - 121, - 169, - 137, - 214, - 169, - 83, - 231, - 164, - 166, - 90, - 166, - 110, - 249, - 172, - 127, - 188, - 162, - 102, - 230, - 84, - 251, - 248, - 229, - 209, - 5, - 83, - 103, - 198, - 198, - 198, - 154, - 102, - 206, - 116, - 76, - 77, - 97, - 0, - 146, - 8, - 128, - 209, - 161, - 197, - 83, - 166, - 54, - 133, - 111, - 76, - 114, - 179, - 122, - 47, - 191, - 49, - 117, - 202, - 210, - 161, - 209, - 197, - 111, - 220, - 54, - 245, - 13, - 199, - 212, - 219, - 142, - 36, - 115, - 123, - 208, - 78, - 159, - 209, - 55, - 5, - 133, - 160, - 19, - 67, - 238, - 220, - 243, - 211, - 149, - 54, - 51, - 153, - 84, - 137, - 44, - 137, - 137, - 169, - 21, - 227, - 11, - 230, - 77, - 88, - 6, - 199, - 195, - 19, - 77, - 51, - 71, - 199, - 83, - 195, - 227, - 11, - 110, - 236, - 233, - 185, - 113, - 247, - 196, - 236, - 5, - 163, - 77, - 55, - 164, - 6, - 45, - 241, - 145, - 153, - 83, - 199, - 70, - 18, - 99, - 0, - 192, - 102, - 4, - 128, - 125, - 74, - 111, - 111, - 18, - 56, - 96, - 170, - 99, - 60, - 120, - 195, - 145, - 222, - 169, - 246, - 203, - 51, - 167, - 198, - 14, - 89, - 230, - 113, - 139, - 103, - 94, - 187, - 94, - 16, - 136, - 182, - 21, - 148, - 228, - 156, - 149, - 1, - 5, - 96, - 211, - 79, - 37, - 26, - 146, - 229, - 242, - 136, - 165, - 166, - 102, - 233, - 141, - 19, - 51, - 231, - 196, - 198, - 1, - 128, - 241, - 241, - 243, - 231, - 199, - 23, - 44, - 24, - 31, - 119, - 206, - 185, - 106, - 73, - 141, - 95, - 158, - 218, - 84, - 51, - 123, - 34, - 213, - 52, - 21, - 148, - 32, - 0, - 48, - 190, - 153, - 179, - 207, - 26, - 155, - 234, - 76, - 70, - 135, - 130, - 83, - 110, - 91, - 156, - 28, - 157, - 183, - 32, - 153, - 124, - 99, - 234, - 229, - 153, - 246, - 100, - 210, - 210, - 155, - 60, - 2, - 122, - 33, - 215, - 103, - 206, - 64, - 149, - 57, - 117, - 37, - 34, - 58, - 199, - 214, - 51, - 72, - 7, - 196, - 45, - 118, - 187, - 189, - 98, - 226, - 114, - 197, - 148, - 217, - 8, - 128, - 139, - 8, - 128, - 165, - 227, - 227, - 53, - 51, - 199, - 44, - 169, - 212, - 216, - 204, - 154, - 138, - 121, - 169, - 241, - 238, - 169, - 19, - 9, - 12, - 128, - 47, - 186, - 116, - 246, - 229, - 41, - 111, - 112, - 126, - 238, - 144, - 101, - 234, - 204, - 161, - 209, - 217, - 75, - 147, - 209, - 67, - 83, - 46, - 207, - 116, - 2, - 0, - 156, - 22, - 0, - 109, - 149, - 152, - 54, - 171, - 109, - 76, - 181, - 86, - 116, - 212, - 198, - 195, - 122, - 167, - 150, - 22, - 23, - 152, - 8, - 154, - 116, - 200, - 50, - 62, - 110, - 73, - 140, - 143, - 79, - 76, - 128, - 42, - 188, - 49, - 222, - 10, - 28, - 48, - 14, - 0, - 192, - 159, - 121, - 11, - 38, - 110, - 104, - 253, - 108, - 228, - 134, - 120, - 247, - 148, - 241, - 241, - 138, - 169, - 99, - 111, - 15, - 142, - 119, - 223, - 56, - 62, - 196, - 205, - 92, - 124, - 121, - 22, - 116, - 59, - 82, - 130, - 192, - 251, - 75, - 103, - 37, - 147, - 139, - 103, - 27, - 2, - 160, - 173, - 18, - 51, - 72, - 125, - 233, - 19, - 180, - 189, - 22, - 73, - 116, - 150, - 42, - 1, - 56, - 169, - 160, - 184, - 116, - 18, - 89, - 76, - 68, - 150, - 209, - 241, - 138, - 27, - 230, - 205, - 89, - 60, - 49, - 117, - 193, - 188, - 41, - 227, - 35, - 55, - 204, - 11, - 34, - 0, - 166, - 204, - 158, - 115, - 195, - 224, - 68, - 211, - 141, - 243, - 166, - 46, - 72, - 143, - 205, - 156, - 57, - 111, - 230, - 212, - 212, - 137, - 183, - 71, - 199, - 231, - 220, - 56, - 107, - 202, - 84, - 110, - 232, - 200, - 13, - 179, - 102, - 85, - 128, - 18, - 236, - 189, - 225, - 200, - 232, - 212, - 153, - 179, - 111, - 236, - 29, - 55, - 0, - 64, - 167, - 74, - 76, - 155, - 213, - 206, - 68, - 5, - 196, - 145, - 213, - 166, - 70, - 175, - 31, - 89, - 46, - 142, - 142, - 159, - 107, - 13, - 143, - 79, - 12, - 182, - 118, - 131, - 69, - 60, - 215, - 122, - 14, - 1, - 96, - 79, - 4, - 83, - 99, - 35, - 169, - 17, - 112, - 1, - 70, - 82, - 151, - 195, - 221, - 169, - 196, - 216, - 197, - 247, - 222, - 27, - 226, - 130, - 111, - 28, - 226, - 184, - 246, - 100, - 236, - 141, - 221, - 67, - 224, - 8, - 37, - 123, - 123, - 199, - 135, - 14, - 29, - 74, - 14, - 13, - 245, - 130, - 33, - 12, - 98, - 15, - 73, - 174, - 4, - 245, - 170, - 196, - 52, - 193, - 72, - 6, - 18, - 235, - 53, - 42, - 179, - 177, - 192, - 53, - 144, - 5, - 185, - 127, - 96, - 252, - 241, - 1, - 191, - 188, - 136, - 0, - 24, - 3, - 7, - 32, - 145, - 72, - 167, - 6, - 39, - 38, - 226, - 9, - 64, - 34, - 61, - 242, - 197, - 249, - 247, - 134, - 3, - 1, - 46, - 186, - 231, - 135, - 212, - 250, - 6, - 52, - 210, - 19, - 13, - 180, - 195, - 63, - 22, - 94, - 250, - 2, - 209, - 168, - 159, - 141, - 178, - 62, - 86, - 233, - 30, - 235, - 85, - 137, - 105, - 130, - 145, - 12, - 36, - 213, - 107, - 88, - 179, - 156, - 233, - 214, - 40, - 191, - 206, - 72, - 200, - 199, - 168, - 3, - 3, - 61, - 210, - 139, - 6, - 199, - 237, - 21, - 99, - 241, - 88, - 60, - 145, - 30, - 139, - 39, - 194, - 40, - 26, - 2, - 0, - 18, - 195, - 40, - 66, - 166, - 208, - 37, - 148, - 137, - 219, - 34, - 210, - 175, - 18, - 83, - 7, - 35, - 25, - 72, - 170, - 215, - 144, - 123, - 196, - 109, - 40, - 213, - 161, - 106, - 26, - 109, - 85, - 9, - 9, - 142, - 179, - 155, - 15, - 32, - 125, - 187, - 41, - 196, - 71, - 24, - 250, - 169, - 21, - 21, - 0, - 195, - 3, - 177, - 88, - 124, - 100, - 12, - 8, - 103, - 136, - 33, - 8, - 12, - 163, - 236, - 208, - 216, - 96, - 12, - 127, - 75, - 161, - 75, - 168, - 236, - 143, - 142, - 201, - 160, - 74, - 140, - 82, - 6, - 35, - 198, - 36, - 171, - 215, - 40, - 149, - 201, - 0, - 9, - 33, - 85, - 231, - 170, - 198, - 15, - 58, - 248, - 188, - 194, - 230, - 77, - 128, - 215, - 62, - 226, - 109, - 25, - 228, - 150, - 100, - 0, - 160, - 182, - 139, - 233, - 225, - 177, - 68, - 188, - 59, - 28, - 135, - 246, - 67, - 44, - 24, - 15, - 115, - 95, - 144, - 83, - 230, - 210, - 140, - 33, - 0, - 102, - 43, - 72, - 41, - 241, - 160, - 71, - 224, - 202, - 88, - 173, - 164, - 185, - 242, - 122, - 13, - 137, - 5, - 240, - 136, - 159, - 214, - 131, - 182, - 202, - 223, - 240, - 209, - 100, - 200, - 231, - 163, - 124, - 161, - 200, - 62, - 156, - 36, - 208, - 178, - 13, - 33, - 9, - 128, - 47, - 112, - 246, - 99, - 108, - 48, - 30, - 14, - 3, - 223, - 119, - 135, - 19, - 19, - 233, - 120, - 119, - 247, - 88, - 122, - 36, - 54, - 44, - 242, - 71, - 101, - 134, - 232, - 204, - 108, - 5, - 41, - 37, - 30, - 244, - 8, - 141, - 9, - 251, - 136, - 212, - 200, - 235, - 53, - 100, - 67, - 197, - 161, - 236, - 0, - 136, - 169, - 37, - 250, - 135, - 62, - 146, - 115, - 13, - 133, - 12, - 24, - 128, - 177, - 12, - 15, - 15, - 247, - 247, - 115, - 125, - 129, - 190, - 225, - 112, - 28, - 218, - 30, - 71, - 169, - 143, - 65, - 194, - 4, - 152, - 27, - 226, - 3, - 50, - 1, - 97, - 50, - 32, - 96, - 182, - 130, - 148, - 18, - 15, - 70, - 228, - 109, - 14, - 133, - 188, - 110, - 69, - 189, - 134, - 77, - 248, - 89, - 60, - 144, - 20, - 210, - 14, - 48, - 40, - 68, - 64, - 224, - 128, - 72, - 27, - 10, - 43, - 153, - 205, - 252, - 128, - 185, - 238, - 143, - 137, - 195, - 227, - 253, - 56, - 246, - 199, - 20, - 31, - 155, - 136, - 143, - 196, - 195, - 241, - 48, - 202, - 134, - 13, - 159, - 151, - 3, - 192, - 184, - 173, - 6, - 222, - 166, - 32, - 177, - 126, - 20, - 26, - 70, - 51, - 181, - 207, - 152, - 64, - 147, - 241, - 18, - 208, - 220, - 108, - 179, - 233, - 98, - 213, - 28, - 49, - 96, - 101, - 57, - 181, - 225, - 124, - 120, - 243, - 250, - 245, - 235, - 105, - 186, - 13, - 177, - 81, - 7, - 202, - 51, - 234, - 159, - 43, - 2, - 208, - 215, - 47, - 2, - 144, - 136, - 167, - 227, - 152, - 19, - 128, - 255, - 101, - 253, - 127, - 30, - 143, - 17, - 25, - 165, - 233, - 5, - 137, - 221, - 19, - 204, - 223, - 176, - 144, - 31, - 74, - 241, - 27, - 6, - 123, - 186, - 84, - 202, - 119, - 99, - 41, - 86, - 233, - 168, - 102, - 7, - 253, - 165, - 148, - 39, - 241, - 93, - 25, - 242, - 9, - 7, - 61, - 34, - 3, - 174, - 166, - 244, - 173, - 8, - 192, - 230, - 205, - 125, - 220, - 224, - 0, - 199, - 245, - 245, - 69, - 184, - 240, - 68, - 10, - 1, - 16, - 158, - 72, - 135, - 85, - 70, - 130, - 156, - 75, - 235, - 120, - 102, - 162, - 196, - 38, - 91, - 150, - 36, - 73, - 14, - 153, - 141, - 113, - 185, - 32, - 192, - 15, - 93, - 136, - 196, - 151, - 106, - 80, - 224, - 233, - 202, - 126, - 15, - 51, - 245, - 206, - 77, - 40, - 93, - 78, - 135, - 20, - 242, - 168, - 141, - 3, - 41, - 241, - 96, - 76, - 34, - 0, - 168, - 1, - 175, - 17, - 217, - 225, - 226, - 35, - 19, - 97, - 204, - 8, - 131, - 95, - 168, - 92, - 4, - 50, - 76, - 72, - 23, - 104, - 93, - 51, - 65, - 98, - 217, - 96, - 254, - 140, - 249, - 100, - 60, - 53, - 58, - 191, - 37, - 135, - 4, - 137, - 219, - 170, - 254, - 68, - 8, - 111, - 132, - 246, - 99, - 123, - 222, - 12, - 38, - 206, - 183, - 121, - 61, - 176, - 244, - 129, - 144, - 79, - 161, - 112, - 105, - 205, - 13, - 40, - 241, - 96, - 76, - 34, - 0, - 120, - 108, - 151, - 7, - 0, - 154, - 62, - 50, - 152, - 136, - 79, - 140, - 197, - 212, - 62, - 210, - 48, - 63, - 62, - 80, - 172, - 147, - 172, - 37, - 63, - 151, - 188, - 37, - 8, - 94, - 49, - 210, - 4, - 81, - 0, - 0, - 142, - 237, - 160, - 19, - 134, - 134, - 224, - 144, - 121, - 148, - 185, - 214, - 88, - 189, - 242, - 13, - 11, - 97, - 41, - 14, - 161, - 164, - 255, - 51, - 184, - 175, - 66, - 234, - 17, - 66, - 253, - 39, - 202, - 244, - 171, - 50, - 0, - 248, - 51, - 67, - 24, - 128, - 240, - 200, - 4, - 30, - 15, - 208, - 122, - 137, - 2, - 2, - 110, - 171, - 134, - 9, - 200, - 207, - 37, - 111, - 137, - 69, - 3, - 254, - 228, - 252, - 59, - 102, - 180, - 36, - 231, - 231, - 231, - 79, - 223, - 157, - 92, - 187, - 118, - 198, - 218, - 253, - 243, - 17, - 95, - 100, - 122, - 144, - 90, - 195, - 246, - 243, - 61, - 27, - 34, - 238, - 79, - 40, - 226, - 197, - 10, - 23, - 213, - 253, - 11, - 37, - 173, - 228, - 82, - 243, - 9, - 116, - 57, - 169, - 1, - 112, - 183, - 33, - 0, - 18, - 200, - 25, - 8, - 111, - 24, - 214, - 2, - 32, - 77, - 96, - 208, - 252, - 28, - 190, - 158, - 138, - 238, - 190, - 101, - 97, - 140, - 171, - 158, - 159, - 228, - 166, - 143, - 206, - 95, - 146, - 228, - 110, - 25, - 93, - 114, - 71, - 50, - 89, - 13, - 255, - 102, - 196, - 114, - 211, - 137, - 152, - 108, - 162, - 4, - 68, - 136, - 37, - 7, - 39, - 159, - 40, - 92, - 250, - 96, - 225, - 220, - 185, - 214, - 202, - 218, - 82, - 190, - 233, - 98, - 37, - 65, - 101, - 78, - 64, - 88, - 92, - 34, - 242, - 168, - 1, - 205, - 155, - 65, - 198, - 56, - 222, - 24, - 236, - 210, - 105, - 191, - 178, - 100, - 68, - 39, - 255, - 228, - 141, - 38, - 55, - 76, - 231, - 150, - 204, - 200, - 207, - 191, - 101, - 116, - 254, - 238, - 0, - 55, - 125, - 96, - 201, - 90, - 54, - 176, - 101, - 97, - 52, - 122, - 103, - 48, - 163, - 12, - 232, - 231, - 61, - 10, - 220, - 124, - 193, - 16, - 46, - 122, - 11, - 225, - 170, - 239, - 16, - 86, - 184, - 197, - 214, - 31, - 130, - 149, - 43, - 64, - 141, - 47, - 69, - 215, - 86, - 210, - 116, - 37, - 122, - 28, - 91, - 110, - 73, - 49, - 139, - 147, - 97, - 60, - 226, - 59, - 226, - 62, - 32, - 0, - 226, - 177, - 24, - 167, - 11, - 128, - 162, - 92, - 162, - 22, - 204, - 182, - 77, - 105, - 18, - 88, - 54, - 192, - 45, - 172, - 94, - 187, - 22, - 73, - 254, - 252, - 106, - 54, - 121, - 203, - 208, - 146, - 13, - 8, - 0, - 54, - 27, - 0, - 250, - 58, - 160, - 212, - 202, - 119, - 44, - 180, - 189, - 19, - 121, - 116, - 200, - 10, - 212, - 34, - 133, - 101, - 149, - 73, - 55, - 186, - 182, - 214, - 106, - 53, - 40, - 45, - 204, - 72, - 32, - 2, - 180, - 221, - 197, - 191, - 241, - 18, - 37, - 216, - 151, - 24, - 140, - 66, - 67, - 250, - 244, - 1, - 80, - 87, - 13, - 41, - 51, - 119, - 94, - 110, - 73, - 117, - 245, - 244, - 88, - 108, - 250, - 150, - 253, - 91, - 134, - 230, - 223, - 209, - 242, - 120, - 190, - 25, - 0, - 74, - 11, - 138, - 245, - 147, - 248, - 12, - 115, - 47, - 223, - 40, - 48, - 237, - 72, - 237, - 225, - 74, - 160, - 72, - 39, - 133, - 6, - 251, - 40, - 233, - 25, - 172, - 181, - 149, - 242, - 172, - 73, - 14, - 201, - 100, - 164, - 3, - 92, - 194, - 21, - 194, - 148, - 171, - 1, - 232, - 102, - 150, - 59, - 175, - 15, - 192, - 121, - 189, - 129, - 226, - 98, - 107, - 1, - 159, - 137, - 79, - 110, - 89, - 178, - 182, - 55, - 202, - 246, - 174, - 93, - 219, - 146, - 108, - 137, - 173, - 221, - 144, - 140, - 6, - 131, - 129, - 64, - 111, - 11, - 27, - 221, - 173, - 235, - 21, - 16, - 231, - 218, - 56, - 143, - 79, - 23, - 20, - 243, - 6, - 0, - 123, - 245, - 33, - 60, - 4, - 30, - 161, - 144, - 105, - 144, - 0, - 0, - 86, - 84, - 92, - 100, - 37, - 127, - 124, - 38, - 148, - 14, - 175, - 4, - 157, - 78, - 90, - 4, - 224, - 95, - 65, - 247, - 5, - 128, - 151, - 207, - 27, - 140, - 154, - 234, - 143, - 148, - 211, - 197, - 86, - 220, - 20, - 95, - 128, - 101, - 161, - 167, - 253, - 32, - 10, - 190, - 0, - 188, - 246, - 225, - 186, - 233, - 64, - 192, - 235, - 99, - 181, - 78, - 1, - 92, - 100, - 51, - 211, - 91, - 66, - 201, - 139, - 72, - 20, - 18, - 123, - 202, - 240, - 124, - 94, - 13, - 24, - 184, - 255, - 10, - 226, - 1, - 240, - 56, - 237, - 72, - 19, - 52, - 119, - 132, - 250, - 134, - 249, - 70, - 6, - 244, - 155, - 127, - 254, - 11, - 19, - 15, - 108, - 146, - 74, - 141, - 248, - 94, - 77, - 32, - 252, - 111, - 21, - 225, - 130, - 145, - 189, - 219, - 138, - 138, - 138, - 172, - 5, - 214, - 204, - 51, - 41, - 104, - 98, - 18, - 155, - 141, - 34, - 32, - 25, - 137, - 102, - 208, - 37, - 104, - 2, - 110, - 24, - 3, - 224, - 215, - 179, - 129, - 106, - 37, - 168, - 165, - 82, - 177, - 75, - 233, - 220, - 71, - 117, - 12, - 233, - 96, - 81, - 209, - 246, - 142, - 131, - 200, - 16, - 188, - 133, - 142, - 69, - 217, - 206, - 239, - 8, - 21, - 226, - 191, - 128, - 88, - 182, - 250, - 100, - 201, - 15, - 16, - 17, - 64, - 16, - 244, - 51, - 237, - 231, - 13, - 68, - 32, - 75, - 205, - 88, - 32, - 122, - 226, - 125, - 224, - 117, - 182, - 103, - 123, - 193, - 214, - 247, - 63, - 226, - 184, - 235, - 50, - 82, - 130, - 84, - 191, - 172, - 212, - 183, - 48, - 27, - 182, - 145, - 200, - 65, - 220, - 245, - 29, - 57, - 112, - 0, - 136, - 129, - 136, - 0, - 211, - 119, - 190, - 143, - 61, - 111, - 160, - 4, - 51, - 3, - 208, - 30, - 203, - 207, - 207, - 255, - 48, - 192, - 237, - 191, - 51, - 201, - 36, - 239, - 156, - 49, - 227, - 126, - 14, - 12, - 89, - 169, - 27, - 180, - 145, - 87, - 209, - 23, - 149, - 5, - 57, - 57, - 110, - 252, - 20, - 0, - 190, - 152, - 42, - 107, - 213, - 128, - 24, - 254, - 119, - 228, - 192, - 1, - 50, - 227, - 65, - 244, - 128, - 1, - 0, - 153, - 171, - 133, - 162, - 247, - 87, - 39, - 183, - 44, - 76, - 110, - 152, - 127, - 11, - 0, - 48, - 29, - 156, - 1, - 56, - 157, - 229, - 184, - 30, - 136, - 13, - 184, - 46, - 155, - 213, - 134, - 67, - 93, - 52, - 132, - 151, - 163, - 223, - 42, - 47, - 27, - 9, - 133, - 42, - 51, - 15, - 128, - 242, - 229, - 21, - 248, - 101, - 54, - 22, - 80, - 205, - 28, - 117, - 210, - 126, - 182, - 79, - 96, - 253, - 93, - 122, - 50, - 32, - 115, - 3, - 154, - 201, - 132, - 24, - 31, - 142, - 210, - 72, - 145, - 57, - 195, - 174, - 93, - 152, - 92, - 184, - 129, - 139, - 37, - 111, - 73, - 122, - 185, - 25, - 16, - 11, - 65, - 223, - 115, - 119, - 220, - 159, - 63, - 125, - 109, - 254, - 140, - 37, - 81, - 218, - 93, - 140, - 236, - 85, - 229, - 36, - 28, - 22, - 9, - 1, - 220, - 164, - 226, - 76, - 76, - 192, - 231, - 131, - 200, - 67, - 102, - 185, - 175, - 26, - 0, - 215, - 105, - 169, - 173, - 253, - 47, - 235, - 32, - 32, - 1, - 208, - 62, - 124, - 126, - 216, - 139, - 196, - 5, - 7, - 72, - 240, - 6, - 65, - 224, - 79, - 222, - 113, - 75, - 126, - 210, - 239, - 7, - 0, - 124, - 220, - 140, - 59, - 167, - 175, - 133, - 15, - 185, - 25, - 213, - 220, - 134, - 124, - 46, - 118, - 75, - 142, - 131, - 231, - 10, - 34, - 89, - 45, - 169, - 254, - 75, - 27, - 61, - 75, - 132, - 192, - 58, - 88, - 110, - 166, - 192, - 80, - 3, - 0, - 195, - 244, - 15, - 100, - 70, - 64, - 2, - 0, - 151, - 17, - 251, - 241, - 52, - 3, - 150, - 33, - 39, - 14, - 247, - 143, - 174, - 93, - 24, - 155, - 191, - 54, - 202, - 0, - 0, - 136, - 245, - 147, - 211, - 123, - 125, - 0, - 64, - 48, - 80, - 189, - 48, - 144, - 188, - 38, - 0, - 208, - 120, - 0, - 244, - 166, - 84, - 5, - 102, - 204, - 69, - 216, - 161, - 221, - 251, - 86, - 54, - 0, - 72, - 102, - 77, - 3, - 64, - 207, - 155, - 221, - 138, - 52, - 224, - 176, - 218, - 28, - 138, - 103, - 178, - 253, - 253, - 220, - 48, - 231, - 133, - 174, - 239, - 243, - 242, - 90, - 227, - 124, - 63, - 154, - 36, - 192, - 205, - 224, - 16, - 0, - 222, - 40, - 132, - 5, - 119, - 244, - 2, - 68, - 51, - 122, - 27, - 170, - 23, - 66, - 88, - 96, - 164, - 63, - 115, - 25, - 50, - 54, - 65, - 33, - 62, - 35, - 154, - 241, - 36, - 95, - 136, - 232, - 71, - 237, - 236, - 241, - 214, - 55, - 79, - 139, - 98, - 128, - 114, - 24, - 15, - 23, - 190, - 125, - 66, - 14, - 128, - 148, - 146, - 100, - 81, - 171, - 155, - 217, - 62, - 78, - 194, - 136, - 139, - 222, - 127, - 255, - 254, - 249, - 75, - 48, - 7, - 4, - 130, - 75, - 170, - 23, - 130, - 49, - 64, - 0, - 248, - 51, - 2, - 144, - 211, - 144, - 113, - 118, - 34, - 234, - 34, - 179, - 240, - 55, - 11, - 90, - 82, - 111, - 250, - 60, - 119, - 250, - 159, - 248, - 84, - 8, - 122, - 226, - 103, - 118, - 86, - 22, - 204, - 45, - 234, - 98, - 57, - 196, - 11, - 195, - 195, - 178, - 5, - 13, - 200, - 28, - 155, - 128, - 95, - 18, - 25, - 0, - 54, - 217, - 178, - 161, - 37, - 233, - 103, - 146, - 224, - 249, - 39, - 119, - 111, - 216, - 143, - 170, - 134, - 216, - 253, - 92, - 67, - 111, - 208, - 207, - 85, - 27, - 37, - 139, - 115, - 27, - 50, - 22, - 200, - 176, - 126, - 40, - 100, - 156, - 4, - 23, - 169, - 67, - 128, - 72, - 119, - 253, - 128, - 96, - 119, - 236, - 252, - 105, - 80, - 106, - 88, - 224, - 197, - 244, - 188, - 187, - 0, - 165, - 172, - 101, - 202, - 183, - 143, - 8, - 137, - 79, - 76, - 28, - 34, - 116, - 188, - 1, - 22, - 249, - 62, - 190, - 128, - 15, - 191, - 68, - 108, - 182, - 190, - 221, - 191, - 179, - 170, - 125, - 253, - 250, - 61, - 70, - 54, - 57, - 151, - 33, - 99, - 137, - 172, - 70, - 95, - 248, - 68, - 35, - 152, - 129, - 58, - 248, - 41, - 117, - 6, - 11, - 40, - 112, - 167, - 91, - 187, - 161, - 235, - 136, - 219, - 75, - 137, - 7, - 80, - 61, - 50, - 239, - 157, - 229, - 251, - 189, - 79, - 18, - 1, - 93, - 63, - 57, - 235, - 140, - 212, - 92, - 134, - 140, - 37, - 42, - 214, - 9, - 36, - 248, - 241, - 57, - 38, - 100, - 34, - 12, - 34, - 100, - 184, - 130, - 68, - 211, - 35, - 118, - 23, - 159, - 41, - 161, - 196, - 3, - 33, - 136, - 80, - 73, - 236, - 205, - 145, - 86, - 251, - 3, - 253, - 106, - 0, - 252, - 28, - 199, - 245, - 247, - 127, - 209, - 223, - 207, - 234, - 222, - 65, - 77, - 57, - 12, - 25, - 139, - 20, - 10, - 233, - 132, - 82, - 230, - 87, - 120, - 17, - 200, - 120, - 9, - 13, - 151, - 253, - 17, - 187, - 19, - 191, - 162, - 196, - 3, - 79, - 116, - 169, - 13, - 143, - 223, - 4, - 192, - 5, - 24, - 198, - 173, - 230, - 161, - 16, - 220, - 68, - 150, - 235, - 240, - 183, - 131, - 21, - 236, - 63, - 175, - 229, - 33, - 93, - 162, - 204, - 14, - 25, - 75, - 4, - 46, - 65, - 137, - 56, - 144, - 42, - 80, - 198, - 241, - 57, - 52, - 240, - 170, - 113, - 159, - 50, - 173, - 33, - 226, - 178, - 219, - 237, - 116, - 198, - 168, - 238, - 4, - 71, - 251, - 250, - 112, - 47, - 19, - 38, - 16, - 156, - 4, - 145, - 35, - 120, - 149, - 73, - 137, - 7, - 3, - 202, - 122, - 130, - 14, - 137, - 195, - 157, - 210, - 3, - 102, - 94, - 1, - 193, - 86, - 170, - 51, - 7, - 43, - 227, - 34, - 42, - 85, - 118, - 167, - 171, - 202, - 105, - 215, - 249, - 134, - 70, - 41, - 4, - 15, - 179, - 189, - 176, - 128, - 239, - 3, - 150, - 101, - 251, - 165, - 64, - 137, - 7, - 96, - 56, - 151, - 233, - 86, - 198, - 0, - 232, - 15, - 188, - 251, - 68, - 0, - 10, - 196, - 192, - 32, - 251, - 10, - 8, - 54, - 117, - 16, - 145, - 17, - 0, - 218, - 97, - 183, - 195, - 255, - 46, - 218, - 165, - 230, - 2, - 135, - 139, - 241, - 56, - 156, - 4, - 124, - 228, - 146, - 129, - 91, - 124, - 94, - 90, - 241, - 166, - 131, - 55, - 162, - 230, - 211, - 224, - 148, - 120, - 208, - 33, - 125, - 193, - 110, - 147, - 6, - 188, - 197, - 121, - 85, - 25, - 86, - 64, - 16, - 180, - 183, - 188, - 154, - 14, - 76, - 85, - 123, - 102, - 14, - 112, - 217, - 29, - 85, - 140, - 7, - 180, - 193, - 35, - 252, - 39, - 180, - 203, - 233, - 116, - 216, - 113, - 26, - 17, - 181, - 222, - 99, - 231, - 245, - 100, - 241, - 71, - 138, - 30, - 239, - 151, - 233, - 3, - 66, - 154, - 66, - 254, - 28, - 72, - 38, - 216, - 78, - 89, - 203, - 36, - 0, - 104, - 65, - 180, - 51, - 216, - 27, - 81, - 250, - 173, - 232, - 80, - 91, - 90, - 9, - 239, - 185, - 112, - 50, - 110, - 201, - 144, - 91, - 162, - 171, - 60, - 118, - 210, - 247, - 46, - 135, - 131, - 113, - 226, - 196, - 153, - 221, - 233, - 116, - 73, - 105, - 116, - 4, - 132, - 11, - 62, - 160, - 25, - 91, - 73, - 215, - 38, - 182, - 63, - 0, - 224, - 163, - 128, - 23, - 20, - 131, - 114, - 210, - 109, - 91, - 164, - 99, - 18, - 163, - 34, - 132, - 228, - 130, - 45, - 101, - 109, - 48, - 0, - 194, - 61, - 37, - 209, - 166, - 196, - 131, - 146, - 164, - 114, - 75, - 212, - 160, - 130, - 130, - 98, - 80, - 136, - 5, - 201, - 151, - 94, - 106, - 181, - 84, - 102, - 200, - 203, - 209, - 50, - 222, - 119, - 61, - 2, - 93, - 255, - 8, - 121, - 75, - 187, - 28, - 146, - 98, - 112, - 193, - 231, - 118, - 167, - 167, - 242, - 129, - 185, - 15, - 0, - 163, - 218, - 230, - 62, - 92, - 160, - 45, - 33, - 240, - 226, - 169, - 252, - 205, - 147, - 227, - 2, - 133, - 96, - 67, - 31, - 56, - 121, - 12, - 34, - 178, - 162, - 15, - 113, - 186, - 53, - 37, - 30, - 20, - 68, - 138, - 78, - 121, - 226, - 155, - 236, - 143, - 142, - 191, - 244, - 82, - 210, - 66, - 230, - 232, - 103, - 207, - 223, - 185, - 92, - 85, - 30, - 116, - 150, - 3, - 169, - 69, - 80, - 13, - 242, - 239, - 64, - 48, - 236, - 246, - 103, - 118, - 46, - 117, - 18, - 70, - 165, - 53, - 217, - 10, - 92, - 214, - 210, - 38, - 250, - 167, - 190, - 156, - 22, - 18, - 80, - 9, - 54, - 237, - 178, - 139, - 55, - 149, - 252, - 125, - 33, - 191, - 68, - 137, - 7, - 5, - 41, - 170, - 200, - 106, - 105, - 52, - 31, - 56, - 16, - 127, - 179, - 123, - 244, - 205, - 55, - 121, - 29, - 80, - 108, - 186, - 198, - 220, - 227, - 4, - 5, - 8, - 34, - 81, - 165, - 248, - 212, - 97, - 119, - 56, - 156, - 207, - 58, - 159, - 162, - 157, - 46, - 222, - 2, - 217, - 172, - 197, - 98, - 210, - 167, - 141, - 207, - 80, - 8, - 61, - 102, - 106, - 46, - 160, - 72, - 162, - 96, - 163, - 62, - 128, - 247, - 85, - 60, - 248, - 222, - 144, - 44, - 222, - 81, - 116, - 177, - 134, - 72, - 165, - 165, - 24, - 116, - 250, - 184, - 112, - 107, - 52, - 249, - 82, - 56, - 201, - 13, - 188, - 36, - 42, - 193, - 98, - 235, - 3, - 38, - 98, - 82, - 33, - 227, - 235, - 178, - 171, - 121, - 230, - 197, - 23, - 65, - 46, - 128, - 190, - 251, - 44, - 67, - 148, - 4, - 93, - 74, - 146, - 215, - 224, - 71, - 72, - 217, - 28, - 212, - 245, - 62, - 148, - 214, - 200, - 137, - 7, - 40, - 114, - 0, - 97, - 3, - 240, - 153, - 42, - 221, - 115, - 50, - 86, - 211, - 18, - 13, - 40, - 6, - 157, - 209, - 238, - 71, - 90, - 185, - 240, - 75, - 225, - 214, - 55, - 99, - 49, - 201, - 10, - 108, - 90, - 85, - 96, - 62, - 38, - 213, - 122, - 71, - 192, - 168, - 47, - 218, - 237, - 207, - 62, - 245, - 172, - 3, - 112, - 96, - 170, - 156, - 88, - 53, - 98, - 43, - 105, - 181, - 150, - 202, - 150, - 254, - 196, - 229, - 58, - 29, - 217, - 211, - 213, - 114, - 162, - 196, - 131, - 199, - 94, - 149, - 203, - 184, - 23, - 79, - 188, - 134, - 16, - 131, - 206, - 246, - 100, - 252, - 145, - 216, - 80, - 211, - 155, - 173, - 221, - 47, - 113, - 24, - 0, - 214, - 39, - 255, - 58, - 231, - 251, - 147, - 187, - 83, - 212, - 19, - 118, - 39, - 50, - 85, - 200, - 86, - 128, - 190, - 4, - 226, - 109, - 8, - 26, - 56, - 171, - 109, - 70, - 35, - 155, - 178, - 245, - 14, - 39, - 9, - 128, - 195, - 174, - 225, - 61, - 129, - 140, - 103, - 201, - 200, - 102, - 141, - 242, - 65, - 39, - 219, - 253, - 82, - 50, - 154, - 76, - 134, - 223, - 228, - 44, - 43, - 26, - 144, - 27, - 7, - 18, - 69, - 190, - 158, - 235, - 206, - 45, - 38, - 149, - 63, - 38, - 237, - 240, - 144, - 199, - 196, - 252, - 129, - 28, - 233, - 71, - 156, - 252, - 119, - 238, - 90, - 52, - 126, - 87, - 42, - 46, - 112, - 147, - 45, - 88, - 53, - 32, - 164, - 109, - 29, - 78, - 131, - 47, - 221, - 217, - 102, - 139, - 200, - 131, - 78, - 238, - 205, - 55, - 147, - 67, - 47, - 189, - 153, - 244, - 91, - 234, - 151, - 255, - 52, - 192, - 165, - 216, - 100, - 212, - 31, - 245, - 195, - 215, - 155, - 10, - 172, - 255, - 56, - 185, - 135, - 83, - 43, - 224, - 42, - 16, - 88, - 165, - 164, - 84, - 22, - 131, - 237, - 125, - 43, - 210, - 214, - 102, - 106, - 14, - 132, - 30, - 121, - 104, - 87, - 21, - 227, - 208, - 87, - 2, - 12, - 154, - 33, - 168, - 87, - 86, - 94, - 41, - 62, - 132, - 44, - 232, - 244, - 37, - 95, - 106, - 141, - 15, - 113, - 126, - 198, - 82, - 95, - 191, - 110, - 93, - 11, - 155, - 74, - 182, - 251, - 55, - 145, - 175, - 221, - 15, - 232, - 196, - 76, - 38, - 168, - 189, - 23, - 185, - 126, - 20, - 122, - 117, - 121, - 2, - 94, - 54, - 36, - 122, - 145, - 50, - 180, - 123, - 180, - 167, - 210, - 197, - 215, - 82, - 255, - 238, - 114, - 56, - 13, - 33, - 208, - 44, - 54, - 160, - 172, - 157, - 162, - 164, - 160, - 51, - 218, - 250, - 18, - 158, - 220, - 0, - 0, - 212, - 215, - 47, - 95, - 209, - 224, - 147, - 127, - 93, - 105, - 84, - 13, - 153, - 137, - 70, - 203, - 174, - 240, - 142, - 25, - 87, - 86, - 6, - 81, - 209, - 232, - 221, - 103, - 208, - 15, - 184, - 92, - 122, - 39, - 235, - 60, - 104, - 14, - 4, - 16, - 72, - 93, - 164, - 140, - 83, - 180, - 227, - 77, - 242, - 159, - 162, - 196, - 3, - 19, - 141, - 145, - 57, - 126, - 24, - 0, - 128, - 160, - 140, - 150, - 127, - 77, - 235, - 35, - 144, - 105, - 188, - 221, - 155, - 254, - 106, - 26, - 180, - 201, - 232, - 232, - 196, - 208, - 104, - 222, - 177, - 116, - 210, - 151, - 62, - 126, - 117, - 52, - 125, - 121, - 212, - 96, - 48, - 221, - 236, - 184, - 176, - 1, - 185, - 28, - 2, - 99, - 129, - 86, - 144, - 67, - 172, - 241, - 7, - 220, - 242, - 0, - 152, - 18, - 15, - 16, - 192, - 99, - 53, - 236, - 226, - 1, - 168, - 223, - 177, - 124, - 197, - 22, - 217, - 215, - 180, - 36, - 5, - 236, - 144, - 144, - 203, - 100, - 175, - 92, - 65, - 3, - 93, - 151, - 117, - 211, - 251, - 129, - 147, - 121, - 163, - 12, - 123, - 41, - 47, - 239, - 104, - 99, - 122, - 90, - 99, - 94, - 217, - 232, - 165, - 69, - 233, - 178, - 198, - 188, - 188, - 84, - 187, - 75, - 87, - 107, - 93, - 27, - 0, - 160, - 98, - 69, - 49, - 240, - 56, - 228, - 8, - 168, - 74, - 164, - 104, - 133, - 56, - 83, - 226, - 129, - 191, - 212, - 105, - 23, - 0, - 0, - 90, - 247, - 80, - 89, - 157, - 142, - 27, - 57, - 177, - 40, - 205, - 191, - 74, - 46, - 106, - 4, - 44, - 46, - 231, - 165, - 244, - 74, - 93, - 162, - 63, - 41, - 75, - 194, - 119, - 141, - 87, - 167, - 53, - 94, - 250, - 211, - 198, - 171, - 95, - 189, - 122, - 116, - 209, - 68, - 94, - 222, - 165, - 188, - 198, - 40, - 10, - 96, - 244, - 149, - 138, - 45, - 163, - 247, - 146, - 153, - 170, - 36, - 4, - 104, - 99, - 181, - 200, - 100, - 20, - 102, - 15, - 132, - 250, - 150, - 29, - 18, - 2, - 192, - 6, - 143, - 150, - 85, - 139, - 95, - 147, - 146, - 35, - 127, - 106, - 218, - 101, - 16, - 235, - 116, - 58, - 125, - 249, - 242, - 180, - 11, - 233, - 81, - 127, - 250, - 66, - 26, - 21, - 17, - 170, - 83, - 220, - 67, - 139, - 142, - 70, - 129, - 11, - 224, - 164, - 51, - 71, - 243, - 46, - 79, - 124, - 245, - 106, - 89, - 99, - 250, - 79, - 83, - 233, - 188, - 15, - 88, - 102, - 207, - 30, - 239, - 27, - 246, - 61, - 94, - 45, - 213, - 22, - 88, - 221, - 58, - 31, - 155, - 163, - 42, - 135, - 240, - 106, - 143, - 195, - 94, - 37, - 191, - 43, - 45, - 189, - 46, - 40, - 149, - 125, - 193, - 168, - 124, - 15, - 143, - 29, - 4, - 201, - 178, - 98, - 121, - 189, - 130, - 214, - 61, - 186, - 98, - 3, - 169, - 215, - 39, - 234, - 51, - 122, - 116, - 209, - 16, - 195, - 157, - 156, - 150, - 7, - 13, - 154, - 182, - 104, - 90, - 217, - 232, - 201, - 69, - 19, - 139, - 202, - 166, - 77, - 75, - 73, - 5, - 47, - 232, - 222, - 204, - 229, - 105, - 151, - 134, - 70, - 27, - 23, - 141, - 94, - 248, - 106, - 186, - 172, - 108, - 40, - 53, - 45, - 157, - 119, - 242, - 204, - 180, - 203, - 233, - 175, - 94, - 194, - 122, - 195, - 227, - 226, - 115, - 8, - 42, - 162, - 117, - 66, - 71, - 179, - 36, - 56, - 4, - 40, - 54, - 149, - 235, - 1, - 89, - 205, - 169, - 94, - 226, - 88, - 246, - 227, - 78, - 15, - 202, - 8, - 85, - 175, - 88, - 167, - 132, - 160, - 126, - 7, - 128, - 80, - 182, - 133, - 38, - 213, - 219, - 136, - 239, - 125, - 87, - 167, - 157, - 73, - 127, - 245, - 248, - 201, - 63, - 61, - 121, - 21, - 53, - 47, - 253, - 213, - 21, - 233, - 188, - 163, - 81, - 54, - 26, - 229, - 184, - 100, - 138, - 16, - 119, - 229, - 171, - 103, - 206, - 164, - 26, - 243, - 174, - 228, - 229, - 93, - 205, - 91, - 4, - 156, - 15, - 58, - 17, - 193, - 129, - 216, - 135, - 255, - 57, - 187, - 174, - 46, - 152, - 196, - 100, - 79, - 190, - 217, - 224, - 111, - 226, - 168, - 3, - 94, - 57, - 156, - 114, - 61, - 224, - 54, - 87, - 120, - 4, - 228, - 36, - 41, - 49, - 45, - 4, - 8, - 132, - 229, - 143, - 174, - 184, - 251, - 175, - 182, - 84, - 143, - 230, - 157, - 9, - 68, - 161, - 41, - 105, - 96, - 234, - 69, - 67, - 208, - 172, - 69, - 199, - 82, - 127, - 122, - 53, - 13, - 140, - 190, - 238, - 239, - 214, - 237, - 216, - 177, - 142, - 167, - 250, - 11, - 121, - 160, - 255, - 174, - 46, - 202, - 59, - 222, - 8, - 204, - 159, - 87, - 118, - 233, - 210, - 138, - 75, - 141, - 199, - 134, - 78, - 150, - 13, - 73, - 143, - 172, - 143, - 192, - 36, - 9, - 57, - 197, - 14, - 232, - 123, - 23, - 142, - 212, - 161, - 45, - 242, - 0, - 189, - 18, - 141, - 225, - 100, - 159, - 121, - 142, - 180, - 19, - 9, - 134, - 170, - 213, - 130, - 32, - 192, - 176, - 98, - 29, - 52, - 185, - 254, - 88, - 89, - 217, - 165, - 99, - 211, - 174, - 46, - 106, - 60, - 115, - 60, - 47, - 61, - 237, - 210, - 209, - 188, - 11, - 87, - 224, - 83, - 37, - 157, - 60, - 115, - 230, - 204, - 201, - 163, - 103, - 46, - 92, - 58, - 115, - 236, - 194, - 133, - 11, - 39, - 235, - 63, - 61, - 115, - 236, - 204, - 201, - 117, - 199, - 142, - 183, - 72, - 191, - 71, - 219, - 117, - 125, - 2, - 147, - 179, - 99, - 85, - 84, - 5, - 17, - 184, - 139, - 217, - 249, - 148, - 144, - 153, - 164, - 245, - 114, - 183, - 217, - 0, - 112, - 73, - 73, - 209, - 186, - 199, - 30, - 213, - 178, - 1, - 208, - 209, - 51, - 211, - 26, - 27, - 47, - 52, - 230, - 29, - 207, - 91, - 4, - 42, - 224, - 248, - 180, - 227, - 87, - 166, - 93, - 93, - 81, - 118, - 225, - 88, - 222, - 5, - 93, - 196, - 180, - 180, - 238, - 209, - 50, - 113, - 226, - 156, - 107, - 233, - 19, - 250, - 17, - 183, - 45, - 119, - 207, - 112, - 211, - 122, - 231, - 179, - 235, - 81, - 240, - 42, - 116, - 189, - 97, - 144, - 100, - 68, - 248, - 124, - 135, - 44, - 41, - 186, - 97, - 197, - 242, - 29, - 154, - 231, - 255, - 201, - 153, - 178, - 178, - 178, - 51, - 87, - 225, - 223, - 241, - 116, - 227, - 177, - 69, - 199, - 47, - 164, - 26, - 47, - 53, - 158, - 57, - 115, - 252, - 216, - 201, - 178, - 178, - 250, - 250, - 111, - 153, - 129, - 0, - 92, - 12, - 222, - 180, - 108, - 122, - 214, - 190, - 84, - 63, - 226, - 206, - 88, - 240, - 161, - 75, - 154, - 224, - 213, - 233, - 200, - 173, - 46, - 13, - 171, - 64, - 218, - 169, - 200, - 10, - 215, - 149, - 173, - 88, - 174, - 226, - 131, - 187, - 227, - 159, - 77, - 212, - 28, - 171, - 63, - 121, - 242, - 228, - 177, - 163, - 232, - 80, - 127, - 236, - 211, - 163, - 159, - 162, - 99, - 253, - 183, - 30, - 171, - 175, - 255, - 115, - 125, - 166, - 65, - 210, - 160, - 248, - 104, - 249, - 138, - 58, - 254, - 161, - 171, - 40, - 230, - 9, - 189, - 136, - 59, - 211, - 124, - 52, - 67, - 0, - 148, - 3, - 170, - 46, - 172, - 21, - 76, - 223, - 192, - 133, - 163, - 20, - 90, - 147, - 22, - 223, - 178, - 226, - 81, - 5, - 8, - 83, - 207, - 110, - 40, - 211, - 237, - 216, - 187, - 239, - 54, - 0, - 224, - 234, - 34, - 160, - 99, - 199, - 148, - 31, - 18, - 95, - 27, - 61, - 180, - 103, - 169, - 83, - 47, - 226, - 54, - 158, - 47, - 160, - 79, - 250, - 3, - 170, - 85, - 134, - 193, - 178, - 150, - 136, - 51, - 173, - 55, - 46, - 80, - 183, - 165, - 12, - 161, - 176, - 14, - 11, - 196, - 99, - 250, - 205, - 175, - 175, - 47, - 3, - 14, - 184, - 91, - 246, - 30, - 89, - 132, - 229, - 143, - 62, - 186, - 98, - 197, - 165, - 227, - 199, - 143, - 79, - 59, - 3, - 65, - 166, - 226, - 244, - 29, - 15, - 109, - 152, - 228, - 40, - 176, - 1, - 233, - 15, - 168, - 58, - 120, - 219, - 152, - 141, - 196, - 115, - 140, - 7, - 70, - 0, - 134, - 199, - 86, - 172, - 120, - 244, - 209, - 229, - 0, - 5, - 216, - 187, - 250, - 250, - 79, - 47, - 32, - 82, - 162, - 128, - 48, - 90, - 183, - 28, - 44, - 230, - 138, - 178, - 178, - 45, - 213, - 68, - 219, - 249, - 254, - 21, - 28, - 162, - 241, - 80, - 221, - 6, - 64, - 81, - 174, - 84, - 214, - 173, - 240, - 240, - 15, - 237, - 89, - 170, - 219, - 221, - 40, - 112, - 15, - 105, - 214, - 130, - 50, - 36, - 74, - 103, - 64, - 181, - 202, - 1, - 134, - 209, - 238, - 204, - 10, - 1, - 54, - 72, - 120, - 121, - 7, - 19, - 27, - 44, - 212, - 85, - 111, - 217, - 176, - 1, - 84, - 33, - 248, - 56, - 121, - 121, - 199, - 234, - 31, - 125, - 244, - 161, - 229, - 162, - 144, - 192, - 223, - 29, - 101, - 138, - 138, - 148, - 206, - 72, - 164, - 239, - 202, - 180, - 75, - 125, - 56, - 241, - 91, - 87, - 166, - 176, - 45, - 15, - 109, - 225, - 31, - 122, - 169, - 46, - 163, - 86, - 90, - 113, - 225, - 198, - 129, - 205, - 230, - 234, - 133, - 40, - 241, - 32, - 181, - 203, - 133, - 211, - 166, - 250, - 225, - 151, - 130, - 16, - 68, - 104, - 121, - 7, - 173, - 14, - 48, - 36, - 239, - 196, - 149, - 43, - 169, - 105, - 87, - 112, - 181, - 122, - 228, - 231, - 101, - 82, - 231, - 150, - 241, - 9, - 79, - 146, - 230, - 2, - 0, - 70, - 23, - 149, - 141, - 70, - 58, - 219, - 72, - 87, - 42, - 92, - 140, - 117, - 119, - 147, - 135, - 246, - 232, - 11, - 124, - 165, - 21, - 79, - 113, - 237, - 52, - 151, - 155, - 165, - 196, - 131, - 140, - 240, - 80, - 14, - 173, - 151, - 133, - 145, - 19, - 254, - 26, - 47, - 239, - 224, - 201, - 97, - 139, - 141, - 182, - 200, - 248, - 162, - 198, - 30, - 33, - 173, - 23, - 18, - 33, - 216, - 177, - 66, - 172, - 224, - 12, - 161, - 165, - 77, - 255, - 245, - 204, - 180, - 116, - 36, - 132, - 75, - 123, - 241, - 202, - 96, - 93, - 50, - 8, - 118, - 172, - 16, - 242, - 14, - 154, - 241, - 86, - 68, - 165, - 214, - 16, - 73, - 152, - 154, - 201, - 205, - 82, - 226, - 65, - 67, - 46, - 35, - 151, - 75, - 248, - 154, - 17, - 150, - 119, - 200, - 5, - 0, - 134, - 133, - 150, - 29, - 180, - 30, - 20, - 19, - 187, - 101, - 127, - 183, - 67, - 80, - 241, - 242, - 101, - 237, - 39, - 242, - 142, - 125, - 17, - 233, - 144, - 198, - 2, - 154, - 229, - 190, - 246, - 142, - 135, - 234, - 240, - 67, - 123, - 244, - 31, - 177, - 210, - 90, - 27, - 138, - 116, - 208, - 116, - 174, - 245, - 66, - 106, - 170, - 178, - 27, - 184, - 92, - 136, - 136, - 1, - 36, - 203, - 59, - 56, - 115, - 1, - 224, - 114, - 222, - 113, - 46, - 210, - 89, - 41, - 78, - 77, - 6, - 73, - 224, - 69, - 28, - 124, - 157, - 247, - 133, - 246, - 254, - 107, - 170, - 108, - 34, - 18, - 121, - 38, - 212, - 209, - 214, - 214, - 129, - 102, - 57, - 226, - 241, - 176, - 13, - 143, - 74, - 138, - 128, - 120, - 69, - 180, - 193, - 120, - 220, - 166, - 77, - 161, - 200, - 206, - 107, - 182, - 20, - 155, - 94, - 180, - 191, - 104, - 36, - 72, - 132, - 61, - 248, - 229, - 29, - 126, - 144, - 3, - 0, - 28, - 196, - 249, - 160, - 10, - 124, - 205, - 251, - 14, - 10, - 0, - 68, - 34, - 239, - 61, - 196, - 115, - 193, - 186, - 117, - 63, - 231, - 17, - 248, - 226, - 124, - 95, - 164, - 153, - 230, - 81, - 234, - 104, - 38, - 195, - 96, - 117, - 18, - 19, - 60, - 42, - 164, - 28, - 244, - 37, - 117, - 61, - 101, - 45, - 121, - 184, - 54, - 183, - 122, - 33, - 13, - 81, - 160, - 14, - 13, - 6, - 57, - 248, - 136, - 68, - 88, - 222, - 193, - 242, - 138, - 250, - 123, - 163, - 170, - 77, - 255, - 213, - 105, - 23, - 80, - 150, - 179, - 35, - 18, - 178, - 21, - 32, - 8, - 154, - 9, - 4, - 63, - 23, - 60, - 232, - 117, - 203, - 5, - 8, - 34, - 145, - 205, - 146, - 68, - 8, - 245, - 90, - 27, - 150, - 171, - 17, - 112, - 234, - 35, - 64, - 209, - 149, - 15, - 204, - 157, - 59, - 151, - 154, - 100, - 219, - 249, - 123, - 48, - 206, - 39, - 116, - 11, - 239, - 104, - 133, - 171, - 228, - 97, - 44, - 223, - 156, - 171, - 12, - 200, - 13, - 171, - 54, - 163, - 23, - 142, - 146, - 208, - 22, - 77, - 220, - 40, - 182, - 22, - 20, - 90, - 75, - 249, - 149, - 255, - 68, - 125, - 184, - 110, - 249, - 46, - 190, - 213, - 12, - 209, - 137, - 29, - 29, - 178, - 201, - 238, - 213, - 15, - 169, - 17, - 208, - 215, - 85, - 148, - 120, - 40, - 181, - 78, - 118, - 129, - 32, - 104, - 197, - 179, - 246, - 23, - 245, - 4, - 201, - 229, - 144, - 73, - 30, - 68, - 232, - 150, - 183, - 223, - 126, - 242, - 222, - 2, - 217, - 111, - 24, - 143, - 144, - 177, - 56, - 11, - 230, - 245, - 122, - 65, - 182, - 81, - 114, - 137, - 70, - 249, - 150, - 226, - 130, - 18, - 100, - 186, - 118, - 241, - 158, - 193, - 142, - 71, - 177, - 46, - 8, - 53, - 11, - 166, - 17, - 113, - 64, - 39, - 63, - 14, - 84, - 39, - 34, - 240, - 80, - 93, - 6, - 4, - 36, - 0, - 24, - 188, - 68, - 212, - 164, - 102, - 196, - 174, - 127, - 2, - 2, - 3, - 221, - 194, - 59, - 185, - 230, - 241, - 184, - 92, - 150, - 109, - 111, - 191, - 253, - 171, - 95, - 61, - 121, - 175, - 152, - 67, - 209, - 6, - 25, - 237, - 100, - 103, - 4, - 31, - 203, - 178, - 94, - 175, - 118, - 72, - 207, - 109, - 181, - 97, - 125, - 200, - 11, - 57, - 146, - 131, - 66, - 90, - 102, - 20, - 58, - 219, - 196, - 145, - 176, - 58, - 65, - 97, - 212, - 63, - 196, - 255, - 156, - 75, - 71, - 10, - 40, - 241, - 32, - 251, - 141, - 130, - 202, - 218, - 210, - 92, - 114, - 71, - 52, - 42, - 110, - 210, - 22, - 222, - 105, - 221, - 100, - 75, - 9, - 66, - 224, - 159, - 255, - 249, - 103, - 79, - 222, - 75, - 82, - 245, - 90, - 127, - 221, - 159, - 74, - 250, - 0, - 130, - 246, - 148, - 209, - 34, - 154, - 219, - 201, - 250, - 183, - 239, - 19, - 8, - 150, - 119, - 69, - 10, - 133, - 198, - 35, - 99, - 136, - 87, - 56, - 230, - 135, - 242, - 233, - 21, - 2, - 2, - 43, - 132, - 39, - 114, - 232, - 223, - 83, - 77, - 181, - 197, - 5, - 136, - 21, - 244, - 86, - 46, - 208, - 167, - 167, - 170, - 60, - 78, - 23, - 165, - 250, - 80, - 101, - 124, - 209, - 91, - 75, - 9, - 143, - 192, - 63, - 255, - 243, - 175, - 0, - 132, - 123, - 239, - 181, - 130, - 254, - 177, - 206, - 181, - 22, - 88, - 231, - 22, - 20, - 216, - 138, - 139, - 75, - 43, - 221, - 76, - 52, - 149, - 74, - 5, - 252, - 108, - 74, - 59, - 239, - 15, - 147, - 173, - 80, - 176, - 249, - 4, - 130, - 21, - 37, - 123, - 101, - 12, - 32, - 238, - 128, - 70, - 60, - 69, - 1, - 129, - 29, - 101, - 252, - 213, - 180, - 126, - 182, - 212, - 128, - 244, - 86, - 46, - 208, - 39, - 10, - 233, - 59, - 135, - 218, - 210, - 42, - 223, - 227, - 148, - 216, - 127, - 187, - 111, - 251, - 91, - 60, - 2, - 152, - 126, - 37, - 167, - 159, - 253, - 236, - 103, - 47, - 63, - 249, - 228, - 247, - 158, - 124, - 242, - 229, - 183, - 127, - 253, - 153, - 1, - 0, - 160, - 9, - 36, - 167, - 167, - 12, - 218, - 183, - 188, - 48, - 34, - 7, - 64, - 246, - 186, - 77, - 134, - 192, - 58, - 49, - 255, - 238, - 145, - 137, - 1, - 237, - 113, - 58, - 51, - 135, - 50, - 165, - 38, - 39, - 91, - 81, - 232, - 240, - 132, - 98, - 196, - 128, - 118, - 33, - 43, - 142, - 94, - 145, - 35, - 152, - 33, - 154, - 177, - 212, - 204, - 190, - 235, - 225, - 174, - 247, - 100, - 8, - 232, - 211, - 175, - 0, - 137, - 239, - 221, - 139, - 56, - 196, - 86, - 92, - 169, - 245, - 96, - 196, - 110, - 70, - 14, - 207, - 202, - 136, - 17, - 161, - 167, - 120, - 72, - 37, - 4, - 152, - 120, - 217, - 164, - 237, - 118, - 39, - 170, - 55, - 114, - 17, - 119, - 157, - 214, - 131, - 162, - 54, - 151, - 68, - 122, - 149, - 92, - 194, - 156, - 14, - 154, - 77, - 198, - 146, - 81, - 95, - 20, - 142, - 1, - 198, - 159, - 140, - 181, - 162, - 21, - 37, - 43, - 102, - 125, - 227, - 59, - 30, - 174, - 231, - 95, - 178, - 32, - 32, - 113, - 8, - 98, - 10, - 12, - 69, - 129, - 13, - 100, - 164, - 184, - 20, - 164, - 164, - 150, - 70, - 83, - 187, - 145, - 180, - 255, - 29, - 210, - 112, - 134, - 0, - 160, - 248, - 72, - 176, - 5, - 59, - 228, - 230, - 8, - 185, - 4, - 40, - 134, - 35, - 17, - 130, - 199, - 131, - 197, - 211, - 99, - 183, - 163, - 89, - 76, - 154, - 86, - 229, - 50, - 170, - 38, - 79, - 149, - 186, - 60, - 129, - 224, - 140, - 249, - 51, - 90, - 146, - 91, - 238, - 152, - 63, - 131, - 99, - 185, - 252, - 252, - 94, - 0, - 96, - 222, - 156, - 217, - 179, - 110, - 251, - 198, - 139, - 135, - 122, - 199, - 39, - 254, - 253, - 223, - 204, - 162, - 32, - 8, - 203, - 207, - 48, - 189, - 12, - 136, - 0, - 97, - 6, - 89, - 4, - 77, - 91, - 116, - 80, - 119, - 75, - 35, - 158, - 5, - 252, - 213, - 200, - 35, - 58, - 124, - 184, - 190, - 76, - 254, - 160, - 30, - 52, - 127, - 89, - 245, - 240, - 30, - 0, - 130, - 70, - 176, - 120, - 104, - 218, - 131, - 7, - 65, - 61, - 57, - 168, - 11, - 66, - 226, - 32, - 42, - 22, - 127, - 52, - 169, - 175, - 101, - 254, - 232, - 116, - 46, - 185, - 176, - 122, - 40, - 127, - 3, - 6, - 96, - 164, - 6, - 35, - 240, - 157, - 245, - 47, - 30, - 138, - 197, - 7, - 207, - 141, - 79, - 32, - 74, - 161, - 127, - 255, - 254, - 217, - 175, - 223, - 219, - 245, - 50, - 208, - 207, - 178, - 201, - 135, - 146, - 254, - 207, - 39, - 31, - 191, - 187, - 119, - 219, - 182, - 242, - 162, - 114, - 157, - 101, - 195, - 25, - 95, - 42, - 85, - 191, - 3, - 218, - 127, - 248, - 240, - 215, - 205, - 38, - 193, - 60, - 168, - 248, - 202, - 238, - 244, - 160, - 202, - 84, - 89, - 107, - 76, - 145, - 200, - 64, - 56, - 68, - 102, - 247, - 223, - 201, - 230, - 239, - 78, - 222, - 191, - 176, - 229, - 142, - 24, - 203, - 5, - 49, - 0, - 13, - 225, - 193, - 165, - 128, - 192, - 109, - 119, - 253, - 195, - 143, - 105, - 102, - 79, - 67, - 67, - 67, - 48, - 60, - 146, - 78, - 28, - 58, - 4, - 175, - 248, - 25, - 30, - 181, - 165, - 54, - 43, - 244, - 45, - 104, - 194, - 159, - 253, - 202, - 20, - 20, - 255, - 231, - 75, - 158, - 182, - 109, - 215, - 97, - 1, - 46, - 149, - 90, - 142, - 1, - 40, - 187, - 223, - 108, - 43, - 104, - 90, - 150, - 240, - 197, - 58, - 210, - 180, - 30, - 16, - 149, - 0, - 210, - 128, - 32, - 246, - 119, - 78, - 191, - 147, - 99, - 217, - 233, - 183, - 44, - 73, - 110, - 174, - 106, - 193, - 0, - 48, - 76, - 67, - 60, - 60, - 239, - 182, - 219, - 238, - 153, - 117, - 207, - 143, - 241, - 175, - 248, - 82, - 137, - 145, - 244, - 72, - 131, - 206, - 99, - 84, - 22, - 99, - 36, - 238, - 253, - 30, - 64, - 145, - 137, - 43, - 68, - 0, - 190, - 252, - 252, - 118, - 45, - 0, - 126, - 54, - 186, - 127, - 29, - 2, - 224, - 112, - 189, - 249, - 128, - 79, - 138, - 80, - 60, - 78, - 60, - 0, - 96, - 122, - 226, - 173, - 67, - 174, - 72, - 145, - 8, - 84, - 231, - 143, - 206, - 232, - 77, - 206, - 223, - 210, - 224, - 219, - 207, - 3, - 192, - 120, - 15, - 37, - 106, - 110, - 123, - 202, - 191, - 120, - 214, - 223, - 98, - 4, - 184, - 212, - 161, - 65, - 93, - 4, - 164, - 199, - 169, - 219, - 94, - 92, - 128, - 177, - 192, - 112, - 240, - 36, - 50, - 200, - 151, - 18, - 125, - 100, - 83, - 139, - 0, - 166, - 175, - 175, - 196, - 8, - 60, - 102, - 26, - 0, - 121, - 132, - 226, - 65, - 186, - 210, - 244, - 34, - 147, - 46, - 60, - 241, - 133, - 215, - 47, - 220, - 140, - 88, - 52, - 57, - 125, - 104, - 122, - 50, - 90, - 125, - 127, - 192, - 191, - 63, - 63, - 42, - 228, - 4, - 247, - 28, - 9, - 223, - 117, - 215, - 158, - 67, - 119, - 221, - 133, - 204, - 102, - 52, - 197, - 246, - 198, - 211, - 169, - 67, - 134, - 247, - 244, - 129, - 216, - 112, - 237, - 126, - 84, - 240, - 138, - 198, - 156, - 3, - 169, - 20, - 56, - 73, - 180, - 187, - 180, - 216, - 134, - 80, - 121, - 178, - 168, - 188, - 124, - 219, - 182, - 189, - 239, - 126, - 66, - 120, - 192, - 166, - 211, - 126, - 134, - 190, - 9, - 3, - 240, - 219, - 181, - 102, - 1, - 80, - 70, - 40, - 200, - 68, - 88, - 77, - 93, - 71, - 147, - 146, - 255, - 71, - 30, - 33, - 254, - 0, - 91, - 125, - 199, - 194, - 59, - 182, - 12, - 205, - 207, - 191, - 127, - 70, - 208, - 239, - 111, - 149, - 0, - 64, - 16, - 60, - 241, - 141, - 123, - 162, - 111, - 204, - 94, - 76, - 179, - 104, - 176, - 55, - 14, - 8, - 24, - 241, - 128, - 255, - 80, - 119, - 119, - 107, - 107, - 119, - 124, - 36, - 133, - 198, - 134, - 3, - 62, - 64, - 108, - 72, - 83, - 59, - 67, - 187, - 87, - 21, - 124, - 132, - 16, - 248, - 205, - 178, - 182, - 80, - 72, - 213, - 126, - 134, - 249, - 206, - 127, - 197, - 8, - 28, - 203, - 5, - 0, - 89, - 132, - 2, - 156, - 106, - 53, - 147, - 252, - 102, - 240, - 24, - 162, - 253, - 17, - 167, - 16, - 117, - 69, - 193, - 250, - 69, - 253, - 209, - 222, - 96, - 178, - 97, - 167, - 55, - 22, - 102, - 229, - 89, - 225, - 61, - 47, - 222, - 243, - 141, - 55, - 162, - 115, - 230, - 188, - 49, - 10, - 237, - 241, - 122, - 227, - 6, - 82, - 224, - 13, - 180, - 118, - 99, - 106, - 69, - 32, - 32, - 176, - 252, - 16, - 37, - 232, - 254, - 246, - 173, - 159, - 35, - 4, - 62, - 46, - 29, - 74, - 13, - 69, - 3, - 29, - 138, - 109, - 31, - 188, - 83, - 49, - 0, - 167, - 170, - 117, - 47, - 212, - 146, - 54, - 66, - 113, - 98, - 179, - 144, - 157, - 170, - 236, - 143, - 32, - 253, - 15, - 110, - 49, - 118, - 182, - 188, - 126, - 60, - 145, - 207, - 239, - 69, - 11, - 103, - 33, - 126, - 146, - 101, - 132, - 118, - 254, - 195, - 223, - 126, - 227, - 174, - 89, - 179, - 102, - 47, - 29, - 65, - 237, - 105, - 72, - 164, - 227, - 58, - 213, - 156, - 62, - 150, - 111, - 191, - 0, - 66, - 120, - 144, - 245, - 233, - 112, - 0, - 166, - 251, - 8, - 2, - 239, - 224, - 10, - 2, - 86, - 113, - 183, - 134, - 175, - 99, - 4, - 206, - 152, - 45, - 24, - 213, - 14, - 131, - 160, - 2, - 129, - 236, - 3, - 0, - 224, - 10, - 241, - 21, - 101, - 85, - 184, - 176, - 138, - 44, - 245, - 76, - 225, - 3, - 46, - 96, - 83, - 108, - 186, - 186, - 122, - 211, - 61, - 179, - 110, - 155, - 53, - 123, - 78, - 77, - 131, - 151, - 137, - 38, - 82, - 105, - 173, - 26, - 240, - 31, - 145, - 183, - 31, - 83, - 83, - 138, - 77, - 166, - 244, - 235, - 199, - 252, - 54, - 5, - 2, - 228, - 67, - 190, - 201, - 127, - 70, - 88, - 224, - 131, - 236, - 45, - 32, - 68, - 105, - 135, - 65, - 220, - 224, - 28, - 101, - 25, - 1, - 240, - 200, - 87, - 73, - 66, - 8, - 144, - 165, - 158, - 241, - 93, - 40, - 7, - 50, - 145, - 202, - 141, - 151, - 191, - 255, - 224, - 139, - 119, - 205, - 154, - 61, - 123, - 78, - 69, - 60, - 120, - 238, - 220, - 17, - 173, - 16, - 176, - 9, - 77, - 251, - 187, - 91, - 195, - 208, - 56, - 221, - 173, - 4, - 123, - 19, - 9, - 27, - 214, - 132, - 159, - 148, - 156, - 195, - 178, - 130, - 56, - 48, - 48, - 228, - 15, - 32, - 131, - 70, - 253, - 5, - 86, - 131, - 39, - 133, - 177, - 115, - 111, - 150, - 197, - 7, - 41, - 241, - 32, - 146, - 149, - 81, - 76, - 219, - 209, - 33, - 85, - 186, - 193, - 229, - 88, - 67, - 150, - 122, - 38, - 55, - 195, - 206, - 133, - 50, - 41, - 250, - 218, - 131, - 223, - 127, - 238, - 111, - 231, - 204, - 158, - 61, - 123, - 241, - 96, - 60, - 234, - 77, - 164, - 195, - 252, - 231, - 188, - 25, - 222, - 156, - 210, - 52, - 31, - 33, - 144, - 74, - 13, - 233, - 4, - 138, - 94, - 188, - 38, - 163, - 149, - 119, - 137, - 74, - 222, - 63, - 151, - 66, - 219, - 144, - 98, - 94, - 240, - 33, - 131, - 70, - 88, - 160, - 190, - 49, - 64, - 26, - 238, - 141, - 114, - 129, - 76, - 43, - 77, - 105, - 1, - 16, - 202, - 42, - 144, - 135, - 168, - 59, - 30, - 168, - 232, - 126, - 130, - 128, - 118, - 169, - 103, - 85, - 86, - 248, - 251, - 15, - 62, - 253, - 220, - 230, - 121, - 179, - 43, - 106, - 230, - 116, - 179, - 204, - 161, - 9, - 193, - 18, - 240, - 102, - 152, - 77, - 169, - 24, - 32, - 30, - 71, - 199, - 115, - 81, - 159, - 78, - 166, - 136, - 65, - 0, - 36, - 214, - 20, - 9, - 62, - 209, - 222, - 242, - 173, - 149, - 8, - 128, - 161, - 88, - 180, - 151, - 221, - 180, - 243, - 153, - 123, - 238, - 38, - 8, - 156, - 33, - 203, - 236, - 249, - 192, - 67, - 204, - 180, - 224, - 138, - 6, - 0, - 249, - 20, - 64, - 23, - 216, - 121, - 117, - 232, - 200, - 123, - 76, - 10, - 90, - 163, - 88, - 234, - 25, - 79, - 8, - 84, - 0, - 112, - 224, - 192, - 211, - 15, - 174, - 126, - 238, - 53, - 102, - 233, - 188, - 193, - 248, - 188, - 154, - 6, - 102, - 80, - 208, - 2, - 188, - 25, - 78, - 170, - 36, - 32, - 49, - 50, - 18, - 70, - 40, - 36, - 89, - 157, - 125, - 212, - 24, - 63, - 94, - 144, - 109, - 176, - 228, - 19, - 209, - 45, - 250, - 120, - 91, - 121, - 121, - 73, - 121, - 73, - 145, - 109, - 251, - 1, - 239, - 250, - 157, - 127, - 78, - 0, - 56, - 154, - 66, - 211, - 235, - 252, - 208, - 254, - 220, - 182, - 167, - 80, - 22, - 20, - 208, - 218, - 161, - 22, - 173, - 134, - 116, - 43, - 151, - 122, - 198, - 89, - 105, - 37, - 0, - 117, - 79, - 3, - 7, - 236, - 100, - 14, - 184, - 230, - 180, - 142, - 204, - 171, - 104, - 141, - 11, - 50, - 192, - 155, - 97, - 182, - 91, - 213, - 254, - 4, - 250, - 19, - 14, - 27, - 116, - 29, - 139, - 0, - 24, - 73, - 189, - 245, - 238, - 151, - 26, - 250, - 228, - 221, - 251, - 30, - 190, - 13, - 3, - 112, - 244, - 210, - 25, - 224, - 10, - 110, - 104, - 100, - 132, - 211, - 91, - 165, - 209, - 144, - 180, - 155, - 15, - 32, - 15, - 81, - 104, - 52, - 173, - 171, - 28, - 85, - 75, - 61, - 187, - 52, - 0, - 48, - 93, - 79, - 63, - 248, - 220, - 115, - 128, - 108, - 87, - 195, - 188, - 154, - 177, - 5, - 142, - 116, - 154, - 223, - 92, - 136, - 55, - 195, - 190, - 67, - 10, - 254, - 31, - 73, - 12, - 146, - 87, - 156, - 129, - 49, - 139, - 99, - 0, - 82, - 49, - 219, - 222, - 207, - 181, - 24, - 124, - 254, - 192, - 95, - 97, - 59, - 144, - 190, - 128, - 245, - 194, - 96, - 98, - 164, - 206, - 93, - 224, - 187, - 182, - 29, - 26, - 80, - 50, - 5, - 99, - 224, - 49, - 83, - 39, - 129, - 189, - 35, - 37, - 0, - 59, - 87, - 175, - 126, - 238, - 199, - 76, - 59, - 218, - 199, - 117, - 129, - 99, - 204, - 190, - 120, - 66, - 176, - 3, - 188, - 25, - 222, - 163, - 96, - 128, - 193, - 238, - 48, - 136, - 64, - 120, - 176, - 219, - 143, - 182, - 31, - 210, - 249, - 5, - 16, - 130, - 93, - 184, - 117, - 167, - 223, - 41, - 41, - 255, - 141, - 6, - 2, - 44, - 3, - 191, - 77, - 167, - 143, - 195, - 25, - 35, - 137, - 196, - 135, - 41, - 111, - 97, - 52, - 165, - 95, - 92, - 157, - 19, - 6, - 102, - 235, - 100, - 116, - 0, - 120, - 229, - 251, - 207, - 61, - 87, - 203, - 111, - 90, - 184, - 120, - 105, - 124, - 206, - 130, - 177, - 176, - 223, - 239, - 71, - 218, - 153, - 194, - 102, - 120, - 147, - 66, - 4, - 64, - 254, - 1, - 129, - 145, - 110, - 84, - 65, - 173, - 255, - 224, - 241, - 216, - 254, - 148, - 64, - 61, - 219, - 139, - 182, - 253, - 242, - 147, - 207, - 101, - 188, - 240, - 53, - 44, - 3, - 87, - 211, - 169, - 146, - 237, - 239, - 39, - 18, - 225, - 53, - 189, - 76, - 97, - 42, - 167, - 117, - 104, - 229, - 36, - 197, - 139, - 180, - 126, - 133, - 190, - 150, - 60, - 2, - 0, - 18, - 3, - 255, - 248, - 193, - 23, - 94, - 243, - 10, - 219, - 54, - 46, - 5, - 123, - 56, - 47, - 158, - 72, - 97, - 237, - 68, - 161, - 175, - 41, - 175, - 198, - 6, - 198, - 71, - 226, - 221, - 73, - 136, - 135, - 88, - 93, - 41, - 0, - 79, - 128, - 248, - 64, - 108, - 42, - 137, - 146, - 234, - 149, - 171, - 138, - 139, - 139, - 151, - 253, - 181, - 173, - 8, - 43, - 198, - 199, - 203, - 16, - 0, - 23, - 210, - 233, - 79, - 191, - 220, - 118, - 111, - 225, - 46, - 206, - 231, - 99, - 77, - 182, - 95, - 39, - 51, - 42, - 139, - 23, - 205, - 166, - 204, - 240, - 40, - 161, - 165, - 161, - 97, - 143, - 143, - 223, - 177, - 153, - 97, - 158, - 126, - 240, - 5, - 183, - 180, - 145, - 175, - 3, - 28, - 130, - 57, - 77, - 131, - 216, - 60, - 241, - 0, - 168, - 253, - 160, - 48, - 232, - 193, - 115, - 41, - 63, - 50, - 237, - 122, - 20, - 72, - 12, - 162, - 246, - 71, - 253, - 76, - 59, - 27, - 197, - 167, - 28, - 168, - 235, - 234, - 218, - 220, - 213, - 181, - 111, - 153, - 109, - 219, - 151, - 159, - 96, - 95, - 232, - 211, - 244, - 213, - 127, - 67, - 42, - 161, - 40, - 6, - 254, - 148, - 185, - 77, - 106, - 212, - 43, - 16, - 227, - 103, - 147, - 226, - 69, - 179, - 201, - 18, - 194, - 1, - 19, - 19, - 169, - 48, - 200, - 60, - 233, - 192, - 239, - 63, - 184, - 89, - 6, - 64, - 56, - 12, - 60, - 48, - 167, - 102, - 80, - 2, - 192, - 175, - 105, - 127, - 28, - 28, - 65, - 206, - 103, - 40, - 185, - 137, - 4, - 135, - 0, - 16, - 223, - 251, - 16, - 0, - 64, - 104, - 109, - 177, - 226, - 34, - 162, - 4, - 254, - 253, - 255, - 18, - 137, - 120, - 247, - 45, - 179, - 155, - 244, - 20, - 232, - 88, - 139, - 73, - 172, - 67, - 66, - 116, - 192, - 200, - 200, - 68, - 58, - 33, - 52, - 249, - 233, - 213, - 110, - 183, - 180, - 147, - 115, - 48, - 221, - 61, - 15, - 152, - 160, - 34, - 193, - 91, - 121, - 47, - 27, - 212, - 180, - 31, - 249, - 129, - 108, - 192, - 144, - 115, - 227, - 137, - 56, - 10, - 175, - 69, - 213, - 206, - 183, - 31, - 16, - 240, - 118, - 165, - 122, - 176, - 25, - 56, - 44, - 0, - 240, - 229, - 111, - 204, - 86, - 204, - 90, - 117, - 62, - 155, - 68, - 5, - 26, - 118, - 162, - 45, - 94, - 239, - 161, - 145, - 244, - 32, - 191, - 125, - 243, - 234, - 213, - 110, - 65, - 7, - 118, - 245, - 196, - 209, - 10, - 219, - 243, - 102, - 207, - 89, - 80, - 17, - 39, - 29, - 220, - 158, - 138, - 183, - 106, - 218, - 15, - 17, - 49, - 103, - 44, - 185, - 108, - 47, - 27, - 72, - 113, - 81, - 86, - 58, - 161, - 78, - 64, - 184, - 231, - 92, - 215, - 174, - 29, - 68, - 13, - 78, - 92, - 36, - 8, - 236, - 53, - 201, - 188, - 186, - 217, - 32, - 89, - 188, - 104, - 114, - 0, - 141, - 248, - 1, - 208, - 185, - 13, - 131, - 233, - 56, - 126, - 166, - 125, - 171, - 95, - 144, - 0, - 136, - 37, - 194, - 233, - 177, - 56, - 59, - 207, - 14, - 14, - 65, - 28, - 59, - 105, - 224, - 199, - 42, - 21, - 96, - 28, - 7, - 2, - 172, - 34, - 140, - 81, - 214, - 23, - 192, - 221, - 105, - 159, - 98, - 243, - 203, - 174, - 174, - 246, - 3, - 7, - 152, - 230, - 174, - 46, - 142, - 235, - 122, - 103, - 35, - 6, - 224, - 247, - 233, - 244, - 196, - 255, - 194, - 8, - 44, - 51, - 7, - 128, - 62, - 81, - 98, - 188, - 168, - 93, - 100, - 91, - 151, - 60, - 40, - 80, - 182, - 112, - 104, - 134, - 219, - 216, - 68, - 16, - 181, - 185, - 22, - 0, - 216, - 39, - 0, - 16, - 143, - 143, - 164, - 209, - 94, - 155, - 11, - 42, - 198, - 22, - 47, - 238, - 198, - 87, - 36, - 83, - 234, - 96, - 160, - 53, - 161, - 246, - 2, - 149, - 245, - 5, - 94, - 13, - 191, - 182, - 31, - 96, - 200, - 253, - 135, - 216, - 174, - 151, - 15, - 19, - 2, - 59, - 80, - 95, - 136, - 0, - 40, - 191, - 70, - 0, - 132, - 104, - 33, - 243, - 130, - 155, - 152, - 132, - 108, - 177, - 5, - 55, - 0, - 34, - 95, - 132, - 0, - 243, - 227, - 87, - 36, - 29, - 40, - 44, - 178, - 207, - 46, - 176, - 143, - 217, - 23, - 28, - 97, - 188, - 129, - 246, - 33, - 112, - 88, - 148, - 8, - 180, - 158, - 83, - 235, - 127, - 101, - 246, - 78, - 11, - 0, - 64, - 64, - 238, - 255, - 69, - 87, - 215, - 74, - 30, - 0, - 84, - 86, - 219, - 184, - 23, - 5, - 11, - 215, - 50, - 155, - 142, - 18, - 15, - 102, - 200, - 78, - 123, - 240, - 152, - 129, - 5, - 139, - 183, - 55, - 158, - 158, - 24, - 73, - 4, - 247, - 84, - 29, - 58, - 18, - 227, - 98, - 74, - 0, - 18, - 236, - 226, - 197, - 99, - 21, - 11, - 186, - 219, - 177, - 69, - 87, - 133, - 67, - 173, - 131, - 41, - 149, - 3, - 160, - 212, - 198, - 94, - 198, - 173, - 39, - 176, - 93, - 0, - 115, - 127, - 87, - 215, - 46, - 34, - 2, - 135, - 27, - 209, - 80, - 89, - 137, - 121, - 25, - 208, - 46, - 80, - 32, - 182, - 157, - 50, - 217, - 126, - 218, - 193, - 71, - 79, - 22, - 144, - 224, - 64, - 128, - 105, - 143, - 163, - 253, - 150, - 247, - 188, - 216, - 112, - 228, - 195, - 120, - 92, - 0, - 96, - 132, - 32, - 48, - 200, - 62, - 177, - 32, - 213, - 52, - 47, - 30, - 30, - 68, - 74, - 79, - 37, - 2, - 41, - 53, - 0, - 74, - 109, - 236, - 101, - 106, - 117, - 21, - 18, - 0, - 192, - 193, - 111, - 124, - 27, - 183, - 127, - 199, - 113, - 4, - 192, - 239, - 76, - 3, - 96, - 48, - 171, - 209, - 240, - 116, - 189, - 170, - 39, - 23, - 41, - 174, - 175, - 114, - 90, - 2, - 169, - 40, - 120, - 122, - 192, - 150, - 193, - 238, - 68, - 32, - 16, - 109, - 216, - 192, - 183, - 191, - 7, - 156, - 211, - 20, - 217, - 105, - 39, - 222, - 229, - 156, - 87, - 49, - 123, - 206, - 200, - 200, - 238, - 45, - 27, - 246, - 139, - 109, - 111, - 34, - 0, - 12, - 169, - 127, - 79, - 145, - 189, - 51, - 76, - 249, - 117, - 29, - 64, - 0, - 244, - 16, - 4, - 26, - 63, - 172, - 63, - 153, - 158, - 64, - 0, - 216, - 204, - 236, - 86, - 99, - 98, - 231, - 45, - 57, - 139, - 168, - 171, - 158, - 112, - 192, - 232, - 176, - 187, - 104, - 218, - 131, - 0, - 72, - 14, - 121, - 163, - 126, - 210, - 230, - 186, - 127, - 168, - 138, - 14, - 165, - 71, - 186, - 121, - 35, - 0, - 8, - 12, - 166, - 241, - 78, - 11, - 108, - 151, - 11, - 185, - 68, - 225, - 234, - 221, - 187, - 215, - 108, - 17, - 250, - 126, - 13, - 254, - 147, - 210, - 46, - 145, - 71, - 201, - 178, - 119, - 198, - 57, - 79, - 47, - 6, - 0, - 59, - 195, - 135, - 191, - 205, - 29, - 7, - 254, - 67, - 238, - 192, - 54, - 218, - 196, - 106, - 88, - 214, - 172, - 237, - 87, - 156, - 162, - 170, - 122, - 2, - 247, - 167, - 10, - 229, - 202, - 29, - 40, - 161, - 232, - 114, - 90, - 192, - 255, - 108, - 247, - 226, - 109, - 203, - 145, - 17, - 168, - 2, - 167, - 32, - 157, - 10, - 131, - 70, - 196, - 226, - 31, - 15, - 98, - 4, - 64, - 43, - 84, - 35, - 4, - 154, - 100, - 204, - 191, - 101, - 119, - 55, - 206, - 7, - 106, - 125, - 64, - 74, - 60, - 100, - 2, - 128, - 65, - 155, - 164, - 119, - 253, - 142, - 104, - 129, - 141, - 31, - 94, - 74, - 167, - 145, - 51, - 240, - 249, - 125, - 217, - 119, - 171, - 49, - 99, - 228, - 212, - 0, - 200, - 125, - 68, - 26, - 45, - 68, - 225, - 176, - 67, - 228, - 236, - 241, - 31, - 233, - 181, - 36, - 185, - 84, - 187, - 143, - 99, - 32, - 30, - 232, - 170, - 92, - 141, - 34, - 129, - 96, - 98, - 34, - 61, - 22, - 38, - 0, - 36, - 214, - 132, - 211, - 35, - 240, - 10, - 177, - 199, - 156, - 217, - 118, - 94, - 1, - 236, - 70, - 109, - 95, - 211, - 74, - 116, - 160, - 214, - 123, - 85, - 1, - 96, - 196, - 175, - 24, - 116, - 142, - 200, - 192, - 198, - 207, - 128, - 5, - 176, - 12, - 252, - 230, - 71, - 25, - 87, - 195, - 194, - 100, - 66, - 3, - 200, - 1, - 208, - 245, - 17, - 209, - 74, - 48, - 78, - 127, - 114, - 97, - 190, - 133, - 1, - 9, - 224, - 192, - 89, - 111, - 111, - 239, - 122, - 97, - 117, - 37, - 182, - 130, - 193, - 248, - 4, - 8, - 63, - 66, - 32, - 182, - 43, - 158, - 72, - 199, - 145, - 12, - 116, - 117, - 53, - 60, - 193, - 246, - 242, - 8, - 44, - 225, - 255, - 33, - 55, - 64, - 235, - 4, - 170, - 0, - 208, - 115, - 220, - 17, - 249, - 251, - 36, - 37, - 112, - 120, - 99, - 234, - 2, - 145, - 129, - 47, - 109, - 89, - 87, - 195, - 50, - 67, - 10, - 53, - 161, - 55, - 181, - 194, - 3, - 44, - 64, - 247, - 206, - 95, - 27, - 179, - 4, - 162, - 120, - 222, - 163, - 175, - 93, - 30, - 9, - 4, - 7, - 201, - 102, - 91, - 107, - 208, - 86, - 51, - 35, - 251, - 19, - 61, - 130, - 251, - 74, - 16, - 216, - 45, - 168, - 128, - 214, - 120, - 150, - 4, - 142, - 151, - 223, - 253, - 64, - 143, - 2, - 92, - 31, - 247, - 197, - 239, - 136, - 55, - 188, - 177, - 231, - 195, - 244, - 5, - 108, - 8, - 191, - 188, - 207, - 112, - 53, - 172, - 28, - 168, - 22, - 135, - 196, - 194, - 174, - 35, - 148, - 102, - 76, - 193, - 133, - 203, - 112, - 184, - 233, - 225, - 55, - 44, - 124, - 186, - 226, - 28, - 116, - 178, - 60, - 18, - 8, - 38, - 208, - 62, - 83, - 137, - 48, - 50, - 134, - 187, - 18, - 49, - 49, - 64, - 80, - 217, - 193, - 96, - 150, - 7, - 241, - 234, - 237, - 125, - 34, - 144, - 47, - 0, - 63, - 204, - 179, - 192, - 202, - 174, - 227, - 233, - 48, - 14, - 9, - 126, - 243, - 215, - 70, - 171, - 97, - 137, - 100, - 26, - 27, - 158, - 17, - 40, - 241, - 192, - 19, - 180, - 31, - 217, - 130, - 238, - 59, - 119, - 199, - 5, - 0, - 82, - 44, - 68, - 2, - 207, - 73, - 0, - 0, - 4, - 168, - 82, - 34, - 157, - 128, - 144, - 232, - 74, - 88, - 66, - 0, - 152, - 64, - 6, - 65, - 171, - 241, - 16, - 50, - 33, - 164, - 4, - 141, - 141, - 22, - 2, - 128, - 247, - 133, - 54, - 190, - 255, - 254, - 213, - 120, - 250, - 99, - 60, - 134, - 80, - 201, - 100, - 1, - 192, - 154, - 229, - 87, - 17, - 225, - 81, - 3, - 126, - 238, - 48, - 37, - 30, - 48, - 33, - 13, - 136, - 49, - 108, - 8, - 231, - 231, - 139, - 0, - 112, - 202, - 72, - 128, - 80, - 119, - 10, - 52, - 0, - 138, - 137, - 18, - 113, - 65, - 10, - 186, - 88, - 25, - 23, - 180, - 102, - 11, - 224, - 51, - 15, - 253, - 33, - 233, - 251, - 140, - 71, - 160, - 172, - 235, - 248, - 96, - 122, - 43, - 78, - 152, - 217, - 104, - 198, - 71, - 101, - 186, - 206, - 154, - 229, - 87, - 17, - 97, - 205, - 99, - 37, - 107, - 88, - 83, - 226, - 1, - 147, - 84, - 160, - 203, - 38, - 147, - 34, - 0, - 169, - 158, - 85, - 90, - 0, - 186, - 194, - 0, - 0, - 222, - 112, - 176, - 91, - 206, - 4, - 92, - 24, - 143, - 13, - 119, - 183, - 102, - 147, - 128, - 204, - 0, - 120, - 147, - 169, - 145, - 84, - 138, - 15, - 8, - 190, - 221, - 213, - 146, - 26, - 27, - 33, - 195, - 40, - 247, - 249, - 82, - 209, - 118, - 227, - 75, - 77, - 5, - 123, - 232, - 156, - 74, - 155, - 77, - 171, - 130, - 93, - 146, - 8, - 249, - 185, - 222, - 106, - 9, - 128, - 212, - 166, - 213, - 242, - 124, - 24, - 239, - 15, - 167, - 227, - 131, - 97, - 180, - 239, - 220, - 88, - 56, - 28, - 150, - 62, - 102, - 123, - 82, - 231, - 226, - 225, - 238, - 112, - 214, - 12, - 78, - 102, - 14, - 104, - 239, - 29, - 140, - 167, - 142, - 19, - 22, - 216, - 177, - 43, - 152, - 30, - 57, - 182, - 163, - 28, - 143, - 26, - 44, - 227, - 82, - 169, - 33, - 86, - 39, - 203, - 192, - 111, - 65, - 106, - 98, - 74, - 149, - 85, - 247, - 83, - 112, - 254, - 236, - 210, - 120, - 97, - 32, - 56, - 255, - 113, - 25, - 0, - 254, - 23, - 86, - 85, - 170, - 218, - 31, - 156, - 72, - 133, - 227, - 169, - 116, - 56, - 30, - 79, - 143, - 77, - 140, - 176, - 50, - 4, - 128, - 123, - 163, - 108, - 32, - 107, - 246, - 5, - 1, - 144, - 193, - 108, - 31, - 73, - 199, - 69, - 53, - 248, - 109, - 224, - 182, - 250, - 250, - 50, - 60, - 138, - 242, - 203, - 82, - 36, - 148, - 58, - 23, - 32, - 251, - 248, - 35, - 235, - 143, - 76, - 76, - 169, - 210, - 55, - 190, - 16, - 0, - 202, - 135, - 17, - 217, - 40, - 43, - 1, - 112, - 206, - 231, - 251, - 145, - 91, - 5, - 64, - 24, - 111, - 60, - 136, - 66, - 34, - 120, - 21, - 23, - 210, - 89, - 93, - 61, - 236, - 122, - 159, - 239, - 135, - 102, - 22, - 65, - 69, - 0, - 24, - 57, - 2, - 64, - 135, - 32, - 12, - 15, - 127, - 192, - 123, - 131, - 35, - 233, - 32, - 42, - 51, - 197, - 9, - 227, - 109, - 117, - 40, - 149, - 24, - 208, - 48, - 16, - 118, - 17, - 107, - 205, - 76, - 169, - 178, - 89, - 149, - 203, - 135, - 96, - 114, - 105, - 135, - 211, - 37, - 0, - 146, - 62, - 127, - 109, - 173, - 10, - 128, - 4, - 248, - 129, - 241, - 48, - 7, - 93, - 95, - 23, - 12, - 174, - 17, - 0, - 240, - 250, - 252, - 248, - 1, - 168, - 172, - 79, - 65, - 56, - 192, - 184, - 158, - 105, - 207, - 8, - 90, - 142, - 227, - 42, - 41, - 24, - 74, - 39, - 186, - 126, - 94, - 127, - 244, - 18, - 241, - 6, - 182, - 85, - 202, - 74, - 10, - 36, - 2, - 0, - 106, - 77, - 166, - 62, - 43, - 181, - 155, - 129, - 121, - 116, - 214, - 225, - 146, - 0, - 136, - 85, - 85, - 53, - 28, - 137, - 199, - 130, - 114, - 9, - 72, - 143, - 12, - 6, - 24, - 175, - 15, - 13, - 21, - 212, - 185, - 249, - 246, - 31, - 192, - 15, - 34, - 30, - 50, - 19, - 238, - 66, - 171, - 234, - 67, - 89, - 124, - 218, - 16, - 31, - 73, - 196, - 70, - 82, - 120, - 140, - 44, - 190, - 129, - 97, - 142, - 130, - 71, - 76, - 20, - 225, - 187, - 184, - 170, - 66, - 229, - 103, - 209, - 15, - 204, - 181, - 62, - 12, - 110, - 45, - 157, - 53, - 245, - 169, - 55, - 147, - 156, - 118, - 232, - 204, - 207, - 32, - 0, - 64, - 63, - 199, - 143, - 4, - 162, - 201, - 81, - 188, - 215, - 172, - 136, - 65, - 56, - 29, - 110, - 199, - 131, - 215, - 93, - 7, - 188, - 13, - 110, - 55, - 242, - 19, - 249, - 1, - 144, - 204, - 0, - 176, - 177, - 211, - 3, - 177, - 24, - 43, - 0, - 160, - 126, - 22, - 121, - 124, - 138, - 166, - 96, - 52, - 124, - 248, - 251, - 223, - 66, - 76, - 124, - 234, - 4, - 195, - 180, - 92, - 73, - 167, - 199, - 203, - 73, - 142, - 184, - 4, - 35, - 192, - 75, - 129, - 251, - 222, - 90, - 212, - 169, - 149, - 235, - 169, - 127, - 180, - 206, - 181, - 90, - 43, - 179, - 201, - 128, - 222, - 234, - 33, - 78, - 189, - 249, - 41, - 150, - 64, - 87, - 207, - 57, - 188, - 201, - 104, - 116, - 207, - 158, - 61, - 135, - 130, - 225, - 4, - 216, - 188, - 17, - 164, - 225, - 99, - 225, - 32, - 0, - 192, - 111, - 13, - 226, - 99, - 200, - 128, - 17, - 107, - 98, - 236, - 146, - 59, - 43, - 80, - 148, - 145, - 206, - 151, - 213, - 243, - 104, - 103, - 229, - 108, - 40, - 43, - 251, - 244, - 212, - 169, - 83, - 63, - 223, - 202, - 124, - 8, - 79, - 114, - 154, - 32, - 240, - 121, - 81, - 15, - 146, - 76, - 22, - 155, - 154, - 82, - 193, - 157, - 162, - 144, - 91, - 251, - 240, - 3, - 84, - 150, - 135, - 208, - 42, - 30, - 143, - 110, - 251, - 25, - 11, - 3, - 156, - 62, - 6, - 162, - 30, - 254, - 225, - 63, - 8, - 161, - 80, - 42, - 45, - 236, - 189, - 155, - 78, - 200, - 212, - 144, - 159, - 5, - 202, - 58, - 134, - 31, - 19, - 155, - 127, - 118, - 0, - 129, - 17, - 31, - 32, - 218, - 156, - 150, - 126, - 93, - 103, - 12, - 99, - 37, - 106, - 255, - 169, - 83, - 27, - 183, - 50, - 39, - 225, - 119, - 143, - 23, - 144, - 52, - 249, - 47, - 183, - 158, - 59, - 183, - 189, - 188, - 92, - 33, - 203, - 184, - 229, - 84, - 214, - 113, - 116, - 13, - 7, - 160, - 74, - 124, - 189, - 19, - 1, - 128, - 224, - 68, - 26, - 153, - 248, - 213, - 210, - 160, - 72, - 176, - 59, - 1, - 50, - 1, - 129, - 96, - 122, - 196, - 228, - 88, - 141, - 64, - 1, - 212, - 254, - 56, - 252, - 119, - 54, - 134, - 218, - 207, - 68, - 79, - 35, - 36, - 122, - 149, - 39, - 233, - 196, - 167, - 181, - 239, - 96, - 0, - 78, - 173, - 116, - 215, - 165, - 78, - 28, - 171, - 175, - 183, - 146, - 49, - 212, - 207, - 203, - 17, - 51, - 124, - 110, - 180, - 26, - 86, - 6, - 162, - 213, - 17, - 136, - 126, - 255, - 99, - 0, - 70, - 112, - 251, - 15, - 172, - 126, - 90, - 30, - 9, - 160, - 138, - 149, - 20, - 88, - 0, - 214, - 159, - 203, - 202, - 135, - 66, - 247, - 15, - 240, - 12, - 192, - 120, - 79, - 35, - 4, - 98, - 140, - 194, - 1, - 211, - 141, - 79, - 223, - 39, - 8, - 84, - 51, - 107, - 208, - 130, - 5, - 141, - 203, - 126, - 41, - 27, - 71, - 255, - 101, - 165, - 116, - 49, - 37, - 30, - 50, - 147, - 10, - 0, - 163, - 217, - 196, - 180, - 101, - 79, - 55, - 25, - 23, - 66, - 145, - 64, - 157, - 4, - 192, - 1, - 47, - 14, - 147, - 123, - 15, - 29, - 49, - 207, - 3, - 129, - 129, - 179, - 137, - 211, - 146, - 4, - 32, - 37, - 200, - 13, - 182, - 54, - 4, - 17, - 0, - 180, - 195, - 197, - 216, - 93, - 178, - 86, - 168, - 107, - 222, - 214, - 236, - 32, - 8, - 108, - 45, - 248, - 41, - 74, - 145, - 126, - 80, - 90, - 36, - 13, - 164, - 127, - 190, - 74, - 93, - 17, - 146, - 107, - 250, - 156, - 54, - 154, - 152, - 84, - 101, - 129, - 136, - 15, - 107, - 125, - 247, - 234, - 23, - 54, - 211, - 50, - 19, - 216, - 14, - 161, - 26, - 23, - 237, - 61, - 114, - 196, - 116, - 229, - 78, - 44, - 142, - 26, - 61, - 16, - 71, - 93, - 94, - 211, - 20, - 59, - 132, - 74, - 144, - 26, - 236, - 118, - 135, - 131, - 127, - 4, - 84, - 181, - 42, - 172, - 0, - 45, - 30, - 36, - 90, - 115, - 24, - 3, - 176, - 235, - 175, - 152, - 70, - 240, - 6, - 210, - 31, - 210, - 18, - 19, - 124, - 190, - 76, - 29, - 0, - 100, - 94, - 34, - 74, - 67, - 198, - 115, - 211, - 156, - 150, - 9, - 52, - 36, - 2, - 202, - 239, - 149, - 7, - 159, - 126, - 238, - 53, - 121, - 40, - 224, - 227, - 2, - 94, - 63, - 123, - 164, - 215, - 100, - 205, - 74, - 59, - 102, - 123, - 6, - 47, - 179, - 222, - 78, - 87, - 57, - 92, - 168, - 229, - 180, - 211, - 30, - 166, - 101, - 206, - 135, - 176, - 6, - 28, - 37, - 30, - 100, - 180, - 242, - 247, - 208, - 254, - 79, - 127, - 187, - 113, - 141, - 247, - 232, - 85, - 240, - 142, - 90, - 192, - 131, - 218, - 203, - 215, - 83, - 124, - 188, - 140, - 86, - 114, - 128, - 113, - 146, - 65, - 143, - 104, - 131, - 73, - 186, - 232, - 43, - 75, - 16, - 187, - 54, - 93, - 7, - 190, - 79, - 202, - 163, - 160, - 235, - 5, - 0, - 114, - 210, - 127, - 188, - 244, - 15, - 28, - 114, - 224, - 181, - 199, - 241, - 242, - 175, - 85, - 46, - 123, - 69, - 171, - 130, - 129, - 92, - 120, - 230, - 190, - 195, - 249, - 4, - 122, - 67, - 169, - 239, - 113, - 224, - 212, - 169, - 223, - 131, - 71, - 116, - 247, - 93, - 117, - 187, - 222, - 79, - 167, - 175, - 180, - 48, - 237, - 7, - 86, - 45, - 187, - 143, - 48, - 65, - 193, - 92, - 101, - 81, - 152, - 53, - 203, - 243, - 200, - 57, - 68, - 91, - 47, - 40, - 35, - 75, - 23, - 121, - 66, - 250, - 193, - 7, - 159, - 123, - 142, - 127, - 14, - 30, - 129, - 44, - 63, - 33, - 167, - 118, - 196, - 248, - 136, - 5, - 154, - 28, - 221, - 241, - 24, - 230, - 25, - 36, - 115, - 180, - 189, - 53, - 22, - 67, - 85, - 70, - 78, - 178, - 16, - 115, - 21, - 89, - 146, - 27, - 254, - 232, - 175, - 127, - 183, - 235, - 20, - 118, - 138, - 255, - 226, - 174, - 234, - 75, - 96, - 12, - 47, - 181, - 128, - 137, - 102, - 126, - 244, - 49, - 65, - 224, - 147, - 162, - 101, - 114, - 8, - 172, - 70, - 143, - 66, - 134, - 21, - 30, - 120, - 88, - 58, - 217, - 227, - 200, - 180, - 160, - 132, - 80, - 35, - 132, - 106, - 68, - 127, - 204, - 191, - 230, - 57, - 32, - 91, - 171, - 37, - 130, - 238, - 7, - 229, - 151, - 56, - 27, - 174, - 177, - 215, - 156, - 29, - 136, - 113, - 14, - 180, - 208, - 151, - 179, - 102, - 61, - 21, - 59, - 27, - 179, - 87, - 52, - 245, - 186, - 28, - 104, - 137, - 89, - 135, - 11, - 55, - 27, - 61, - 153, - 83, - 71, - 38, - 225, - 247, - 220, - 124, - 110, - 224, - 235, - 247, - 128, - 71, - 56, - 241, - 105, - 253, - 110, - 232, - 11, - 102, - 153, - 80, - 101, - 247, - 121, - 145, - 172, - 91, - 75, - 141, - 100, - 128, - 12, - 43, - 216, - 100, - 57, - 85, - 35, - 3, - 72, - 72, - 0, - 224, - 21, - 0, - 224, - 21, - 254, - 245, - 1, - 148, - 140, - 48, - 191, - 249, - 17, - 145, - 254, - 112, - 83, - 141, - 179, - 2, - 248, - 31, - 58, - 60, - 214, - 228, - 168, - 169, - 168, - 176, - 55, - 57, - 192, - 44, - 0, - 48, - 77, - 200, - 9, - 116, - 56, - 232, - 42, - 162, - 138, - 112, - 17, - 23, - 216, - 3, - 81, - 49, - 181, - 15, - 12, - 4, - 24, - 118, - 0, - 213, - 166, - 59, - 151, - 16, - 4, - 254, - 252, - 59, - 45, - 87, - 143, - 173, - 92, - 185, - 178, - 112, - 59, - 200, - 103, - 229, - 125, - 66, - 129, - 217, - 54, - 19, - 202, - 95, - 51, - 172, - 224, - 201, - 92, - 80, - 45, - 0, - 240, - 28, - 0, - 240, - 63, - 228, - 95, - 152, - 222, - 252, - 168, - 247, - 244, - 217, - 193, - 214, - 38, - 123, - 205, - 238, - 134, - 216, - 233, - 32, - 64, - 16, - 27, - 136, - 67, - 147, - 155, - 226, - 206, - 138, - 110, - 2, - 13, - 118, - 8, - 92, - 18, - 211, - 123, - 156, - 104, - 114, - 164, - 244, - 137, - 255, - 44, - 98, - 160, - 1, - 228, - 49, - 58, - 239, - 249, - 58, - 143, - 192, - 83, - 251, - 241, - 228, - 194, - 93, - 93, - 72, - 68, - 87, - 149, - 243, - 38, - 81, - 25, - 223, - 233, - 5, - 60, - 218, - 77, - 54, - 140, - 21, - 160, - 2, - 128, - 213, - 200, - 8, - 200, - 191, - 48, - 187, - 208, - 52, - 210, - 126, - 173, - 53, - 173, - 113, - 104, - 193, - 64, - 47, - 93, - 21, - 24, - 136, - 215, - 216, - 29, - 85, - 235, - 99, - 3, - 53, - 208, - 114, - 248, - 50, - 129, - 151, - 109, - 166, - 21, - 15, - 235, - 66, - 171, - 29, - 161, - 79, - 208, - 204, - 64, - 28, - 59, - 116, - 55, - 57, - 223, - 232, - 6, - 165, - 233, - 185, - 231, - 47, - 4, - 4, - 156, - 208, - 254, - 75, - 151, - 90, - 176, - 142, - 166, - 151, - 217, - 176, - 42, - 216, - 171, - 112, - 129, - 221, - 58, - 131, - 164, - 224, - 93, - 254, - 200, - 42, - 31, - 86, - 112, - 100, - 169, - 168, - 23, - 0, - 32, - 70, - 64, - 5, - 128, - 137, - 200, - 155, - 3, - 237, - 23, - 182, - 199, - 207, - 246, - 114, - 16, - 247, - 208, - 85, - 206, - 67, - 65, - 59, - 252, - 228, - 166, - 205, - 76, - 140, - 183, - 11, - 113, - 189, - 171, - 104, - 59, - 222, - 187, - 3, - 44, - 165, - 195, - 225, - 26, - 168, - 169, - 176, - 87, - 212, - 160, - 119, - 40, - 87, - 123, - 23, - 153, - 76, - 115, - 248, - 166, - 197, - 246, - 122, - 80, - 133, - 87, - 63, - 228, - 47, - 41, - 221, - 198, - 123, - 68, - 242, - 219, - 104, - 215, - 166, - 5, - 7, - 211, - 74, - 201, - 135, - 21, - 208, - 100, - 220, - 76, - 105, - 116, - 11, - 17, - 114, - 153, - 17, - 224, - 201, - 76, - 209, - 81, - 128, - 67, - 142, - 127, - 83, - 69, - 24, - 115, - 57, - 131, - 54, - 31, - 112, - 56, - 60, - 85, - 78, - 215, - 179, - 224, - 231, - 6, - 101, - 14, - 161, - 154, - 208, - 98, - 128, - 100, - 233, - 117, - 52, - 70, - 229, - 168, - 105, - 170, - 113, - 160, - 5, - 250, - 113, - 137, - 231, - 44, - 1, - 129, - 77, - 40, - 54, - 78, - 167, - 5, - 4, - 108, - 122, - 195, - 231, - 154, - 122, - 65, - 136, - 20, - 209, - 226, - 27, - 148, - 248, - 1, - 174, - 158, - 213, - 123, - 120, - 94, - 201, - 89, - 214, - 111, - 238, - 5, - 187, - 37, - 55, - 2, - 60, - 101, - 217, - 252, - 168, - 61, - 24, - 27, - 72, - 160, - 6, - 182, - 58, - 226, - 103, - 99, - 138, - 86, - 122, - 92, - 216, - 207, - 133, - 72, - 48, - 62, - 32, - 64, - 163, - 36, - 143, - 67, - 62, - 37, - 216, - 131, - 108, - 132, - 211, - 89, - 133, - 192, - 112, - 185, - 28, - 51, - 239, - 230, - 17, - 216, - 220, - 130, - 172, - 97, - 250, - 40, - 169, - 238, - 94, - 166, - 85, - 2, - 186, - 219, - 10, - 96, - 135, - 129, - 146, - 127, - 162, - 207, - 2, - 188, - 146, - 179, - 4, - 27, - 80, - 43, - 218, - 101, - 70, - 64, - 32, - 74, - 103, - 173, - 46, - 145, - 216, - 10, - 187, - 189, - 6, - 91, - 126, - 187, - 189, - 233, - 172, - 238, - 24, - 57, - 195, - 190, - 17, - 12, - 170, - 35, - 65, - 66, - 180, - 203, - 35, - 215, - 76, - 85, - 14, - 59, - 63, - 5, - 202, - 227, - 116, - 84, - 85, - 221, - 38, - 32, - 240, - 90, - 203, - 133, - 116, - 250, - 100, - 125, - 61, - 70, - 64, - 23, - 0, - 157, - 220, - 231, - 92, - 225, - 231, - 179, - 16, - 175, - 228, - 44, - 103, - 131, - 3, - 241, - 193, - 179, - 175, - 169, - 141, - 0, - 147, - 57, - 238, - 10, - 84, - 216, - 91, - 227, - 21, - 224, - 243, - 87, - 84, - 56, - 237, - 78, - 157, - 225, - 1, - 116, - 29, - 253, - 132, - 179, - 166, - 187, - 23, - 47, - 93, - 154, - 153, - 104, - 112, - 20, - 248, - 147, - 122, - 89, - 127, - 59, - 115, - 27, - 239, - 15, - 220, - 180, - 179, - 225, - 12, - 94, - 189, - 109, - 63, - 124, - 243, - 194, - 39, - 230, - 170, - 168, - 180, - 143, - 237, - 210, - 76, - 203, - 198, - 167, - 16, - 37, - 103, - 57, - 27, - 235, - 61, - 205, - 177, - 26, - 35, - 160, - 123, - 39, - 145, - 2, - 187, - 155, - 90, - 43, - 104, - 187, - 19, - 148, - 57, - 27, - 220, - 173, - 103, - 103, - 208, - 117, - 244, - 19, - 246, - 138, - 112, - 76, - 59, - 43, - 92, - 75, - 158, - 42, - 212, - 253, - 222, - 134, - 134, - 166, - 166, - 26, - 59, - 136, - 211, - 109, - 164, - 112, - 98, - 199, - 77, - 180, - 23, - 215, - 15, - 93, - 58, - 238, - 101, - 106, - 145, - 67, - 180, - 45, - 235, - 173, - 244, - 238, - 174, - 107, - 8, - 121, - 37, - 103, - 65, - 66, - 238, - 213, - 26, - 1, - 38, - 35, - 0, - 193, - 138, - 65, - 224, - 237, - 42, - 82, - 150, - 109, - 188, - 146, - 37, - 112, - 118, - 171, - 62, - 252, - 122, - 228, - 173, - 169, - 104, - 106, - 109, - 114, - 217, - 145, - 47, - 59, - 133, - 32, - 80, - 182, - 146, - 102, - 32, - 58, - 6, - 65, - 56, - 211, - 194, - 32, - 51, - 240, - 75, - 237, - 64, - 99, - 109, - 182, - 73, - 180, - 180, - 193, - 96, - 42, - 81, - 114, - 22, - 16, - 114, - 118, - 192, - 167, - 49, - 2, - 25, - 137, - 181, - 135, - 73, - 150, - 195, - 248, - 71, - 105, - 228, - 242, - 33, - 59, - 103, - 188, - 20, - 190, - 154, - 2, - 103, - 207, - 114, - 3, - 12, - 253, - 162, - 29, - 124, - 217, - 245, - 127, - 86, - 198, - 15, - 151, - 212, - 50, - 251, - 143, - 33, - 85, - 120, - 229, - 67, - 148, - 28, - 250, - 132, - 216, - 65, - 239, - 59, - 89, - 72, - 118, - 91, - 227, - 133, - 213, - 40, - 164, - 228, - 44, - 20, - 218, - 27, - 253, - 53, - 0, - 224, - 199, - 6, - 167, - 105, - 137, - 109, - 106, - 61, - 155, - 25, - 0, - 180, - 207, - 136, - 211, - 229, - 82, - 237, - 198, - 147, - 141, - 218, - 113, - 252, - 249, - 140, - 235, - 153, - 222, - 96, - 128, - 190, - 137, - 47, - 28, - 248, - 182, - 155, - 105, - 56, - 137, - 51, - 148, - 133, - 146, - 29, - 124, - 167, - 231, - 252, - 197, - 12, - 116, - 190, - 71, - 66, - 128, - 54, - 14, - 133, - 40, - 116, - 176, - 80, - 49, - 191, - 50, - 18, - 200, - 78, - 193, - 10, - 100, - 56, - 178, - 229, - 9, - 240, - 6, - 76, - 224, - 220, - 184, - 170, - 114, - 42, - 117, - 0, - 223, - 171, - 97, - 224, - 244, - 0, - 237, - 184, - 137, - 175, - 162, - 252, - 246, - 90, - 134, - 249, - 16, - 60, - 130, - 147, - 24, - 128, - 251, - 150, - 189, - 0, - 183, - 123, - 39, - 99, - 251, - 1, - 1, - 9, - 128, - 12, - 43, - 235, - 81, - 232, - 96, - 193, - 71, - 77, - 36, - 144, - 137, - 2, - 224, - 248, - 36, - 194, - 167, - 201, - 27, - 65, - 1, - 232, - 55, - 146, - 246, - 120, - 80, - 8, - 12, - 114, - 128, - 215, - 62, - 174, - 50, - 49, - 211, - 21, - 28, - 47, - 79, - 239, - 217, - 196, - 233, - 88, - 195, - 15, - 26, - 249, - 210, - 129, - 53, - 12, - 211, - 114, - 230, - 82, - 253, - 18, - 62, - 46, - 222, - 182, - 140, - 126, - 39, - 115, - 251, - 47, - 94, - 124, - 199, - 45, - 60, - 144, - 195, - 120, - 189, - 97, - 220, - 116, - 226, - 10, - 63, - 173, - 99, - 4, - 228, - 212, - 219, - 59, - 112, - 182, - 162, - 162, - 34, - 140, - 123, - 189, - 117, - 119, - 156, - 56, - 120, - 52, - 74, - 181, - 224, - 77, - 79, - 156, - 153, - 153, - 157, - 44, - 0, - 2, - 241, - 95, - 70, - 167, - 148, - 16, - 242, - 189, - 60, - 103, - 7, - 226, - 30, - 186, - 106, - 169, - 80, - 60, - 1, - 170, - 144, - 249, - 121, - 125, - 153, - 56, - 235, - 104, - 89, - 118, - 0, - 74, - 121, - 39, - 57, - 203, - 210, - 138, - 2, - 0, - 122, - 70, - 64, - 70, - 120, - 168, - 35, - 30, - 107, - 173, - 104, - 5, - 171, - 30, - 176, - 199, - 79, - 227, - 8, - 15, - 140, - 128, - 48, - 95, - 17, - 26, - 150, - 69, - 215, - 129, - 135, - 227, - 180, - 103, - 123, - 24, - 68, - 216, - 247, - 98, - 79, - 163, - 210, - 19, - 215, - 221, - 59, - 4, - 49, - 0, - 229, - 95, - 215, - 88, - 40, - 0, - 240, - 46, - 15, - 64, - 79, - 79, - 207, - 137, - 139, - 23, - 251, - 91, - 250, - 209, - 155, - 225, - 174, - 139, - 253, - 240, - 65, - 79, - 63, - 225, - 128, - 130, - 101, - 182, - 242, - 109, - 69, - 203, - 150, - 102, - 91, - 112, - 26, - 3, - 160, - 141, - 4, - 20, - 196, - 138, - 35, - 29, - 142, - 214, - 1, - 174, - 169, - 105, - 16, - 15, - 250, - 32, - 35, - 151, - 109, - 237, - 78, - 57, - 33, - 119, - 207, - 148, - 69, - 160, - 196, - 131, - 107, - 165, - 192, - 3, - 72, - 12, - 152, - 159, - 22, - 9, - 217, - 33, - 2, - 192, - 240, - 87, - 110, - 191, - 253, - 241, - 139, - 61, - 55, - 255, - 229, - 205, - 61, - 240, - 238, - 111, - 190, - 114, - 177, - 238, - 246, - 219, - 111, - 255, - 74, - 53, - 6, - 192, - 182, - 23, - 159, - 249, - 193, - 134, - 108, - 187, - 153, - 96, - 0, - 50, - 27, - 1, - 60, - 214, - 49, - 120, - 54, - 145, - 56, - 59, - 208, - 10, - 109, - 174, - 24, - 224, - 67, - 252, - 172, - 145, - 166, - 154, - 170, - 28, - 14, - 51, - 128, - 81, - 226, - 129, - 97, - 214, - 240, - 67, - 231, - 135, - 113, - 64, - 224, - 221, - 255, - 206, - 55, - 113, - 142, - 148, - 0, - 112, - 226, - 86, - 116, - 252, - 203, - 234, - 139, - 239, - 220, - 126, - 241, - 98, - 203, - 237, - 55, - 163, - 119, - 253, - 55, - 15, - 99, - 0, - 8, - 82, - 61, - 63, - 253, - 224, - 203, - 119, - 51, - 215, - 31, - 99, - 0, - 50, - 27, - 1, - 212, - 96, - 28, - 246, - 12, - 180, - 54, - 57, - 80, - 206, - 11, - 236, - 53, - 142, - 165, - 60, - 246, - 170, - 44, - 9, - 35, - 245, - 160, - 138, - 203, - 97, - 98, - 198, - 63, - 37, - 30, - 128, - 182, - 226, - 242, - 137, - 141, - 63, - 61, - 117, - 170, - 5, - 49, - 65, - 3, - 25, - 58, - 38, - 0, - 116, - 221, - 186, - 21, - 204, - 1, - 116, - 255, - 240, - 205, - 23, - 251, - 111, - 237, - 255, - 10, - 250, - 236, - 241, - 133, - 23, - 37, - 0, - 78, - 252, - 116, - 255, - 255, - 70, - 213, - 167, - 25, - 195, - 97, - 116, - 200, - 108, - 4, - 184, - 179, - 97, - 62, - 180, - 63, - 221, - 234, - 116, - 116, - 3, - 16, - 113, - 228, - 72, - 2, - 75, - 59, - 179, - 37, - 140, - 180, - 163, - 74, - 154, - 125, - 234, - 50, - 16, - 126, - 110, - 247, - 183, - 55, - 238, - 216, - 88, - 246, - 193, - 169, - 83, - 167, - 14, - 175, - 92, - 195, - 7, - 197, - 60, - 0, - 253, - 143, - 175, - 189, - 253, - 214, - 139, - 55, - 131, - 208, - 127, - 229, - 226, - 237, - 45, - 23, - 17, - 0, - 231, - 111, - 238, - 151, - 0, - 248, - 205, - 150, - 159, - 158, - 192, - 167, - 91, - 51, - 252, - 8, - 6, - 32, - 139, - 17, - 128, - 46, - 63, - 61, - 128, - 178, - 59, - 103, - 195, - 221, - 21, - 177, - 248, - 217, - 112, - 47, - 10, - 37, - 160, - 251, - 179, - 38, - 140, - 180, - 0, - 208, - 235, - 209, - 142, - 100, - 56, - 117, - 235, - 201, - 166, - 56, - 121, - 90, - 178, - 114, - 139, - 189, - 6, - 15, - 154, - 28, - 61, - 192, - 44, - 251, - 92, - 2, - 0, - 209, - 87, - 250, - 111, - 237, - 1, - 190, - 127, - 231, - 230, - 199, - 31, - 255, - 202, - 218, - 139, - 23, - 215, - 254, - 37, - 111, - 5, - 190, - 252, - 188, - 124, - 217, - 11, - 149, - 15, - 148, - 139, - 3, - 43, - 25, - 1, - 200, - 98, - 4, - 8, - 129, - 167, - 26, - 70, - 201, - 187, - 4, - 86, - 0, - 79, - 129, - 73, - 203, - 158, - 48, - 210, - 2, - 128, - 66, - 144, - 103, - 237, - 142, - 23, - 55, - 87, - 57, - 157, - 230, - 182, - 156, - 64, - 171, - 101, - 219, - 143, - 147, - 113, - 179, - 83, - 191, - 91, - 137, - 151, - 229, - 224, - 149, - 32, - 48, - 193, - 87, - 206, - 255, - 205, - 227, - 23, - 171, - 111, - 239, - 217, - 186, - 117, - 43, - 82, - 127, - 88, - 29, - 98, - 37, - 184, - 138, - 70, - 243, - 135, - 233, - 101, - 216, - 118, - 22, - 105, - 238, - 138, - 22, - 181, - 5, - 238, - 253, - 206, - 83, - 24, - 128, - 7, - 201, - 148, - 233, - 44, - 20, - 59, - 11, - 234, - 159, - 29, - 136, - 159, - 109, - 71, - 254, - 165, - 195, - 204, - 44, - 53, - 45, - 0, - 56, - 10, - 135, - 48, - 17, - 173, - 1, - 110, - 206, - 67, - 68, - 190, - 134, - 235, - 196, - 41, - 129, - 14, - 223, - 107, - 43, - 47, - 34, - 0, - 84, - 223, - 124, - 235, - 205, - 213, - 32, - 253, - 183, - 146, - 102, - 131, - 8, - 84, - 223, - 46, - 248, - 1, - 140, - 144, - 13, - 197, - 44, - 163, - 137, - 162, - 239, - 249, - 90, - 93, - 221, - 254, - 27, - 143, - 31, - 255, - 238, - 119, - 17, - 0, - 166, - 35, - 129, - 40, - 10, - 0, - 118, - 7, - 161, - 83, - 92, - 230, - 118, - 203, - 214, - 7, - 0, - 56, - 135, - 118, - 152, - 114, - 10, - 48, - 85, - 65, - 75, - 182, - 226, - 113, - 51, - 66, - 159, - 117, - 173, - 228, - 69, - 224, - 124, - 191, - 192, - 9, - 26, - 71, - 8, - 93, - 136, - 245, - 45, - 206, - 163, - 148, - 51, - 175, - 241, - 104, - 215, - 181, - 180, - 124, - 120, - 252, - 248, - 153, - 178, - 63, - 135, - 24, - 243, - 198, - 116, - 186, - 123, - 158, - 133, - 9, - 133, - 94, - 121, - 240, - 191, - 239, - 219, - 103, - 106, - 183, - 242, - 129, - 179, - 3, - 177, - 128, - 131, - 232, - 49, - 42, - 83, - 194, - 200, - 16, - 0, - 49, - 213, - 248, - 162, - 9, - 109, - 200, - 243, - 8, - 90, - 15, - 140, - 94, - 179, - 81, - 130, - 224, - 84, - 118, - 79, - 16, - 93, - 140, - 183, - 123, - 124, - 247, - 203, - 47, - 255, - 247, - 159, - 89, - 44, - 55, - 237, - 255, - 240, - 248, - 201, - 163, - 101, - 101, - 95, - 175, - 72, - 143, - 204, - 156, - 233, - 56, - 250, - 39, - 245, - 245, - 103, - 0, - 128, - 196, - 76, - 75, - 36, - 18, - 217, - 247, - 252, - 235, - 251, - 126, - 33, - 174, - 251, - 153, - 137, - 144, - 75, - 24, - 115, - 100, - 24, - 224, - 204, - 10, - 128, - 140, - 115, - 64, - 184, - 179, - 4, - 203, - 188, - 201, - 228, - 211, - 14, - 91, - 187, - 62, - 21, - 1, - 200, - 26, - 12, - 157, - 57, - 246, - 210, - 99, - 141, - 199, - 206, - 156, - 193, - 102, - 115, - 239, - 183, - 230, - 164, - 107, - 194, - 233, - 121, - 63, - 249, - 250, - 159, - 60, - 118, - 227, - 200, - 188, - 238, - 176, - 229, - 83, - 0, - 224, - 228, - 148, - 137, - 244, - 200, - 84, - 4, - 192, - 235, - 0, - 0, - 90, - 1, - 48, - 251, - 46, - 136, - 200, - 18, - 8, - 249, - 191, - 73, - 2, - 32, - 231, - 28, - 136, - 148, - 204, - 56, - 135, - 98, - 2, - 85, - 228, - 130, - 143, - 178, - 133, - 195, - 61, - 233, - 227, - 143, - 253, - 164, - 254, - 100, - 186, - 7, - 87, - 29, - 46, - 121, - 236, - 63, - 167, - 29, - 21, - 233, - 165, - 63, - 248, - 47, - 255, - 165, - 254, - 91, - 77, - 179, - 103, - 87, - 180, - 158, - 252, - 147, - 250, - 198, - 15, - 230, - 125, - 109, - 233, - 148, - 187, - 16, - 0, - 175, - 2, - 0, - 230, - 54, - 54, - 64, - 67, - 61, - 66, - 150, - 243, - 26, - 0, - 144, - 93, - 87, - 133, - 85, - 129, - 65, - 82, - 137, - 111, - 184, - 228, - 206, - 211, - 75, - 86, - 254, - 142, - 32, - 144, - 57, - 31, - 210, - 51, - 145, - 254, - 159, - 24, - 0, - 82, - 121, - 107, - 253, - 171, - 155, - 38, - 154, - 22, - 164, - 43, - 190, - 123, - 247, - 215, - 235, - 191, - 53, - 239, - 7, - 55, - 253, - 96, - 118, - 221, - 13, - 232, - 158, - 247, - 124, - 237, - 59, - 12, - 2, - 224, - 249, - 231, - 247, - 237, - 51, - 185, - 19, - 238, - 64, - 123, - 76, - 55, - 205, - 107, - 64, - 217, - 1, - 32, - 123, - 197, - 85, - 233, - 234, - 3, - 126, - 80, - 75, - 53, - 146, - 122, - 96, - 199, - 41, - 57, - 253, - 254, - 112, - 99, - 215, - 174, - 149, - 43, - 215, - 108, - 5, - 203, - 86, - 215, - 178, - 255, - 167, - 141, - 141, - 199, - 46, - 92, - 186, - 116, - 229, - 202, - 79, - 30, - 171, - 175, - 255, - 244, - 106, - 57, - 63, - 174, - 250, - 159, - 246, - 206, - 189, - 97, - 203, - 77, - 95, - 123, - 118, - 10, - 243, - 212, - 93, - 204, - 119, - 190, - 118, - 143, - 20, - 190, - 3, - 0, - 161, - 231, - 65, - 7, - 202, - 23, - 250, - 98, - 154, - 59, - 141, - 164, - 33, - 199, - 21, - 62, - 76, - 0, - 128, - 138, - 103, - 92, - 14, - 93, - 73, - 112, - 160, - 202, - 86, - 90, - 179, - 109, - 138, - 123, - 229, - 198, - 223, - 158, - 82, - 211, - 239, - 127, - 119, - 226, - 231, - 27, - 87, - 174, - 220, - 234, - 93, - 179, - 102, - 205, - 214, - 58, - 111, - 93, - 245, - 15, - 182, - 212, - 1, - 217, - 248, - 248, - 241, - 107, - 95, - 126, - 121, - 235, - 159, - 81, - 79, - 209, - 207, - 104, - 166, - 227, - 1, - 0, - 191, - 120, - 254, - 213, - 125, - 191, - 16, - 90, - 220, - 44, - 44, - 158, - 79, - 86, - 13, - 15, - 25, - 66, - 97, - 138, - 76, - 21, - 88, - 225, - 5, - 239, - 116, - 85, - 1, - 10, - 230, - 117, - 22, - 85, - 5, - 125, - 120, - 224, - 215, - 26, - 8, - 68, - 40, - 142, - 30, - 254, - 117, - 207, - 174, - 149, - 119, - 175, - 172, - 6, - 48, - 10, - 164, - 21, - 108, - 222, - 165, - 116, - 167, - 227, - 89, - 4, - 35, - 128, - 85, - 64, - 115, - 91, - 40, - 212, - 214, - 204, - 239, - 140, - 237, - 107, - 238, - 232, - 8, - 73, - 203, - 130, - 162, - 253, - 35, - 180, - 235, - 226, - 93, - 7, - 0, - 112, - 164, - 236, - 241, - 232, - 68, - 73, - 168, - 200, - 198, - 32, - 158, - 119, - 175, - 92, - 121, - 216, - 16, - 131, - 83, - 167, - 62, - 253, - 201, - 63, - 17, - 48, - 94, - 46, - 218, - 198, - 231, - 81, - 62, - 182, - 46, - 169, - 213, - 153, - 142, - 103, - 17, - 140, - 64, - 51, - 191, - 67, - 54, - 179, - 89, - 177, - 51, - 184, - 79, - 185, - 52, - 50, - 15, - 133, - 217, - 157, - 96, - 76, - 151, - 216, - 185, - 132, - 90, - 42, - 5, - 217, - 237, - 85, - 25, - 246, - 75, - 217, - 186, - 181, - 235, - 119, - 191, - 55, - 0, - 224, - 131, - 159, - 124, - 32, - 190, - 254, - 245, - 46, - 108, - 11, - 203, - 127, - 255, - 219, - 223, - 125, - 216, - 184, - 113, - 227, - 198, - 158, - 30, - 56, - 108, - 252, - 136, - 63, - 90, - 120, - 35, - 16, - 226, - 55, - 8, - 71, - 132, - 23, - 196, - 247, - 117, - 192, - 31, - 111, - 91, - 73, - 200, - 108, - 111, - 235, - 81, - 46, - 53, - 134, - 58, - 188, - 142, - 246, - 140, - 201, - 104, - 38, - 107, - 215, - 172, - 92, - 89, - 214, - 115, - 226, - 183, - 159, - 170, - 1, - 248, - 167, - 159, - 200, - 63, - 194, - 170, - 112, - 111, - 201, - 191, - 232, - 65, - 101, - 33, - 70, - 32, - 36, - 91, - 30, - 91, - 100, - 246, - 80, - 91, - 40, - 178, - 175, - 252, - 143, - 5, - 192, - 100, - 182, - 139, - 227, - 175, - 92, - 179, - 100, - 229, - 202, - 246, - 234, - 141, - 59, - 14, - 11, - 202, - 241, - 184, - 140, - 1, - 128, - 248, - 245, - 204, - 182, - 149, - 232, - 2, - 128, - 141, - 0, - 209, - 125, - 161, - 230, - 54, - 53, - 179, - 231, - 248, - 40, - 170, - 186, - 154, - 140, - 0, - 52, - 135, - 58, - 178, - 251, - 158, - 230, - 137, - 162, - 107, - 183, - 62, - 190, - 243, - 31, - 87, - 2, - 117, - 237, - 127, - 172, - 241, - 176, - 76, - 58, - 118, - 9, - 203, - 153, - 149, - 107, - 33, - 176, - 96, - 35, - 176, - 19, - 115, - 62, - 218, - 222, - 69, - 209, - 126, - 153, - 168, - 155, - 91, - 149, - 67, - 93, - 87, - 147, - 17, - 0, - 159, - 108, - 247, - 137, - 235, - 64, - 138, - 130, - 134, - 218, - 173, - 91, - 215, - 48, - 117, - 24, - 140, - 247, - 55, - 30, - 62, - 92, - 184, - 87, - 40, - 180, - 42, - 223, - 165, - 1, - 96, - 223, - 243, - 175, - 50, - 191, - 0, - 77, - 23, - 242, - 169, - 181, - 157, - 212, - 126, - 183, - 201, - 109, - 130, - 213, - 117, - 53, - 153, - 69, - 32, - 148, - 211, - 14, - 236, - 89, - 73, - 138, - 50, - 92, - 138, - 196, - 27, - 237, - 222, - 186, - 213, - 109, - 21, - 107, - 205, - 202, - 75, - 176, - 250, - 67, - 170, - 112, - 199, - 142, - 30, - 80, - 130, - 175, - 3, - 7, - 116, - 134, - 152, - 72, - 71, - 179, - 74, - 6, - 38, - 241, - 12, - 234, - 186, - 154, - 108, - 0, - 248, - 24, - 166, - 35, - 196, - 215, - 227, - 153, - 47, - 75, - 51, - 252, - 117, - 33, - 202, - 208, - 171, - 139, - 170, - 20, - 43, - 111, - 63, - 47, - 95, - 134, - 75, - 150, - 200, - 122, - 213, - 150, - 200, - 171, - 171, - 193, - 10, - 242, - 58, - 48, - 212, - 38, - 110, - 15, - 147, - 195, - 174, - 103, - 18, - 169, - 235, - 106, - 178, - 139, - 64, - 165, - 184, - 216, - 172, - 233, - 178, - 52, - 67, - 162, - 196, - 131, - 254, - 94, - 110, - 54, - 17, - 130, - 34, - 41, - 71, - 102, - 193, - 70, - 128, - 102, - 120, - 37, - 8, - 215, - 19, - 125, - 216, - 1, - 93, - 51, - 137, - 103, - 80, - 165, - 73, - 50, - 2, - 128, - 182, - 168, - 177, - 189, - 37, - 178, - 154, - 217, - 178, - 52, - 99, - 162, - 248, - 131, - 203, - 104, - 189, - 221, - 23, - 108, - 31, - 139, - 16, - 8, - 229, - 86, - 150, - 16, - 0, - 240, - 58, - 136, - 64, - 167, - 248, - 36, - 33, - 193, - 22, - 78, - 242, - 33, - 100, - 105, - 146, - 44, - 50, - 30, - 10, - 213, - 122, - 69, - 99, - 51, - 137, - 181, - 176, - 180, - 191, - 141, - 15, - 25, - 134, - 3, - 37, - 8, - 126, - 115, - 31, - 249, - 29, - 203, - 62, - 20, - 9, - 244, - 97, - 119, - 143, - 180, - 184, - 243, - 26, - 116, - 128, - 58, - 212, - 201, - 2, - 0, - 90, - 140, - 215, - 139, - 119, - 37, - 107, - 158, - 212, - 90, - 88, - 250, - 148, - 185, - 52, - 244, - 71, - 54, - 97, - 136, - 177, - 8, - 143, - 25, - 89, - 94, - 121, - 254, - 213, - 215, - 127, - 1, - 26, - 95, - 210, - 250, - 161, - 63, - 26, - 0, - 60, - 193, - 111, - 33, - 69, - 104, - 42, - 203, - 104, - 130, - 178, - 148, - 134, - 74, - 16, - 148, - 35, - 4, - 44, - 96, - 4, - 94, - 111, - 131, - 30, - 144, - 56, - 158, - 168, - 131, - 206, - 146, - 131, - 154, - 75, - 179, - 47, - 240, - 50, - 41, - 0, - 218, - 120, - 222, - 163, - 76, - 100, - 25, - 179, - 147, - 137, - 100, - 251, - 42, - 178, - 224, - 51, - 154, - 166, - 206, - 88, - 94, - 125, - 254, - 199, - 208, - 100, - 89, - 62, - 144, - 55, - 132, - 109, - 109, - 90, - 0, - 178, - 47, - 240, - 162, - 34, - 147, - 118, - 190, - 141, - 88, - 66, - 74, - 60, - 92, - 11, - 101, - 29, - 16, - 71, - 180, - 108, - 175, - 48, - 96, - 98, - 121, - 254, - 249, - 231, - 94, - 87, - 4, - 184, - 136, - 1, - 246, - 234, - 43, - 64, - 177, - 18, - 219, - 108, - 205, - 71, - 70, - 0, - 52, - 27, - 132, - 83, - 226, - 225, - 90, - 72, - 103, - 122, - 172, - 154, - 94, - 176, - 149, - 111, - 251, - 146, - 31, - 48, - 65, - 0, - 236, - 67, - 42, - 144, - 79, - 124, - 144, - 238, - 47, - 210, - 245, - 1, - 118, - 174, - 167, - 231, - 22, - 96, - 101, - 237, - 182, - 154, - 91, - 250, - 47, - 19, - 0, - 252, - 196, - 54, - 90, - 194, - 129, - 18, - 15, - 58, - 103, - 155, - 117, - 147, - 76, - 48, - 128, - 52, - 25, - 105, - 155, - 8, - 0, - 67, - 218, - 223, - 204, - 155, - 128, - 131, - 50, - 14, - 160, - 221, - 5, - 86, - 235, - 189, - 232, - 105, - 55, - 231, - 188, - 192, - 139, - 9, - 17, - 40, - 54, - 55, - 1, - 204, - 180, - 155, - 148, - 189, - 46, - 107, - 149, - 180, - 210, - 51, - 230, - 128, - 213, - 178, - 145, - 113, - 201, - 17, - 38, - 44, - 64, - 163, - 77, - 220, - 11, - 138, - 209, - 42, - 166, - 52, - 169, - 94, - 201, - 109, - 129, - 151, - 235, - 231, - 235, - 155, - 117, - 147, - 244, - 118, - 111, - 83, - 81, - 185, - 216, - 254, - 79, - 176, - 14, - 80, - 140, - 140, - 119, - 10, - 94, - 128, - 94, - 42, - 144, - 87, - 209, - 148, - 249, - 199, - 54, - 6, - 192, - 6, - 200, - 154, - 191, - 143, - 105, - 55, - 41, - 203, - 244, - 16, - 68, - 149, - 216, - 21, - 122, - 247, - 221, - 143, - 127, - 89, - 142, - 28, - 98, - 196, - 1, - 138, - 145, - 241, - 80, - 38, - 39, - 128, - 18, - 15, - 230, - 232, - 250, - 113, - 128, - 89, - 55, - 41, - 251, - 86, - 76, - 171, - 144, - 9, - 44, - 170, - 173, - 37, - 235, - 171, - 0, - 0, - 170, - 145, - 113, - 25, - 0, - 248, - 148, - 74, - 177, - 30, - 187, - 178, - 88, - 9, - 128, - 137, - 133, - 192, - 175, - 99, - 184, - 107, - 202, - 77, - 210, - 203, - 172, - 50, - 170, - 217, - 95, - 88, - 5, - 72, - 227, - 229, - 8, - 0, - 61, - 14, - 232, - 68, - 73, - 128, - 218, - 226, - 2, - 171, - 108, - 117, - 74, - 155, - 141, - 66, - 127, - 40, - 242, - 206, - 204, - 220, - 69, - 93, - 0, - 116, - 106, - 252, - 205, - 16, - 101, - 194, - 77, - 210, - 115, - 2, - 155, - 85, - 226, - 236, - 70, - 54, - 96, - 175, - 216, - 233, - 42, - 29, - 192, - 111, - 149, - 25, - 41, - 57, - 88, - 104, - 45, - 40, - 45, - 80, - 237, - 247, - 89, - 44, - 111, - 179, - 219, - 132, - 37, - 212, - 5, - 32, - 151, - 53, - 228, - 101, - 68, - 137, - 7, - 67, - 210, - 157, - 33, - 220, - 172, - 206, - 59, - 225, - 9, - 233, - 226, - 220, - 27, - 4, - 192, - 43, - 242, - 179, - 121, - 59, - 248, - 205, - 125, - 122, - 191, - 80, - 169, - 93, - 151, - 36, - 35, - 233, - 1, - 96, - 184, - 182, - 88, - 22, - 162, - 196, - 131, - 33, - 121, - 244, - 18, - 1, - 94, - 245, - 168, - 183, - 114, - 238, - 141, - 229, - 121, - 101, - 137, - 32, - 223, - 254, - 66, - 131, - 124, - 72, - 165, - 233, - 45, - 47, - 201, - 143, - 235, - 124, - 54, - 217, - 69, - 210, - 40, - 241, - 96, - 68, - 166, - 156, - 96, - 177, - 100, - 130, - 39, - 203, - 171, - 223, - 87, - 104, - 65, - 180, - 77, - 108, - 251, - 222, - 162, - 156, - 243, - 193, - 250, - 116, - 93, - 115, - 126, - 89, - 41, - 67, - 30, - 64, - 78, - 181, - 123, - 229, - 50, - 96, - 121, - 125, - 245, - 106, - 69, - 137, - 28, - 234, - 255, - 162, - 76, - 99, - 95, - 250, - 171, - 164, - 234, - 147, - 2, - 0, - 19, - 177, - 228, - 53, - 145, - 73, - 6, - 224, - 101, - 64, - 176, - 3, - 150, - 125, - 207, - 43, - 230, - 139, - 33, - 37, - 176, - 173, - 232, - 96, - 196, - 56, - 99, - 15, - 158, - 241, - 228, - 60, - 65, - 228, - 199, - 254, - 227, - 195, - 215, - 184, - 82, - 162, - 49, - 209, - 38, - 162, - 32, - 66, - 216, - 16, - 10, - 165, - 115, - 150, - 78, - 0, - 64, - 46, - 3, - 33, - 99, - 79, - 80, - 32, - 243, - 170, - 80, - 1, - 0, - 242, - 99, - 31, - 40, - 205, - 176, - 88, - 232, - 181, - 145, - 105, - 6, - 224, - 89, - 0, - 47, - 100, - 238, - 101, - 45, - 12, - 146, - 1, - 153, - 39, - 112, - 240, - 160, - 106, - 72, - 68, - 135, - 240, - 162, - 62, - 102, - 72, - 14, - 0, - 246, - 99, - 231, - 22, - 23, - 103, - 88, - 44, - 244, - 90, - 40, - 235, - 166, - 83, - 50, - 90, - 69, - 138, - 7, - 107, - 233, - 202, - 150, - 207, - 44, - 204, - 62, - 185, - 33, - 116, - 91, - 75, - 66, - 38, - 210, - 97, - 86, - 164, - 7, - 10, - 138, - 179, - 106, - 3, - 57, - 0, - 216, - 143, - 253, - 230, - 3, - 59, - 65, - 4, - 220, - 185, - 45, - 128, - 97, - 134, - 52, - 237, - 207, - 24, - 63, - 147, - 85, - 25, - 10, - 42, - 183, - 191, - 191, - 213, - 194, - 180, - 65, - 52, - 32, - 148, - 73, - 66, - 208, - 151, - 93, - 2, - 152, - 98, - 50, - 105, - 153, - 174, - 204, - 26, - 209, - 200, - 1, - 192, - 126, - 172, - 21, - 199, - 146, - 116, - 49, - 127, - 93, - 78, - 91, - 232, - 102, - 34, - 151, - 176, - 165, - 154, - 72, - 25, - 227, - 231, - 157, - 100, - 184, - 180, - 232, - 179, - 0, - 42, - 149, - 125, - 85, - 244, - 134, - 43, - 11, - 219, - 120, - 71, - 96, - 115, - 46, - 131, - 52, - 165, - 214, - 98, - 3, - 165, - 32, - 0, - 80, - 90, - 92, - 74, - 252, - 88, - 171, - 232, - 72, - 163, - 67, - 78, - 27, - 11, - 101, - 34, - 157, - 85, - 226, - 50, - 199, - 207, - 165, - 100, - 164, - 108, - 219, - 42, - 4, - 192, - 235, - 0, - 192, - 43, - 248, - 227, - 131, - 17, - 33, - 35, - 16, - 50, - 64, - 79, - 127, - 89, - 115, - 128, - 64, - 127, - 205, - 60, - 2, - 192, - 214, - 194, - 173, - 216, - 129, - 122, - 192, - 10, - 29, - 78, - 201, - 239, - 150, - 161, - 77, - 185, - 145, - 182, - 236, - 54, - 75, - 252, - 188, - 138, - 100, - 133, - 62, - 94, - 6, - 0, - 52, - 63, - 255, - 60, - 177, - 3, - 72, - 246, - 249, - 124, - 64, - 200, - 0, - 61, - 155, - 17, - 203, - 10, - 161, - 145, - 102, - 127, - 5, - 166, - 13, - 109, - 69, - 206, - 63, - 19, - 125, - 29, - 114, - 126, - 90, - 210, - 157, - 154, - 153, - 45, - 126, - 94, - 69, - 6, - 140, - 63, - 71, - 181, - 194, - 175, - 255, - 253, - 243, - 207, - 253, - 143, - 182, - 237, - 88, - 249, - 9, - 99, - 2, - 6, - 232, - 217, - 178, - 233, - 61, - 205, - 254, - 10, - 29, - 82, - 122, - 129, - 18, - 15, - 215, - 151, - 156, - 106, - 249, - 199, - 148, - 45, - 126, - 126, - 129, - 232, - 1, - 0, - 32, - 100, - 253, - 230, - 125, - 223, - 188, - 175, - 112, - 47, - 30, - 28, - 108, - 70, - 142, - 0, - 99, - 136, - 94, - 105, - 54, - 165, - 165, - 218, - 95, - 193, - 237, - 38, - 128, - 254, - 1, - 125, - 98, - 151, - 145, - 40, - 81, - 153, - 226, - 103, - 224, - 209, - 90, - 188, - 96, - 153, - 5, - 60, - 223, - 123, - 161, - 253, - 184, - 86, - 54, - 20, - 226, - 213, - 255, - 228, - 6, - 105, - 80, - 180, - 172, - 20, - 61, - 183, - 149, - 38, - 28, - 48, - 217, - 253, - 163, - 178, - 147, - 177, - 253, - 167, - 196, - 131, - 14, - 225, - 33, - 14, - 180, - 58, - 11, - 0, - 240, - 86, - 209, - 235, - 247, - 61, - 143, - 170, - 165, - 101, - 37, - 49, - 212, - 164, - 6, - 105, - 74, - 173, - 74, - 209, - 195, - 46, - 99, - 232, - 15, - 200, - 1, - 59, - 41, - 104, - 191, - 81, - 120, - 65, - 137, - 7, - 29, - 34, - 67, - 28, - 171, - 182, - 33, - 0, - 58, - 75, - 66, - 115, - 81, - 157, - 20, - 174, - 143, - 48, - 115, - 177, - 49, - 65, - 139, - 101, - 204, - 195, - 111, - 8, - 229, - 109, - 235, - 184, - 166, - 106, - 203, - 12, - 180, - 249, - 217, - 165, - 78, - 195, - 60, - 61, - 37, - 30, - 180, - 196, - 47, - 54, - 211, - 118, - 208, - 6, - 0, - 20, - 118, - 70, - 150, - 253, - 61, - 174, - 23, - 55, - 119, - 113, - 38, - 170, - 180, - 74, - 204, - 67, - 91, - 105, - 157, - 142, - 247, - 50, - 62, - 115, - 165, - 81, - 102, - 130, - 199, - 103, - 170, - 190, - 107, - 207, - 184, - 23, - 129, - 17, - 145, - 33, - 142, - 205, - 145, - 182, - 77, - 150, - 208, - 189, - 224, - 252, - 239, - 251, - 251, - 87, - 65, - 6, - 66, - 146, - 164, - 82, - 226, - 65, - 73, - 181, - 217, - 226, - 160, - 90, - 197, - 165, - 106, - 0, - 124, - 29, - 109, - 145, - 80, - 134, - 72, - 83, - 241, - 140, - 217, - 7, - 34, - 161, - 31, - 159, - 181, - 47, - 205, - 184, - 23, - 129, - 1, - 97, - 30, - 245, - 118, - 118, - 108, - 166, - 45, - 86, - 84, - 163, - 17, - 34, - 181, - 130, - 38, - 200, - 109, - 205, - 118, - 6, - 37, - 30, - 20, - 0, - 248, - 80, - 69, - 16, - 95, - 124, - 98, - 242, - 25, - 179, - 236, - 52, - 65, - 250, - 241, - 217, - 165, - 246, - 73, - 68, - 216, - 152, - 71, - 193, - 221, - 67, - 21, - 34, - 88, - 75, - 163, - 106, - 81, - 115, - 53, - 65, - 217, - 13, - 97, - 173, - 21, - 252, - 94, - 178, - 176, - 189, - 0, - 0, - 95, - 130, - 141, - 142, - 222, - 144, - 57, - 0, - 228, - 75, - 66, - 129, - 163, - 137, - 126, - 149, - 86, - 255, - 52, - 238, - 71, - 231, - 210, - 73, - 237, - 69, - 64, - 161, - 26, - 45, - 10, - 215, - 10, - 35, - 66, - 117, - 34, - 160, - 5, - 38, - 81, - 23, - 165, - 67, - 16, - 235, - 240, - 65, - 142, - 8, - 64, - 68, - 70, - 230, - 76, - 162, - 114, - 32, - 146, - 164, - 160, - 11, - 84, - 142, - 56, - 238, - 71, - 143, - 157, - 154, - 204, - 67, - 82, - 72, - 20, - 41, - 17, - 128, - 208, - 125, - 196, - 14, - 92, - 31, - 4, - 68, - 82, - 3, - 224, - 99, - 154, - 59, - 76, - 90, - 68, - 221, - 129, - 200, - 98, - 117, - 240, - 73, - 161, - 209, - 176, - 39, - 38, - 243, - 104, - 181, - 208, - 126, - 247, - 125, - 229, - 229, - 60, - 0, - 145, - 85, - 127, - 253, - 250, - 206, - 95, - 252, - 193, - 0, - 16, - 38, - 33, - 228, - 112, - 169, - 56, - 16, - 153, - 49, - 235, - 64, - 161, - 69, - 26, - 168, - 73, - 60, - 89, - 40, - 178, - 189, - 152, - 46, - 32, - 158, - 32, - 166, - 95, - 204, - 253, - 239, - 175, - 239, - 51, - 249, - 132, - 58, - 27, - 94, - 26, - 144, - 0, - 64, - 155, - 207, - 155, - 43, - 0, - 162, - 54, - 205, - 104, - 119, - 40, - 241, - 144, - 27, - 117, - 130, - 203, - 91, - 252, - 77, - 228, - 10, - 255, - 63, - 107, - 43, - 43, - 245, - 20, - 183, - 135, - 247, - 0, - 0, - 0, - 0, - 73, - 69, - 78, - 68, - 174, - 66, - 96, - 130 -]; +// Will be replaced automatically by GitHub Actions +final landTileBytes = []; diff --git a/tile_server/static/generated/sea.dart b/tile_server/static/generated/sea.dart index 5b3fad18..196bd5af 100644 --- a/tile_server/static/generated/sea.dart +++ b/tile_server/static/generated/sea.dart @@ -1,105 +1,2 @@ -final seaTileBytes = [ - 137, - 80, - 78, - 71, - 13, - 10, - 26, - 10, - 0, - 0, - 0, - 13, - 73, - 72, - 68, - 82, - 0, - 0, - 1, - 0, - 0, - 0, - 1, - 0, - 1, - 3, - 0, - 0, - 0, - 102, - 188, - 58, - 37, - 0, - 0, - 0, - 3, - 80, - 76, - 84, - 69, - 170, - 211, - 223, - 207, - 236, - 188, - 245, - 0, - 0, - 0, - 31, - 73, - 68, - 65, - 84, - 104, - 129, - 237, - 193, - 1, - 13, - 0, - 0, - 0, - 194, - 160, - 247, - 79, - 109, - 14, - 55, - 160, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 190, - 13, - 33, - 0, - 0, - 1, - 154, - 96, - 225, - 213, - 0, - 0, - 0, - 0, - 73, - 69, - 78, - 68, - 174, - 66, - 96, - 130 -]; +// Will be replaced automatically by GitHub Actions +final seaTileBytes = []; From 7de0de3bd75e4f0c3cb522ddd48f08c8f32e517d Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Fri, 28 Jul 2023 13:26:40 +0100 Subject: [PATCH 048/168] Minor update to GitHub workflow Former-commit-id: 1784de14016d9d4a5f244c5f5177f84b3a158902 [formerly 38bdd36e5bd8339dda767c4d2508a3635737c5fd] Former-commit-id: 0a54d8c4e4f9e4f4ca965b031411290786531649 --- tile_server/.gitignore | 3 ++- tile_server/bin/generate_dart_images.dart | 1 + tile_server/static/source/{ => images}/favicon.ico | Bin tile_server/static/source/{ => images}/land.png | Bin tile_server/static/source/{ => images}/sea.png | Bin tile_server/static/source/placeholders/favicon.dart | 2 ++ tile_server/static/source/placeholders/land.dart | 2 ++ tile_server/static/source/placeholders/sea.dart | 2 ++ 8 files changed, 9 insertions(+), 1 deletion(-) rename tile_server/static/source/{ => images}/favicon.ico (100%) rename tile_server/static/source/{ => images}/land.png (100%) rename tile_server/static/source/{ => images}/sea.png (100%) create mode 100644 tile_server/static/source/placeholders/favicon.dart create mode 100644 tile_server/static/source/placeholders/land.dart create mode 100644 tile_server/static/source/placeholders/sea.dart diff --git a/tile_server/.gitignore b/tile_server/.gitignore index ae7e7d36..fc374886 100644 --- a/tile_server/.gitignore +++ b/tile_server/.gitignore @@ -1 +1,2 @@ -bin/tile_server.exe \ No newline at end of file +bin/tile_server.exe +source/placeholders \ No newline at end of file diff --git a/tile_server/bin/generate_dart_images.dart b/tile_server/bin/generate_dart_images.dart index ea517673..723b4857 100644 --- a/tile_server/bin/generate_dart_images.dart +++ b/tile_server/bin/generate_dart_images.dart @@ -27,6 +27,7 @@ void main(List _) { p.join( staticPath, 'source', + 'images', '${staticFile.name}.${staticFile.extension}', ), ); diff --git a/tile_server/static/source/favicon.ico b/tile_server/static/source/images/favicon.ico similarity index 100% rename from tile_server/static/source/favicon.ico rename to tile_server/static/source/images/favicon.ico diff --git a/tile_server/static/source/land.png b/tile_server/static/source/images/land.png similarity index 100% rename from tile_server/static/source/land.png rename to tile_server/static/source/images/land.png diff --git a/tile_server/static/source/sea.png b/tile_server/static/source/images/sea.png similarity index 100% rename from tile_server/static/source/sea.png rename to tile_server/static/source/images/sea.png diff --git a/tile_server/static/source/placeholders/favicon.dart b/tile_server/static/source/placeholders/favicon.dart new file mode 100644 index 00000000..7dd43c1b --- /dev/null +++ b/tile_server/static/source/placeholders/favicon.dart @@ -0,0 +1,2 @@ +// Will be replaced automatically by GitHub Actions +final faviconTileBytes = []; diff --git a/tile_server/static/source/placeholders/land.dart b/tile_server/static/source/placeholders/land.dart new file mode 100644 index 00000000..1029ac5f --- /dev/null +++ b/tile_server/static/source/placeholders/land.dart @@ -0,0 +1,2 @@ +// Will be replaced automatically by GitHub Actions +final landTileBytes = []; diff --git a/tile_server/static/source/placeholders/sea.dart b/tile_server/static/source/placeholders/sea.dart new file mode 100644 index 00000000..196bd5af --- /dev/null +++ b/tile_server/static/source/placeholders/sea.dart @@ -0,0 +1,2 @@ +// Will be replaced automatically by GitHub Actions +final seaTileBytes = []; From 1c2a8f216a9ef84a4ac73ca36991fc9663841bee Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sat, 29 Jul 2023 11:39:21 +0100 Subject: [PATCH 049/168] Updated dependencies Former-commit-id: 8f6159964facf0bf888e5cc95594804da7468f1f [formerly b63cd1bf7d3570fad4b8212ba6de8bb95484ce8f] Former-commit-id: 2c83347f63b66fcf9ded7b8a6d5f17504849de40 --- example/pubspec.yaml | 8 ++------ pubspec.yaml | 4 +--- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 17bd609f..8a5c1a77 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -16,7 +16,7 @@ dependencies: flutter: sdk: flutter flutter_foreground_task: ^6.0.0+1 - flutter_map: ^5.0.0 + flutter_map: ^6.0.0-dev.2 flutter_map_location_marker: ^7.0.1 flutter_map_tile_caching: flutter_speed_dial: ^7.0.0 @@ -26,7 +26,7 @@ dependencies: http: ^1.0.0 intl: ^0.18.0 latlong2: ^0.9.0 - osm_nominatim: ^2.0.1 + osm_nominatim: ^3.0.0 path: ^1.8.3 provider: ^6.0.3 stream_transform: ^2.0.0 @@ -34,16 +34,12 @@ dependencies: version: ^3.0.2 dependency_overrides: - flutter_map: - git: - url: https://github.com/fleaflet/flutter_map.git flutter_map_location_marker: git: url: https://github.com/JaffaKetchup/_fmlm ref: 'fm-v6' flutter_map_tile_caching: path: ../ - http: ^1.0.0 flutter: uses-material-design: true diff --git a/pubspec.yaml b/pubspec.yaml index 97fe7787..d9753982 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,9 +30,7 @@ dependencies: collection: ^1.16.0 flutter: sdk: flutter - flutter_map: - git: - url: https://github.com/fleaflet/flutter_map.git + flutter_map: ^6.0.0-dev.2 http: ^1.1.0 isar: ^3.1.0+1 isar_flutter_libs: ^3.1.0+1 From 865ee7b39f36ba11dfc2efcfeba0b4f468a1c064 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sat, 29 Jul 2023 14:46:28 +0100 Subject: [PATCH 050/168] Reduced code duplication Former-commit-id: b66797bf5845f2c7c4e27f5bc078ae7737986c25 [formerly 35d708c32b32f8e20e8921ee2018ab0d784337b2] Former-commit-id: 9883a7c077b1d8bc999c7f6d9b6209d2587046fb --- lib/src/bulk_download/manager.dart | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/lib/src/bulk_download/manager.dart b/lib/src/bulk_download/manager.dart index 20e0543c..537a2185 100644 --- a/lib/src/bulk_download/manager.dart +++ b/lib/src/bulk_download/manager.dart @@ -122,11 +122,12 @@ Future _downloadManager( } // Setup cancel, pause, and resume handling - final threadPausedStates = List.generate( - input.parallelThreads, - (_) => Completer(), - growable: false, - ); + List> generateThreadPausedStates() => List.generate( + input.parallelThreads, + (_) => Completer(), + growable: false, + ); + final threadPausedStates = generateThreadPausedStates(); final cancelSignal = Completer(); var pauseResumeSignal = Completer()..complete(); rootRecievePort.listen( @@ -138,9 +139,7 @@ Future _downloadManager( } on StateError {} } else if (e == 1) { pauseResumeSignal = Completer(); - for (int i = 0; i < input.parallelThreads; i++) { - threadPausedStates[i] = Completer(); - } + threadPausedStates.setAll(0, generateThreadPausedStates()); await Future.wait(threadPausedStates.map((e) => e.future)); downloadDuration.stop(); send(1); From cd5d90160ef5f2ed1a1017ebede3e3463829ec0a Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sun, 30 Jul 2023 16:42:45 +0100 Subject: [PATCH 051/168] Added earcutting triangulation algorithm Fixed bugs in example application Former-commit-id: 166b91c1829fbb33d3122d03af300a734c7c658a [formerly ecd3898acd73fc672e74d9a2b5e0489247f5491d] Former-commit-id: 2870baa6eb1ee764b07120f87d7ce6cc00dc1231 --- .../pages/downloader/components/map_view.dart | 4 +- lib/src/misc/earcut.dart | 839 ++++++++++++++++++ ..._tile_caching_test.dart => fmtc_test.dart} | 79 ++ 3 files changed, 921 insertions(+), 1 deletion(-) create mode 100644 lib/src/misc/earcut.dart rename test/{flutter_map_tile_caching_test.dart => fmtc_test.dart} (60%) diff --git a/example/lib/screens/main/pages/downloader/components/map_view.dart b/example/lib/screens/main/pages/downloader/components/map_view.dart index 82630534..72aeabee 100644 --- a/example/lib/screens/main/pages/downloader/components/map_view.dart +++ b/example/lib/screens/main/pages/downloader/components/map_view.dart @@ -268,7 +268,9 @@ class _MapViewState extends State { const VerticalDivider(), IconButton( onPressed: () async { - await FilePicker.platform.clearTemporaryFiles(); + if (Platform.isAndroid || Platform.isIOS) { + await FilePicker.platform.clearTemporaryFiles(); + } late final FilePickerResult? result; try { diff --git a/lib/src/misc/earcut.dart b/lib/src/misc/earcut.dart new file mode 100644 index 00000000..cf2efac8 --- /dev/null +++ b/lib/src/misc/earcut.dart @@ -0,0 +1,839 @@ +// ignore_for_file: parameter_assignments + +import 'dart:math'; + +/// Earcutting triangulation algorithm, ported (with minor API differences) from +/// [earcut4j/earcut4j](https://github.com/earcut4j/earcut4j) which itself is +/// ported from [mapbox/earcut](https://github.com/mapbox/earcut). +final class Earcut { + /// Triangulates the given polygon + /// + /// [polygonVertices] should be a list of all the [Point]s defining + /// the polygon. + /// + /// [holeIndices] should be a list of hole indicies, if any. For example, + /// `[5, 8]` for a 12-vertice input would mean one hole with vertices 5-7 and + /// another with 8-11. + /// + /// Returns a list of vertice indicies where a group of 3 forms a triangle. + static List triangulateFromPoints( + List> polygonVertices, { + List? holeIndices, + }) => + triangulateRaw( + polygonVertices.map((e) => [e.x, e.y]).expand((e) => e).toList(), + holeIndices: holeIndices, + ); + + /// Triangulates the given polygon + /// + /// [polygonVertices] should be a flat list of all the coordinates defining + /// the polygon. If [dimensions] is 2, it is expected to be in the format + /// `[x0, y0, x1, y1, x2, y2]`. If [dimensions] is 3, it is expected to be in + /// the format `[x0, y0, z0, x1, y1, z1, x2, y2, z2]`. + /// + /// [holeIndices] should be a list of hole indicies, if any. For example, + /// `[5, 8]` for a 12-vertice input would mean one hole with vertices 5-7 and + /// another with 8-11. + /// + /// Returns a list of vertice indicies where a group of 3 forms a triangle. + static List triangulateRaw( + List polygonVertices, { + List? holeIndices, + int dimensions = 2, + }) { + final bool hasHoles = holeIndices != null && holeIndices.isNotEmpty; + final int outerLen = + hasHoles ? holeIndices[0] * dimensions : polygonVertices.length; + + _Node? outerNode = + _linkedList(polygonVertices, 0, outerLen, dimensions, clockwise: true); + + final triangles = []; + + if (outerNode == null || outerNode.next == outerNode.prev) { + return triangles; + } + + double minX = 0; + double minY = 0; + double maxX = 0; + double maxY = 0; + double invSize = 4.9E-324; + + if (hasHoles) { + outerNode = + _eliminateHoles(polygonVertices, holeIndices, outerNode, dimensions); + } + + // if the shape is not too simple, we'll use z-order curve hash later; + // calculate polygon bbox + if (polygonVertices.length > 80 * dimensions) { + minX = maxX = polygonVertices[0]; + minY = maxY = polygonVertices[1]; + + for (int i = dimensions; i < outerLen; i += dimensions) { + final double x = polygonVertices[i]; + final double y = polygonVertices[i + 1]; + if (x < minX) { + minX = x; + } + if (y < minY) { + minY = y; + } + if (x > maxX) { + maxX = x; + } + if (y > maxY) { + maxY = y; + } + } + + // minX, minY and size are later used to transform coords into + // ints for z-order calculation + invSize = max(maxX - minX, maxY - minY); + invSize = invSize != 0.0 ? 1.0 / invSize : 0.0; + } + + _earcutLinked( + outerNode, + triangles, + dimensions, + minX, + minY, + invSize, + -2147483648, + ); + + return triangles; + } + + static void _earcutLinked( + _Node? ear, + List triangles, + int dim, + double minX, + double minY, + double invSize, + int pass, + ) { + if (ear == null) return; + + // interlink polygon nodes in z-order + if (pass == -2147483648 && invSize != 4.9E-324) { + _indexCurve(ear, minX, minY, invSize); + } + + _Node? stop = ear; + + // iterate through ears, slicing them one by one + while (ear!.prev != ear.next) { + final prev = ear.prev; + final next = ear.next; + + if (invSize != 4.9E-324 + ? _isEarHashed(ear, minX, minY, invSize) + : _isEar(ear)) { + // cut off the triangle + triangles + ..add(prev!.i ~/ dim) + ..add(ear.i ~/ dim) + ..add(next!.i ~/ dim); + + _removeNode(ear); + + // skipping the next vertice leads to less sliver triangles + ear = next.next; + stop = next.next; + + continue; + } + + ear = next; + + // if we looped through the whole remaining polygon and can't find + // any more ears + if (ear == stop) { + // try filtering points and slicing again + if (pass == -2147483648) { + _earcutLinked( + _filterPoints(ear, null), + triangles, + dim, + minX, + minY, + invSize, + 1, + ); + + // if this didn't work, try curing all small + // self-intersections locally + } else if (pass == 1) { + ear = _cureLocalIntersections( + _filterPoints(ear, null)!, + triangles, + dim, + ); + _earcutLinked(ear, triangles, dim, minX, minY, invSize, 2); + + // as a last resort, try splitting the remaining polygon + // into two + } else if (pass == 2) { + _splitEarcut(ear!, triangles, dim, minX, minY, invSize); + } + + break; + } + } + } + + static void _splitEarcut( + _Node start, + List triangles, + int dim, + double minX, + double minY, + double size, + ) { + // look for a valid diagonal that divides the polygon into two + _Node a = start; + do { + _Node? b = a.next!.next; + while (b != a.prev) { + if (a.i != b!.i && _isValidDiagonal(a, b)) { + // split the polygon in two by the diagonal + _Node c = _splitPolygon(a, b); + + // filter colinear points around the cuts + a = _filterPoints(a, a.next)!; + c = _filterPoints(c, c.next)!; + + // run earcut on each half + _earcutLinked(a, triangles, dim, minX, minY, size, -2147483648); + _earcutLinked(c, triangles, dim, minX, minY, size, -2147483648); + return; + } + b = b.next; + } + a = a.next!; + } while (a != start); + } + + static bool _isValidDiagonal(_Node a, _Node b) => + a.next!.i != b.i && + a.prev!.i != b.i && + !_intersectsPolygon(a, b) && // dones't intersect other edges + (_locallyInside(a, b) && + _locallyInside(b, a) && + _middleInside(a, b) && // locally visible + (_area(a.prev!, a, b.prev!) != 0 || + _area(a, b.prev!, b) != + 0) || // does not create opposite-facing sectors + _equals(a, b) && + _area(a.prev!, a, a.next!) > 0 && + _area(b.prev!, b, b.next!) > 0); // special zero-length case + + static bool _middleInside(_Node a, _Node b) { + _Node p = a; + bool inside = false; + final px = (a.x + b.x) / 2; + final py = (a.y + b.y) / 2; + do { + if (((p.y > py) != (p.next!.y > py)) && + (px < (p.next!.x - p.x) * (py - p.y) / (p.next!.y - p.y) + p.x)) { + inside = !inside; + } + p = p.next!; + } while (p != a); + + return inside; + } + + static bool _intersectsPolygon(_Node a, _Node b) { + _Node p = a; + do { + if (p.i != a.i && + p.next!.i != a.i && + p.i != b.i && + p.next!.i != b.i && + _intersects(p, p.next, a, b)) { + return true; + } + p = p.next!; + } while (p != a); + + return false; + } + + static bool _intersects(_Node p1, _Node? q1, _Node p2, _Node? q2) { + if ((_equals(p1, p2) && _equals(q1!, q2!)) || + (_equals(p1, q2!) && _equals(p2, q1!))) { + return true; + } + final o1 = _sign(_area(p1, q1!, p2)); + final o2 = _sign(_area(p1, q1, q2)); + final o3 = _sign(_area(p2, q2, p1)); + final o4 = _sign(_area(p2, q2, q1)); + + if (o1 != o2 && o3 != o4) { + return true; // general case + } + + if (o1 == 0 && _onSegment(p1, p2, q1)) { + return true; // p1, q1 and p2 are collinear and p2 lies on p1q1 + } + if (o2 == 0 && _onSegment(p1, q2, q1)) { + return true; // p1, q1 and q2 are collinear and q2 lies on p1q1 + } + if (o3 == 0 && _onSegment(p2, p1, q2)) { + return true; // p2, q2 and p1 are collinear and p1 lies on p2q2 + } + if (o4 == 0 && _onSegment(p2, q1, q2)) { + return true; // p2, q2 and q1 are collinear and q1 lies on p2q2 + } + + return false; + } + + // for collinear points p, q, r, check if point q lies on segment pr + static bool _onSegment(_Node p, _Node q, _Node r) => + q.x <= max(p.x, r.x) && + q.x >= min(p.x, r.x) && + q.y <= max(p.y, r.y) && + q.y >= min(p.y, r.y); + + static double _sign(double num) => num > 0 + ? 1 + : num < 0 + ? -1 + : 0; + + static _Node? _cureLocalIntersections( + _Node start, + List triangles, + int dim, + ) { + _Node p = start; + do { + final _Node? a = p.prev; + final b = p.next!.next; + + if (!_equals(a!, b!) && + _intersects(a, p, p.next!, b) && + _locallyInside(a, b) && + _locallyInside(b, a)) { + triangles + ..add(a.i ~/ dim) + ..add(p.i ~/ dim) + ..add(b.i ~/ dim); + + // remove two nodes involved + _removeNode(p); + _removeNode(p.next!); + + p = start = b; + } + p = p.next!; + } while (p != start); + + return _filterPoints(p, null); + } + + static bool _isEar(_Node ear) { + final a = ear.prev; + final b = ear; + final c = ear.next; + + if (_area(a!, b, c!) >= 0) { + return false; // reflex, can't be an ear + } + + // now make sure we don't have other points inside the potential ear + _Node? p = ear.next!.next; + + while (p != ear.prev) { + if (_pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, p!.x, p.y) && + _area(p.prev!, p, p.next!) >= 0) { + return false; + } + p = p.next; + } + + return true; + } + + static bool _isEarHashed( + _Node ear, + double minX, + double minY, + double invSize, + ) { + final a = ear.prev!; + final b = ear; + final c = ear.next!; + + if (_area(a, b, c) >= 0) { + return false; // reflex, can't be an ear + } + + // triangle bbox; min & max are calculated like this for speed + + final minTX = a.x < b.x ? (a.x < c.x ? a.x : c.x) : (b.x < c.x ? b.x : c.x); + final minTY = a.y < b.y ? (a.y < c.y ? a.y : c.y) : (b.y < c.y ? b.y : c.y); + final maxTX = a.x > b.x ? (a.x > c.x ? a.x : c.x) : (b.x > c.x ? b.x : c.x); + final maxTY = a.y > b.y ? (a.y > c.y ? a.y : c.y) : (b.y > c.y ? b.y : c.y); + + // z-order range for the current triangle bbox; + final minZ = _zOrder(minTX, minTY, minX, minY, invSize); + final maxZ = _zOrder(maxTX, maxTY, minX, minY, invSize); + + // first look for points inside the triangle in increasing z-order + _Node? p = ear.prevZ; + _Node? n = ear.nextZ; + + while (p != null && p.z >= minZ && n != null && n.z <= maxZ) { + if (p != ear.prev && + p != ear.next && + _pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y) && + _area(p.prev!, p, p.next!) >= 0) return false; + p = p.prevZ; + + if (n != ear.prev && + n != ear.next && + _pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, n.x, n.y) && + _area(n.prev!, n, n.next!) >= 0) return false; + n = n.nextZ; + } + + // look for remaining points in decreasing z-order + while (p != null && p.z >= minZ) { + if (p != ear.prev && + p != ear.next && + _pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y) && + _area(p.prev!, p, p.next!) >= 0) return false; + p = p.prevZ; + } + + // look for remaining points in increasing z-order + while (n != null && n.z <= maxZ) { + if (n != ear.prev && + n != ear.next && + _pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, n.x, n.y) && + _area(n.prev!, n, n.next!) >= 0) return false; + n = n.nextZ; + } + + return true; + } + + // z-order of a point given coords and inverse of the longer side of data bbox + static int _zOrder( + double x, + double y, + double minX, + double minY, + double invSize, + ) { + // coords are transformed into non-negative 15-bit int range + int lx = (32767 * (x - minX) * invSize).toInt(); + int ly = (32767 * (y - minY) * invSize).toInt(); + + lx = (lx | (lx << 8)) & 0x00FF00FF; + lx = (lx | (lx << 4)) & 0x0F0F0F0F; + lx = (lx | (lx << 2)) & 0x33333333; + lx = (lx | (lx << 1)) & 0x55555555; + + ly = (ly | (ly << 8)) & 0x00FF00FF; + ly = (ly | (ly << 4)) & 0x0F0F0F0F; + ly = (ly | (ly << 2)) & 0x33333333; + ly = (ly | (ly << 1)) & 0x55555555; + + return lx | (ly << 1); + } + + static void _indexCurve( + _Node start, + double minX, + double minY, + double invSize, + ) { + _Node p = start; + do { + if (p.z == 4.9E-324) { + p.z = _zOrder(p.x, p.y, minX, minY, invSize).toDouble(); + } + p + ..prevZ = p.prev + ..nextZ = p.next; + p = p.next!; + } while (p != start); + + p.prevZ!.nextZ = null; + p.prevZ = null; + + _sortLinked(p); + } + + static _Node? _sortLinked(_Node? list) { + int inSize = 1; + + int numMerges; + do { + _Node? p = list; + list = null; + _Node? tail; + numMerges = 0; + + while (p != null) { + numMerges++; + _Node? q = p; + int pSize = 0; + for (int i = 0; i < inSize; i++) { + pSize++; + q = q?.nextZ; + if (q == null) { + break; + } + } + + int qSize = inSize; + + while (pSize > 0 || (qSize > 0 && q != null)) { + _Node? e; + if (pSize == 0) { + e = q; + q = q!.nextZ; + qSize--; + } else if (qSize == 0 || q == null) { + e = p; + p = p!.nextZ; + pSize--; + } else if (p!.z <= q.z) { + e = p; + p = p.nextZ; + pSize--; + } else { + e = q; + q = q.nextZ; + qSize--; + } + + if (tail != null) { + tail.nextZ = e; + } else { + list = e; + } + + e!.prevZ = tail; + tail = e; + } + + p = q; + } + + tail!.nextZ = null; + inSize *= 2; + } while (numMerges > 1); + + return list; + } + + static _Node? _eliminateHoles( + List data, + List holeIndices, + _Node outerNode, + int dim, + ) { + final queue = <_Node>[]; + + final int len = holeIndices.length; + for (int i = 0; i < len; i++) { + final int start = holeIndices[i] * dim; + final int end = i < len - 1 ? holeIndices[i + 1] * dim : data.length; + final _Node? list = _linkedList(data, start, end, dim, clockwise: false); + if (list == list?.next) list?.steiner = true; + queue.add(_getLeftmost(list!)); + } + + queue.sort((a, b) { + if (a.x - b.x > 0) { + return 1; + } else if (a.x - b.x < 0) { + return -1; + } + return 0; + }); + + for (final _Node node in queue) { + _eliminateHole(node, outerNode); + outerNode = _filterPoints(outerNode, outerNode.next)!; + } + + return outerNode; + } + + static _Node? _filterPoints(_Node? start, _Node? end) { + if (start == null) return start; + end ??= start; + + _Node p = start; + bool again; + + do { + again = false; + + if (!p.steiner && _equals(p, p.next!) || + _area(p.prev!, p, p.next!) == 0) { + _removeNode(p); + p = (end = p.prev)!; + if (p == p.next) { + break; + } + again = true; + } else { + p = p.next!; + } + } while (again || p != end); + + return end; + } + + static bool _equals(_Node p1, _Node p2) => p1.x == p2.x && p1.y == p2.y; + + static double _area(_Node p, _Node q, _Node r) => + (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y); + + static void _eliminateHole(_Node hole, _Node? outerNode) { + outerNode = _findHoleBridge(hole, outerNode!); + if (outerNode != null) { + final _Node b = _splitPolygon(outerNode, hole); + + // filter collinear points around the cuts + _filterPoints(outerNode, outerNode.next); + _filterPoints(b, b.next); + } + } + + static _Node _splitPolygon(_Node a, _Node b) { + final a2 = _Node(a.i, a.x, a.y); + final b2 = _Node(b.i, b.x, b.y); + final an = a.next!; + final bp = b.prev!; + + a.next = b; + b.prev = a; + + a2.next = an; + an.prev = a2; + + b2.next = a2; + a2.prev = b2; + + bp.next = b2; + b2.prev = bp; + + return b2; + } + + // David Eberly's algorithm for finding a bridge between hole and outer + // polygon + static _Node? _findHoleBridge(_Node hole, _Node outerNode) { + _Node p = outerNode; + final double hx = hole.x; + final double hy = hole.y; + double qx = -1.7976931348623157E308; + _Node? m; + + // find a segment intersected by a ray from the hole's leftmost point to + // the left; + // segment's endpoint with lesser x will be potential connection point + do { + if (hy <= p.y && hy >= p.next!.y) { + final double x = + p.x + (hy - p.y) * (p.next!.x - p.x) / (p.next!.y - p.y); + if (x <= hx && x > qx) { + qx = x; + if (x == hx) { + if (hy == p.y) { + return p; + } + if (hy == p.next!.y) { + return p.next; + } + } + m = p.x < p.next!.x ? p : p.next; + } + } + p = p.next!; + } while (p != outerNode); + + if (m == null) { + return null; + } + + if (hx == qx) { + return m; // hole touches outer segment; pick leftmost endpoint + } + + // look for points inside the triangle of hole point, segment + // intersection and endpoint; + // if there are no points found, we have a valid connection; + // otherwise choose the point of the minimum angle with the ray as + // connection point + + final _Node stop = m; + final double mx = m.x; + final double my = m.y; + double tanMin = 1.7976931348623157E308; + double tan; + + p = m; + + do { + if (hx >= p.x && + p.x >= mx && + _pointInTriangle( + hy < my ? hx : qx, + hy, + mx, + my, + hy < my ? qx : hx, + hy, + p.x, + p.y, + )) { + tan = (hy - p.y).abs() / (hx - p.x); // tangential + + if (_locallyInside(p, hole) && + (tan < tanMin || + (tan == tanMin && + (p.x > m!.x || + (p.x == m.x && _sectorContainsSector(m, p)))))) { + m = p; + tanMin = tan; + } + } + + p = p.next!; + } while (p != stop); + + return m; + } + + static bool _locallyInside(_Node a, _Node? b) => + _area(a.prev!, a, a.next!) < 0 + ? _area(a, b!, a.next!) >= 0 && _area(a, a.prev!, b) >= 0 + : _area(a, b!, a.prev!) < 0 || _area(a, a.next!, b) < 0; + + // whether sector in vertex m contains sector in vertex p in the same + // coordinates + static bool _sectorContainsSector(_Node m, _Node p) => + _area(m.prev!, m, p.prev!) < 0 && _area(p.next!, m, m.next!) < 0; + + static bool _pointInTriangle( + double ax, + double ay, + double bx, + double by, + double cx, + double cy, + double px, + double py, + ) => + (cx - px) * (ay - py) - (ax - px) * (cy - py) >= 0 && + (ax - px) * (by - py) - (bx - px) * (ay - py) >= 0 && + (bx - px) * (cy - py) - (cx - px) * (by - py) >= 0; + + static _Node _getLeftmost(_Node start) { + _Node p = start; + _Node leftmost = start; + do { + if (p.x < leftmost.x || (p.x == leftmost.x && p.y < leftmost.y)) { + leftmost = p; + } + p = p.next!; + } while (p != start); + return leftmost; + } + + static _Node? _linkedList( + List data, + int start, + int end, + int dim, { + required bool clockwise, + }) { + _Node? last; + if (clockwise == (_signedArea(data, start, end, dim) > 0)) { + for (int i = start; i < end; i += dim) { + last = _insertNode(i, data[i], data[i + 1], last); + } + } else { + for (int i = end - dim; i >= start; i -= dim) { + last = _insertNode(i, data[i], data[i + 1], last); + } + } + + if (last != null && _equals(last, last.next!)) { + _removeNode(last); + last = last.next; + } + + return last; + } + + static void _removeNode(_Node p) { + p.next!.prev = p.prev; + p.prev!.next = p.next; + + p.prevZ?.nextZ = p.nextZ; + + p.nextZ?.prevZ = p.prevZ; + } + + static _Node _insertNode(int i, double x, double y, _Node? last) { + final _Node p = _Node(i, x, y); + + if (last == null) { + p + ..prev = p + ..next = p; + } else { + p + ..next = last.next + ..prev = last; + last.next!.prev = p; + last.next = p; + } + return p; + } + + static double _signedArea(List data, int start, int end, int dim) { + double sum = 0; + int j = end - dim; + for (int i = start; i < end; i += dim) { + sum += (data[j] - data[i]) * (data[i + 1] + data[j + 1]); + j = i; + } + return sum; + } +} + +class _Node { + int i; + double x; + double y; + double z; + bool steiner; + _Node? prev; + _Node? next; + _Node? prevZ; + _Node? nextZ; + + _Node(this.i, this.x, this.y) + : z = 4.9E-324, + steiner = false; + + @override + String toString() => 'i: $i, x: $x, y: $y, prev: $prev, next: $next'; +} diff --git a/test/flutter_map_tile_caching_test.dart b/test/fmtc_test.dart similarity index 60% rename from test/flutter_map_tile_caching_test.dart rename to test/fmtc_test.dart index 2a321629..d8ec8a21 100644 --- a/test/flutter_map_tile_caching_test.dart +++ b/test/fmtc_test.dart @@ -4,6 +4,7 @@ import 'package:collection/collection.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; import 'package:flutter_map_tile_caching/src/bulk_download/tile_loops/shared.dart'; +import 'package:flutter_map_tile_caching/src/misc/earcut.dart'; import 'package:latlong2/latlong.dart'; import 'package:test/test.dart'; @@ -83,4 +84,82 @@ void main() { ), ); }); + + group('Test Earcutting Triangulation', () { + test( + 'Simple Triangle', + () => expect(Earcut.triangulateRaw([0, 0, 0, 50, 50, 00]), [1, 0, 2]), + ); + + test( + 'Complex Triangle', + () => expect( + Earcut.triangulateRaw([0, 0, 0, 25, 0, 50, 25, 25, 50, 0, 25, 0]), + [1, 0, 5, 5, 4, 3, 3, 2, 1, 1, 5, 3], + ), + ); + + test( + 'L Shape', + () => expect( + Earcut.triangulateRaw([0, 0, 10, 0, 10, 5, 5, 5, 5, 15, 0, 15]), + [4, 5, 0, 0, 1, 2, 3, 4, 0, 0, 2, 3], + ), + ); + + test( + 'Simple Polygon', + () => expect( + Earcut.triangulateRaw([10, 0, 0, 50, 60, 60, 70, 10]), + [1, 0, 3, 3, 2, 1], + ), + ); + + test( + 'Polygon With Hole', + () => expect( + Earcut.triangulateRaw( + [0, 0, 100, 0, 100, 100, 0, 100, 20, 20, 80, 20, 80, 80, 20, 80], + holeIndices: [4], + ), + [ + 3, + 0, + 4, + 5, + 4, + 0, + 3, + 4, + 7, + 5, + 0, + 1, + 2, + 3, + 7, + 6, + 5, + 1, + 2, + 7, + 6, + 6, + 1, + 2 + ], + ), + ); + + test( + 'Polygon With 3D Coords', + () => expect( + Earcut.triangulateRaw( + [10, 0, 1, 0, 50, 2, 60, 60, 3, 70, 10, 4], + dimensions: 3, + ), + [1, 0, 3, 3, 2, 1], + ), + ); + }); } From 908431dd4b9109ed39c933135162a703d363bc85 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sun, 30 Jul 2023 16:52:47 +0100 Subject: [PATCH 052/168] =?UTF-8?q?Added=20GPL=20notice=20to=20top=20of=20?= =?UTF-8?q?files=20Readded=20Bresenham=E2=80=99s=20Line=20Generation=20Alg?= =?UTF-8?q?orithm?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Former-commit-id: debedca318b24a7cdb5275b74bb98bb768c7897f [formerly 2733b478a1365663987d0c394eeead7a755378d9] Former-commit-id: ac045a831e7aa9432bd2d929016354691e04f432 --- .../tile_loops/custom_polygon_tools/bres.dart | 50 +++++++++++++++++++ .../custom_polygon_tools}/earcut.dart | 3 ++ test/fmtc_test.dart | 5 +- tile_server/bin/generate_dart_images.dart | 3 ++ 4 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 lib/src/bulk_download/tile_loops/custom_polygon_tools/bres.dart rename lib/src/{misc => bulk_download/tile_loops/custom_polygon_tools}/earcut.dart (99%) diff --git a/lib/src/bulk_download/tile_loops/custom_polygon_tools/bres.dart b/lib/src/bulk_download/tile_loops/custom_polygon_tools/bres.dart new file mode 100644 index 00000000..94e854b4 --- /dev/null +++ b/lib/src/bulk_download/tile_loops/custom_polygon_tools/bres.dart @@ -0,0 +1,50 @@ +import 'dart:math'; + +/// Bresenham’s Line Generation Algorithm +Iterable> bresenhamLGA(Point start, Point end) sync* { + var x1 = start.x; + var x2 = end.x; + var y1 = start.y; + var y2 = end.y; + + var x = x1; + var y = y1; + + var dx = (x2 - x1).abs(); + var dy = (y2 - y1).abs(); + + if (dy / dx > 1) { + final intermediateDx = dx; + dx = dy; + dy = intermediateDx; + + final intermediateX = x; + x = y; + y = intermediateX; + + final intermediateX1 = x1; + x1 = y1; + y1 = intermediateX1; + + final intermediateX2 = x2; + x2 = y2; + y2 = intermediateX2; + } + + var p = 2 * dy - dx; + + yield Point(x, y); + + for (int k = 2; k < dx + 2; k++) { + if (p > 0) { + y += y < y2 ? 1 : -1; + p += 2 * (dy - dx); + } else { + p += 2 * dy; + } + + x += x < x2 ? 1 : -1; + + yield Point(x, y); + } +} diff --git a/lib/src/misc/earcut.dart b/lib/src/bulk_download/tile_loops/custom_polygon_tools/earcut.dart similarity index 99% rename from lib/src/misc/earcut.dart rename to lib/src/bulk_download/tile_loops/custom_polygon_tools/earcut.dart index cf2efac8..e02604ff 100644 --- a/lib/src/misc/earcut.dart +++ b/lib/src/bulk_download/tile_loops/custom_polygon_tools/earcut.dart @@ -1,3 +1,6 @@ +// Copyright © Luka S (JaffaKetchup) under GPL-v3 +// A full license can be found at .\LICENSE + // ignore_for_file: parameter_assignments import 'dart:math'; diff --git a/test/fmtc_test.dart b/test/fmtc_test.dart index d8ec8a21..6002296d 100644 --- a/test/fmtc_test.dart +++ b/test/fmtc_test.dart @@ -1,10 +1,13 @@ +// Copyright © Luka S (JaffaKetchup) under GPL-v3 +// A full license can be found at .\LICENSE + // ignore_for_file: avoid_print import 'package:collection/collection.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; +import 'package:flutter_map_tile_caching/src/bulk_download/tile_loops/custom_polygon_tools/earcut.dart'; import 'package:flutter_map_tile_caching/src/bulk_download/tile_loops/shared.dart'; -import 'package:flutter_map_tile_caching/src/misc/earcut.dart'; import 'package:latlong2/latlong.dart'; import 'package:test/test.dart'; diff --git a/tile_server/bin/generate_dart_images.dart b/tile_server/bin/generate_dart_images.dart index 723b4857..0a5c8fde 100644 --- a/tile_server/bin/generate_dart_images.dart +++ b/tile_server/bin/generate_dart_images.dart @@ -1,3 +1,6 @@ +// Copyright © Luka S (JaffaKetchup) under GPL-v3 +// A full license can be found at .\LICENSE + import 'dart:io'; import 'package:path/path.dart' as p; From f3b71223beef46db301cdad76c9a6f5ba9f64693 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Mon, 31 Jul 2023 23:15:40 +0100 Subject: [PATCH 053/168] Implemented `CustomPolygonRegion` Former-commit-id: 7100f0eed03a0c90919da9e0f7db04cf43cdf6d7 [formerly 8054ca488ed4d726e828ccd0a1bb55ffecf94fae] Former-commit-id: ac44b4a018ac8902286d89f7203f526e3530de98 --- .../components/region_information.dart | 1 + lib/flutter_map_tile_caching.dart | 1 + lib/src/bulk_download/manager.dart | 2 + lib/src/bulk_download/tile_loops/count.dart | 56 +++++++++++++ .../tile_loops/custom_polygon_tools/bres.dart | 47 ++++++++++- .../custom_polygon_tools/earcut.dart | 6 +- .../bulk_download/tile_loops/generate.dart | 62 ++++++++++++++ lib/src/bulk_download/tile_loops/shared.dart | 2 + lib/src/regions/base_region.dart | 2 + lib/src/regions/custom_polygon.dart | 82 +++++++++++++++++++ lib/src/regions/downloadable_region.dart | 3 + lib/src/root/recovery.dart | 1 + lib/src/store/download.dart | 1 + test/fmtc_test.dart | 30 +++++++ 14 files changed, 289 insertions(+), 7 deletions(-) create mode 100644 lib/src/regions/custom_polygon.dart diff --git a/example/lib/screens/download_region/components/region_information.dart b/example/lib/screens/download_region/components/region_information.dart index 0cdae024..43840dcd 100644 --- a/example/lib/screens/download_region/components/region_information.dart +++ b/example/lib/screens/download_region/components/region_information.dart @@ -101,6 +101,7 @@ class RegionInformation extends StatelessWidget { ), ]; }, + customPolygon: (_) => throw UnimplementedError(), ), const SizedBox(height: 10), const Text('MIN/MAX ZOOM LEVELS'), diff --git a/lib/flutter_map_tile_caching.dart b/lib/flutter_map_tile_caching.dart index 31e0246e..94c4ce05 100644 --- a/lib/flutter_map_tile_caching.dart +++ b/lib/flutter_map_tile_caching.dart @@ -68,6 +68,7 @@ part 'src/misc/store_db_impl.dart'; part 'src/providers/tile_provider.dart'; part 'src/regions/base_region.dart'; part 'src/regions/circle.dart'; +part 'src/regions/custom_polygon.dart'; part 'src/regions/downloadable_region.dart'; part 'src/regions/line.dart'; part 'src/regions/recovered_region.dart'; diff --git a/lib/src/bulk_download/manager.dart b/lib/src/bulk_download/manager.dart index 537a2185..750d18d2 100644 --- a/lib/src/bulk_download/manager.dart +++ b/lib/src/bulk_download/manager.dart @@ -35,6 +35,7 @@ Future _downloadManager( rectangle: TilesCounter.rectangleTiles, circle: TilesCounter.circleTiles, line: TilesCounter.lineTiles, + customPolygon: TilesCounter.customPolygonTiles, ); // Setup sea tile removal system @@ -72,6 +73,7 @@ Future _downloadManager( rectangle: (_) => TilesGenerator.rectangleTiles, circle: (_) => TilesGenerator.circleTiles, line: (_) => TilesGenerator.lineTiles, + customPolygon: (_) => throw UnimplementedError(), ), (sendPort: tileRecievePort.sendPort, region: input.region), onExit: tileRecievePort.sendPort, diff --git a/lib/src/bulk_download/tile_loops/count.dart b/lib/src/bulk_download/tile_loops/count.dart index 9234ecb3..cbbf5315 100644 --- a/lib/src/bulk_download/tile_loops/count.dart +++ b/lib/src/bulk_download/tile_loops/count.dart @@ -225,4 +225,60 @@ class TilesCounter { return numberOfTiles; } + + static int customPolygonTiles(DownloadableRegion region) { + region as DownloadableRegion; + + final customPolygonOutline = region.originalRegion.toOutline(); + + int numberOfTiles = 0; + + for (double zoomLvl = region.minZoom.toDouble(); + zoomLvl <= region.maxZoom; + zoomLvl++) { + final tiles = >{}; + final outlineTiles = >{}; + + for (final triangle in Earcut.triangulateFromPoints( + customPolygonOutline.map(region.crs.projection.project), + ).map(customPolygonOutline.elementAt).slices(3)) { + final vertex1 = region.crs.latLngToPoint(triangle[0], zoomLvl).round(); + final vertex2 = region.crs.latLngToPoint(triangle[1], zoomLvl).round(); + final vertex3 = region.crs.latLngToPoint(triangle[2], zoomLvl).round(); + + outlineTiles.addAll([ + ...bresenhamsLGA( + Point(vertex1.x, vertex1.y), + Point(vertex2.x, vertex2.y), + ).map((e) => (e / region.options.tileSize).floor()), + ...bresenhamsLGA( + Point(vertex2.x, vertex2.y), + Point(vertex3.x, vertex3.y), + ).map((e) => (e / region.options.tileSize).floor()), + ...bresenhamsLGA( + Point(vertex3.x, vertex3.y), + Point(vertex1.x, vertex1.y), + ).map((e) => (e / region.options.tileSize).floor()), + ]); + } + + tiles.addAll(outlineTiles); + + final byY = >{}; + for (final tile in outlineTiles) { + (byY[tile.y] ?? (byY[tile.y] = [])).add(tile.x); + } + + for (int y = byY.keys.min; y <= byY.keys.max; y++) { + byY[y]!.sort(); + for (int x = byY[y]!.first + 1; x < byY[y]!.last; x++) { + tiles.add(Point(x, y)); + } + } + + numberOfTiles += tiles.length; + } + + return numberOfTiles; + } } diff --git a/lib/src/bulk_download/tile_loops/custom_polygon_tools/bres.dart b/lib/src/bulk_download/tile_loops/custom_polygon_tools/bres.dart index 94e854b4..df2c7a40 100644 --- a/lib/src/bulk_download/tile_loops/custom_polygon_tools/bres.dart +++ b/lib/src/bulk_download/tile_loops/custom_polygon_tools/bres.dart @@ -1,8 +1,47 @@ import 'dart:math'; -/// Bresenham’s Line Generation Algorithm -Iterable> bresenhamLGA(Point start, Point end) sync* { - var x1 = start.x; +/// Bresenham’s line generation algorithm, ported from +/// [anushaihalapathirana/Bresenham-line-drawing-algorithm](https://github.com/anushaihalapathirana/Bresenham-line-drawing-algorithm). +Iterable> bresenhamsLGA(Point start, Point end) sync* { + final dx = end.x - start.x; + final dy = end.y - start.y; + final absdx = dx.abs(); + final absdy = dy.abs(); + + var x = start.x; + var y = start.y; + yield Point(x, y); + + if (absdx > absdy) { + var d = 2 * absdy - absdx; + + for (var i = 0; i < absdx; i++) { + x = dx < 0 ? x - 1 : x + 1; + if (d < 0) { + d = d + 2 * absdy; + } else { + y = dy < 0 ? y - 1 : y + 1; + d = d + (2 * absdy - 2 * absdx); + } + yield Point(x, y); + } + } else { + // case when slope is greater than or equals to 1 + var d = 2 * absdx - absdy; + + for (var i = 0; i < absdy; i++) { + y = dy < 0 ? y - 1 : y + 1; + if (d < 0) { + d = d + 2 * absdx; + } else { + x = dx < 0 ? x - 1 : x + 1; + d = d + (2 * absdx) - (2 * absdy); + } + yield Point(x, y); + } + } + + /*var x1 = start.x; var x2 = end.x; var y1 = start.y; var y2 = end.y; @@ -46,5 +85,5 @@ Iterable> bresenhamLGA(Point start, Point end) sync* { x += x < x2 ? 1 : -1; yield Point(x, y); - } + }*/ } diff --git a/lib/src/bulk_download/tile_loops/custom_polygon_tools/earcut.dart b/lib/src/bulk_download/tile_loops/custom_polygon_tools/earcut.dart index e02604ff..1e55a902 100644 --- a/lib/src/bulk_download/tile_loops/custom_polygon_tools/earcut.dart +++ b/lib/src/bulk_download/tile_loops/custom_polygon_tools/earcut.dart @@ -6,8 +6,8 @@ import 'dart:math'; /// Earcutting triangulation algorithm, ported (with minor API differences) from -/// [earcut4j/earcut4j](https://github.com/earcut4j/earcut4j) which itself is -/// ported from [mapbox/earcut](https://github.com/mapbox/earcut). +/// [earcut4j/earcut4j](https://github.com/earcut4j/earcut4j) and +/// [mapbox/earcut](https://github.com/mapbox/earcut). final class Earcut { /// Triangulates the given polygon /// @@ -20,7 +20,7 @@ final class Earcut { /// /// Returns a list of vertice indicies where a group of 3 forms a triangle. static List triangulateFromPoints( - List> polygonVertices, { + Iterable> polygonVertices, { List? holeIndices, }) => triangulateRaw( diff --git a/lib/src/bulk_download/tile_loops/generate.dart b/lib/src/bulk_download/tile_loops/generate.dart index c1302fc9..8b6cb82a 100644 --- a/lib/src/bulk_download/tile_loops/generate.dart +++ b/lib/src/bulk_download/tile_loops/generate.dart @@ -242,4 +242,66 @@ class TilesGenerator { Isolate.exit(); } + + static Future customPolygonTiles( + ({SendPort sendPort, DownloadableRegion region}) input, + ) async { + final region = input.region as DownloadableRegion; + final customPolygonOutline = region.originalRegion.outline; + + final recievePort = ReceivePort(); + input.sendPort.send(recievePort.sendPort); + final requestQueue = StreamQueue(recievePort); + + for (double zoomLvl = region.minZoom.toDouble(); + zoomLvl <= region.maxZoom; + zoomLvl++) { + final tiles = >{}; + final outlineTiles = >{}; + + for (final triangle in Earcut.triangulateFromPoints( + customPolygonOutline.map(region.crs.projection.project), + ).map(customPolygonOutline.elementAt).slices(3)) { + final vertex1 = region.crs.latLngToPoint(triangle[0], zoomLvl).round(); + final vertex2 = region.crs.latLngToPoint(triangle[1], zoomLvl).round(); + final vertex3 = region.crs.latLngToPoint(triangle[2], zoomLvl).round(); + + outlineTiles.addAll([ + ...bresenhamsLGA( + Point(vertex1.x, vertex1.y), + Point(vertex2.x, vertex2.y), + ).map((e) => (e / region.options.tileSize).floor()), + ...bresenhamsLGA( + Point(vertex2.x, vertex2.y), + Point(vertex3.x, vertex3.y), + ).map((e) => (e / region.options.tileSize).floor()), + ...bresenhamsLGA( + Point(vertex3.x, vertex3.y), + Point(vertex1.x, vertex1.y), + ).map((e) => (e / region.options.tileSize).floor()), + ]); + } + + tiles.addAll(outlineTiles); + + final byY = >{}; + for (final tile in outlineTiles) { + (byY[tile.y] ?? (byY[tile.y] = [])).add(tile.x); + } + + for (int y = byY.keys.min; y <= byY.keys.max; y++) { + byY[y]!.sort(); + for (int x = byY[y]!.first + 1; x < byY[y]!.last; x++) { + tiles.add(Point(x, y)); + } + } + + for (final tile in tiles) { + await requestQueue.next; + input.sendPort.send((tile.x, tile.y, zoomLvl.toInt())); + } + } + + Isolate.exit(); + } } diff --git a/lib/src/bulk_download/tile_loops/shared.dart b/lib/src/bulk_download/tile_loops/shared.dart index e9972d7a..a9a66958 100644 --- a/lib/src/bulk_download/tile_loops/shared.dart +++ b/lib/src/bulk_download/tile_loops/shared.dart @@ -12,6 +12,8 @@ import 'package:latlong2/latlong.dart'; import '../../../flutter_map_tile_caching.dart'; import '../../misc/int_extremes.dart'; +import 'custom_polygon_tools/bres.dart'; +import 'custom_polygon_tools/earcut.dart'; part 'count.dart'; part 'generate.dart'; diff --git a/lib/src/regions/base_region.dart b/lib/src/regions/base_region.dart index 80ab4655..b1311e05 100644 --- a/lib/src/regions/base_region.dart +++ b/lib/src/regions/base_region.dart @@ -49,11 +49,13 @@ sealed class BaseRegion { required T Function(RectangleRegion rectangle) rectangle, required T Function(CircleRegion circle) circle, required T Function(LineRegion line) line, + required T Function(CustomPolygonRegion customPolygon) customPolygon, }) => switch (this) { RectangleRegion() => rectangle(this as RectangleRegion), CircleRegion() => circle(this as CircleRegion), LineRegion() => line(this as LineRegion), + CustomPolygonRegion() => customPolygon(this as CustomPolygonRegion), }; /// Generate the [DownloadableRegion] ready for bulk downloading diff --git a/lib/src/regions/custom_polygon.dart b/lib/src/regions/custom_polygon.dart new file mode 100644 index 00000000..73f5037b --- /dev/null +++ b/lib/src/regions/custom_polygon.dart @@ -0,0 +1,82 @@ +// Copyright © Luka S (JaffaKetchup) under GPL-v3 +// A full license can be found at .\LICENSE + +part of flutter_map_tile_caching; + +/// A geographical region who's outline is defined by a list of coordinates +/// +/// It can be converted to a: +/// - [DownloadableRegion] for downloading: [toDownloadable] +/// - [Widget] layer to be placed in a map: [toDrawable] +/// - list of [LatLng]s forming the outline: [toOutline] +class CustomPolygonRegion extends BaseRegion { + /// A geographical region who's outline is defined by a list of coordinates + /// + /// It can be converted to a: + /// - [DownloadableRegion] for downloading: [toDownloadable] + /// - [Widget] layer to be placed in a map: [toDrawable] + /// - list of [LatLng]s forming the outline: [toOutline] + CustomPolygonRegion(this.outline, {super.name}) : super(); + + /// The outline coordinates + final List outline; + + @override + DownloadableRegion toDownloadable({ + required int minZoom, + required int maxZoom, + required TileLayer options, + int start = 0, + int? end, + Crs crs = const Epsg3857(), + void Function(Object?)? errorHandler, + }) => + DownloadableRegion._( + this, + minZoom: minZoom, + maxZoom: maxZoom, + options: options, + start: start, + end: end, + crs: crs, + ); + + @override + PolygonLayer toDrawable({ + Color? fillColor, + Color borderColor = const Color(0x00000000), + double borderStrokeWidth = 3.0, + bool isDotted = false, + String? label, + TextStyle labelStyle = const TextStyle(), + PolygonLabelPlacement labelPlacement = PolygonLabelPlacement.polylabel, + }) => + PolygonLayer( + polygons: [ + Polygon( + points: outline, + isFilled: fillColor != null, + color: fillColor ?? Colors.transparent, + borderColor: borderColor, + borderStrokeWidth: borderStrokeWidth, + isDotted: isDotted, + label: label, + labelStyle: labelStyle, + labelPlacement: labelPlacement, + ) + ], + ); + + @override + List toOutline() => outline; + + @override + bool operator ==(Object other) => + identical(this, other) || + (other is CustomPolygonRegion && + other.outline == outline && + super == other); + + @override + int get hashCode => Object.hash(outline, super.hashCode); +} diff --git a/lib/src/regions/downloadable_region.dart b/lib/src/regions/downloadable_region.dart index c9fd55ad..08c635da 100644 --- a/lib/src/regions/downloadable_region.dart +++ b/lib/src/regions/downloadable_region.dart @@ -69,11 +69,14 @@ class DownloadableRegion { rectangle, required T Function(DownloadableRegion circle) circle, required T Function(DownloadableRegion line) line, + required T Function(DownloadableRegion customPolygon) + customPolygon, }) => switch (originalRegion) { RectangleRegion() => rectangle(_cast()), CircleRegion() => circle(_cast()), LineRegion() => line(_cast()), + CustomPolygonRegion() => customPolygon(_cast()), }; @override diff --git a/lib/src/root/recovery.dart b/lib/src/root/recovery.dart index 5358477f..c0a92d7d 100644 --- a/lib/src/root/recovery.dart +++ b/lib/src/root/recovery.dart @@ -100,6 +100,7 @@ class RootRecovery { rectangle: (_) => RegionType.rectangle, circle: (_) => RegionType.circle, line: (_) => RegionType.line, + customPolygon: (_) => throw UnimplementedError(), ), minZoom: region.minZoom, maxZoom: region.maxZoom, diff --git a/lib/src/store/download.dart b/lib/src/store/download.dart index ccb368ec..eaa2909b 100644 --- a/lib/src/store/download.dart +++ b/lib/src/store/download.dart @@ -245,6 +245,7 @@ class DownloadManagement { rectangle: (_) => TilesCounter.rectangleTiles, circle: (_) => TilesCounter.circleTiles, line: (_) => TilesCounter.lineTiles, + customPolygon: (_) => TilesCounter.customPolygonTiles, ), region, ); diff --git a/test/fmtc_test.dart b/test/fmtc_test.dart index 6002296d..7b97ff76 100644 --- a/test/fmtc_test.dart +++ b/test/fmtc_test.dart @@ -86,6 +86,36 @@ void main() { ).average} ms', ), ); + + final customPolygonRegion = CustomPolygonRegion([ + const LatLng(51.45818683312154, -0.9674646220840917), + const LatLng(51.55859639937614, -0.9185366064186982), + const LatLng(51.476641197796724, -0.7494743298246318), + const LatLng(51.56029831737391, -0.5322770067805148), + const LatLng(51.235701626195365, -0.5746290119276093), + const LatLng(51.38781341753136, -0.6779891095601829), + ]).toDownloadable(minZoom: 2, maxZoom: 18, options: TileLayer()); + + test( + 'Custom Polygon Region Count', + () => expect(TilesCounter.customPolygonTiles(customPolygonRegion), 79895), + ); + + test( + 'Custom Polygon Region Duration', + () => print( + '${List.generate( + 100, + (index) { + final clock = Stopwatch()..start(); + TilesCounter.customPolygonTiles(customPolygonRegion); + clock.stop(); + return clock.elapsedMilliseconds; + }, + growable: false, + ).average} ms', + ), + ); }); group('Test Earcutting Triangulation', () { From 7bcbb12e9737c914a1a36d8db758527a553b6ff8 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Tue, 1 Aug 2023 13:57:27 +0100 Subject: [PATCH 054/168] Fixed Custom Polygon tile generation Improved performance of Custom Polygon tile generation Former-commit-id: 45b9590c3ffb4d9f242aed903ca718ae3add317b [formerly 72047de6fb9e1e789100d19e9932fa0122953b4b] Former-commit-id: e12f82c0aa6140a17d8a94d724b1d7f784aa3fb2 --- lib/src/bulk_download/tile_loops/count.dart | 114 ++++++++-------- .../tile_loops/custom_polygon_tools/bres.dart | 62 ++------- .../bulk_download/tile_loops/generate.dart | 122 +++++++++--------- lib/src/bulk_download/tile_loops/shared.dart | 3 - test/fmtc_test.dart | 4 +- 5 files changed, 128 insertions(+), 177 deletions(-) diff --git a/lib/src/bulk_download/tile_loops/count.dart b/lib/src/bulk_download/tile_loops/count.dart index cbbf5315..ebf47472 100644 --- a/lib/src/bulk_download/tile_loops/count.dart +++ b/lib/src/bulk_download/tile_loops/count.dart @@ -7,7 +7,6 @@ class TilesCounter { static int rectangleTiles(DownloadableRegion region) { region as DownloadableRegion; - final tileSize = _getTileSize(region); final northWest = region.originalRegion.bounds.northWest; final southEast = region.originalRegion.bounds.southEast; @@ -16,13 +15,11 @@ class TilesCounter { for (double zoomLvl = region.minZoom.toDouble(); zoomLvl <= region.maxZoom; zoomLvl++) { - final nwPoint = region.crs - .latLngToPoint(northWest, zoomLvl) - .unscaleBy(tileSize) + final nwPoint = (region.crs.latLngToPoint(northWest, zoomLvl) / + region.options.tileSize) .floor(); - final sePoint = region.crs - .latLngToPoint(southEast, zoomLvl) - .unscaleBy(tileSize) + final sePoint = (region.crs.latLngToPoint(southEast, zoomLvl) / + region.options.tileSize) .ceil() - const Point(1, 1); @@ -43,7 +40,6 @@ class TilesCounter { // 4. Loop over these XY values and add them to the list // Theoretically, this could have been done using the same method as `lineTiles`, but `lineTiles` was built after this algorithm and this makes more sense for a circle - final tileSize = _getTileSize(region); final circleOutline = region.originalRegion.toOutline(); // Format: Map>> @@ -55,9 +51,8 @@ class TilesCounter { outlineTileNums[zoomLvl] = {}; for (final node in circleOutline) { - final tile = region.crs - .latLngToPoint(node, zoomLvl.toDouble()) - .unscaleBy(tileSize) + final tile = (region.crs.latLngToPoint(node, zoomLvl.toDouble()) / + region.options.tileSize) .floor(); outlineTileNums[zoomLvl]![tile.x] ??= [largestInt, smallestInt]; @@ -126,7 +121,6 @@ class TilesCounter { return true; } - final tileSize = _getTileSize(region); final lineOutline = region.originalRegion.toOutlines(1); int numberOfTiles = 0; @@ -157,39 +151,40 @@ class TilesCounter { rotatedRectangle.bottomRight.longitude, ]; - final rotatedRectangleNW = region.crs - .latLngToPoint(rotatedRectangle.topLeft, zoomLvl) - .unscaleBy(tileSize) - .floor(); - final rotatedRectangleNE = region.crs - .latLngToPoint(rotatedRectangle.topRight, zoomLvl) - .unscaleBy(tileSize) - .ceil() - - const Point(1, 0); - final rotatedRectangleSW = region.crs - .latLngToPoint(rotatedRectangle.bottomLeft, zoomLvl) - .unscaleBy(tileSize) - .ceil() - - const Point(0, 1); - final rotatedRectangleSE = region.crs - .latLngToPoint(rotatedRectangle.bottomRight, zoomLvl) - .unscaleBy(tileSize) - .ceil() - - const Point(1, 1); - - final straightRectangleNW = region.crs - .latLngToPoint( - LatLng(rotatedRectangleLats.max, rotatedRectangleLngs.min), - zoomLvl, - ) - .unscaleBy(tileSize) - .floor(); - final straightRectangleSE = region.crs - .latLngToPoint( - LatLng(rotatedRectangleLats.min, rotatedRectangleLngs.max), + final rotatedRectangleNW = + (region.crs.latLngToPoint(rotatedRectangle.topLeft, zoomLvl) / + region.options.tileSize) + .floor(); + final rotatedRectangleNE = + (region.crs.latLngToPoint(rotatedRectangle.topRight, zoomLvl) / + region.options.tileSize) + .ceil() - + const Point(1, 0); + final rotatedRectangleSW = + (region.crs.latLngToPoint(rotatedRectangle.bottomLeft, zoomLvl) / + region.options.tileSize) + .ceil() - + const Point(0, 1); + final rotatedRectangleSE = + (region.crs.latLngToPoint(rotatedRectangle.bottomRight, zoomLvl) / + region.options.tileSize) + .ceil() - + const Point(1, 1); + + final straightRectangleNW = (region.crs.latLngToPoint( + LatLng(rotatedRectangleLats.max, rotatedRectangleLngs.min), zoomLvl, - ) - .unscaleBy(tileSize) + ) / + region.options.tileSize) + .floor(); + final straightRectangleSE = (region.crs.latLngToPoint( + LatLng( + rotatedRectangleLats.min, + rotatedRectangleLngs.max, + ), + zoomLvl, + ) / + region.options.tileSize) .ceil() - const Point(1, 1); @@ -237,11 +232,12 @@ class TilesCounter { zoomLvl <= region.maxZoom; zoomLvl++) { final tiles = >{}; - final outlineTiles = >{}; for (final triangle in Earcut.triangulateFromPoints( customPolygonOutline.map(region.crs.projection.project), ).map(customPolygonOutline.elementAt).slices(3)) { + final outlineTiles = >{}; + final vertex1 = region.crs.latLngToPoint(triangle[0], zoomLvl).round(); final vertex2 = region.crs.latLngToPoint(triangle[1], zoomLvl).round(); final vertex3 = region.crs.latLngToPoint(triangle[2], zoomLvl).round(); @@ -250,29 +246,31 @@ class TilesCounter { ...bresenhamsLGA( Point(vertex1.x, vertex1.y), Point(vertex2.x, vertex2.y), - ).map((e) => (e / region.options.tileSize).floor()), + unscaleBy: region.options.tileSize, + ), ...bresenhamsLGA( Point(vertex2.x, vertex2.y), Point(vertex3.x, vertex3.y), - ).map((e) => (e / region.options.tileSize).floor()), + unscaleBy: region.options.tileSize, + ), ...bresenhamsLGA( Point(vertex3.x, vertex3.y), Point(vertex1.x, vertex1.y), - ).map((e) => (e / region.options.tileSize).floor()), + unscaleBy: region.options.tileSize, + ), ]); - } - tiles.addAll(outlineTiles); + tiles.addAll(outlineTiles); - final byY = >{}; - for (final tile in outlineTiles) { - (byY[tile.y] ?? (byY[tile.y] = [])).add(tile.x); - } + final byY = >{}; + for (final Point(:x, :y) in outlineTiles) { + (byY[y] ?? (byY[y] = {})).add(x); + } - for (int y = byY.keys.min; y <= byY.keys.max; y++) { - byY[y]!.sort(); - for (int x = byY[y]!.first + 1; x < byY[y]!.last; x++) { - tiles.add(Point(x, y)); + for (final MapEntry(key: y, value: xs) in byY.entries) { + for (int x = xs.min + 1; x < xs.max; x++) { + tiles.add(Point(x, y)); + } } } diff --git a/lib/src/bulk_download/tile_loops/custom_polygon_tools/bres.dart b/lib/src/bulk_download/tile_loops/custom_polygon_tools/bres.dart index df2c7a40..0bfcf1bf 100644 --- a/lib/src/bulk_download/tile_loops/custom_polygon_tools/bres.dart +++ b/lib/src/bulk_download/tile_loops/custom_polygon_tools/bres.dart @@ -1,8 +1,12 @@ import 'dart:math'; -/// Bresenham’s line generation algorithm, ported from -/// [anushaihalapathirana/Bresenham-line-drawing-algorithm](https://github.com/anushaihalapathirana/Bresenham-line-drawing-algorithm). -Iterable> bresenhamsLGA(Point start, Point end) sync* { +/// Bresenham’s line generation algorithm, ported (with minor API differences) +/// from [anushaihalapathirana/Bresenham-line-drawing-algorithm](https://github.com/anushaihalapathirana/Bresenham-line-drawing-algorithm). +Iterable> bresenhamsLGA( + Point start, + Point end, { + double unscaleBy = 1, +}) sync* { final dx = end.x - start.x; final dy = end.y - start.y; final absdx = dx.abs(); @@ -10,7 +14,7 @@ Iterable> bresenhamsLGA(Point start, Point end) sync* { var x = start.x; var y = start.y; - yield Point(x, y); + yield Point((x / unscaleBy).floor(), (y / unscaleBy).floor()); if (absdx > absdy) { var d = 2 * absdy - absdx; @@ -23,7 +27,7 @@ Iterable> bresenhamsLGA(Point start, Point end) sync* { y = dy < 0 ? y - 1 : y + 1; d = d + (2 * absdy - 2 * absdx); } - yield Point(x, y); + yield Point((x / unscaleBy).floor(), (y / unscaleBy).floor()); } } else { // case when slope is greater than or equals to 1 @@ -37,53 +41,7 @@ Iterable> bresenhamsLGA(Point start, Point end) sync* { x = dx < 0 ? x - 1 : x + 1; d = d + (2 * absdx) - (2 * absdy); } - yield Point(x, y); + yield Point((x / unscaleBy).floor(), (y / unscaleBy).floor()); } } - - /*var x1 = start.x; - var x2 = end.x; - var y1 = start.y; - var y2 = end.y; - - var x = x1; - var y = y1; - - var dx = (x2 - x1).abs(); - var dy = (y2 - y1).abs(); - - if (dy / dx > 1) { - final intermediateDx = dx; - dx = dy; - dy = intermediateDx; - - final intermediateX = x; - x = y; - y = intermediateX; - - final intermediateX1 = x1; - x1 = y1; - y1 = intermediateX1; - - final intermediateX2 = x2; - x2 = y2; - y2 = intermediateX2; - } - - var p = 2 * dy - dx; - - yield Point(x, y); - - for (int k = 2; k < dx + 2; k++) { - if (p > 0) { - y += y < y2 ? 1 : -1; - p += 2 * (dy - dx); - } else { - p += 2 * dy; - } - - x += x < x2 ? 1 : -1; - - yield Point(x, y); - }*/ } diff --git a/lib/src/bulk_download/tile_loops/generate.dart b/lib/src/bulk_download/tile_loops/generate.dart index 8b6cb82a..b7c977dd 100644 --- a/lib/src/bulk_download/tile_loops/generate.dart +++ b/lib/src/bulk_download/tile_loops/generate.dart @@ -8,7 +8,6 @@ class TilesGenerator { ({SendPort sendPort, DownloadableRegion region}) input, ) async { final region = input.region as DownloadableRegion; - final tileSize = _getTileSize(region); final northWest = region.originalRegion.bounds.northWest; final southEast = region.originalRegion.bounds.southEast; @@ -19,13 +18,11 @@ class TilesGenerator { for (double zoomLvl = region.minZoom.toDouble(); zoomLvl <= region.maxZoom; zoomLvl++) { - final nwPoint = region.crs - .latLngToPoint(northWest, zoomLvl) - .unscaleBy(tileSize) + final nwPoint = (region.crs.latLngToPoint(northWest, zoomLvl) / + region.options.tileSize) .floor(); - final sePoint = region.crs - .latLngToPoint(southEast, zoomLvl) - .unscaleBy(tileSize) + final sePoint = (region.crs.latLngToPoint(southEast, zoomLvl) / + region.options.tileSize) .ceil() - const Point(1, 1); @@ -51,7 +48,6 @@ class TilesGenerator { // Theoretically, this could have been done using the same method as `lineTiles`, but `lineTiles` was built after this algorithm and this makes more sense for a circle final region = input.region as DownloadableRegion; - final tileSize = _getTileSize(region); final circleOutline = region.originalRegion.toOutline(); final recievePort = ReceivePort(); @@ -65,9 +61,8 @@ class TilesGenerator { outlineTileNums[zoomLvl] = {}; for (final node in circleOutline) { - final tile = region.crs - .latLngToPoint(node, zoomLvl.toDouble()) - .unscaleBy(tileSize) + final tile = (region.crs.latLngToPoint(node, zoomLvl.toDouble()) / + region.options.tileSize) .floor(); outlineTileNums[zoomLvl]![tile.x] ??= [largestInt, smallestInt]; @@ -140,7 +135,6 @@ class TilesGenerator { } final region = input.region as DownloadableRegion; - final tileSize = _getTileSize(region); final lineOutline = region.originalRegion.toOutlines(1); final recievePort = ReceivePort(); @@ -173,39 +167,40 @@ class TilesGenerator { rotatedRectangle.bottomRight.longitude, ]; - final rotatedRectangleNW = region.crs - .latLngToPoint(rotatedRectangle.topLeft, zoomLvl) - .unscaleBy(tileSize) - .floor(); - final rotatedRectangleNE = region.crs - .latLngToPoint(rotatedRectangle.topRight, zoomLvl) - .unscaleBy(tileSize) - .ceil() - - const Point(1, 0); - final rotatedRectangleSW = region.crs - .latLngToPoint(rotatedRectangle.bottomLeft, zoomLvl) - .unscaleBy(tileSize) - .ceil() - - const Point(0, 1); - final rotatedRectangleSE = region.crs - .latLngToPoint(rotatedRectangle.bottomRight, zoomLvl) - .unscaleBy(tileSize) - .ceil() - - const Point(1, 1); - - final straightRectangleNW = region.crs - .latLngToPoint( - LatLng(rotatedRectangleLats.max, rotatedRectangleLngs.min), - zoomLvl, - ) - .unscaleBy(tileSize) - .floor(); - final straightRectangleSE = region.crs - .latLngToPoint( - LatLng(rotatedRectangleLats.min, rotatedRectangleLngs.max), + final rotatedRectangleNW = + (region.crs.latLngToPoint(rotatedRectangle.topLeft, zoomLvl) / + region.options.tileSize) + .floor(); + final rotatedRectangleNE = + (region.crs.latLngToPoint(rotatedRectangle.topRight, zoomLvl) / + region.options.tileSize) + .ceil() - + const Point(1, 0); + final rotatedRectangleSW = + (region.crs.latLngToPoint(rotatedRectangle.bottomLeft, zoomLvl) / + region.options.tileSize) + .ceil() - + const Point(0, 1); + final rotatedRectangleSE = + (region.crs.latLngToPoint(rotatedRectangle.bottomRight, zoomLvl) / + region.options.tileSize) + .ceil() - + const Point(1, 1); + + final straightRectangleNW = (region.crs.latLngToPoint( + LatLng(rotatedRectangleLats.max, rotatedRectangleLngs.min), zoomLvl, - ) - .unscaleBy(tileSize) + ) / + region.options.tileSize) + .floor(); + final straightRectangleSE = (region.crs.latLngToPoint( + LatLng( + rotatedRectangleLats.min, + rotatedRectangleLngs.max, + ), + zoomLvl, + ) / + region.options.tileSize) .ceil() - const Point(1, 1); @@ -257,11 +252,12 @@ class TilesGenerator { zoomLvl <= region.maxZoom; zoomLvl++) { final tiles = >{}; - final outlineTiles = >{}; for (final triangle in Earcut.triangulateFromPoints( customPolygonOutline.map(region.crs.projection.project), ).map(customPolygonOutline.elementAt).slices(3)) { + final outlineTiles = >{}; + final vertex1 = region.crs.latLngToPoint(triangle[0], zoomLvl).round(); final vertex2 = region.crs.latLngToPoint(triangle[1], zoomLvl).round(); final vertex3 = region.crs.latLngToPoint(triangle[2], zoomLvl).round(); @@ -270,35 +266,37 @@ class TilesGenerator { ...bresenhamsLGA( Point(vertex1.x, vertex1.y), Point(vertex2.x, vertex2.y), - ).map((e) => (e / region.options.tileSize).floor()), + unscaleBy: region.options.tileSize, + ), ...bresenhamsLGA( Point(vertex2.x, vertex2.y), Point(vertex3.x, vertex3.y), - ).map((e) => (e / region.options.tileSize).floor()), + unscaleBy: region.options.tileSize, + ), ...bresenhamsLGA( Point(vertex3.x, vertex3.y), Point(vertex1.x, vertex1.y), - ).map((e) => (e / region.options.tileSize).floor()), + unscaleBy: region.options.tileSize, + ), ]); - } - tiles.addAll(outlineTiles); + tiles.addAll(outlineTiles); - final byY = >{}; - for (final tile in outlineTiles) { - (byY[tile.y] ?? (byY[tile.y] = [])).add(tile.x); - } + final byY = >{}; + for (final Point(:x, :y) in outlineTiles) { + (byY[y] ?? (byY[y] = {})).add(x); + } - for (int y = byY.keys.min; y <= byY.keys.max; y++) { - byY[y]!.sort(); - for (int x = byY[y]!.first + 1; x < byY[y]!.last; x++) { - tiles.add(Point(x, y)); + for (final MapEntry(key: y, value: xs) in byY.entries) { + for (int x = xs.min + 1; x < xs.max; x++) { + tiles.add(Point(x, y)); + } } - } - for (final tile in tiles) { - await requestQueue.next; - input.sendPort.send((tile.x, tile.y, zoomLvl.toInt())); + for (final tile in tiles) { + await requestQueue.next; + input.sendPort.send((tile.x, tile.y, zoomLvl.toInt())); + } } } diff --git a/lib/src/bulk_download/tile_loops/shared.dart b/lib/src/bulk_download/tile_loops/shared.dart index a9a66958..8685eae7 100644 --- a/lib/src/bulk_download/tile_loops/shared.dart +++ b/lib/src/bulk_download/tile_loops/shared.dart @@ -34,6 +34,3 @@ class _Polygon { identical(this, other) || (other is _Polygon && hashCode == other.hashCode); } - -Point _getTileSize(DownloadableRegion region) => - Point(region.options.tileSize, region.options.tileSize); diff --git a/test/fmtc_test.dart b/test/fmtc_test.dart index 7b97ff76..571d3f32 100644 --- a/test/fmtc_test.dart +++ b/test/fmtc_test.dart @@ -94,11 +94,11 @@ void main() { const LatLng(51.56029831737391, -0.5322770067805148), const LatLng(51.235701626195365, -0.5746290119276093), const LatLng(51.38781341753136, -0.6779891095601829), - ]).toDownloadable(minZoom: 2, maxZoom: 18, options: TileLayer()); + ]).toDownloadable(minZoom: 1, maxZoom: 18, options: TileLayer()); test( 'Custom Polygon Region Count', - () => expect(TilesCounter.customPolygonTiles(customPolygonRegion), 79895), + () => expect(TilesCounter.customPolygonTiles(customPolygonRegion), 62234), ); test( From 87a340384f695c9b6dc452c4e89d7e7b0ba0c90c Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Wed, 2 Aug 2023 13:48:04 +0100 Subject: [PATCH 055/168] Started reimplementing example app's region selector page Former-commit-id: c80d01494264cbeb78559e5d08c7fbbab355ba55 [formerly b906072fae07ae1d399dc950971281e98f540740] Former-commit-id: 9c8139d7ef539152959132e6cb9ac166b342bb52 --- example/lib/main.dart | 4 +- .../components/region_information.dart | 4 +- .../components/store_selector.dart | 2 +- .../download_region/download_region.dart | 4 +- example/lib/screens/main/main.dart | 9 +- .../downloader/components/crosshairs.dart | 2 +- .../custom_polygon_snapping_indicator.dart | 44 +++ .../components/custom_slider_track_shape.dart | 19 ++ .../pages/downloader/components/header.dart | 28 -- .../pages/downloader/components/map_view.dart | 222 ++++++++++++++- .../min_max_zoom_controller_popup.dart | 2 +- .../downloader/components/region_shape.dart | 103 +++++++ .../components/shape_controller_popup.dart | 66 ----- .../downloader/components/side_panel.dart | 256 ++++++++++++++++++ .../components/usage_instructions.dart | 93 +++++++ .../main/pages/downloader/downloader.dart | 37 --- .../components/download_layout.dart | 4 +- .../components/main_statistics.dart | 2 +- .../main/pages/downloading/downloading.dart | 4 +- .../components/recovery_start_button.dart | 12 +- .../pages/stores/components/store_tile.dart | 2 +- .../store_editor/components/header.dart | 10 +- .../screens/store_editor/store_editor.dart | 2 +- .../shared/misc/region_selection_method.dart | 4 + example/lib/shared/misc/region_type.dart | 6 + .../shared/{vars => misc}/size_formatter.dart | 0 .../lib/shared/state/download_provider.dart | 88 +++++- example/lib/shared/vars/region_mode.dart | 7 - 28 files changed, 840 insertions(+), 196 deletions(-) create mode 100644 example/lib/screens/main/pages/downloader/components/custom_polygon_snapping_indicator.dart create mode 100644 example/lib/screens/main/pages/downloader/components/custom_slider_track_shape.dart create mode 100644 example/lib/screens/main/pages/downloader/components/region_shape.dart delete mode 100644 example/lib/screens/main/pages/downloader/components/shape_controller_popup.dart create mode 100644 example/lib/screens/main/pages/downloader/components/side_panel.dart create mode 100644 example/lib/screens/main/pages/downloader/components/usage_instructions.dart create mode 100644 example/lib/shared/misc/region_selection_method.dart create mode 100644 example/lib/shared/misc/region_type.dart rename example/lib/shared/{vars => misc}/size_formatter.dart (100%) delete mode 100644 example/lib/shared/vars/region_mode.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index aaa7fbe5..467b00c8 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -54,7 +54,7 @@ class AppContainer extends StatelessWidget { Widget build(BuildContext context) => MultiProvider( providers: [ ChangeNotifierProvider(create: (context) => GeneralProvider()), - ChangeNotifierProvider(create: (context) => DownloadProvider()), + ChangeNotifierProvider(create: (context) => DownloaderProvider()), ChangeNotifierProvider(create: (context) => MapProvider()), ], child: MaterialApp( @@ -63,7 +63,7 @@ class AppContainer extends StatelessWidget { brightness: Brightness.dark, useMaterial3: true, textTheme: GoogleFonts.openSansTextTheme(const TextTheme()), - colorSchemeSeed: Colors.deepOrange, + colorSchemeSeed: Colors.red, switchTheme: SwitchThemeData( thumbIcon: MaterialStateProperty.resolveWith( (states) => Icon( diff --git a/example/lib/screens/download_region/components/region_information.dart b/example/lib/screens/download_region/components/region_information.dart index 43840dcd..f14bdbbc 100644 --- a/example/lib/screens/download_region/components/region_information.dart +++ b/example/lib/screens/download_region/components/region_information.dart @@ -105,7 +105,7 @@ class RegionInformation extends StatelessWidget { ), const SizedBox(height: 10), const Text('MIN/MAX ZOOM LEVELS'), - Consumer( + Consumer( builder: (context, provider, _) => provider.regionTiles == null ? Padding( @@ -140,7 +140,7 @@ class RegionInformation extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.end, children: [ const Text('TOTAL TILES'), - Consumer( + Consumer( builder: (context, provider, _) => provider.regionTiles == null ? Padding( diff --git a/example/lib/screens/download_region/components/store_selector.dart b/example/lib/screens/download_region/components/store_selector.dart index 3fdb1c5f..66a50b78 100644 --- a/example/lib/screens/download_region/components/store_selector.dart +++ b/example/lib/screens/download_region/components/store_selector.dart @@ -18,7 +18,7 @@ class _StoreSelectorState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('CHOOSE A STORE'), - Consumer2( + Consumer2( builder: (context, downloadProvider, generalProvider, _) => FutureBuilder>( future: FMTC.instance.rootDirectory.stats.storesAvailableAsync, diff --git a/example/lib/screens/download_region/download_region.dart b/example/lib/screens/download_region/download_region.dart index 120f2ac8..180273c1 100644 --- a/example/lib/screens/download_region/download_region.dart +++ b/example/lib/screens/download_region/download_region.dart @@ -30,7 +30,7 @@ class _DownloadRegionPopupState extends State { final String? currentStore = Provider.of(context, listen: false).currentStore; if (currentStore != null) { - Provider.of(context, listen: false) + Provider.of(context, listen: false) .setSelectedStore(FMTC.instance(currentStore), notify: false); } @@ -38,7 +38,7 @@ class _DownloadRegionPopupState extends State { } @override - Widget build(BuildContext context) => Consumer( + Widget build(BuildContext context) => Consumer( builder: (context, provider, _) => Scaffold( appBar: AppBar(title: const Text('Configure Bulk Download')), floatingActionButton: provider.selectedStore == null diff --git a/example/lib/screens/main/main.dart b/example/lib/screens/main/main.dart index ba11c00e..9b235fbb 100644 --- a/example/lib/screens/main/main.dart +++ b/example/lib/screens/main/main.dart @@ -3,10 +3,10 @@ import 'package:flutter/material.dart' hide Badge; import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; import 'package:provider/provider.dart'; -import '../../shared/state/download_provider.dart'; +//import '../../shared/state/download_provider.dart'; import '../../shared/state/map_provider.dart'; import 'pages/downloader/downloader.dart'; -import 'pages/downloading/downloading.dart'; +//import 'pages/downloading/downloading.dart'; import 'pages/map/map_view.dart'; import 'pages/recovery/recovery.dart'; import 'pages/stores/stores.dart'; @@ -66,11 +66,12 @@ class _MainScreenState extends State { List get _pages => [ const MapPage(), const StoresPage(), - Consumer( + const DownloaderPage(), + /*Consumer( builder: (context, provider, _) => provider.downloadProgress == null ? const DownloaderPage() : const DownloadingPage(), - ), + ),*/ RecoveryPage(moveToDownloadPage: () => _onDestinationSelected(2)), ]; diff --git a/example/lib/screens/main/pages/downloader/components/crosshairs.dart b/example/lib/screens/main/pages/downloader/components/crosshairs.dart index 67be9251..943a3a1f 100644 --- a/example/lib/screens/main/pages/downloader/components/crosshairs.dart +++ b/example/lib/screens/main/pages/downloader/components/crosshairs.dart @@ -4,7 +4,7 @@ class Crosshairs extends StatelessWidget { const Crosshairs({ super.key, this.size = 20, - this.thickness = 2, + this.thickness = 3, }); final double size; diff --git a/example/lib/screens/main/pages/downloader/components/custom_polygon_snapping_indicator.dart b/example/lib/screens/main/pages/downloader/components/custom_polygon_snapping_indicator.dart new file mode 100644 index 00000000..260048bb --- /dev/null +++ b/example/lib/screens/main/pages/downloader/components/custom_polygon_snapping_indicator.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:provider/provider.dart'; + +import '../../../../../shared/state/download_provider.dart'; + +class CustomPolygonSnappingIndicator extends StatelessWidget { + const CustomPolygonSnappingIndicator({ + super.key, + }); + + @override + Widget build(BuildContext context) => MarkerLayer( + markers: [ + if (context + .select>( + (p) => p.coordinates, + ) + .isNotEmpty && + context.select( + (p) => p.customPolygonSnap, + )) + Marker( + height: 25, + width: 25, + point: context + .select>( + (p) => p.coordinates, + ) + .first, + builder: (context) => DecoratedBox( + decoration: BoxDecoration( + color: Colors.green, + borderRadius: BorderRadius.circular(1028), + ), + child: const Center( + child: Icon(Icons.auto_awesome, size: 15), + ), + ), + ), + ], + ); +} diff --git a/example/lib/screens/main/pages/downloader/components/custom_slider_track_shape.dart b/example/lib/screens/main/pages/downloader/components/custom_slider_track_shape.dart new file mode 100644 index 00000000..df3f11f3 --- /dev/null +++ b/example/lib/screens/main/pages/downloader/components/custom_slider_track_shape.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; + +// From https://stackoverflow.com/a/65662764/11846040 +class CustomSliderTrackShape extends RoundedRectSliderTrackShape { + @override + Rect getPreferredRect({ + required RenderBox parentBox, + Offset offset = Offset.zero, + required SliderThemeData sliderTheme, + bool isEnabled = false, + bool isDiscrete = false, + }) { + final trackHeight = sliderTheme.trackHeight; + final trackLeft = offset.dx; + final trackTop = offset.dy + (parentBox.size.height - trackHeight!) / 2; + final trackWidth = parentBox.size.width; + return Rect.fromLTWH(trackLeft, trackTop, trackWidth, trackHeight); + } +} diff --git a/example/lib/screens/main/pages/downloader/components/header.dart b/example/lib/screens/main/pages/downloader/components/header.dart index 08fb0131..9e60902c 100644 --- a/example/lib/screens/main/pages/downloader/components/header.dart +++ b/example/lib/screens/main/pages/downloader/components/header.dart @@ -2,10 +2,7 @@ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:provider/provider.dart'; -import '../../../../../shared/state/download_provider.dart'; import '../../../../../shared/state/general_provider.dart'; -import 'min_max_zoom_controller_popup.dart'; -import 'shape_controller_popup.dart'; class Header extends StatelessWidget { const Header({ @@ -36,31 +33,6 @@ class Header extends StatelessWidget { ), ], ), - const Spacer(), - IconButton( - onPressed: () => showModalBottomSheet( - context: context, - useRootNavigator: true, - isScrollControlled: true, - builder: (_) => const MinMaxZoomControllerPopup(), - ).then( - (_) => Provider.of(context, listen: false) - .triggerManualPolygonRecalc(), - ), - icon: const Icon(Icons.zoom_in), - ), - IconButton( - onPressed: () => showModalBottomSheet( - context: context, - useRootNavigator: true, - isScrollControlled: true, - builder: (_) => const ShapeControllerPopup(), - ).then( - (_) => Provider.of(context, listen: false) - .triggerManualPolygonRecalc(), - ), - icon: const Icon(Icons.select_all), - ), ], ); } diff --git a/example/lib/screens/main/pages/downloader/components/map_view.dart b/example/lib/screens/main/pages/downloader/components/map_view.dart index 72aeabee..b1b94c66 100644 --- a/example/lib/screens/main/pages/downloader/components/map_view.dart +++ b/example/lib/screens/main/pages/downloader/components/map_view.dart @@ -1,36 +1,230 @@ -import 'dart:async'; -import 'dart:io'; import 'dart:math'; -import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/plugin_api.dart'; import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; -import 'package:gpx/gpx.dart'; import 'package:latlong2/latlong.dart'; import 'package:provider/provider.dart'; -import 'package:stream_transform/stream_transform.dart'; -import '../../../../../shared/components/loading_indicator.dart'; +import '../../../../../shared/misc/region_selection_method.dart'; +import '../../../../../shared/misc/region_type.dart'; import '../../../../../shared/state/download_provider.dart'; -import '../../../../../shared/state/general_provider.dart'; -import '../../../../../shared/vars/region_mode.dart'; import '../../map/build_attribution.dart'; import 'crosshairs.dart'; +import 'custom_polygon_snapping_indicator.dart'; +import 'region_shape.dart'; +import 'side_panel.dart'; +import 'usage_instructions.dart'; class MapView extends StatefulWidget { - const MapView({ - super.key, - }); + const MapView({super.key}); @override State createState() => _MapViewState(); } class _MapViewState extends State { - static const double _shapePadding = 15; + final mapController = MapController(); + final urlTemplate = 'https://tile.openstreetmap.org/{z}/{x}/{y}.png'; + + late final mapOptions = MapOptions( + initialCenter: const LatLng(51.509364, -0.128928), + initialZoom: 11, + maxZoom: 20, + cameraConstraint: CameraConstraint.contain( + bounds: LatLngBounds.fromPoints([ + const LatLng(-90, 180), + const LatLng(90, 180), + const LatLng(90, -180), + const LatLng(-90, -180), + ]), + ), + interactionOptions: const InteractionOptions( + flags: InteractiveFlag.all & + ~InteractiveFlag.rotate & + ~InteractiveFlag.doubleTapZoom, + scrollWheelVelocity: 0.002, + ), + keepAlive: true, + onTap: (_, __) { + final provider = Provider.of(context, listen: false); + + if (provider.isCustomPolygonComplete) return; + + final List coords; + if (provider.customPolygonSnap && + provider.regionType == RegionType.customPolygon) { + coords = provider.addCoordinate(provider.coordinates.first); + provider.customPolygonSnap = false; + } else { + coords = provider.addCoordinate(provider.currentNewPointPos); + } + + if (coords.length < 2) return; + + switch (provider.regionType) { + case RegionType.square: + if (coords.length == 2) { + provider.region = RectangleRegion(LatLngBounds.fromPoints(coords)); + break; + } + provider + ..clearCoordinates() + ..addCoordinate(provider.currentNewPointPos); + + break; + case RegionType.circle: + if (coords.length == 2) { + provider.region = CircleRegion( + coords[0], + const Distance(roundResult: false) + .distance(coords[0], coords[1]) / + 1000, + ); + break; + } + provider + ..clearCoordinates() + ..addCoordinate(provider.currentNewPointPos); + + break; + case RegionType.line: + provider.region = LineRegion(coords, provider.lineRadius); + break; + case RegionType.customPolygon: + if (!provider.isCustomPolygonComplete) break; + provider.region = CustomPolygonRegion(coords); + break; + } + }, + onSecondaryTap: (_, __) => + Provider.of(context, listen: false) + .removeLastCoordinate(), + onLongPress: (_, __) => + Provider.of(context, listen: false) + .removeLastCoordinate(), + onPointerHover: (evt, point) { + final provider = Provider.of(context, listen: false); + + if (provider.regionSelectionMethod == RegionSelectionMethod.usePointer) { + provider.currentNewPointPos = point; + } + + if (provider.regionType == RegionType.customPolygon) { + final coords = provider.coordinates; + if (coords.length > 1) { + final newPointPos = + mapController.camera.latLngToScreenPoint(coords.first).toOffset(); + provider.customPolygonSnap = coords.first != coords.last && + sqrt( + pow(newPointPos.dx - evt.localPosition.dx, 2) + + pow(newPointPos.dy - evt.localPosition.dy, 2), + ) < + 15; + } + } + }, + onPositionChanged: (position, _) { + final provider = Provider.of(context, listen: false); + + if (provider.regionSelectionMethod == + RegionSelectionMethod.useMapCenter) { + provider.currentNewPointPos = position.center!; + } + + if (provider.regionType == RegionType.customPolygon) { + final coords = provider.coordinates; + if (coords.length > 1) { + final newPointPos = + mapController.camera.latLngToScreenPoint(coords.first).toOffset(); + final centerPos = mapController.camera + .latLngToScreenPoint(provider.currentNewPointPos) + .toOffset(); + provider.customPolygonSnap = coords.first != coords.last && + sqrt( + pow(newPointPos.dx - centerPos.dx, 2) + + pow(newPointPos.dy - centerPos.dy, 2), + ) < + 30; + } + } + }, + ); + + @override + Widget build(BuildContext context) => LayoutBuilder( + builder: (context, constraints) => Stack( + children: [ + MouseRegion( + opaque: false, + cursor: context.select( + (p) => p.regionSelectionMethod, + ) == + RegionSelectionMethod.useMapCenter + ? MouseCursor.defer + : context.select( + (p) => p.customPolygonSnap, + ) + ? SystemMouseCursors.none + : SystemMouseCursors.precise, + child: FlutterMap( + mapController: mapController, + options: mapOptions, + nonRotatedChildren: buildStdAttribution( + urlTemplate, + alignment: AttributionAlignment.bottomLeft, + ), + children: [ + TileLayer( + urlTemplate: urlTemplate, + maxZoom: 20, + //reset: generalProvider.resetController.stream, + keepBuffer: 5, + userAgentPackageName: 'dev.jaffaketchup.fmtc.demo', + backgroundColor: const Color(0xFFaad3df), + /*tileBuilder: (context, widget, tile) => FutureBuilder( + future: generalProvider.currentStore == null + ? Future.sync(() => null) + : FMTC + .instance(generalProvider.currentStore!) + .getTileProvider() + .checkTileCachedAsync( + coords: tile.coordinates, + options: TileLayer( + urlTemplate: urlTemplate, + ), + ), + builder: (context, snapshot) => DecoratedBox( + position: DecorationPosition.foreground, + decoration: BoxDecoration( + color: (snapshot.data ?? false) + ? Colors.deepOrange.withOpacity(0.33) + : Colors.transparent, + ), + child: widget, + ), + ),*/ + ), + const RegionShape(), + const CustomPolygonSnappingIndicator(), + ], + ), + ), + SidePanel(constraints: constraints), + if (context.select( + (p) => p.regionSelectionMethod, + ) == + RegionSelectionMethod.useMapCenter && + !context.select( + (p) => p.customPolygonSnap, + )) + const Center(child: Crosshairs()), + UsageInstructions(constraints: constraints), + ], + ), + ); + /*static const double _shapePadding = 15; static const _crosshairsMovement = Point(10, 10); final _mapKey = GlobalKey>(); @@ -468,5 +662,5 @@ class _MapViewState extends State { ), ); } - } + }*/ } diff --git a/example/lib/screens/main/pages/downloader/components/min_max_zoom_controller_popup.dart b/example/lib/screens/main/pages/downloader/components/min_max_zoom_controller_popup.dart index 6623f006..439bbf70 100644 --- a/example/lib/screens/main/pages/downloader/components/min_max_zoom_controller_popup.dart +++ b/example/lib/screens/main/pages/downloader/components/min_max_zoom_controller_popup.dart @@ -18,7 +18,7 @@ class MinMaxZoomControllerPopup extends StatelessWidget { right: 12, bottom: 12 + MediaQuery.of(context).viewInsets.bottom, ), - child: Consumer( + child: Consumer( child: Text( 'Change Min/Max Zoom Levels', style: GoogleFonts.openSans( diff --git a/example/lib/screens/main/pages/downloader/components/region_shape.dart b/example/lib/screens/main/pages/downloader/components/region_shape.dart new file mode 100644 index 00000000..c404b912 --- /dev/null +++ b/example/lib/screens/main/pages/downloader/components/region_shape.dart @@ -0,0 +1,103 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map/plugin_api.dart'; +import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:provider/provider.dart'; + +import '../../../../../shared/misc/region_type.dart'; +import '../../../../../shared/state/download_provider.dart'; + +class RegionShape extends StatelessWidget { + const RegionShape({ + super.key, + }); + + @override + Widget build(BuildContext context) => Consumer( + builder: (context, provider, _) { + if (provider.regionType == RegionType.line) { + if (provider.coordinates.isEmpty) return const SizedBox.shrink(); + return PolylineLayer( + polylines: [ + Polyline( + points: [ + ...provider.coordinates, + provider.currentNewPointPos + ], + borderColor: Colors.black, + borderStrokeWidth: 2, + color: Colors.green.withOpacity(2 / 3), + strokeWidth: provider.lineRadius * 2, + useStrokeWidthInMeter: true, + ), + ], + ); + } + + final List holePoints; + if (provider.coordinates.isEmpty) { + holePoints = []; + } else { + switch (provider.regionType) { + case RegionType.square: + final bounds = LatLngBounds.fromPoints( + provider.coordinates.length == 1 + ? [provider.coordinates[0], provider.currentNewPointPos] + : provider.coordinates, + ); + holePoints = [ + bounds.northWest, + bounds.northEast, + bounds.southEast, + bounds.southWest, + ]; + break; + case RegionType.circle: + holePoints = CircleRegion( + provider.coordinates[0], + const Distance(roundResult: false).distance( + provider.coordinates[0], + provider.coordinates.length == 1 + ? provider.currentNewPointPos + : provider.coordinates[1], + ) / + 1000, + ).toOutline().toList(); + break; + case RegionType.line: + throw Error(); + case RegionType.customPolygon: + holePoints = provider.isCustomPolygonComplete + ? provider.coordinates + : [ + ...provider.coordinates, + provider.customPolygonSnap + ? provider.coordinates.first + : provider.currentNewPointPos, + ]; + break; + } + } + + return PolygonLayer( + polygons: [ + Polygon( + points: [ + const LatLng(-90, 180), + const LatLng(90, 180), + const LatLng(90, -180), + const LatLng(-90, -180), + ], + holePointsList: [holePoints], + isFilled: true, + borderColor: Colors.black, + borderStrokeWidth: 2, + color: + Theme.of(context).colorScheme.background.withOpacity(0.5), + ), + ], + ); + }, + ); +} diff --git a/example/lib/screens/main/pages/downloader/components/shape_controller_popup.dart b/example/lib/screens/main/pages/downloader/components/shape_controller_popup.dart deleted file mode 100644 index 6b228baf..00000000 --- a/example/lib/screens/main/pages/downloader/components/shape_controller_popup.dart +++ /dev/null @@ -1,66 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -import '../../../../../shared/state/download_provider.dart'; -import '../../../../../shared/vars/region_mode.dart'; - -class ShapeControllerPopup extends StatelessWidget { - const ShapeControllerPopup({super.key}); - - static const Map - regionShapes = { - 'Square': ( - icon: Icons.crop_square_sharp, - mode: RegionMode.square, - hint: null, - ), - 'Vertical Rectangle': ( - icon: Icons.crop_portrait_sharp, - mode: RegionMode.rectangleVertical, - hint: null, - ), - 'Horizontal Rectangle': ( - icon: Icons.crop_landscape_sharp, - mode: RegionMode.rectangleHorizontal, - hint: null, - ), - 'Circle': ( - icon: Icons.circle_outlined, - mode: RegionMode.circle, - hint: null, - ), - 'Line/Path': ( - icon: Icons.timeline, - mode: RegionMode.line, - hint: - 'Tap/click to add point to line\nHold/secondary click to remove last point from line', - ), - }; - - @override - Widget build(BuildContext context) => Padding( - padding: const EdgeInsets.all(12), - child: Consumer( - builder: (context, provider, _) => ListView.builder( - itemCount: regionShapes.length, - shrinkWrap: true, - itemBuilder: (context, i) { - final value = regionShapes.values.elementAt(i); - return ListTile( - visualDensity: VisualDensity.compact, - title: Text(regionShapes.keys.elementAt(i)), - leading: Icon(value.icon), - trailing: provider.regionMode == value.mode - ? const Icon(Icons.done) - : null, - subtitle: value.hint != null ? Text(value.hint!) : null, - onTap: () { - provider.regionMode = value.mode; - Navigator.of(context).pop(); - }, - ); - }, - ), - ), - ); -} diff --git a/example/lib/screens/main/pages/downloader/components/side_panel.dart b/example/lib/screens/main/pages/downloader/components/side_panel.dart new file mode 100644 index 00000000..f3d18617 --- /dev/null +++ b/example/lib/screens/main/pages/downloader/components/side_panel.dart @@ -0,0 +1,256 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../../../../../shared/misc/region_selection_method.dart'; +import '../../../../../shared/misc/region_type.dart'; +import '../../../../../shared/state/download_provider.dart'; +import 'custom_slider_track_shape.dart'; +import 'min_max_zoom_controller_popup.dart'; + +class SidePanel extends StatelessWidget { + SidePanel({ + super.key, + required this.constraints, + }) : layoutDirection = + constraints.maxWidth > 800 ? Axis.vertical : Axis.horizontal; + + final BoxConstraints constraints; + final Axis layoutDirection; + + @override + Widget build(BuildContext context) => PositionedDirectional( + top: layoutDirection == Axis.vertical ? 12 : null, + bottom: 12, + start: layoutDirection == Axis.vertical ? 24 : 12, + end: layoutDirection == Axis.vertical ? null : 12, + child: Center( + child: layoutDirection == Axis.vertical + ? IntrinsicHeight( + child: PaneGroup(layoutDirection: layoutDirection), + ) + : FittedBox( + child: IntrinsicWidth( + child: PaneGroup(layoutDirection: layoutDirection), + ), + ), + ), + ); +} + +class PaneGroup extends StatelessWidget { + const PaneGroup({ + super.key, + required this.layoutDirection, + }); + + final Axis layoutDirection; + + @override + Widget build(BuildContext context) => Flex( + direction: + layoutDirection == Axis.vertical ? Axis.horizontal : Axis.vertical, + crossAxisAlignment: CrossAxisAlignment.stretch, + verticalDirection: layoutDirection == Axis.horizontal + ? VerticalDirection.up + : VerticalDirection.down, + children: [ + Flex( + direction: layoutDirection, + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background, + borderRadius: BorderRadius.circular(1028), + ), + padding: const EdgeInsets.all(12), + child: Flex( + direction: layoutDirection, + mainAxisSize: MainAxisSize.min, + children: const [ + RegionShapeButton( + type: RegionType.square, + selectedIcon: Icon(Icons.square), + unselectedIcon: Icon(Icons.square_outlined), + tooltip: 'Rectangle', + ), + SizedBox.square(dimension: 12), + RegionShapeButton( + type: RegionType.circle, + selectedIcon: Icon(Icons.circle), + unselectedIcon: Icon(Icons.circle_outlined), + tooltip: 'Circle', + ), + SizedBox.square(dimension: 12), + RegionShapeButton( + type: RegionType.line, + selectedIcon: Icon(Icons.polyline), + unselectedIcon: Icon(Icons.polyline_outlined), + tooltip: 'Line', + ), + SizedBox.square(dimension: 12), + RegionShapeButton( + type: RegionType.customPolygon, + selectedIcon: Icon(Icons.pentagon), + unselectedIcon: Icon(Icons.pentagon_outlined), + tooltip: 'Custom Polygon', + ), + ], + ), + ), + const SizedBox.square(dimension: 12), + Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background, + borderRadius: BorderRadius.circular(1028), + ), + padding: const EdgeInsets.all(12), + child: Consumer( + builder: (context, provider, _) => IconButton( + onPressed: () => provider.regionSelectionMethod = + provider.regionSelectionMethod == + RegionSelectionMethod.useMapCenter + ? RegionSelectionMethod.usePointer + : RegionSelectionMethod.useMapCenter, + icon: Icon( + provider.regionSelectionMethod == + RegionSelectionMethod.useMapCenter + ? Icons.filter_center_focus + : Icons.ads_click, + ), + tooltip: provider.regionSelectionMethod == + RegionSelectionMethod.useMapCenter + ? 'Use Map Center' + : 'Use Pointer', + ), + ), + ), + const SizedBox.square(dimension: 12), + Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background, + borderRadius: BorderRadius.circular(1028), + ), + padding: const EdgeInsets.all(12), + child: Consumer( + builder: (context, provider, _) => Flex( + direction: layoutDirection, + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + onPressed: () => provider + ..clearCoordinates() + ..region = null, + icon: const Icon(Icons.delete_forever), + tooltip: 'Remove All Points', + ), + const SizedBox.square(dimension: 12), + IconButton( + onPressed: () => showModalBottomSheet( + context: context, + builder: (_) => const MinMaxZoomControllerPopup(), + ), + icon: const Icon(Icons.zoom_in), + tooltip: 'Adjust Zoom Levels', + ), + const SizedBox.square(dimension: 12), + IconButton.filled( + onPressed: provider.region != null ? () {} : null, + icon: const Icon(Icons.done), + ), + ], + ), + ), + ), + ], + ), + const SizedBox.square(dimension: 12), + Consumer( + builder: (context, provider, _) => IgnorePointer( + ignoring: provider.regionType != RegionType.line, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 150), + curve: Curves.easeInOut, + opacity: provider.regionType == RegionType.line ? 1 : 0, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background, + borderRadius: BorderRadius.circular(1028), + ), + padding: layoutDirection == Axis.vertical + ? const EdgeInsets.symmetric(vertical: 24, horizontal: 12) + : const EdgeInsets.symmetric( + vertical: 12, + horizontal: 24, + ), + child: Flex( + direction: layoutDirection, + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + if (layoutDirection == Axis.vertical) ...[ + Text('${provider.lineRadius.round()}m'), + const Text('radius'), + ], + if (layoutDirection == Axis.horizontal) + Text('${provider.lineRadius.round()}m radius'), + Expanded( + child: Padding( + padding: layoutDirection == Axis.vertical + ? const EdgeInsets.only(bottom: 12, top: 28) + : const EdgeInsets.only(left: 28, right: 12), + child: RotatedBox( + quarterTurns: + layoutDirection == Axis.vertical ? 3 : 0, + child: SliderTheme( + data: SliderThemeData( + trackShape: CustomSliderTrackShape(), + ), + child: Slider( + value: provider.lineRadius, + onChanged: (v) => provider.lineRadius = v, + min: 100, + max: 5000, + ), + ), + ), + ), + ), + ], + ), + ), + ), + ), + ), + ], + ); +} + +class RegionShapeButton extends StatelessWidget { + const RegionShapeButton({ + super.key, + required this.type, + required this.selectedIcon, + required this.unselectedIcon, + required this.tooltip, + }); + + final RegionType type; + final Icon selectedIcon; + final Icon unselectedIcon; + final String tooltip; + + @override + Widget build(BuildContext context) => Consumer( + builder: (context, provider, _) => IconButton( + isSelected: provider.regionType == type, + onPressed: () => provider + ..regionType = type + ..clearCoordinates(), + icon: unselectedIcon, + selectedIcon: selectedIcon, + tooltip: tooltip, + ), + ); +} diff --git a/example/lib/screens/main/pages/downloader/components/usage_instructions.dart b/example/lib/screens/main/pages/downloader/components/usage_instructions.dart new file mode 100644 index 00000000..bfed45d9 --- /dev/null +++ b/example/lib/screens/main/pages/downloader/components/usage_instructions.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../../../../../shared/misc/region_selection_method.dart'; +import '../../../../../shared/misc/region_type.dart'; +import '../../../../../shared/state/download_provider.dart'; + +class UsageInstructions extends StatelessWidget { + UsageInstructions({ + super.key, + required this.constraints, + }) : layoutDirection = + constraints.maxWidth > 1325 ? Axis.vertical : Axis.horizontal; + + final BoxConstraints constraints; + final Axis layoutDirection; + + @override + Widget build(BuildContext context) => PositionedDirectional( + top: layoutDirection == Axis.vertical ? 0 : 24, + bottom: layoutDirection == Axis.vertical ? 0 : null, + start: layoutDirection == Axis.vertical ? null : 0, + end: layoutDirection == Axis.vertical ? 164 : 0, + child: UnconstrainedBox( + child: Consumer( + builder: (context, provider, _) => AnimatedOpacity( + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + opacity: provider.coordinates.isEmpty ? 1 : 0, + child: IgnorePointer( + child: DecoratedBox( + decoration: BoxDecoration( + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(1 / 3), + spreadRadius: 50, + blurRadius: 90, + ), + ], + ), + child: Flex( + direction: layoutDirection, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + if (layoutDirection == Axis.vertical) + const Icon(Icons.touch_app, size: 68), + if (layoutDirection == Axis.vertical) + const SizedBox.square(dimension: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + 'Tap/click to add ${provider.regionType == RegionType.circle ? 'center' : 'point'} at ${provider.regionSelectionMethod == RegionSelectionMethod.useMapCenter ? 'map center' : 'pointer'}', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 22, + color: Colors.white, + ), + ), + provider.regionType == RegionType.circle + ? const Text( + 'Tap/click again to set radius', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 22, + color: Colors.white, + ), + ) + : const Text( + 'Long press/right click to remove last point', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + color: Colors.white, + ), + ), + ], + ), + if (layoutDirection == Axis.horizontal) + const SizedBox.square(dimension: 12), + if (layoutDirection == Axis.horizontal) + const Icon(Icons.touch_app, size: 68), + ], + ), + ), + ), + ), + ), + ), + ); +} diff --git a/example/lib/screens/main/pages/downloader/downloader.dart b/example/lib/screens/main/pages/downloader/downloader.dart index 4ab5f121..d06bc44a 100644 --- a/example/lib/screens/main/pages/downloader/downloader.dart +++ b/example/lib/screens/main/pages/downloader/downloader.dart @@ -1,8 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import '../../../../shared/state/download_provider.dart'; -import '../../../download_region/download_region.dart'; import 'components/header.dart'; import 'components/map_view.dart'; @@ -39,39 +36,5 @@ class _DownloaderPageState extends State { ), ], ), - floatingActionButton: Consumer( - builder: (context, provider, _) => FloatingActionButton.extended( - onPressed: provider.region == null || - provider.regionTiles == null || - provider.regionTiles == 0 - ? () {} - : () => Navigator.of(context).push( - MaterialPageRoute( - builder: (BuildContext context) => - DownloadRegionPopup(region: provider.region!), - fullscreenDialog: true, - ), - ), - icon: const Icon(Icons.arrow_forward), - label: Padding( - padding: const EdgeInsets.only(left: 10), - child: provider.regionTiles == null - ? SizedBox( - height: 36, - width: 36, - child: Center( - child: SizedBox( - height: 28, - width: 28, - child: CircularProgressIndicator( - color: Theme.of(context).colorScheme.secondary, - ), - ), - ), - ) - : Text('${provider.regionTiles} tiles'), - ), - ), - ), ); } diff --git a/example/lib/screens/main/pages/downloading/components/download_layout.dart b/example/lib/screens/main/pages/downloading/components/download_layout.dart index 2f73d9c7..d5cd855b 100644 --- a/example/lib/screens/main/pages/downloading/components/download_layout.dart +++ b/example/lib/screens/main/pages/downloading/components/download_layout.dart @@ -3,8 +3,8 @@ import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:provider/provider.dart'; +import '../../../../../shared/misc/size_formatter.dart'; import '../../../../../shared/state/download_provider.dart'; -import '../../../../../shared/vars/size_formatter.dart'; import 'main_statistics.dart'; import 'multi_linear_progress_indicator.dart'; import 'stat_display.dart'; @@ -189,7 +189,7 @@ class DownloadLayout extends StatelessWidget { ), Expanded( child: RepaintBoundary( - child: Consumer( + child: Consumer( builder: (context, provider, _) => provider .failedTiles.isEmpty ? const Column( diff --git a/example/lib/screens/main/pages/downloading/components/main_statistics.dart b/example/lib/screens/main/pages/downloading/components/main_statistics.dart index a2015a4c..f3d922db 100644 --- a/example/lib/screens/main/pages/downloading/components/main_statistics.dart +++ b/example/lib/screens/main/pages/downloading/components/main_statistics.dart @@ -114,7 +114,7 @@ class _MainStatisticsState extends State { child: OutlinedButton( onPressed: () { WidgetsBinding.instance.addPostFrameCallback( - (_) => Provider.of( + (_) => Provider.of( context, listen: false, ).setDownloadProgress(null), diff --git a/example/lib/screens/main/pages/downloading/downloading.dart b/example/lib/screens/main/pages/downloading/downloading.dart index af5af1a9..fb9ec165 100644 --- a/example/lib/screens/main/pages/downloading/downloading.dart +++ b/example/lib/screens/main/pages/downloading/downloading.dart @@ -25,7 +25,7 @@ class _DownloadingPageState extends State child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Consumer( + Consumer( builder: (context, provider, _) => Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -48,7 +48,7 @@ class _DownloadingPageState extends State Expanded( child: Padding( padding: const EdgeInsets.all(6), - child: Consumer( + child: Consumer( builder: (context, provider, _) => StreamBuilder( stream: provider.downloadProgress, diff --git a/example/lib/screens/main/pages/recovery/components/recovery_start_button.dart b/example/lib/screens/main/pages/recovery/components/recovery_start_button.dart index 235f875f..19ee6ab4 100644 --- a/example/lib/screens/main/pages/recovery/components/recovery_start_button.dart +++ b/example/lib/screens/main/pages/recovery/components/recovery_start_button.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; -import 'package:provider/provider.dart'; +//import 'package:provider/provider.dart'; -import '../../../../../shared/state/download_provider.dart'; -import '../../../../download_region/download_region.dart'; +//import '../../../../../shared/state/download_provider.dart'; +//import '../../../../download_region/download_region.dart'; class RecoveryStartButton extends StatelessWidget { const RecoveryStartButton({ @@ -33,8 +33,8 @@ class RecoveryStartButton extends StatelessWidget { onPressed: isFailed.data == null ? null : () async { - final DownloadProvider downloadProvider = - Provider.of( + /* final DownloaderProvider downloadProvider = + Provider.of( context, listen: false, ) @@ -60,7 +60,7 @@ class RecoveryStartButton extends StatelessWidget { ), ); - moveToDownloadPage(); + moveToDownloadPage();*/ }, ) : const Padding( diff --git a/example/lib/screens/main/pages/stores/components/store_tile.dart b/example/lib/screens/main/pages/stores/components/store_tile.dart index 6e9e8069..88b622ef 100644 --- a/example/lib/screens/main/pages/stores/components/store_tile.dart +++ b/example/lib/screens/main/pages/stores/components/store_tile.dart @@ -3,8 +3,8 @@ import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; import 'package:fmtc_plus_sharing/fmtc_plus_sharing.dart'; import 'package:provider/provider.dart'; +import '../../../../../shared/misc/size_formatter.dart'; import '../../../../../shared/state/general_provider.dart'; -import '../../../../../shared/vars/size_formatter.dart'; import '../../../../store_editor/store_editor.dart'; import 'stat_display.dart'; diff --git a/example/lib/screens/store_editor/components/header.dart b/example/lib/screens/store_editor/components/header.dart index 30e18ade..9625b02c 100644 --- a/example/lib/screens/store_editor/components/header.dart +++ b/example/lib/screens/store_editor/components/header.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; import 'package:provider/provider.dart'; -import '../../../shared/state/download_provider.dart'; +//import '../../../shared/state/download_provider.dart'; import '../../../shared/state/general_provider.dart'; import '../store_editor.dart'; @@ -50,12 +50,12 @@ AppBar buildHeader({ : await existingStore.manage.rename(newValues['storeName']!); if (!mounted) return; - final downloadProvider = - Provider.of(context, listen: false); - if (existingStore != null && + /*final downloadProvider = + Provider.of(context, listen: false); + if (existingStore != null && downloadProvider.selectedStore == existingStore) { downloadProvider.setSelectedStore(newStore); - } + }*/ await newStore.manage.createAsync(); diff --git a/example/lib/screens/store_editor/store_editor.dart b/example/lib/screens/store_editor/store_editor.dart index c6632b2a..4b4041ab 100644 --- a/example/lib/screens/store_editor/store_editor.dart +++ b/example/lib/screens/store_editor/store_editor.dart @@ -44,7 +44,7 @@ class _StoreEditorPopupState extends State { } @override - Widget build(BuildContext context) => Consumer( + Widget build(BuildContext context) => Consumer( builder: (context, downloadProvider, _) => WillPopScope( onWillPop: () async { scaffoldMessenger.showSnackBar( diff --git a/example/lib/shared/misc/region_selection_method.dart b/example/lib/shared/misc/region_selection_method.dart new file mode 100644 index 00000000..10b42276 --- /dev/null +++ b/example/lib/shared/misc/region_selection_method.dart @@ -0,0 +1,4 @@ +enum RegionSelectionMethod { + useMapCenter, + usePointer, +} diff --git a/example/lib/shared/misc/region_type.dart b/example/lib/shared/misc/region_type.dart new file mode 100644 index 00000000..171bad1a --- /dev/null +++ b/example/lib/shared/misc/region_type.dart @@ -0,0 +1,6 @@ +enum RegionType { + square, + circle, + line, + customPolygon, +} diff --git a/example/lib/shared/vars/size_formatter.dart b/example/lib/shared/misc/size_formatter.dart similarity index 100% rename from example/lib/shared/vars/size_formatter.dart rename to example/lib/shared/misc/size_formatter.dart diff --git a/example/lib/shared/state/download_provider.dart b/example/lib/shared/state/download_provider.dart index 92af8e65..df3e1ab7 100644 --- a/example/lib/shared/state/download_provider.dart +++ b/example/lib/shared/state/download_provider.dart @@ -1,19 +1,88 @@ import 'dart:async'; +import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; import 'package:latlong2/latlong.dart'; -import '../vars/region_mode.dart'; +import '../misc/region_selection_method.dart'; +import '../misc/region_type.dart'; + +class DownloaderProvider extends ChangeNotifier { + RegionSelectionMethod _regionSelectionMethod = + Platform.isAndroid || Platform.isIOS + ? RegionSelectionMethod.useMapCenter + : RegionSelectionMethod.usePointer; + RegionSelectionMethod get regionSelectionMethod => _regionSelectionMethod; + set regionSelectionMethod(RegionSelectionMethod newMethod) { + _regionSelectionMethod = newMethod; + notifyListeners(); + } + + LatLng _currentNewPointPos = const LatLng(51.509364, -0.128928); + LatLng get currentNewPointPos => _currentNewPointPos; + set currentNewPointPos(LatLng newPos) { + _currentNewPointPos = newPos; + notifyListeners(); + } + + RegionType _regionType = RegionType.square; + RegionType get regionType => _regionType; + set regionType(RegionType newType) { + _regionType = newType; + notifyListeners(); + } + + BaseRegion? _region; + BaseRegion? get region => _region; + set region(BaseRegion? newRegion) { + _region = newRegion; + notifyListeners(); + } + + final List _coordinates = []; + List get coordinates => List.from(_coordinates); + List addCoordinate(LatLng coord) { + _coordinates.add(coord); + notifyListeners(); + return _coordinates; + } -class DownloadProvider extends ChangeNotifier { - RegionMode _regionMode = RegionMode.square; - RegionMode get regionMode => _regionMode; - set regionMode(RegionMode newMode) { - _regionMode = newMode; + void clearCoordinates() { + _coordinates.clear(); + _region = null; notifyListeners(); } + void removeLastCoordinate() { + _coordinates.removeLast(); + if (_regionType == RegionType.customPolygon + ? !isCustomPolygonComplete + : _coordinates.length < 2) _region = null; + notifyListeners(); + } + + double _lineRadius = 100; + double get lineRadius => _lineRadius; + set lineRadius(double newNum) { + _lineRadius = newNum; + notifyListeners(); + } + + bool _customPolygonSnap = false; + bool get customPolygonSnap => _customPolygonSnap; + set customPolygonSnap(bool newState) { + _customPolygonSnap = newState; + notifyListeners(); + } + + bool get isCustomPolygonComplete => + _regionType == RegionType.customPolygon && + _coordinates.length >= 2 && + _coordinates.first == _coordinates.last; + + // OLD + double _lineRegionRadius = 1000; double get lineRegionRadius => _lineRegionRadius; set lineRegionRadius(double newNum) { @@ -28,13 +97,6 @@ class DownloadProvider extends ChangeNotifier { notifyListeners(); } - BaseRegion? _region; - BaseRegion? get region => _region; - set region(BaseRegion? newRegion) { - _region = newRegion; - notifyListeners(); - } - int? _regionTiles; int? get regionTiles => _regionTiles; set regionTiles(int? newNum) { diff --git a/example/lib/shared/vars/region_mode.dart b/example/lib/shared/vars/region_mode.dart deleted file mode 100644 index 1af1233a..00000000 --- a/example/lib/shared/vars/region_mode.dart +++ /dev/null @@ -1,7 +0,0 @@ -enum RegionMode { - square, - rectangleVertical, - rectangleHorizontal, - circle, - line, -} From ebff008e5eb72b4f969e90decb0a08fb56705362 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 3 Aug 2023 13:42:11 +0100 Subject: [PATCH 056/168] Improved example application Former-commit-id: a2199540ec4a9b3eddbd07454fa2187b138dd473 [formerly 05de5aaa394209bb790eb7f09779dff7cc1bd64a] Former-commit-id: 04e0e8758d922bb9026dde24bae356d0f16484e7 --- .../components/region_information.dart | 182 +++-- .../download_region/download_region.dart | 23 +- .../pages/downloader/components/header.dart | 38 - .../pages/downloader/components/map_view.dart | 666 ------------------ .../min_max_zoom_controller_popup.dart | 115 --- .../downloader/components/side_panel.dart | 256 ------- .../additional_panes/additional_pane.dart | 38 + .../adjust_zoom_lvls_pane.dart | 56 ++ .../additional_panes/line_region_pane.dart | 100 +++ .../additional_panes/slider_panel_base.dart | 55 ++ .../custom_slider_track_shape.dart | 4 +- .../components/side_panel/parent.dart | 66 ++ .../components/side_panel/primary_pane.dart | 188 +++++ .../side_panel/region_shape_button.dart | 28 + .../components/usage_instructions.dart | 138 ++-- .../main/pages/downloader/downloader.dart | 38 +- .../main/pages/downloader/map_view.dart | 276 ++++++++ .../lib/screens/main/pages/map/map_view.dart | 13 +- .../screens/main/pages/recovery/recovery.dart | 2 +- .../lib/screens/main/pages/stores/stores.dart | 4 +- .../screens/store_editor/store_editor.dart | 41 +- .../shared/components/loading_indicator.dart | 17 +- .../lib/shared/state/download_provider.dart | 38 +- example/pubspec.yaml | 2 + .../tile_loops/custom_polygon_tools/bres.dart | 3 + lib/src/regions/base_region.dart | 1 - lib/src/regions/circle.dart | 1 - lib/src/regions/custom_polygon.dart | 1 - lib/src/regions/line.dart | 1 - lib/src/regions/recovered_region.dart | 1 - lib/src/regions/rectangle.dart | 1 - 31 files changed, 1096 insertions(+), 1297 deletions(-) delete mode 100644 example/lib/screens/main/pages/downloader/components/header.dart delete mode 100644 example/lib/screens/main/pages/downloader/components/map_view.dart delete mode 100644 example/lib/screens/main/pages/downloader/components/min_max_zoom_controller_popup.dart delete mode 100644 example/lib/screens/main/pages/downloader/components/side_panel.dart create mode 100644 example/lib/screens/main/pages/downloader/components/side_panel/additional_panes/additional_pane.dart create mode 100644 example/lib/screens/main/pages/downloader/components/side_panel/additional_panes/adjust_zoom_lvls_pane.dart create mode 100644 example/lib/screens/main/pages/downloader/components/side_panel/additional_panes/line_region_pane.dart create mode 100644 example/lib/screens/main/pages/downloader/components/side_panel/additional_panes/slider_panel_base.dart rename example/lib/screens/main/pages/downloader/components/{ => side_panel}/custom_slider_track_shape.dart (83%) create mode 100644 example/lib/screens/main/pages/downloader/components/side_panel/parent.dart create mode 100644 example/lib/screens/main/pages/downloader/components/side_panel/primary_pane.dart create mode 100644 example/lib/screens/main/pages/downloader/components/side_panel/region_shape_button.dart create mode 100644 example/lib/screens/main/pages/downloader/map_view.dart diff --git a/example/lib/screens/download_region/components/region_information.dart b/example/lib/screens/download_region/components/region_information.dart index f14bdbbc..e12c92e5 100644 --- a/example/lib/screens/download_region/components/region_information.dart +++ b/example/lib/screens/download_region/components/region_information.dart @@ -1,18 +1,47 @@ +// ignore_for_file: implementation_imports + +import 'dart:math'; + +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; +import 'package:flutter_map_tile_caching/src/bulk_download/tile_loops/custom_polygon_tools/earcut.dart'; import 'package:intl/intl.dart'; import 'package:latlong2/latlong.dart'; -import 'package:provider/provider.dart'; -import '../../../shared/state/download_provider.dart'; - -class RegionInformation extends StatelessWidget { +class RegionInformation extends StatefulWidget { const RegionInformation({ super.key, required this.region, + required this.minZoom, + required this.maxZoom, }); final BaseRegion region; + final int minZoom; + final int maxZoom; + + @override + State createState() => _RegionInformationState(); +} + +class _RegionInformationState extends State { + final distance = const Distance(roundResult: false).distance; + + late Future numOfTiles; + + @override + void initState() { + super.initState(); + numOfTiles = FMTC.instance('').download.check( + widget.region.toDownloadable( + minZoom: widget.minZoom, + maxZoom: widget.maxZoom, + options: TileLayer(), + ), + ); + } @override Widget build(BuildContext context) => Column( @@ -25,8 +54,17 @@ class RegionInformation extends StatelessWidget { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - ...region.when( + ...widget.region.when( rectangle: (rectangle) => [ + const Text('TOTAL AREA'), + Text( + '${(distance(rectangle.bounds.northWest, rectangle.bounds.northEast) * distance(rectangle.bounds.northEast, rectangle.bounds.southEast) / 1000000).toStringAsFixed(3)} km²', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 24, + ), + ), + const SizedBox(height: 10), const Text('APPROX. NORTH WEST'), Text( '${rectangle.bounds.northWest.latitude.toStringAsFixed(3)}, ${rectangle.bounds.northWest.longitude.toStringAsFixed(3)}', @@ -46,9 +84,9 @@ class RegionInformation extends StatelessWidget { ), ], circle: (circle) => [ - const Text('APPROX. CENTER'), + const Text('TOTAL AREA'), Text( - '${circle.center.latitude.toStringAsFixed(3)}, ${circle.center.longitude.toStringAsFixed(3)}', + '${(pi * pow(circle.radius, 2)).toStringAsFixed(3)} km²', style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 24, @@ -63,13 +101,22 @@ class RegionInformation extends StatelessWidget { fontSize: 24, ), ), + const SizedBox(height: 10), + const Text('APPROX. CENTER'), + Text( + '${circle.center.latitude.toStringAsFixed(3)}, ${circle.center.longitude.toStringAsFixed(3)}', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 24, + ), + ), ], line: (line) { - const distCalc = Distance(roundResult: false); double totalDistance = 0; + for (int i = 0; i < line.line.length - 1; i++) { totalDistance += - distCalc.distance(line.line[i], line.line[i + 1]); + distance(line.line[i], line.line[i + 1]); } return [ @@ -101,74 +148,77 @@ class RegionInformation extends StatelessWidget { ), ]; }, - customPolygon: (_) => throw UnimplementedError(), - ), - const SizedBox(height: 10), - const Text('MIN/MAX ZOOM LEVELS'), - Consumer( - builder: (context, provider, _) => - provider.regionTiles == null - ? Padding( - padding: const EdgeInsets.only(top: 4), - child: SizedBox( - height: 36, - width: 36, - child: Center( - child: SizedBox( - height: 28, - width: 28, - child: CircularProgressIndicator( - color: Theme.of(context) - .colorScheme - .secondary, - ), - ), - ), - ), - ) - : Text( - '${provider.minZoom} - ${provider.maxZoom}', - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 24, - ), - ), + customPolygon: (customPolygon) { + double area = 0; + + for (final triangle in Earcut.triangulateFromPoints( + customPolygon.outline + .map(const Epsg3857().projection.project), + ).map(customPolygon.outline.elementAt).slices(3)) { + final a = distance(triangle[0], triangle[1]); + final b = distance(triangle[1], triangle[2]); + final c = distance(triangle[2], triangle[0]); + + area += 0.25 * + sqrt( + 4 * a * a * b * b - pow(a * a + b * b - c * c, 2), + ); + } + + return [ + const Text('TOTAL AREA'), + Text( + '${(area / 1000000).toStringAsFixed(3)} km²', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 24, + ), + ), + ]; + }, ), ], ), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ + const Text('MIN/MAX ZOOM LEVELS'), + Text( + '${widget.minZoom} - ${widget.maxZoom}', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 24, + ), + ), + const SizedBox(height: 10), const Text('TOTAL TILES'), - Consumer( - builder: (context, provider, _) => - provider.regionTiles == null - ? Padding( - padding: const EdgeInsets.only(top: 4), + FutureBuilder( + future: numOfTiles, + builder: (context, snapshot) => snapshot.data == null + ? Padding( + padding: const EdgeInsets.only(top: 4), + child: SizedBox( + height: 36, + width: 36, + child: Center( child: SizedBox( - height: 36, - width: 36, - child: Center( - child: SizedBox( - height: 28, - width: 28, - child: CircularProgressIndicator( - color: Theme.of(context) - .colorScheme - .secondary, - ), - ), + height: 28, + width: 28, + child: CircularProgressIndicator( + color: + Theme.of(context).colorScheme.secondary, ), ), - ) - : Text( - NumberFormat('###,###') - .format(provider.regionTiles), - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 24, - ), ), + ), + ) + : Text( + NumberFormat('###,###').format(snapshot.data), + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 24, + ), + ), ), ], ), diff --git a/example/lib/screens/download_region/download_region.dart b/example/lib/screens/download_region/download_region.dart index 180273c1..02ef7e60 100644 --- a/example/lib/screens/download_region/download_region.dart +++ b/example/lib/screens/download_region/download_region.dart @@ -5,7 +5,6 @@ import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; import 'package:provider/provider.dart'; import '../../shared/state/download_provider.dart'; -import '../../shared/state/general_provider.dart'; import 'components/region_information.dart'; import 'components/section_separator.dart'; import 'components/store_selector.dart'; @@ -14,9 +13,13 @@ class DownloadRegionPopup extends StatefulWidget { const DownloadRegionPopup({ super.key, required this.region, + required this.minZoom, + required this.maxZoom, }); final BaseRegion region; + final int minZoom; + final int maxZoom; @override State createState() => _DownloadRegionPopupState(); @@ -25,18 +28,6 @@ class DownloadRegionPopup extends StatefulWidget { class _DownloadRegionPopupState extends State { bool isReady = false; - @override - void didChangeDependencies() { - final String? currentStore = - Provider.of(context, listen: false).currentStore; - if (currentStore != null) { - Provider.of(context, listen: false) - .setSelectedStore(FMTC.instance(currentStore), notify: false); - } - - super.didChangeDependencies(); - } - @override Widget build(BuildContext context) => Consumer( builder: (context, provider, _) => Scaffold( @@ -147,7 +138,11 @@ class _DownloadRegionPopupState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - RegionInformation(region: widget.region), + RegionInformation( + region: widget.region, + minZoom: widget.minZoom, + maxZoom: widget.maxZoom, + ), const SectionSeparator(), const StoreSelector(), const SectionSeparator(), diff --git a/example/lib/screens/main/pages/downloader/components/header.dart b/example/lib/screens/main/pages/downloader/components/header.dart deleted file mode 100644 index 9e60902c..00000000 --- a/example/lib/screens/main/pages/downloader/components/header.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:google_fonts/google_fonts.dart'; -import 'package:provider/provider.dart'; - -import '../../../../../shared/state/general_provider.dart'; - -class Header extends StatelessWidget { - const Header({ - super.key, - }); - - @override - Widget build(BuildContext context) => Row( - children: [ - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Downloader', - style: GoogleFonts.openSans( - fontWeight: FontWeight.bold, - fontSize: 24, - ), - ), - Consumer( - builder: (context, provider, _) => provider.currentStore == null - ? const SizedBox.shrink() - : const Text( - 'Existing tiles will appear in red', - style: TextStyle(fontStyle: FontStyle.italic), - ), - ), - ], - ), - ], - ); -} diff --git a/example/lib/screens/main/pages/downloader/components/map_view.dart b/example/lib/screens/main/pages/downloader/components/map_view.dart deleted file mode 100644 index b1b94c66..00000000 --- a/example/lib/screens/main/pages/downloader/components/map_view.dart +++ /dev/null @@ -1,666 +0,0 @@ -import 'dart:math'; - -import 'package:flutter/material.dart'; -import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/plugin_api.dart'; -import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; -import 'package:latlong2/latlong.dart'; -import 'package:provider/provider.dart'; - -import '../../../../../shared/misc/region_selection_method.dart'; -import '../../../../../shared/misc/region_type.dart'; -import '../../../../../shared/state/download_provider.dart'; -import '../../map/build_attribution.dart'; -import 'crosshairs.dart'; -import 'custom_polygon_snapping_indicator.dart'; -import 'region_shape.dart'; -import 'side_panel.dart'; -import 'usage_instructions.dart'; - -class MapView extends StatefulWidget { - const MapView({super.key}); - - @override - State createState() => _MapViewState(); -} - -class _MapViewState extends State { - final mapController = MapController(); - final urlTemplate = 'https://tile.openstreetmap.org/{z}/{x}/{y}.png'; - - late final mapOptions = MapOptions( - initialCenter: const LatLng(51.509364, -0.128928), - initialZoom: 11, - maxZoom: 20, - cameraConstraint: CameraConstraint.contain( - bounds: LatLngBounds.fromPoints([ - const LatLng(-90, 180), - const LatLng(90, 180), - const LatLng(90, -180), - const LatLng(-90, -180), - ]), - ), - interactionOptions: const InteractionOptions( - flags: InteractiveFlag.all & - ~InteractiveFlag.rotate & - ~InteractiveFlag.doubleTapZoom, - scrollWheelVelocity: 0.002, - ), - keepAlive: true, - onTap: (_, __) { - final provider = Provider.of(context, listen: false); - - if (provider.isCustomPolygonComplete) return; - - final List coords; - if (provider.customPolygonSnap && - provider.regionType == RegionType.customPolygon) { - coords = provider.addCoordinate(provider.coordinates.first); - provider.customPolygonSnap = false; - } else { - coords = provider.addCoordinate(provider.currentNewPointPos); - } - - if (coords.length < 2) return; - - switch (provider.regionType) { - case RegionType.square: - if (coords.length == 2) { - provider.region = RectangleRegion(LatLngBounds.fromPoints(coords)); - break; - } - provider - ..clearCoordinates() - ..addCoordinate(provider.currentNewPointPos); - - break; - case RegionType.circle: - if (coords.length == 2) { - provider.region = CircleRegion( - coords[0], - const Distance(roundResult: false) - .distance(coords[0], coords[1]) / - 1000, - ); - break; - } - provider - ..clearCoordinates() - ..addCoordinate(provider.currentNewPointPos); - - break; - case RegionType.line: - provider.region = LineRegion(coords, provider.lineRadius); - break; - case RegionType.customPolygon: - if (!provider.isCustomPolygonComplete) break; - provider.region = CustomPolygonRegion(coords); - break; - } - }, - onSecondaryTap: (_, __) => - Provider.of(context, listen: false) - .removeLastCoordinate(), - onLongPress: (_, __) => - Provider.of(context, listen: false) - .removeLastCoordinate(), - onPointerHover: (evt, point) { - final provider = Provider.of(context, listen: false); - - if (provider.regionSelectionMethod == RegionSelectionMethod.usePointer) { - provider.currentNewPointPos = point; - } - - if (provider.regionType == RegionType.customPolygon) { - final coords = provider.coordinates; - if (coords.length > 1) { - final newPointPos = - mapController.camera.latLngToScreenPoint(coords.first).toOffset(); - provider.customPolygonSnap = coords.first != coords.last && - sqrt( - pow(newPointPos.dx - evt.localPosition.dx, 2) + - pow(newPointPos.dy - evt.localPosition.dy, 2), - ) < - 15; - } - } - }, - onPositionChanged: (position, _) { - final provider = Provider.of(context, listen: false); - - if (provider.regionSelectionMethod == - RegionSelectionMethod.useMapCenter) { - provider.currentNewPointPos = position.center!; - } - - if (provider.regionType == RegionType.customPolygon) { - final coords = provider.coordinates; - if (coords.length > 1) { - final newPointPos = - mapController.camera.latLngToScreenPoint(coords.first).toOffset(); - final centerPos = mapController.camera - .latLngToScreenPoint(provider.currentNewPointPos) - .toOffset(); - provider.customPolygonSnap = coords.first != coords.last && - sqrt( - pow(newPointPos.dx - centerPos.dx, 2) + - pow(newPointPos.dy - centerPos.dy, 2), - ) < - 30; - } - } - }, - ); - - @override - Widget build(BuildContext context) => LayoutBuilder( - builder: (context, constraints) => Stack( - children: [ - MouseRegion( - opaque: false, - cursor: context.select( - (p) => p.regionSelectionMethod, - ) == - RegionSelectionMethod.useMapCenter - ? MouseCursor.defer - : context.select( - (p) => p.customPolygonSnap, - ) - ? SystemMouseCursors.none - : SystemMouseCursors.precise, - child: FlutterMap( - mapController: mapController, - options: mapOptions, - nonRotatedChildren: buildStdAttribution( - urlTemplate, - alignment: AttributionAlignment.bottomLeft, - ), - children: [ - TileLayer( - urlTemplate: urlTemplate, - maxZoom: 20, - //reset: generalProvider.resetController.stream, - keepBuffer: 5, - userAgentPackageName: 'dev.jaffaketchup.fmtc.demo', - backgroundColor: const Color(0xFFaad3df), - /*tileBuilder: (context, widget, tile) => FutureBuilder( - future: generalProvider.currentStore == null - ? Future.sync(() => null) - : FMTC - .instance(generalProvider.currentStore!) - .getTileProvider() - .checkTileCachedAsync( - coords: tile.coordinates, - options: TileLayer( - urlTemplate: urlTemplate, - ), - ), - builder: (context, snapshot) => DecoratedBox( - position: DecorationPosition.foreground, - decoration: BoxDecoration( - color: (snapshot.data ?? false) - ? Colors.deepOrange.withOpacity(0.33) - : Colors.transparent, - ), - child: widget, - ), - ),*/ - ), - const RegionShape(), - const CustomPolygonSnappingIndicator(), - ], - ), - ), - SidePanel(constraints: constraints), - if (context.select( - (p) => p.regionSelectionMethod, - ) == - RegionSelectionMethod.useMapCenter && - !context.select( - (p) => p.customPolygonSnap, - )) - const Center(child: Crosshairs()), - UsageInstructions(constraints: constraints), - ], - ), - ); - /*static const double _shapePadding = 15; - static const _crosshairsMovement = Point(10, 10); - - final _mapKey = GlobalKey>(); - final MapController _mapController = MapController(); - - late final DownloadProvider downloadProvider; - - late final StreamSubscription _polygonVisualizerStream; - late final StreamSubscription _tileCounterTriggerStream; - late final StreamSubscription _manualPolygonRecalcTriggerStream; - - Point? _crosshairsTop; - Point? _crosshairsBottom; - LatLng? _coordsTopLeft; - LatLng? _coordsBottomRight; - LatLng? _center; - double? _radius; - - PolygonLayer _buildTargetPolygon(BaseRegion region) => PolygonLayer( - polygons: [ - Polygon( - points: [ - const LatLng(-90, 180), - const LatLng(90, 180), - const LatLng(90, -180), - const LatLng(-90, -180), - ], - holePointsList: [region.toOutline().toList()], - isFilled: true, - borderColor: Colors.black, - borderStrokeWidth: 2, - color: Theme.of(context).colorScheme.background.withOpacity(2 / 3), - ), - ], - ); - - @override - void initState() { - super.initState(); - - downloadProvider = Provider.of(context, listen: false); - - _manualPolygonRecalcTriggerStream = - downloadProvider.manualPolygonRecalcTrigger.stream.listen((_) { - if (downloadProvider.regionMode == RegionMode.line) { - _updateLineRegion(); - return; - } - _updatePointLatLng(); - _countTiles(); - }); - - _polygonVisualizerStream = _mapController.mapEventStream.listen((_) { - if (downloadProvider.regionMode != RegionMode.line) { - _updatePointLatLng(); - } - }); - - _tileCounterTriggerStream = _mapController.mapEventStream - .debounce(const Duration(seconds: 1)) - .listen((_) { - if (downloadProvider.regionMode != RegionMode.line) _countTiles(); - }); - } - - @override - void dispose() { - super.dispose(); - - _polygonVisualizerStream.cancel(); - _tileCounterTriggerStream.cancel(); - _manualPolygonRecalcTriggerStream.cancel(); - } - - @override - Widget build(BuildContext context) => - Consumer2( - key: _mapKey, - builder: (context, generalProvider, downloadProvider, _) => - FutureBuilder?>( - future: generalProvider.currentStore == null - ? Future.sync(() => {}) - : FMTC.instance(generalProvider.currentStore!).metadata.readAsync, - builder: (context, metadata) { - if (!metadata.hasData || - metadata.data == null || - (generalProvider.currentStore != null && - (metadata.data ?? {}).isEmpty)) { - return const LoadingIndicator( - message: - 'Loading Settings...\n\nSeeing this screen for a long time?\nThere may be a misconfiguration of the\nstore. Try disabling caching and deleting\n faulty stores.', - ); - } - - final String urlTemplate = - generalProvider.currentStore != null && metadata.data != null - ? metadata.data!['sourceURL']! - : 'https://tile.openstreetmap.org/{z}/{x}/{y}.png'; - - return Stack( - children: [ - FlutterMap( - mapController: _mapController, - options: MapOptions( - initialCenter: const LatLng(51.509364, -0.128928), - initialZoom: 9.2, - maxZoom: 22, - cameraConstraint: CameraConstraint.contain( - bounds: LatLngBounds.fromPoints([ - const LatLng(-90, 180), - const LatLng(90, 180), - const LatLng(90, -180), - const LatLng(-90, -180), - ]), - ), - interactionOptions: const InteractionOptions( - flags: InteractiveFlag.all & ~InteractiveFlag.rotate, - scrollWheelVelocity: 0.002, - ), - onMapReady: () { - WidgetsBinding.instance - .addPostFrameCallback((_) => _updatePointLatLng()); - _countTiles(); - }, - onTap: (_, point) => _addLinePoint(point), - onSecondaryTap: (_, point) => _removeLinePoint(), - onLongPress: (_, point) => _removeLinePoint(), - keepAlive: true, - ), - nonRotatedChildren: buildStdAttribution( - urlTemplate, - alignment: AttributionAlignment.bottomLeft, - ), - children: [ - TileLayer( - urlTemplate: urlTemplate, - maxZoom: 20, - reset: generalProvider.resetController.stream, - keepBuffer: 5, - userAgentPackageName: 'dev.jaffaketchup.fmtc.demo', - backgroundColor: const Color(0xFFaad3df), - tileBuilder: (context, widget, tile) => - FutureBuilder( - future: generalProvider.currentStore == null - ? Future.sync(() => null) - : FMTC - .instance(generalProvider.currentStore!) - .getTileProvider() - .checkTileCachedAsync( - coords: tile.coordinates, - options: TileLayer( - urlTemplate: urlTemplate, - ), - ), - builder: (context, snapshot) => DecoratedBox( - position: DecorationPosition.foreground, - decoration: BoxDecoration( - color: (snapshot.data ?? false) - ? Colors.deepOrange.withOpacity(0.33) - : Colors.transparent, - ), - child: widget, - ), - ), - ), - if (downloadProvider.regionMode == RegionMode.line) - LineRegion( - downloadProvider.lineRegionPoints, - downloadProvider.lineRegionRadius, - ).toDrawable( - borderColor: Colors.black, - borderStrokeWidth: 2, - fillColor: Colors.green.withOpacity(2 / 3), - prettyPaint: false, - ) - else if (_coordsTopLeft != null && - _coordsBottomRight != null && - downloadProvider.regionMode != RegionMode.circle) - _buildTargetPolygon( - RectangleRegion( - LatLngBounds(_coordsTopLeft!, _coordsBottomRight!), - ), - ) - else if (_center != null && - _radius != null && - downloadProvider.regionMode == RegionMode.circle) - _buildTargetPolygon(CircleRegion(_center!, _radius!)) - ], - ), - if (downloadProvider.regionMode != RegionMode.line && - _crosshairsTop != null && - _crosshairsBottom != null) ...[ - Positioned( - top: _crosshairsTop!.y, - left: _crosshairsTop!.x, - child: const Crosshairs(), - ), - Positioned( - top: _crosshairsBottom!.y, - left: _crosshairsBottom!.x, - child: const Crosshairs(), - ), - ], - if (downloadProvider.regionMode == RegionMode.line) - Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.background, - borderRadius: BorderRadius.circular(16), - ), - width: double.infinity, - margin: const EdgeInsets.all(16), - padding: - const EdgeInsets.symmetric(horizontal: 16, vertical: 4), - child: IntrinsicHeight( - child: Row( - children: [ - const Text('Radius'), - Expanded( - child: Slider( - value: downloadProvider.lineRegionRadius, - min: 50, - max: 5000, - divisions: 99, - label: - '${downloadProvider.lineRegionRadius.round()} meters', - onChanged: (val) => setState( - () => downloadProvider.lineRegionRadius = val, - ), - onChangeEnd: (val) { - downloadProvider.lineRegionRadius = val; - _updateLineRegion(); - }, - ), - ), - const VerticalDivider(), - IconButton( - onPressed: () async { - if (Platform.isAndroid || Platform.isIOS) { - await FilePicker.platform.clearTemporaryFiles(); - } - - late final FilePickerResult? result; - try { - result = await FilePicker.platform.pickFiles( - dialogTitle: 'Parse From GPX', - type: FileType.custom, - allowedExtensions: ['gpx', 'kml'], - allowMultiple: true, - ); - } on PlatformException catch (_) { - result = await FilePicker.platform.pickFiles( - dialogTitle: 'Parse From GPX', - allowMultiple: true, - ); - } - - if (result != null) { - final gpxReader = GpxReader(); - for (final path - in result.files.map((e) => e.path)) { - downloadProvider.lineRegionPoints.addAll( - gpxReader - .fromString( - await File(path!).readAsString(), - ) - .trks - .map( - (e) => e.trksegs.map( - (e) => e.trkpts.map( - (e) => LatLng(e.lat!, e.lon!), - ), - ), - ) - .expand((e) => e) - .expand((e) => e), - ); - _updateLineRegion(); - } - } - }, - icon: const Icon(Icons.download), - tooltip: 'Import from GPX', - ), - IconButton( - onPressed: () { - downloadProvider.lineRegionPoints.clear(); - _updateLineRegion(); - }, - icon: const Icon(Icons.cancel), - tooltip: 'Clear existing points', - ), - ], - ), - ), - ), - ], - ); - }, - ), - ); - - void _updateLineRegion() { - downloadProvider.region = LineRegion( - downloadProvider.lineRegionPoints, - downloadProvider.lineRegionRadius, - ); - - _countTiles(); - } - - void _removeLinePoint() { - if (downloadProvider.regionMode == RegionMode.line) { - downloadProvider.lineRegionPoints.removeLast(); - _updateLineRegion(); - } - } - - void _addLinePoint(LatLng coord) { - if (downloadProvider.regionMode == RegionMode.line) { - downloadProvider.lineRegionPoints.add(coord); - _updateLineRegion(); - } - } - - void _updatePointLatLng() { - if (downloadProvider.regionMode == RegionMode.line || - _mapKey.currentContext == null) return; - - final Size mapSize = _mapKey.currentContext!.size!; - final bool isHeightLongestSide = mapSize.width < mapSize.height; - - final centerNormal = Point(mapSize.width / 2, mapSize.height / 2); - final centerInversed = Point(mapSize.height / 2, mapSize.width / 2); - - late final Point calculatedTopLeft; - late final Point calculatedBottomRight; - - switch (downloadProvider.regionMode) { - case RegionMode.square: - final double offset = (mapSize.shortestSide - (_shapePadding * 2)) / 2; - - calculatedTopLeft = Point( - centerNormal.x - offset, - centerNormal.y - offset, - ); - calculatedBottomRight = Point( - centerNormal.x + offset, - centerNormal.y + offset, - ); - break; - case RegionMode.rectangleVertical: - final allowedArea = Size( - mapSize.width - (_shapePadding * 2), - (mapSize.height - (_shapePadding * 2)) / 1.5 - 50, - ); - - calculatedTopLeft = Point( - centerInversed.y - allowedArea.shortestSide / 2, - _shapePadding, - ); - calculatedBottomRight = Point( - centerInversed.y + allowedArea.shortestSide / 2, - mapSize.height - _shapePadding - 25, - ); - break; - case RegionMode.rectangleHorizontal: - final allowedArea = Size( - mapSize.width - (_shapePadding * 2), - (mapSize.width < mapSize.height + 250) - ? (mapSize.width - (_shapePadding * 2)) / 1.75 - : (mapSize.height - (_shapePadding * 2) - 0), - ); - - calculatedTopLeft = Point( - _shapePadding, - centerNormal.y - allowedArea.height / 2, - ); - calculatedBottomRight = Point( - mapSize.width - _shapePadding, - centerNormal.y + allowedArea.height / 2 - 25, - ); - break; - case RegionMode.circle: - final allowedArea = - Size.square(mapSize.shortestSide - (_shapePadding * 2)); - - final calculatedTop = Point( - centerNormal.x, - (isHeightLongestSide ? centerNormal.y : centerInversed.x) - - allowedArea.width / 2, - ); - - _crosshairsTop = calculatedTop - _crosshairsMovement; - _crosshairsBottom = centerNormal - _crosshairsMovement; - - _center = _mapController.camera.pointToLatLng(centerNormal); - _radius = const Distance(roundResult: false).distance( - _center!, - _mapController.camera.pointToLatLng(calculatedTop), - ) / - 1000; - setState(() {}); - break; - case RegionMode.line: - break; - } - - if (downloadProvider.regionMode != RegionMode.circle) { - _crosshairsTop = calculatedTopLeft - _crosshairsMovement; - _crosshairsBottom = calculatedBottomRight - _crosshairsMovement; - - _coordsTopLeft = _mapController.camera.pointToLatLng(calculatedTopLeft); - _coordsBottomRight = - _mapController.camera.pointToLatLng(calculatedBottomRight); - - setState(() {}); - } - - downloadProvider.region = downloadProvider.regionMode == RegionMode.circle - ? CircleRegion(_center!, _radius!) - : RectangleRegion( - LatLngBounds(_coordsTopLeft!, _coordsBottomRight!), - ); - } - - Future _countTiles() async { - if (downloadProvider.region != null) { - downloadProvider - ..regionTiles = null - ..regionTiles = await FMTC.instance('').download.check( - downloadProvider.region!.toDownloadable( - minZoom: downloadProvider.minZoom, - maxZoom: downloadProvider.maxZoom, - options: TileLayer(), - ), - ); - } - }*/ -} diff --git a/example/lib/screens/main/pages/downloader/components/min_max_zoom_controller_popup.dart b/example/lib/screens/main/pages/downloader/components/min_max_zoom_controller_popup.dart deleted file mode 100644 index 439bbf70..00000000 --- a/example/lib/screens/main/pages/downloader/components/min_max_zoom_controller_popup.dart +++ /dev/null @@ -1,115 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:google_fonts/google_fonts.dart'; -import 'package:provider/provider.dart'; - -import '../../../../../shared/state/download_provider.dart'; - -class MinMaxZoomControllerPopup extends StatelessWidget { - const MinMaxZoomControllerPopup({ - super.key, - }); - - @override - Widget build(BuildContext context) => Padding( - padding: EdgeInsets.only( - top: 12, - left: 12, - right: 12, - bottom: 12 + MediaQuery.of(context).viewInsets.bottom, - ), - child: Consumer( - child: Text( - 'Change Min/Max Zoom Levels', - style: GoogleFonts.openSans( - fontWeight: FontWeight.bold, - fontSize: 20, - ), - ), - builder: (context, provider, child) => Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - child!, - const SizedBox(height: 10), - TextFormField( - decoration: const InputDecoration( - prefixIcon: Icon(Icons.zoom_out), - label: Text('Minimum Zoom Level'), - ), - validator: (input) { - if (input == null || input.isEmpty) return 'Required'; - if (int.parse(input) < 1) return 'Must be 1 or more'; - if (int.parse(input) > provider.maxZoom) { - return 'Must be less than maximum zoom'; - } - - return null; - }, - onChanged: (input) { - if (input.isNotEmpty) provider.minZoom = int.parse(input); - }, - keyboardType: TextInputType.number, - autovalidateMode: AutovalidateMode.onUserInteraction, - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly, - _NumericalRangeFormatter(min: 1, max: 22), - ], - textInputAction: TextInputAction.next, - initialValue: provider.minZoom.toString(), - ), - const SizedBox(height: 5), - TextFormField( - decoration: const InputDecoration( - prefixIcon: Icon(Icons.zoom_in), - label: Text('Maximum Zoom Level'), - ), - validator: (input) { - if (input == null || input.isEmpty) return 'Required'; - if (int.parse(input) > 22) return 'Must be 22 or less'; - if (int.parse(input) < provider.minZoom) { - return 'Must be more than minimum zoom'; - } - - return null; - }, - onChanged: (input) { - if (input.isNotEmpty) provider.maxZoom = int.parse(input); - }, - keyboardType: TextInputType.number, - autovalidateMode: AutovalidateMode.onUserInteraction, - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly, - _NumericalRangeFormatter(min: 1, max: 22), - ], - textInputAction: TextInputAction.done, - initialValue: provider.maxZoom.toString(), - ), - ], - ), - ), - ); -} - -class _NumericalRangeFormatter extends TextInputFormatter { - final int min; - final int max; - - _NumericalRangeFormatter({ - required this.min, - required this.max, - }); - - @override - TextEditingValue formatEditUpdate( - TextEditingValue oldValue, - TextEditingValue newValue, - ) => - newValue.text == '' - ? newValue - : int.parse(newValue.text) < min - ? TextEditingValue.empty.copyWith(text: min.toString()) - : int.parse(newValue.text) > max - ? TextEditingValue.empty.copyWith(text: max.toString()) - : newValue; -} diff --git a/example/lib/screens/main/pages/downloader/components/side_panel.dart b/example/lib/screens/main/pages/downloader/components/side_panel.dart deleted file mode 100644 index f3d18617..00000000 --- a/example/lib/screens/main/pages/downloader/components/side_panel.dart +++ /dev/null @@ -1,256 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -import '../../../../../shared/misc/region_selection_method.dart'; -import '../../../../../shared/misc/region_type.dart'; -import '../../../../../shared/state/download_provider.dart'; -import 'custom_slider_track_shape.dart'; -import 'min_max_zoom_controller_popup.dart'; - -class SidePanel extends StatelessWidget { - SidePanel({ - super.key, - required this.constraints, - }) : layoutDirection = - constraints.maxWidth > 800 ? Axis.vertical : Axis.horizontal; - - final BoxConstraints constraints; - final Axis layoutDirection; - - @override - Widget build(BuildContext context) => PositionedDirectional( - top: layoutDirection == Axis.vertical ? 12 : null, - bottom: 12, - start: layoutDirection == Axis.vertical ? 24 : 12, - end: layoutDirection == Axis.vertical ? null : 12, - child: Center( - child: layoutDirection == Axis.vertical - ? IntrinsicHeight( - child: PaneGroup(layoutDirection: layoutDirection), - ) - : FittedBox( - child: IntrinsicWidth( - child: PaneGroup(layoutDirection: layoutDirection), - ), - ), - ), - ); -} - -class PaneGroup extends StatelessWidget { - const PaneGroup({ - super.key, - required this.layoutDirection, - }); - - final Axis layoutDirection; - - @override - Widget build(BuildContext context) => Flex( - direction: - layoutDirection == Axis.vertical ? Axis.horizontal : Axis.vertical, - crossAxisAlignment: CrossAxisAlignment.stretch, - verticalDirection: layoutDirection == Axis.horizontal - ? VerticalDirection.up - : VerticalDirection.down, - children: [ - Flex( - direction: layoutDirection, - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.background, - borderRadius: BorderRadius.circular(1028), - ), - padding: const EdgeInsets.all(12), - child: Flex( - direction: layoutDirection, - mainAxisSize: MainAxisSize.min, - children: const [ - RegionShapeButton( - type: RegionType.square, - selectedIcon: Icon(Icons.square), - unselectedIcon: Icon(Icons.square_outlined), - tooltip: 'Rectangle', - ), - SizedBox.square(dimension: 12), - RegionShapeButton( - type: RegionType.circle, - selectedIcon: Icon(Icons.circle), - unselectedIcon: Icon(Icons.circle_outlined), - tooltip: 'Circle', - ), - SizedBox.square(dimension: 12), - RegionShapeButton( - type: RegionType.line, - selectedIcon: Icon(Icons.polyline), - unselectedIcon: Icon(Icons.polyline_outlined), - tooltip: 'Line', - ), - SizedBox.square(dimension: 12), - RegionShapeButton( - type: RegionType.customPolygon, - selectedIcon: Icon(Icons.pentagon), - unselectedIcon: Icon(Icons.pentagon_outlined), - tooltip: 'Custom Polygon', - ), - ], - ), - ), - const SizedBox.square(dimension: 12), - Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.background, - borderRadius: BorderRadius.circular(1028), - ), - padding: const EdgeInsets.all(12), - child: Consumer( - builder: (context, provider, _) => IconButton( - onPressed: () => provider.regionSelectionMethod = - provider.regionSelectionMethod == - RegionSelectionMethod.useMapCenter - ? RegionSelectionMethod.usePointer - : RegionSelectionMethod.useMapCenter, - icon: Icon( - provider.regionSelectionMethod == - RegionSelectionMethod.useMapCenter - ? Icons.filter_center_focus - : Icons.ads_click, - ), - tooltip: provider.regionSelectionMethod == - RegionSelectionMethod.useMapCenter - ? 'Use Map Center' - : 'Use Pointer', - ), - ), - ), - const SizedBox.square(dimension: 12), - Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.background, - borderRadius: BorderRadius.circular(1028), - ), - padding: const EdgeInsets.all(12), - child: Consumer( - builder: (context, provider, _) => Flex( - direction: layoutDirection, - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - onPressed: () => provider - ..clearCoordinates() - ..region = null, - icon: const Icon(Icons.delete_forever), - tooltip: 'Remove All Points', - ), - const SizedBox.square(dimension: 12), - IconButton( - onPressed: () => showModalBottomSheet( - context: context, - builder: (_) => const MinMaxZoomControllerPopup(), - ), - icon: const Icon(Icons.zoom_in), - tooltip: 'Adjust Zoom Levels', - ), - const SizedBox.square(dimension: 12), - IconButton.filled( - onPressed: provider.region != null ? () {} : null, - icon: const Icon(Icons.done), - ), - ], - ), - ), - ), - ], - ), - const SizedBox.square(dimension: 12), - Consumer( - builder: (context, provider, _) => IgnorePointer( - ignoring: provider.regionType != RegionType.line, - child: AnimatedOpacity( - duration: const Duration(milliseconds: 150), - curve: Curves.easeInOut, - opacity: provider.regionType == RegionType.line ? 1 : 0, - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.background, - borderRadius: BorderRadius.circular(1028), - ), - padding: layoutDirection == Axis.vertical - ? const EdgeInsets.symmetric(vertical: 24, horizontal: 12) - : const EdgeInsets.symmetric( - vertical: 12, - horizontal: 24, - ), - child: Flex( - direction: layoutDirection, - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - if (layoutDirection == Axis.vertical) ...[ - Text('${provider.lineRadius.round()}m'), - const Text('radius'), - ], - if (layoutDirection == Axis.horizontal) - Text('${provider.lineRadius.round()}m radius'), - Expanded( - child: Padding( - padding: layoutDirection == Axis.vertical - ? const EdgeInsets.only(bottom: 12, top: 28) - : const EdgeInsets.only(left: 28, right: 12), - child: RotatedBox( - quarterTurns: - layoutDirection == Axis.vertical ? 3 : 0, - child: SliderTheme( - data: SliderThemeData( - trackShape: CustomSliderTrackShape(), - ), - child: Slider( - value: provider.lineRadius, - onChanged: (v) => provider.lineRadius = v, - min: 100, - max: 5000, - ), - ), - ), - ), - ), - ], - ), - ), - ), - ), - ), - ], - ); -} - -class RegionShapeButton extends StatelessWidget { - const RegionShapeButton({ - super.key, - required this.type, - required this.selectedIcon, - required this.unselectedIcon, - required this.tooltip, - }); - - final RegionType type; - final Icon selectedIcon; - final Icon unselectedIcon; - final String tooltip; - - @override - Widget build(BuildContext context) => Consumer( - builder: (context, provider, _) => IconButton( - isSelected: provider.regionType == type, - onPressed: () => provider - ..regionType = type - ..clearCoordinates(), - icon: unselectedIcon, - selectedIcon: selectedIcon, - tooltip: tooltip, - ), - ); -} diff --git a/example/lib/screens/main/pages/downloader/components/side_panel/additional_panes/additional_pane.dart b/example/lib/screens/main/pages/downloader/components/side_panel/additional_panes/additional_pane.dart new file mode 100644 index 00000000..788ed94b --- /dev/null +++ b/example/lib/screens/main/pages/downloader/components/side_panel/additional_panes/additional_pane.dart @@ -0,0 +1,38 @@ +part of '../parent.dart'; + +class _AdditionalPane extends StatelessWidget { + const _AdditionalPane({ + required this.constraints, + required this.layoutDirection, + }); + + final BoxConstraints constraints; + final Axis layoutDirection; + + @override + Widget build(BuildContext context) => Consumer( + builder: (context, provider, _) => Stack( + fit: StackFit.passthrough, + children: [ + _SliderPanelBase( + constraints: constraints, + layoutDirection: layoutDirection, + isVisible: provider.regionType == RegionType.line, + child: layoutDirection == Axis.vertical + ? IntrinsicWidth( + child: LineRegionPane(layoutDirection: layoutDirection), + ) + : IntrinsicHeight( + child: LineRegionPane(layoutDirection: layoutDirection), + ), + ), + _SliderPanelBase( + constraints: constraints, + layoutDirection: layoutDirection, + isVisible: provider.openAdjustZoomLevelsSlider, + child: AdjustZoomLvlsPane(layoutDirection: layoutDirection), + ), + ], + ), + ); +} diff --git a/example/lib/screens/main/pages/downloader/components/side_panel/additional_panes/adjust_zoom_lvls_pane.dart b/example/lib/screens/main/pages/downloader/components/side_panel/additional_panes/adjust_zoom_lvls_pane.dart new file mode 100644 index 00000000..ca2f4a73 --- /dev/null +++ b/example/lib/screens/main/pages/downloader/components/side_panel/additional_panes/adjust_zoom_lvls_pane.dart @@ -0,0 +1,56 @@ +part of '../parent.dart'; + +class AdjustZoomLvlsPane extends StatelessWidget { + const AdjustZoomLvlsPane({ + super.key, + required this.layoutDirection, + }); + + final Axis layoutDirection; + + @override + Widget build(BuildContext context) => Consumer( + builder: (context, provider, _) => Flex( + direction: layoutDirection, + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.zoom_in), + const SizedBox.square(dimension: 4), + Text(provider.maxZoom.toString().padLeft(2, '0')), + Expanded( + child: Padding( + padding: layoutDirection == Axis.vertical + ? const EdgeInsets.only(bottom: 6, top: 6) + : const EdgeInsets.only(left: 6, right: 6), + child: RotatedBox( + quarterTurns: layoutDirection == Axis.vertical ? 3 : 2, + child: SliderTheme( + data: SliderThemeData( + trackShape: _CustomSliderTrackShape(), + showValueIndicator: ShowValueIndicator.never, + ), + child: RangeSlider( + values: RangeValues( + provider.minZoom.toDouble(), + provider.maxZoom.toDouble(), + ), + onChanged: (v) { + provider + ..minZoom = v.start.toInt() + ..maxZoom = v.end.toInt(); + }, + max: 22, + divisions: 23, + ), + ), + ), + ), + ), + Text(provider.minZoom.toString().padLeft(2, '0')), + const SizedBox.square(dimension: 4), + const Icon(Icons.zoom_out), + ], + ), + ); +} diff --git a/example/lib/screens/main/pages/downloader/components/side_panel/additional_panes/line_region_pane.dart b/example/lib/screens/main/pages/downloader/components/side_panel/additional_panes/line_region_pane.dart new file mode 100644 index 00000000..07b84456 --- /dev/null +++ b/example/lib/screens/main/pages/downloader/components/side_panel/additional_panes/line_region_pane.dart @@ -0,0 +1,100 @@ +part of '../parent.dart'; + +class LineRegionPane extends StatelessWidget { + const LineRegionPane({ + super.key, + required this.layoutDirection, + }); + + final Axis layoutDirection; + + @override + Widget build(BuildContext context) => Consumer( + builder: (context, provider, _) => Flex( + direction: layoutDirection, + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + onPressed: () async { + final provider = context.read(); + + if (Platform.isAndroid || Platform.isIOS) { + await FilePicker.platform.clearTemporaryFiles(); + } + + late final FilePickerResult? result; + try { + result = await FilePicker.platform.pickFiles( + dialogTitle: 'Parse From GPX', + type: FileType.custom, + allowedExtensions: ['gpx', 'kml'], + allowMultiple: true, + ); + } on PlatformException catch (_) { + result = await FilePicker.platform.pickFiles( + dialogTitle: 'Parse From GPX', + allowMultiple: true, + ); + } + + if (result != null) { + final gpxReader = GpxReader(); + for (final path in result.files.map((e) => e.path)) { + provider.addCoordinates( + gpxReader + .fromString( + await File(path!).readAsString(), + ) + .trks + .map( + (e) => e.trksegs.map( + (e) => e.trkpts.map( + (e) => LatLng(e.lat!, e.lon!), + ), + ), + ) + .expand((e) => e) + .expand((e) => e), + ); + } + } + }, + icon: const Icon(Icons.route), + tooltip: 'Import from GPX', + ), + layoutDirection == Axis.vertical + ? const Divider(height: 8) + : const VerticalDivider(width: 8), + const SizedBox.square(dimension: 4), + if (layoutDirection == Axis.vertical) ...[ + Text('${provider.lineRadius.round()}m'), + const Text('radius'), + ], + if (layoutDirection == Axis.horizontal) + Text('${provider.lineRadius.round()}m radius'), + Expanded( + child: Padding( + padding: layoutDirection == Axis.vertical + ? const EdgeInsets.only(bottom: 12, top: 28) + : const EdgeInsets.only(left: 28, right: 12), + child: RotatedBox( + quarterTurns: layoutDirection == Axis.vertical ? 3 : 0, + child: SliderTheme( + data: SliderThemeData( + trackShape: _CustomSliderTrackShape(), + ), + child: Slider( + value: provider.lineRadius, + onChanged: (v) => provider.lineRadius = v, + min: 100, + max: 4000, + ), + ), + ), + ), + ), + ], + ), + ); +} diff --git a/example/lib/screens/main/pages/downloader/components/side_panel/additional_panes/slider_panel_base.dart b/example/lib/screens/main/pages/downloader/components/side_panel/additional_panes/slider_panel_base.dart new file mode 100644 index 00000000..17da68f8 --- /dev/null +++ b/example/lib/screens/main/pages/downloader/components/side_panel/additional_panes/slider_panel_base.dart @@ -0,0 +1,55 @@ +part of '../parent.dart'; + +class _SliderPanelBase extends StatelessWidget { + const _SliderPanelBase({ + required this.constraints, + required this.layoutDirection, + required this.isVisible, + required this.child, + }); + + final BoxConstraints constraints; + final Axis layoutDirection; + final bool isVisible; + final Widget child; + + @override + Widget build(BuildContext context) => IgnorePointer( + ignoring: !isVisible, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 200), + curve: Curves.easeInOut, + opacity: isVisible ? 1 : 0, + child: AnimatedSlide( + duration: const Duration(milliseconds: 200), + curve: Curves.easeInOut, + offset: isVisible + ? Offset.zero + : Offset( + layoutDirection == Axis.vertical ? -0.5 : 0, + layoutDirection == Axis.vertical ? 0 : 0.5, + ), + child: Container( + width: layoutDirection == Axis.vertical + ? null + : constraints.maxWidth < 500 + ? constraints.maxWidth + : null, + height: layoutDirection == Axis.horizontal + ? null + : constraints.maxHeight < 500 + ? constraints.maxHeight + : null, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background, + borderRadius: BorderRadius.circular(1028), + ), + padding: layoutDirection == Axis.vertical + ? const EdgeInsets.symmetric(vertical: 22, horizontal: 10) + : const EdgeInsets.symmetric(vertical: 10, horizontal: 22), + child: child, + ), + ), + ), + ); +} diff --git a/example/lib/screens/main/pages/downloader/components/custom_slider_track_shape.dart b/example/lib/screens/main/pages/downloader/components/side_panel/custom_slider_track_shape.dart similarity index 83% rename from example/lib/screens/main/pages/downloader/components/custom_slider_track_shape.dart rename to example/lib/screens/main/pages/downloader/components/side_panel/custom_slider_track_shape.dart index df3f11f3..e8f54d21 100644 --- a/example/lib/screens/main/pages/downloader/components/custom_slider_track_shape.dart +++ b/example/lib/screens/main/pages/downloader/components/side_panel/custom_slider_track_shape.dart @@ -1,7 +1,7 @@ -import 'package:flutter/material.dart'; +part of 'parent.dart'; // From https://stackoverflow.com/a/65662764/11846040 -class CustomSliderTrackShape extends RoundedRectSliderTrackShape { +class _CustomSliderTrackShape extends RoundedRectSliderTrackShape { @override Rect getPreferredRect({ required RenderBox parentBox, diff --git a/example/lib/screens/main/pages/downloader/components/side_panel/parent.dart b/example/lib/screens/main/pages/downloader/components/side_panel/parent.dart new file mode 100644 index 00000000..803014ac --- /dev/null +++ b/example/lib/screens/main/pages/downloader/components/side_panel/parent.dart @@ -0,0 +1,66 @@ +import 'dart:io'; + +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:gpx/gpx.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:provider/provider.dart'; + +import '../../../../../../shared/misc/region_selection_method.dart'; +import '../../../../../../shared/misc/region_type.dart'; +import '../../../../../../shared/state/download_provider.dart'; +import '../../../../../download_region/download_region.dart'; + +part 'additional_panes/additional_pane.dart'; +part 'additional_panes/adjust_zoom_lvls_pane.dart'; +part 'additional_panes/line_region_pane.dart'; +part 'custom_slider_track_shape.dart'; +part 'primary_pane.dart'; +part 'region_shape_button.dart'; +part 'additional_panes/slider_panel_base.dart'; + +class SidePanel extends StatelessWidget { + SidePanel({ + super.key, + required this.constraints, + }) : layoutDirection = + constraints.maxWidth > 850 ? Axis.vertical : Axis.horizontal; + + final BoxConstraints constraints; + final Axis layoutDirection; + + @override + Widget build(BuildContext context) => PositionedDirectional( + top: layoutDirection == Axis.vertical ? 12 : null, + bottom: 12, + start: layoutDirection == Axis.vertical ? 24 : 12, + end: layoutDirection == Axis.vertical ? null : 12, + child: Center( + child: FittedBox( + child: layoutDirection == Axis.vertical + ? IntrinsicHeight( + child: _PrimaryPane( + constraints: constraints, + layoutDirection: layoutDirection, + ), + ) + : IntrinsicWidth( + child: _PrimaryPane( + constraints: constraints, + layoutDirection: layoutDirection, + ), + ), + ), + ), + ); +} + +extension _IterableExt on Iterable { + Iterable interleave(E separator) sync* { + for (int i = 0; i < length; i++) { + yield elementAt(i); + if (i < length - 1) yield separator; + } + } +} diff --git a/example/lib/screens/main/pages/downloader/components/side_panel/primary_pane.dart b/example/lib/screens/main/pages/downloader/components/side_panel/primary_pane.dart new file mode 100644 index 00000000..385b2a80 --- /dev/null +++ b/example/lib/screens/main/pages/downloader/components/side_panel/primary_pane.dart @@ -0,0 +1,188 @@ +part of 'parent.dart'; + +class _PrimaryPane extends StatelessWidget { + const _PrimaryPane({ + required this.constraints, + required this.layoutDirection, + }); + + final BoxConstraints constraints; + final Axis layoutDirection; + + static const regionShapes = { + RegionType.square: ( + selectedIcon: Icons.square, + unselectedIcon: Icons.square_outlined, + label: 'Rectangle', + ), + RegionType.circle: ( + selectedIcon: Icons.circle, + unselectedIcon: Icons.circle_outlined, + label: 'Circle', + ), + RegionType.line: ( + selectedIcon: Icons.polyline, + unselectedIcon: Icons.polyline_outlined, + label: 'Polyline + Radius', + ), + RegionType.customPolygon: ( + selectedIcon: Icons.pentagon, + unselectedIcon: Icons.pentagon_outlined, + label: 'Polygon', + ), + }; + + @override + Widget build(BuildContext context) => Flex( + direction: + layoutDirection == Axis.vertical ? Axis.horizontal : Axis.vertical, + crossAxisAlignment: CrossAxisAlignment.stretch, + verticalDirection: layoutDirection == Axis.horizontal + ? VerticalDirection.up + : VerticalDirection.down, + children: [ + Flex( + direction: layoutDirection, + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background, + borderRadius: BorderRadius.circular(1028), + ), + padding: const EdgeInsets.all(12), + child: (layoutDirection == Axis.vertical + ? constraints.maxHeight + : constraints.maxWidth) < + 500 + ? Consumer( + builder: (context, provider, _) => IconButton( + icon: Icon( + regionShapes[provider.regionType]!.selectedIcon, + ), + onPressed: () => provider + ..regionType = regionShapes.keys.elementAt( + (regionShapes.keys + .toList() + .indexOf(provider.regionType) + + 1) % + 4, + ) + ..clearCoordinates(), + tooltip: 'Switch Region Shape', + ), + ) + : Flex( + direction: layoutDirection, + mainAxisSize: MainAxisSize.min, + children: regionShapes.entries + .map( + (e) => _RegionShapeButton( + type: e.key, + selectedIcon: Icon(e.value.selectedIcon), + unselectedIcon: Icon(e.value.unselectedIcon), + tooltip: e.value.label, + ), + ) + .interleave(const SizedBox.square(dimension: 12)) + .toList(), + ), + ), + const SizedBox.square(dimension: 12), + Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background, + borderRadius: BorderRadius.circular(1028), + ), + padding: const EdgeInsets.all(12), + child: Flex( + direction: layoutDirection, + mainAxisSize: MainAxisSize.min, + children: [ + Selector( + selector: (context, provider) => + provider.regionSelectionMethod, + builder: (context, method, _) => IconButton( + icon: Icon( + method == RegionSelectionMethod.useMapCenter + ? Icons.filter_center_focus + : Icons.ads_click, + ), + onPressed: () => context + .read() + .regionSelectionMethod = + method == RegionSelectionMethod.useMapCenter + ? RegionSelectionMethod.usePointer + : RegionSelectionMethod.useMapCenter, + tooltip: 'Switch Selection Method', + ), + ), + const SizedBox.square(dimension: 12), + IconButton( + icon: const Icon(Icons.delete_forever), + onPressed: () => context.read() + ..clearCoordinates() + ..region = null, + tooltip: 'Remove All Points', + ), + ], + ), + ), + const SizedBox.square(dimension: 12), + Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background, + borderRadius: BorderRadius.circular(1028), + ), + padding: const EdgeInsets.all(12), + child: Consumer( + builder: (context, provider, _) => Flex( + direction: layoutDirection, + mainAxisSize: MainAxisSize.min, + children: [ + provider.openAdjustZoomLevelsSlider + ? IconButton.outlined( + icon: Icon( + layoutDirection == Axis.vertical + ? Icons.arrow_left + : Icons.arrow_drop_down, + ), + onPressed: () => + provider.openAdjustZoomLevelsSlider = false, + ) + : IconButton( + icon: const Icon(Icons.zoom_in), + onPressed: () => + provider.openAdjustZoomLevelsSlider = true, + ), + const SizedBox.square(dimension: 12), + IconButton.filled( + icon: const Icon(Icons.done), + onPressed: provider.region != null + ? () => Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => DownloadRegionPopup( + region: provider.region!, + minZoom: provider.minZoom, + maxZoom: provider.maxZoom, + ), + fullscreenDialog: true, + ), + ) + : null, + ), + ], + ), + ), + ), + ], + ), + const SizedBox.square(dimension: 12), + _AdditionalPane( + constraints: constraints, + layoutDirection: layoutDirection, + ), + ], + ); +} diff --git a/example/lib/screens/main/pages/downloader/components/side_panel/region_shape_button.dart b/example/lib/screens/main/pages/downloader/components/side_panel/region_shape_button.dart new file mode 100644 index 00000000..544c5416 --- /dev/null +++ b/example/lib/screens/main/pages/downloader/components/side_panel/region_shape_button.dart @@ -0,0 +1,28 @@ +part of 'parent.dart'; + +class _RegionShapeButton extends StatelessWidget { + const _RegionShapeButton({ + required this.type, + required this.selectedIcon, + required this.unselectedIcon, + required this.tooltip, + }); + + final RegionType type; + final Icon selectedIcon; + final Icon unselectedIcon; + final String tooltip; + + @override + Widget build(BuildContext context) => Consumer( + builder: (context, provider, _) => IconButton( + icon: unselectedIcon, + selectedIcon: selectedIcon, + onPressed: () => provider + ..regionType = type + ..clearCoordinates(), + isSelected: provider.regionType == type, + tooltip: tooltip, + ), + ); +} diff --git a/example/lib/screens/main/pages/downloader/components/usage_instructions.dart b/example/lib/screens/main/pages/downloader/components/usage_instructions.dart index bfed45d9..d9cb96a2 100644 --- a/example/lib/screens/main/pages/downloader/components/usage_instructions.dart +++ b/example/lib/screens/main/pages/downloader/components/usage_instructions.dart @@ -1,4 +1,6 @@ +import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; import 'package:provider/provider.dart'; import '../../../../../shared/misc/region_selection_method.dart'; @@ -16,73 +18,87 @@ class UsageInstructions extends StatelessWidget { final Axis layoutDirection; @override - Widget build(BuildContext context) => PositionedDirectional( - top: layoutDirection == Axis.vertical ? 0 : 24, - bottom: layoutDirection == Axis.vertical ? 0 : null, - start: layoutDirection == Axis.vertical ? null : 0, - end: layoutDirection == Axis.vertical ? 164 : 0, - child: UnconstrainedBox( - child: Consumer( - builder: (context, provider, _) => AnimatedOpacity( - duration: const Duration(milliseconds: 250), - curve: Curves.easeInOut, - opacity: provider.coordinates.isEmpty ? 1 : 0, - child: IgnorePointer( - child: DecoratedBox( - decoration: BoxDecoration( - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(1 / 3), - spreadRadius: 50, - blurRadius: 90, + Widget build(BuildContext context) => Align( + alignment: layoutDirection == Axis.vertical + ? Alignment.centerRight + : Alignment.topCenter, + child: Padding( + padding: EdgeInsets.only( + left: layoutDirection == Axis.vertical ? 0 : 24, + right: layoutDirection == Axis.vertical ? 164 : 24, + top: 24, + bottom: layoutDirection == Axis.vertical ? 24 : 0, + ), + child: FittedBox( + child: IgnorePointer( + child: DefaultTextStyle( + style: GoogleFonts.ubuntu( + fontSize: 20, + color: Colors.white, + ), + child: Consumer( + builder: (context, provider, _) => AnimatedOpacity( + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + opacity: provider.coordinates.isEmpty ? 1 : 0, + child: DecoratedBox( + decoration: BoxDecoration( + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.4), + spreadRadius: 50, + blurRadius: 90, + ), + ], ), - ], - ), - child: Flex( - direction: layoutDirection, - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - if (layoutDirection == Axis.vertical) - const Icon(Icons.touch_app, size: 68), - if (layoutDirection == Axis.vertical) - const SizedBox.square(dimension: 12), - Column( - crossAxisAlignment: CrossAxisAlignment.end, + child: Flex( + direction: layoutDirection, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: layoutDirection == Axis.vertical + ? CrossAxisAlignment.end + : CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + textDirection: layoutDirection == Axis.vertical + ? null + : TextDirection.rtl, children: [ - Text( - 'Tap/click to add ${provider.regionType == RegionType.circle ? 'center' : 'point'} at ${provider.regionSelectionMethod == RegionSelectionMethod.useMapCenter ? 'map center' : 'pointer'}', - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 22, - color: Colors.white, - ), + Icon( + provider.regionSelectionMethod == + RegionSelectionMethod.usePointer + ? Icons.ads_click + : Icons.filter_center_focus, + size: 60, ), - provider.regionType == RegionType.circle - ? const Text( - 'Tap/click again to set radius', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 22, - color: Colors.white, - ), - ) - : const Text( - 'Long press/right click to remove last point', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - color: Colors.white, - ), + const SizedBox.square(dimension: 8), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + AutoSizeText( + provider.regionSelectionMethod == + RegionSelectionMethod.usePointer + ? '@ Pointer' + : '@ Map Center', + style: const TextStyle( + fontWeight: FontWeight.bold, ), + maxLines: 1, + ), + const SizedBox.square(dimension: 2), + AutoSizeText( + 'Tap/click to add ${provider.regionType == RegionType.circle ? 'center' : 'point'}', + maxLines: 1, + ), + AutoSizeText( + provider.regionType == RegionType.circle + ? 'Tap/click again to set radius' + : 'Hold/right-click to remove last point', + maxLines: 1, + ), + ], + ), ], ), - if (layoutDirection == Axis.horizontal) - const SizedBox.square(dimension: 12), - if (layoutDirection == Axis.horizontal) - const Icon(Icons.touch_app, size: 68), - ], + ), ), ), ), diff --git a/example/lib/screens/main/pages/downloader/downloader.dart b/example/lib/screens/main/pages/downloader/downloader.dart index d06bc44a..6eaa83f9 100644 --- a/example/lib/screens/main/pages/downloader/downloader.dart +++ b/example/lib/screens/main/pages/downloader/downloader.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:provider/provider.dart'; -import 'components/header.dart'; -import 'components/map_view.dart'; +import '../../../../shared/state/general_provider.dart'; +import 'map_view.dart'; class DownloaderPage extends StatefulWidget { const DownloaderPage({super.key}); @@ -15,10 +17,36 @@ class _DownloaderPageState extends State { Widget build(BuildContext context) => Scaffold( body: Column( children: [ - const SafeArea( + SafeArea( child: Padding( - padding: EdgeInsets.all(12), - child: Header(), + padding: const EdgeInsets.all(12), + child: Row( + children: [ + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Downloader', + style: GoogleFonts.openSans( + fontWeight: FontWeight.bold, + fontSize: 24, + ), + ), + Consumer( + builder: (context, provider, _) => provider + .currentStore == + null + ? const SizedBox.shrink() + : const Text( + 'Existing tiles will appear in red', + style: TextStyle(fontStyle: FontStyle.italic), + ), + ), + ], + ), + ], + ), ), ), Expanded( diff --git a/example/lib/screens/main/pages/downloader/map_view.dart b/example/lib/screens/main/pages/downloader/map_view.dart new file mode 100644 index 00000000..fba63a0a --- /dev/null +++ b/example/lib/screens/main/pages/downloader/map_view.dart @@ -0,0 +1,276 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map/plugin_api.dart'; +import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:provider/provider.dart'; + +import '../../../../shared/components/loading_indicator.dart'; +import '../../../../shared/misc/region_selection_method.dart'; +import '../../../../shared/misc/region_type.dart'; +import '../../../../shared/state/download_provider.dart'; +import '../../../../shared/state/general_provider.dart'; +import '../../../download_region/download_region.dart'; +import '../map/build_attribution.dart'; +import 'components/crosshairs.dart'; +import 'components/custom_polygon_snapping_indicator.dart'; +import 'components/region_shape.dart'; +import 'components/side_panel/parent.dart'; +import 'components/usage_instructions.dart'; + +class MapView extends StatefulWidget { + const MapView({super.key}); + + @override + State createState() => _MapViewState(); +} + +class _MapViewState extends State { + final mapController = MapController(); + + late final mapOptions = MapOptions( + initialCenter: const LatLng(51.509364, -0.128928), + initialZoom: 11, + maxZoom: 22, + cameraConstraint: CameraConstraint.contain( + bounds: LatLngBounds.fromPoints([ + const LatLng(-90, 180), + const LatLng(90, 180), + const LatLng(90, -180), + const LatLng(-90, -180), + ]), + ), + interactionOptions: const InteractionOptions( + flags: InteractiveFlag.all & + ~InteractiveFlag.rotate & + ~InteractiveFlag.doubleTapZoom, + scrollWheelVelocity: 0.002, + ), + keepAlive: true, + onTap: (_, __) { + final provider = context.read(); + + if (provider.isCustomPolygonComplete) return; + + final List coords; + if (provider.customPolygonSnap && + provider.regionType == RegionType.customPolygon) { + coords = provider.addCoordinate(provider.coordinates.first); + provider.customPolygonSnap = false; + } else { + coords = provider.addCoordinate(provider.currentNewPointPos); + } + + if (coords.length < 2) return; + + switch (provider.regionType) { + case RegionType.square: + if (coords.length == 2) { + provider.region = RectangleRegion(LatLngBounds.fromPoints(coords)); + break; + } + provider + ..clearCoordinates() + ..addCoordinate(provider.currentNewPointPos); + + break; + case RegionType.circle: + if (coords.length == 2) { + provider.region = CircleRegion( + coords[0], + const Distance(roundResult: false) + .distance(coords[0], coords[1]) / + 1000, + ); + break; + } + provider + ..clearCoordinates() + ..addCoordinate(provider.currentNewPointPos); + + break; + case RegionType.line: + provider.region = LineRegion(coords, provider.lineRadius); + break; + case RegionType.customPolygon: + if (!provider.isCustomPolygonComplete) break; + provider.region = CustomPolygonRegion(coords); + break; + } + }, + onSecondaryTap: (_, __) => + context.read().removeLastCoordinate(), + onLongPress: (_, __) => + context.read().removeLastCoordinate(), + onPointerHover: (evt, point) { + final provider = context.read(); + + if (provider.regionSelectionMethod == RegionSelectionMethod.usePointer) { + provider.currentNewPointPos = point; + + if (provider.regionType == RegionType.customPolygon) { + final coords = provider.coordinates; + if (coords.length > 1) { + final newPointPos = mapController.camera + .latLngToScreenPoint(coords.first) + .toOffset(); + provider.customPolygonSnap = coords.first != coords.last && + sqrt( + pow(newPointPos.dx - evt.localPosition.dx, 2) + + pow(newPointPos.dy - evt.localPosition.dy, 2), + ) < + 15; + } + } + } + }, + onPositionChanged: (position, _) { + final provider = context.read(); + + if (provider.regionSelectionMethod == + RegionSelectionMethod.useMapCenter) { + provider.currentNewPointPos = position.center!; + + if (provider.regionType == RegionType.customPolygon) { + final coords = provider.coordinates; + if (coords.length > 1) { + final newPointPos = mapController.camera + .latLngToScreenPoint(coords.first) + .toOffset(); + final centerPos = mapController.camera + .latLngToScreenPoint(provider.currentNewPointPos) + .toOffset(); + provider.customPolygonSnap = coords.first != coords.last && + sqrt( + pow(newPointPos.dx - centerPos.dx, 2) + + pow(newPointPos.dy - centerPos.dy, 2), + ) < + 30; + } + } + } + }, + ); + + bool keyboardHandler(KeyEvent event) { + final provider = context.read(); + if (provider.region != null && + event is KeyDownEvent && + event.logicalKey == LogicalKeyboardKey.enter) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => DownloadRegionPopup( + region: provider.region!, + minZoom: provider.minZoom, + maxZoom: provider.maxZoom, + ), + fullscreenDialog: true, + ), + ); + } + + return false; + } + + @override + void initState() { + super.initState(); + ServicesBinding.instance.keyboard.addHandler(keyboardHandler); + } + + @override + void dispose() { + ServicesBinding.instance.keyboard.removeHandler(keyboardHandler); + super.dispose(); + } + + @override + Widget build(BuildContext context) => LayoutBuilder( + builder: (context, constraints) => Stack( + children: [ + Selector( + selector: (context, provider) => provider.currentStore, + builder: (context, currentStore, _) => + FutureBuilder?>( + future: currentStore == null + ? Future.value() + : FMTC.instance(currentStore).metadata.readAsync, + builder: (context, metadata) { + if (currentStore != null && metadata.data == null) { + return const LoadingIndicator('Preparing Map'); + } + + final urlTemplate = metadata.data?['sourceURL'] ?? + 'https://tile.openstreetmap.org/{z}/{x}/{y}.png'; + + return MouseRegion( + opaque: false, + cursor: context.select( + (p) => p.regionSelectionMethod, + ) == + RegionSelectionMethod.useMapCenter + ? MouseCursor.defer + : context.select( + (p) => p.customPolygonSnap, + ) + ? SystemMouseCursors.none + : SystemMouseCursors.precise, + child: FlutterMap( + mapController: mapController, + options: mapOptions, + nonRotatedChildren: buildStdAttribution(urlTemplate), + children: [ + TileLayer( + urlTemplate: urlTemplate, + userAgentPackageName: 'dev.jaffaketchup.fmtc.demo', + maxNativeZoom: 20, + backgroundColor: const Color(0xFFaad3df), + tileBuilder: (context, widget, tile) => + FutureBuilder( + future: currentStore == null + ? Future.value() + : FMTC + .instance(currentStore) + .getTileProvider() + .checkTileCachedAsync( + coords: tile.coordinates, + options: + TileLayer(urlTemplate: urlTemplate), + ), + builder: (context, snapshot) => DecoratedBox( + position: DecorationPosition.foreground, + decoration: BoxDecoration( + color: (snapshot.data ?? false) + ? Colors.deepOrange.withOpacity(1 / 3) + : Colors.transparent, + ), + child: widget, + ), + ), + ), + const RegionShape(), + const CustomPolygonSnappingIndicator(), + ], + ), + ); + }, + ), + ), + SidePanel(constraints: constraints), + if (context.select( + (p) => p.regionSelectionMethod, + ) == + RegionSelectionMethod.useMapCenter && + !context.select( + (p) => p.customPolygonSnap, + )) + const Center(child: Crosshairs()), + UsageInstructions(constraints: constraints), + ], + ), + ); +} diff --git a/example/lib/screens/main/pages/map/map_view.dart b/example/lib/screens/main/pages/map/map_view.dart index 3fd6caa7..810f68d2 100644 --- a/example/lib/screens/main/pages/map/map_view.dart +++ b/example/lib/screens/main/pages/map/map_view.dart @@ -32,10 +32,7 @@ class MapPage extends StatelessWidget { if (!metadata.hasData || metadata.data == null || (provider.currentStore != null && metadata.data!.isEmpty)) { - return const LoadingIndicator( - message: - 'Loading Map...\n\nSeeing this screen for a long time?\nThere may be a misconfiguration of the\nstore. Try disabling caching and deleting\n faulty stores.', - ); + return const LoadingIndicator('Preparing Map'); } final String urlTemplate = @@ -73,6 +70,10 @@ class MapPage extends StatelessWidget { children: [ TileLayer( urlTemplate: urlTemplate, + userAgentPackageName: 'dev.jaffaketchup.fmtc.demo', + backgroundColor: const Color(0xFFaad3df), + maxNativeZoom: 20, + panBuffer: 5, tileProvider: provider.currentStore != null ? FMTC.instance(provider.currentStore!).getTileProvider( settings: FMTCTileProviderSettings( @@ -94,10 +95,6 @@ class MapPage extends StatelessWidget { ), ) : NetworkTileProvider(), - maxZoom: 22, - userAgentPackageName: 'dev.jaffaketchup.fmtc.demo', - panBuffer: 3, - backgroundColor: const Color(0xFFaad3df), ), Consumer( builder: (context, mapProvider, _) => CurrentLocationLayer( diff --git a/example/lib/screens/main/pages/recovery/recovery.dart b/example/lib/screens/main/pages/recovery/recovery.dart index 31a09a8e..5fe51200 100644 --- a/example/lib/screens/main/pages/recovery/recovery.dart +++ b/example/lib/screens/main/pages/recovery/recovery.dart @@ -60,7 +60,7 @@ class _RecoveryPageState extends State { moveToDownloadPage: widget.moveToDownloadPage, ) : const LoadingIndicator( - message: 'Loading Recoverable Downloads...', + 'Retrieving Recoverable Downloads', ), ), ), diff --git a/example/lib/screens/main/pages/stores/stores.dart b/example/lib/screens/main/pages/stores/stores.dart index 9e7e3645..7e29accd 100644 --- a/example/lib/screens/main/pages/stores/stores.dart +++ b/example/lib/screens/main/pages/stores/stores.dart @@ -64,9 +64,7 @@ class _StoresPageState extends State { ), ), ) - : const LoadingIndicator( - message: 'Loading Stores...', - ), + : const LoadingIndicator('Retrieving Stores'), ), ), ], diff --git a/example/lib/screens/store_editor/store_editor.dart b/example/lib/screens/store_editor/store_editor.dart index 4b4041ab..07314010 100644 --- a/example/lib/screens/store_editor/store_editor.dart +++ b/example/lib/screens/store_editor/store_editor.dart @@ -74,10 +74,7 @@ class _StoreEditorPopupState extends State { .readAsync, builder: (context, metadata) { if (!metadata.hasData || metadata.data == null) { - return const LoadingIndicator( - message: - 'Loading Settings...\n\nSeeing this screen for a long time?\nThere may be a misconfiguration of the\nstore. Try disabling caching and deleting\n faulty stores.', - ); + return const LoadingIndicator('Retrieving Settings'); } return Form( key: _formKey, @@ -87,7 +84,6 @@ class _StoreEditorPopupState extends State { TextFormField( decoration: const InputDecoration( labelText: 'Store Name', - helperText: 'Must be valid directory name', prefixIcon: Icon(Icons.text_fields), isDense: true, ), @@ -117,29 +113,33 @@ class _StoreEditorPopupState extends State { const SizedBox(height: 5), TextFormField( decoration: const InputDecoration( - labelText: 'Map Source URL (protocol required)', + labelText: 'Map Source URL', helperText: - "Use '{x}', '{y}', '{z}' as placeholders. Omit subdomain.", + "Use '{x}', '{y}', '{z}' as placeholders. Include protocol. Omit subdomain.", prefixIcon: Icon(Icons.link), isDense: true, ), onChanged: (i) async { - _httpRequestFailed = await http - .get( - Uri.parse( - NetworkTileProvider().getTileUrl( - const TileCoordinates(1, 1, 1), - TileLayer(urlTemplate: i), - ), - ), - ) - .then( + final uri = Uri.tryParse( + NetworkTileProvider().getTileUrl( + const TileCoordinates(0, 0, 0), + TileLayer(urlTemplate: i), + ), + ); + + if (uri == null) { + setState( + () => _httpRequestFailed = 'Invalid URL', + ); + return; + } + + _httpRequestFailed = await http.get(uri).then( (res) => res.statusCode == 200 ? null : 'HTTP Request Failed', onError: (_) => 'HTTP Request Failed', ); - setState(() {}); }, validator: (i) { @@ -174,7 +174,7 @@ class _StoreEditorPopupState extends State { TextFormField( decoration: const InputDecoration( labelText: 'Valid Cache Duration', - helperText: 'Use 0 days for infinite duration', + helperText: 'Use 0 to disable expiry', suffixText: 'days', prefixIcon: Icon(Icons.timelapse), isDense: true, @@ -204,8 +204,7 @@ class _StoreEditorPopupState extends State { TextFormField( decoration: const InputDecoration( labelText: 'Maximum Length', - helperText: - 'Use 0 days for infinite number of tiles', + helperText: 'Use 0 to disable limit', suffixText: 'tiles', prefixIcon: Icon(Icons.disc_full), isDense: true, diff --git a/example/lib/shared/components/loading_indicator.dart b/example/lib/shared/components/loading_indicator.dart index 1cb26ebc..99a47058 100644 --- a/example/lib/shared/components/loading_indicator.dart +++ b/example/lib/shared/components/loading_indicator.dart @@ -1,23 +1,22 @@ import 'package:flutter/material.dart'; class LoadingIndicator extends StatelessWidget { - const LoadingIndicator({ - super.key, - this.message = 'Please Wait...', - }); + const LoadingIndicator(this.text, {super.key}); - final String message; + final String text; @override Widget build(BuildContext context) => Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const CircularProgressIndicator(), - const SizedBox(height: 10), - Text( - message, + const CircularProgressIndicator.adaptive(), + const SizedBox(height: 12), + Text(text, textAlign: TextAlign.center), + const Text( + 'This should only take a few moments', textAlign: TextAlign.center, + style: TextStyle(fontStyle: FontStyle.italic), ), ], ), diff --git a/example/lib/shared/state/download_provider.dart b/example/lib/shared/state/download_provider.dart index df3e1ab7..06063fbc 100644 --- a/example/lib/shared/state/download_provider.dart +++ b/example/lib/shared/state/download_provider.dart @@ -48,6 +48,12 @@ class DownloaderProvider extends ChangeNotifier { return _coordinates; } + List addCoordinates(Iterable coords) { + _coordinates.addAll(coords); + notifyListeners(); + return _coordinates; + } + void clearCoordinates() { _coordinates.clear(); _region = null; @@ -81,26 +87,10 @@ class DownloaderProvider extends ChangeNotifier { _coordinates.length >= 2 && _coordinates.first == _coordinates.last; - // OLD - - double _lineRegionRadius = 1000; - double get lineRegionRadius => _lineRegionRadius; - set lineRegionRadius(double newNum) { - _lineRegionRadius = newNum; - notifyListeners(); - } - - List _lineRegionPoints = []; - List get lineRegionPoints => _lineRegionPoints; - set lineRegionPoints(List newList) { - _lineRegionPoints = newList; - notifyListeners(); - } - - int? _regionTiles; - int? get regionTiles => _regionTiles; - set regionTiles(int? newNum) { - _regionTiles = newNum; + bool _openAdjustZoomLevelsSlider = false; + bool get openAdjustZoomLevelsSlider => _openAdjustZoomLevelsSlider; + set openAdjustZoomLevelsSlider(bool newState) { + _openAdjustZoomLevelsSlider = newState; notifyListeners(); } @@ -118,6 +108,8 @@ class DownloaderProvider extends ChangeNotifier { notifyListeners(); } + // OLD + StoreDirectory? _selectedStore; StoreDirectory? get selectedStore => _selectedStore; void setSelectedStore(StoreDirectory? newStore, {bool notify = true}) { @@ -125,12 +117,6 @@ class DownloaderProvider extends ChangeNotifier { if (notify) notifyListeners(); } - final StreamController _manualPolygonRecalcTrigger = - StreamController.broadcast(); - StreamController get manualPolygonRecalcTrigger => - _manualPolygonRecalcTrigger; - void triggerManualPolygonRecalc() => _manualPolygonRecalcTrigger.add(null); - Stream? _downloadProgress; Stream? get downloadProgress => _downloadProgress; void setDownloadProgress( diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 8a5c1a77..16af729d 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -10,8 +10,10 @@ environment: flutter: ">=3.10.0" dependencies: + auto_size_text: ^3.0.0 badges: ^3.0.2 better_open_file: ^3.6.4 + collection: ^1.17.1 file_picker: ^5.2.10 flutter: sdk: flutter diff --git a/lib/src/bulk_download/tile_loops/custom_polygon_tools/bres.dart b/lib/src/bulk_download/tile_loops/custom_polygon_tools/bres.dart index 0bfcf1bf..5aa91e6f 100644 --- a/lib/src/bulk_download/tile_loops/custom_polygon_tools/bres.dart +++ b/lib/src/bulk_download/tile_loops/custom_polygon_tools/bres.dart @@ -1,3 +1,6 @@ +// Copyright © Luka S (JaffaKetchup) under GPL-v3 +// A full license can be found at .\LICENSE + import 'dart:math'; /// Bresenham’s line generation algorithm, ported (with minor API differences) diff --git a/lib/src/regions/base_region.dart b/lib/src/regions/base_region.dart index b1311e05..34e3192d 100644 --- a/lib/src/regions/base_region.dart +++ b/lib/src/regions/base_region.dart @@ -68,7 +68,6 @@ sealed class BaseRegion { int start = 0, int? end, Crs crs = const Epsg3857(), - void Function(Object?)? errorHandler, }); /// Generate a graphical layer to be placed in a [FlutterMap] diff --git a/lib/src/regions/circle.dart b/lib/src/regions/circle.dart index a3e12ea3..ca0f15b0 100644 --- a/lib/src/regions/circle.dart +++ b/lib/src/regions/circle.dart @@ -32,7 +32,6 @@ class CircleRegion extends BaseRegion { int start = 0, int? end, Crs crs = const Epsg3857(), - void Function(Object?)? errorHandler, }) => DownloadableRegion._( this, diff --git a/lib/src/regions/custom_polygon.dart b/lib/src/regions/custom_polygon.dart index 73f5037b..1fe3ad7a 100644 --- a/lib/src/regions/custom_polygon.dart +++ b/lib/src/regions/custom_polygon.dart @@ -29,7 +29,6 @@ class CustomPolygonRegion extends BaseRegion { int start = 0, int? end, Crs crs = const Epsg3857(), - void Function(Object?)? errorHandler, }) => DownloadableRegion._( this, diff --git a/lib/src/regions/line.dart b/lib/src/regions/line.dart index 23f9c268..d19cf492 100644 --- a/lib/src/regions/line.dart +++ b/lib/src/regions/line.dart @@ -80,7 +80,6 @@ class LineRegion extends BaseRegion { int start = 0, int? end, Crs crs = const Epsg3857(), - void Function(Object?)? errorHandler, }) => DownloadableRegion._( this, diff --git a/lib/src/regions/recovered_region.dart b/lib/src/regions/recovered_region.dart index 44bed3a4..f491bd99 100644 --- a/lib/src/regions/recovered_region.dart +++ b/lib/src/regions/recovered_region.dart @@ -92,7 +92,6 @@ class RecoveredRegion { DownloadableRegion toDownloadable( TileLayer options, { Crs crs = const Epsg3857(), - Function(Object?)? errorHandler, }) => DownloadableRegion._( toRegion(rectangle: (r) => r, circle: (c) => c, line: (l) => l), diff --git a/lib/src/regions/rectangle.dart b/lib/src/regions/rectangle.dart index df7432ca..06b8eba4 100644 --- a/lib/src/regions/rectangle.dart +++ b/lib/src/regions/rectangle.dart @@ -31,7 +31,6 @@ class RectangleRegion extends BaseRegion { int start = 0, int? end, Crs crs = const Epsg3857(), - void Function(Object?)? errorHandler, }) => DownloadableRegion._( this, From 75ffca2c3372169c444694751121ac90fee224f9 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 3 Aug 2023 17:42:56 +0100 Subject: [PATCH 057/168] Increased speed & reduced RAM consumption of `CustomPolygonRegion` tile generation Improved test suite to be more thorough Former-commit-id: c96fd5d7eb9b9d51d6c71ae574757e5055e2d2e7 [formerly 05bfb06bbdb990ef43505d14f86032ad95733d20] Former-commit-id: 59ca17a10f9da78ae7b62639402fdd80eeb6dcd3 --- lib/src/bulk_download/manager.dart | 14 +- lib/src/bulk_download/tile_loops/count.dart | 31 +-- .../bulk_download/tile_loops/generate.dart | 34 +-- test/earcut_test.dart | 60 +++++ test/fmtc_test.dart | 219 ++++++++++-------- 5 files changed, 226 insertions(+), 132 deletions(-) create mode 100644 test/earcut_test.dart diff --git a/lib/src/bulk_download/manager.dart b/lib/src/bulk_download/manager.dart index 750d18d2..5bc5d87e 100644 --- a/lib/src/bulk_download/manager.dart +++ b/lib/src/bulk_download/manager.dart @@ -73,7 +73,7 @@ Future _downloadManager( rectangle: (_) => TilesGenerator.rectangleTiles, circle: (_) => TilesGenerator.circleTiles, line: (_) => TilesGenerator.lineTiles, - customPolygon: (_) => throw UnimplementedError(), + customPolygon: (_) => TilesGenerator.customPolygonTiles, ), (sendPort: tileRecievePort.sendPort, region: input.region), onExit: tileRecievePort.sendPort, @@ -96,11 +96,6 @@ Future _downloadManager( ); final requestTilePort = await tileQueue.next as SendPort; - // Setup two-way communications with root - final rootRecievePort = ReceivePort(); - void send(Object? m) => input.sendPort.send(m); - send(rootRecievePort.sendPort); - // Start progress tracking final initialDownloadProgress = DownloadProgress._initial(maxTiles: maxTiles); var lastDownloadProgress = initialDownloadProgress; @@ -123,6 +118,10 @@ Future _downloadManager( return tps; } + // Setup two-way communications with root + final rootRecievePort = ReceivePort(); + void send(Object? m) => input.sendPort.send(m); + // Setup cancel, pause, and resume handling List> generateThreadPausedStates() => List.generate( input.parallelThreads, @@ -172,6 +171,9 @@ Future _downloadManager( }, ); + // Now it's safe, start accepting communications from the root + send(rootRecievePort.sendPort); + // Start download threads & wait for download to complete/cancelled downloadDuration.start(); await Future.wait( diff --git a/lib/src/bulk_download/tile_loops/count.dart b/lib/src/bulk_download/tile_loops/count.dart index ebf47472..dcf23255 100644 --- a/lib/src/bulk_download/tile_loops/count.dart +++ b/lib/src/bulk_download/tile_loops/count.dart @@ -224,25 +224,23 @@ class TilesCounter { static int customPolygonTiles(DownloadableRegion region) { region as DownloadableRegion; - final customPolygonOutline = region.originalRegion.toOutline(); + final customPolygonOutline = region.originalRegion.outline; int numberOfTiles = 0; for (double zoomLvl = region.minZoom.toDouble(); zoomLvl <= region.maxZoom; zoomLvl++) { - final tiles = >{}; + final allOutlineTiles = >{}; for (final triangle in Earcut.triangulateFromPoints( customPolygonOutline.map(region.crs.projection.project), ).map(customPolygonOutline.elementAt).slices(3)) { - final outlineTiles = >{}; - final vertex1 = region.crs.latLngToPoint(triangle[0], zoomLvl).round(); final vertex2 = region.crs.latLngToPoint(triangle[1], zoomLvl).round(); final vertex3 = region.crs.latLngToPoint(triangle[2], zoomLvl).round(); - outlineTiles.addAll([ + final outlineTiles = { ...bresenhamsLGA( Point(vertex1.x, vertex1.y), Point(vertex2.x, vertex2.y), @@ -258,23 +256,30 @@ class TilesCounter { Point(vertex1.x, vertex1.y), unscaleBy: region.options.tileSize, ), - ]); - - tiles.addAll(outlineTiles); + }; + allOutlineTiles.addAll(outlineTiles); final byY = >{}; for (final Point(:x, :y) in outlineTiles) { (byY[y] ?? (byY[y] = {})).add(x); } - for (final MapEntry(key: y, value: xs) in byY.entries) { - for (int x = xs.min + 1; x < xs.max; x++) { - tiles.add(Point(x, y)); - } + for (final xs in byY.values) { + final xsRawMin = xs.min; + int i = 0; + for (; xs.contains(xsRawMin + i); i++) {} + final xsMin = xsRawMin + i; + + final xsRawMax = xs.max; + i = 0; + for (; xs.contains(xsRawMax - i); i++) {} + final xsMax = xsRawMax - i; + + if (xsMin <= xsMax) numberOfTiles += (xsMax - xsMin) + 1; } } - numberOfTiles += tiles.length; + numberOfTiles += allOutlineTiles.length; } return numberOfTiles; diff --git a/lib/src/bulk_download/tile_loops/generate.dart b/lib/src/bulk_download/tile_loops/generate.dart index b7c977dd..eac496dd 100644 --- a/lib/src/bulk_download/tile_loops/generate.dart +++ b/lib/src/bulk_download/tile_loops/generate.dart @@ -251,18 +251,16 @@ class TilesGenerator { for (double zoomLvl = region.minZoom.toDouble(); zoomLvl <= region.maxZoom; zoomLvl++) { - final tiles = >{}; + final allOutlineTiles = >{}; for (final triangle in Earcut.triangulateFromPoints( customPolygonOutline.map(region.crs.projection.project), ).map(customPolygonOutline.elementAt).slices(3)) { - final outlineTiles = >{}; - final vertex1 = region.crs.latLngToPoint(triangle[0], zoomLvl).round(); final vertex2 = region.crs.latLngToPoint(triangle[1], zoomLvl).round(); final vertex3 = region.crs.latLngToPoint(triangle[2], zoomLvl).round(); - outlineTiles.addAll([ + final outlineTiles = { ...bresenhamsLGA( Point(vertex1.x, vertex1.y), Point(vertex2.x, vertex2.y), @@ -278,9 +276,8 @@ class TilesGenerator { Point(vertex1.x, vertex1.y), unscaleBy: region.options.tileSize, ), - ]); - - tiles.addAll(outlineTiles); + }; + allOutlineTiles.addAll(outlineTiles); final byY = >{}; for (final Point(:x, :y) in outlineTiles) { @@ -288,15 +285,26 @@ class TilesGenerator { } for (final MapEntry(key: y, value: xs) in byY.entries) { - for (int x = xs.min + 1; x < xs.max; x++) { - tiles.add(Point(x, y)); + final xsRawMin = xs.min; + int i = 0; + for (; xs.contains(xsRawMin + i); i++) {} + final xsMin = xsRawMin + i; + + final xsRawMax = xs.max; + i = 0; + for (; xs.contains(xsRawMax - i); i++) {} + final xsMax = xsRawMax - i; + + for (int x = xsMin; x <= xsMax; x++) { + await requestQueue.next; + input.sendPort.send((x, y, zoomLvl.toInt())); } } + } - for (final tile in tiles) { - await requestQueue.next; - input.sendPort.send((tile.x, tile.y, zoomLvl.toInt())); - } + for (final Point(:x, :y) in allOutlineTiles) { + await requestQueue.next; + input.sendPort.send((x, y, zoomLvl.toInt())); } } diff --git a/test/earcut_test.dart b/test/earcut_test.dart new file mode 100644 index 00000000..fd3a71c3 --- /dev/null +++ b/test/earcut_test.dart @@ -0,0 +1,60 @@ +// Copyright © Luka S (JaffaKetchup) under GPL-v3 +// A full license can be found at .\LICENSE + +// ignore_for_file: avoid_print + +import 'package:flutter_map_tile_caching/src/bulk_download/tile_loops/custom_polygon_tools/earcut.dart'; +import 'package:test/test.dart'; + +void main() { + test( + 'Simple Triangle', + () => expect(Earcut.triangulateRaw([0, 0, 0, 50, 50, 00]), [1, 0, 2]), + ); + + test( + 'Complex Triangle', + () => expect( + Earcut.triangulateRaw([0, 0, 0, 25, 0, 50, 25, 25, 50, 0, 25, 0]), + [1, 0, 5, 5, 4, 3, 3, 2, 1, 1, 5, 3], + ), + ); + + test( + 'L Shape', + () => expect( + Earcut.triangulateRaw([0, 0, 10, 0, 10, 5, 5, 5, 5, 15, 0, 15]), + [4, 5, 0, 0, 1, 2, 3, 4, 0, 0, 2, 3], + ), + ); + + test( + 'Simple Polygon', + () => expect( + Earcut.triangulateRaw([10, 0, 0, 50, 60, 60, 70, 10]), + [1, 0, 3, 3, 2, 1], + ), + ); + + test( + 'Polygon With Hole', + () => expect( + Earcut.triangulateRaw( + [0, 0, 100, 0, 100, 100, 0, 100, 20, 20, 80, 20, 80, 80, 20, 80], + holeIndices: [4], + ), + [3, 0, 4, 5, 4, 0, 3, 4, 7, 5, 0, 1, 2, 3, 7, 6, 5, 1, 2, 7, 6, 6, 1, 2], + ), + ); + + test( + 'Polygon With 3D Coords', + () => expect( + Earcut.triangulateRaw( + [10, 0, 1, 0, 50, 2, 60, 60, 3, 70, 10, 4], + dimensions: 3, + ), + [1, 0, 3, 3, 2, 1], + ), + ); +} diff --git a/test/fmtc_test.dart b/test/fmtc_test.dart index 571d3f32..3de74f13 100644 --- a/test/fmtc_test.dart +++ b/test/fmtc_test.dart @@ -3,30 +3,66 @@ // ignore_for_file: avoid_print +import 'dart:isolate'; + import 'package:collection/collection.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; -import 'package:flutter_map_tile_caching/src/bulk_download/tile_loops/custom_polygon_tools/earcut.dart'; import 'package:flutter_map_tile_caching/src/bulk_download/tile_loops/shared.dart'; import 'package:latlong2/latlong.dart'; import 'package:test/test.dart'; void main() { - group('Test Region Tile Generation', () { + Future countByGenerator(DownloadableRegion region) async { + final tileRecievePort = ReceivePort(); + final tileIsolate = await Isolate.spawn( + region.when( + rectangle: (_) => TilesGenerator.rectangleTiles, + circle: (_) => TilesGenerator.circleTiles, + line: (_) => TilesGenerator.lineTiles, + customPolygon: (_) => TilesGenerator.customPolygonTiles, + ), + (sendPort: tileRecievePort.sendPort, region: region), + onExit: tileRecievePort.sendPort, + debugName: '[FMTC] Tile Coords Generator Thread', + ); + late final SendPort requestTilePort; + + int evts = -1; + + await for (final evt in tileRecievePort) { + if (evt == null) break; + if (evt is SendPort) requestTilePort = evt; + requestTilePort.send(null); + evts++; + } + + tileIsolate.kill(priority: Isolate.immediate); + tileRecievePort.close(); + + return evts; + } + + group('Rectangle Region', () { final rectRegion = - RectangleRegion(LatLngBounds(const LatLng(-2, -2), const LatLng(2, 2))) - .toDownloadable(minZoom: 1, maxZoom: 18, options: TileLayer()); + RectangleRegion(LatLngBounds(const LatLng(-1, -1), const LatLng(1, 1))) + .toDownloadable(minZoom: 1, maxZoom: 15, options: TileLayer()); + + test( + 'Count By Counter', + () => expect(TilesCounter.rectangleTiles(rectRegion), 45240), + ); test( - 'Rectangle Region Count', - () => expect(TilesCounter.rectangleTiles(rectRegion), 11329252), + 'Count By Generator', + () async => expect(await countByGenerator(rectRegion), 45240), ); test( - 'Rectangle Region Duration', + 'Counter Duration', () => print( '${List.generate( - 1000, + 2000, (index) { final clock = Stopwatch()..start(); TilesCounter.rectangleTiles(rectRegion); @@ -38,19 +74,36 @@ void main() { ), ); - final circleRegion = CircleRegion(const LatLng(0, 0), 1000) - .toDownloadable(minZoom: 1, maxZoom: 18, options: TileLayer()); + test( + 'Generator Duration', + () async { + final clock = Stopwatch()..start(); + await countByGenerator(rectRegion); + clock.stop(); + print('${clock.elapsedMilliseconds / 1000} s'); + }, + ); + }); + + group('Circle Region', () { + final circleRegion = CircleRegion(const LatLng(0, 0), 200) + .toDownloadable(minZoom: 1, maxZoom: 15, options: TileLayer()); + + test( + 'Count By Counter', + () => expect(TilesCounter.circleTiles(circleRegion), 61564), + ); test( - 'Circle Region Count', - () => expect(TilesCounter.circleTiles(circleRegion), 2989468), + 'Count By Generator', + () async => expect(await countByGenerator(circleRegion), 61564), ); test( - 'Circle Region Duration', + 'Counter Duration', () => print( '${List.generate( - 1000, + 500, (index) { final clock = Stopwatch()..start(); TilesCounter.circleTiles(circleRegion); @@ -62,20 +115,37 @@ void main() { ), ); + test( + 'Generator Duration', + () async { + final clock = Stopwatch()..start(); + await countByGenerator(circleRegion); + clock.stop(); + print('${clock.elapsedMilliseconds / 1000} s'); + }, + ); + }); + + group('Line Region', () { final lineRegion = - LineRegion([const LatLng(-1, -1), const LatLng(1, 1)], 100) - .toDownloadable(minZoom: 1, maxZoom: 16, options: TileLayer()); + LineRegion([const LatLng(-1, -1), const LatLng(1, 1)], 5000) + .toDownloadable(minZoom: 1, maxZoom: 15, options: TileLayer()); + + test( + 'Count By Counter', + () => expect(TilesCounter.lineTiles(lineRegion), 3131), + ); test( - 'Line Region Count', - () => expect(TilesCounter.lineTiles(lineRegion), 2936), + 'Count By Generator', + () async => expect(await countByGenerator(lineRegion), 3131), ); test( - 'Line Region Duration', + 'Counter Duration', () => print( '${List.generate( - 100, + 300, (index) { final clock = Stopwatch()..start(); TilesCounter.lineTiles(lineRegion); @@ -87,6 +157,18 @@ void main() { ), ); + test( + 'Generator Duration', + () async { + final clock = Stopwatch()..start(); + await countByGenerator(lineRegion); + clock.stop(); + print('${clock.elapsedMilliseconds / 1000} s'); + }, + ); + }); + + group('Custom Polygon Region', () { final customPolygonRegion = CustomPolygonRegion([ const LatLng(51.45818683312154, -0.9674646220840917), const LatLng(51.55859639937614, -0.9185366064186982), @@ -94,18 +176,23 @@ void main() { const LatLng(51.56029831737391, -0.5322770067805148), const LatLng(51.235701626195365, -0.5746290119276093), const LatLng(51.38781341753136, -0.6779891095601829), - ]).toDownloadable(minZoom: 1, maxZoom: 18, options: TileLayer()); + ]).toDownloadable(minZoom: 1, maxZoom: 17, options: TileLayer()); test( - 'Custom Polygon Region Count', - () => expect(TilesCounter.customPolygonTiles(customPolygonRegion), 62234), + 'Count By Counter', + () => expect(TilesCounter.customPolygonTiles(customPolygonRegion), 15961), ); test( - 'Custom Polygon Region Duration', + 'Count By Generator', + () async => expect(await countByGenerator(customPolygonRegion), 15961), + ); + + test( + 'Counter Duration', () => print( '${List.generate( - 100, + 500, (index) { final clock = Stopwatch()..start(); TilesCounter.customPolygonTiles(customPolygonRegion); @@ -116,83 +203,15 @@ void main() { ).average} ms', ), ); - }); - - group('Test Earcutting Triangulation', () { - test( - 'Simple Triangle', - () => expect(Earcut.triangulateRaw([0, 0, 0, 50, 50, 00]), [1, 0, 2]), - ); - - test( - 'Complex Triangle', - () => expect( - Earcut.triangulateRaw([0, 0, 0, 25, 0, 50, 25, 25, 50, 0, 25, 0]), - [1, 0, 5, 5, 4, 3, 3, 2, 1, 1, 5, 3], - ), - ); test( - 'L Shape', - () => expect( - Earcut.triangulateRaw([0, 0, 10, 0, 10, 5, 5, 5, 5, 15, 0, 15]), - [4, 5, 0, 0, 1, 2, 3, 4, 0, 0, 2, 3], - ), - ); - - test( - 'Simple Polygon', - () => expect( - Earcut.triangulateRaw([10, 0, 0, 50, 60, 60, 70, 10]), - [1, 0, 3, 3, 2, 1], - ), - ); - - test( - 'Polygon With Hole', - () => expect( - Earcut.triangulateRaw( - [0, 0, 100, 0, 100, 100, 0, 100, 20, 20, 80, 20, 80, 80, 20, 80], - holeIndices: [4], - ), - [ - 3, - 0, - 4, - 5, - 4, - 0, - 3, - 4, - 7, - 5, - 0, - 1, - 2, - 3, - 7, - 6, - 5, - 1, - 2, - 7, - 6, - 6, - 1, - 2 - ], - ), - ); - - test( - 'Polygon With 3D Coords', - () => expect( - Earcut.triangulateRaw( - [10, 0, 1, 0, 50, 2, 60, 60, 3, 70, 10, 4], - dimensions: 3, - ), - [1, 0, 3, 3, 2, 1], - ), + 'Generator Duration', + () async { + final clock = Stopwatch()..start(); + await countByGenerator(customPolygonRegion); + clock.stop(); + print('${clock.elapsedMilliseconds / 1000} s'); + }, ); }); } From 8dc88056f57879bea43b3dc32d6715d1f65f52ff Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 3 Aug 2023 20:57:14 +0100 Subject: [PATCH 058/168] Transferred `Earcut` to independent package Former-commit-id: 3f7c4e2c18e012909c14e2ff02d454d293680898 [formerly 36dfbc5111c51ab5a70c7993ee01ed5c7d1e90cc] Former-commit-id: 0e287fab849970616096cb4510d2c57ad9e67ddb --- .pubignore | 1 - .../components/region_information.dart | 4 +- example/pubspec.yaml | 1 + lib/src/bulk_download/tile_loops/count.dart | 6 +- .../tile_loops/custom_polygon_tools/bres.dart | 50 -- .../custom_polygon_tools/earcut.dart | 842 ------------------ .../bulk_download/tile_loops/generate.dart | 6 +- lib/src/bulk_download/tile_loops/shared.dart | 49 +- pubspec.yaml | 1 + test/earcut_test.dart | 60 -- 10 files changed, 56 insertions(+), 964 deletions(-) delete mode 100644 .pubignore delete mode 100644 lib/src/bulk_download/tile_loops/custom_polygon_tools/bres.dart delete mode 100644 lib/src/bulk_download/tile_loops/custom_polygon_tools/earcut.dart delete mode 100644 test/earcut_test.dart diff --git a/.pubignore b/.pubignore deleted file mode 100644 index e5ca5eba..00000000 --- a/.pubignore +++ /dev/null @@ -1 +0,0 @@ -prebuiltExampleApplications/ \ No newline at end of file diff --git a/example/lib/screens/download_region/components/region_information.dart b/example/lib/screens/download_region/components/region_information.dart index e12c92e5..c984bfdb 100644 --- a/example/lib/screens/download_region/components/region_information.dart +++ b/example/lib/screens/download_region/components/region_information.dart @@ -1,12 +1,10 @@ -// ignore_for_file: implementation_imports - import 'dart:math'; import 'package:collection/collection.dart'; +import 'package:dart_earcut/dart_earcut.dart'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; -import 'package:flutter_map_tile_caching/src/bulk_download/tile_loops/custom_polygon_tools/earcut.dart'; import 'package:intl/intl.dart'; import 'package:latlong2/latlong.dart'; diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 16af729d..d8de93dc 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -14,6 +14,7 @@ dependencies: badges: ^3.0.2 better_open_file: ^3.6.4 collection: ^1.17.1 + dart_earcut: ^1.0.0 file_picker: ^5.2.10 flutter: sdk: flutter diff --git a/lib/src/bulk_download/tile_loops/count.dart b/lib/src/bulk_download/tile_loops/count.dart index dcf23255..dd51b286 100644 --- a/lib/src/bulk_download/tile_loops/count.dart +++ b/lib/src/bulk_download/tile_loops/count.dart @@ -241,17 +241,17 @@ class TilesCounter { final vertex3 = region.crs.latLngToPoint(triangle[2], zoomLvl).round(); final outlineTiles = { - ...bresenhamsLGA( + ..._bresenhamsLGA( Point(vertex1.x, vertex1.y), Point(vertex2.x, vertex2.y), unscaleBy: region.options.tileSize, ), - ...bresenhamsLGA( + ..._bresenhamsLGA( Point(vertex2.x, vertex2.y), Point(vertex3.x, vertex3.y), unscaleBy: region.options.tileSize, ), - ...bresenhamsLGA( + ..._bresenhamsLGA( Point(vertex3.x, vertex3.y), Point(vertex1.x, vertex1.y), unscaleBy: region.options.tileSize, diff --git a/lib/src/bulk_download/tile_loops/custom_polygon_tools/bres.dart b/lib/src/bulk_download/tile_loops/custom_polygon_tools/bres.dart deleted file mode 100644 index 5aa91e6f..00000000 --- a/lib/src/bulk_download/tile_loops/custom_polygon_tools/bres.dart +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright © Luka S (JaffaKetchup) under GPL-v3 -// A full license can be found at .\LICENSE - -import 'dart:math'; - -/// Bresenham’s line generation algorithm, ported (with minor API differences) -/// from [anushaihalapathirana/Bresenham-line-drawing-algorithm](https://github.com/anushaihalapathirana/Bresenham-line-drawing-algorithm). -Iterable> bresenhamsLGA( - Point start, - Point end, { - double unscaleBy = 1, -}) sync* { - final dx = end.x - start.x; - final dy = end.y - start.y; - final absdx = dx.abs(); - final absdy = dy.abs(); - - var x = start.x; - var y = start.y; - yield Point((x / unscaleBy).floor(), (y / unscaleBy).floor()); - - if (absdx > absdy) { - var d = 2 * absdy - absdx; - - for (var i = 0; i < absdx; i++) { - x = dx < 0 ? x - 1 : x + 1; - if (d < 0) { - d = d + 2 * absdy; - } else { - y = dy < 0 ? y - 1 : y + 1; - d = d + (2 * absdy - 2 * absdx); - } - yield Point((x / unscaleBy).floor(), (y / unscaleBy).floor()); - } - } else { - // case when slope is greater than or equals to 1 - var d = 2 * absdx - absdy; - - for (var i = 0; i < absdy; i++) { - y = dy < 0 ? y - 1 : y + 1; - if (d < 0) { - d = d + 2 * absdx; - } else { - x = dx < 0 ? x - 1 : x + 1; - d = d + (2 * absdx) - (2 * absdy); - } - yield Point((x / unscaleBy).floor(), (y / unscaleBy).floor()); - } - } -} diff --git a/lib/src/bulk_download/tile_loops/custom_polygon_tools/earcut.dart b/lib/src/bulk_download/tile_loops/custom_polygon_tools/earcut.dart deleted file mode 100644 index 1e55a902..00000000 --- a/lib/src/bulk_download/tile_loops/custom_polygon_tools/earcut.dart +++ /dev/null @@ -1,842 +0,0 @@ -// Copyright © Luka S (JaffaKetchup) under GPL-v3 -// A full license can be found at .\LICENSE - -// ignore_for_file: parameter_assignments - -import 'dart:math'; - -/// Earcutting triangulation algorithm, ported (with minor API differences) from -/// [earcut4j/earcut4j](https://github.com/earcut4j/earcut4j) and -/// [mapbox/earcut](https://github.com/mapbox/earcut). -final class Earcut { - /// Triangulates the given polygon - /// - /// [polygonVertices] should be a list of all the [Point]s defining - /// the polygon. - /// - /// [holeIndices] should be a list of hole indicies, if any. For example, - /// `[5, 8]` for a 12-vertice input would mean one hole with vertices 5-7 and - /// another with 8-11. - /// - /// Returns a list of vertice indicies where a group of 3 forms a triangle. - static List triangulateFromPoints( - Iterable> polygonVertices, { - List? holeIndices, - }) => - triangulateRaw( - polygonVertices.map((e) => [e.x, e.y]).expand((e) => e).toList(), - holeIndices: holeIndices, - ); - - /// Triangulates the given polygon - /// - /// [polygonVertices] should be a flat list of all the coordinates defining - /// the polygon. If [dimensions] is 2, it is expected to be in the format - /// `[x0, y0, x1, y1, x2, y2]`. If [dimensions] is 3, it is expected to be in - /// the format `[x0, y0, z0, x1, y1, z1, x2, y2, z2]`. - /// - /// [holeIndices] should be a list of hole indicies, if any. For example, - /// `[5, 8]` for a 12-vertice input would mean one hole with vertices 5-7 and - /// another with 8-11. - /// - /// Returns a list of vertice indicies where a group of 3 forms a triangle. - static List triangulateRaw( - List polygonVertices, { - List? holeIndices, - int dimensions = 2, - }) { - final bool hasHoles = holeIndices != null && holeIndices.isNotEmpty; - final int outerLen = - hasHoles ? holeIndices[0] * dimensions : polygonVertices.length; - - _Node? outerNode = - _linkedList(polygonVertices, 0, outerLen, dimensions, clockwise: true); - - final triangles = []; - - if (outerNode == null || outerNode.next == outerNode.prev) { - return triangles; - } - - double minX = 0; - double minY = 0; - double maxX = 0; - double maxY = 0; - double invSize = 4.9E-324; - - if (hasHoles) { - outerNode = - _eliminateHoles(polygonVertices, holeIndices, outerNode, dimensions); - } - - // if the shape is not too simple, we'll use z-order curve hash later; - // calculate polygon bbox - if (polygonVertices.length > 80 * dimensions) { - minX = maxX = polygonVertices[0]; - minY = maxY = polygonVertices[1]; - - for (int i = dimensions; i < outerLen; i += dimensions) { - final double x = polygonVertices[i]; - final double y = polygonVertices[i + 1]; - if (x < minX) { - minX = x; - } - if (y < minY) { - minY = y; - } - if (x > maxX) { - maxX = x; - } - if (y > maxY) { - maxY = y; - } - } - - // minX, minY and size are later used to transform coords into - // ints for z-order calculation - invSize = max(maxX - minX, maxY - minY); - invSize = invSize != 0.0 ? 1.0 / invSize : 0.0; - } - - _earcutLinked( - outerNode, - triangles, - dimensions, - minX, - minY, - invSize, - -2147483648, - ); - - return triangles; - } - - static void _earcutLinked( - _Node? ear, - List triangles, - int dim, - double minX, - double minY, - double invSize, - int pass, - ) { - if (ear == null) return; - - // interlink polygon nodes in z-order - if (pass == -2147483648 && invSize != 4.9E-324) { - _indexCurve(ear, minX, minY, invSize); - } - - _Node? stop = ear; - - // iterate through ears, slicing them one by one - while (ear!.prev != ear.next) { - final prev = ear.prev; - final next = ear.next; - - if (invSize != 4.9E-324 - ? _isEarHashed(ear, minX, minY, invSize) - : _isEar(ear)) { - // cut off the triangle - triangles - ..add(prev!.i ~/ dim) - ..add(ear.i ~/ dim) - ..add(next!.i ~/ dim); - - _removeNode(ear); - - // skipping the next vertice leads to less sliver triangles - ear = next.next; - stop = next.next; - - continue; - } - - ear = next; - - // if we looped through the whole remaining polygon and can't find - // any more ears - if (ear == stop) { - // try filtering points and slicing again - if (pass == -2147483648) { - _earcutLinked( - _filterPoints(ear, null), - triangles, - dim, - minX, - minY, - invSize, - 1, - ); - - // if this didn't work, try curing all small - // self-intersections locally - } else if (pass == 1) { - ear = _cureLocalIntersections( - _filterPoints(ear, null)!, - triangles, - dim, - ); - _earcutLinked(ear, triangles, dim, minX, minY, invSize, 2); - - // as a last resort, try splitting the remaining polygon - // into two - } else if (pass == 2) { - _splitEarcut(ear!, triangles, dim, minX, minY, invSize); - } - - break; - } - } - } - - static void _splitEarcut( - _Node start, - List triangles, - int dim, - double minX, - double minY, - double size, - ) { - // look for a valid diagonal that divides the polygon into two - _Node a = start; - do { - _Node? b = a.next!.next; - while (b != a.prev) { - if (a.i != b!.i && _isValidDiagonal(a, b)) { - // split the polygon in two by the diagonal - _Node c = _splitPolygon(a, b); - - // filter colinear points around the cuts - a = _filterPoints(a, a.next)!; - c = _filterPoints(c, c.next)!; - - // run earcut on each half - _earcutLinked(a, triangles, dim, minX, minY, size, -2147483648); - _earcutLinked(c, triangles, dim, minX, minY, size, -2147483648); - return; - } - b = b.next; - } - a = a.next!; - } while (a != start); - } - - static bool _isValidDiagonal(_Node a, _Node b) => - a.next!.i != b.i && - a.prev!.i != b.i && - !_intersectsPolygon(a, b) && // dones't intersect other edges - (_locallyInside(a, b) && - _locallyInside(b, a) && - _middleInside(a, b) && // locally visible - (_area(a.prev!, a, b.prev!) != 0 || - _area(a, b.prev!, b) != - 0) || // does not create opposite-facing sectors - _equals(a, b) && - _area(a.prev!, a, a.next!) > 0 && - _area(b.prev!, b, b.next!) > 0); // special zero-length case - - static bool _middleInside(_Node a, _Node b) { - _Node p = a; - bool inside = false; - final px = (a.x + b.x) / 2; - final py = (a.y + b.y) / 2; - do { - if (((p.y > py) != (p.next!.y > py)) && - (px < (p.next!.x - p.x) * (py - p.y) / (p.next!.y - p.y) + p.x)) { - inside = !inside; - } - p = p.next!; - } while (p != a); - - return inside; - } - - static bool _intersectsPolygon(_Node a, _Node b) { - _Node p = a; - do { - if (p.i != a.i && - p.next!.i != a.i && - p.i != b.i && - p.next!.i != b.i && - _intersects(p, p.next, a, b)) { - return true; - } - p = p.next!; - } while (p != a); - - return false; - } - - static bool _intersects(_Node p1, _Node? q1, _Node p2, _Node? q2) { - if ((_equals(p1, p2) && _equals(q1!, q2!)) || - (_equals(p1, q2!) && _equals(p2, q1!))) { - return true; - } - final o1 = _sign(_area(p1, q1!, p2)); - final o2 = _sign(_area(p1, q1, q2)); - final o3 = _sign(_area(p2, q2, p1)); - final o4 = _sign(_area(p2, q2, q1)); - - if (o1 != o2 && o3 != o4) { - return true; // general case - } - - if (o1 == 0 && _onSegment(p1, p2, q1)) { - return true; // p1, q1 and p2 are collinear and p2 lies on p1q1 - } - if (o2 == 0 && _onSegment(p1, q2, q1)) { - return true; // p1, q1 and q2 are collinear and q2 lies on p1q1 - } - if (o3 == 0 && _onSegment(p2, p1, q2)) { - return true; // p2, q2 and p1 are collinear and p1 lies on p2q2 - } - if (o4 == 0 && _onSegment(p2, q1, q2)) { - return true; // p2, q2 and q1 are collinear and q1 lies on p2q2 - } - - return false; - } - - // for collinear points p, q, r, check if point q lies on segment pr - static bool _onSegment(_Node p, _Node q, _Node r) => - q.x <= max(p.x, r.x) && - q.x >= min(p.x, r.x) && - q.y <= max(p.y, r.y) && - q.y >= min(p.y, r.y); - - static double _sign(double num) => num > 0 - ? 1 - : num < 0 - ? -1 - : 0; - - static _Node? _cureLocalIntersections( - _Node start, - List triangles, - int dim, - ) { - _Node p = start; - do { - final _Node? a = p.prev; - final b = p.next!.next; - - if (!_equals(a!, b!) && - _intersects(a, p, p.next!, b) && - _locallyInside(a, b) && - _locallyInside(b, a)) { - triangles - ..add(a.i ~/ dim) - ..add(p.i ~/ dim) - ..add(b.i ~/ dim); - - // remove two nodes involved - _removeNode(p); - _removeNode(p.next!); - - p = start = b; - } - p = p.next!; - } while (p != start); - - return _filterPoints(p, null); - } - - static bool _isEar(_Node ear) { - final a = ear.prev; - final b = ear; - final c = ear.next; - - if (_area(a!, b, c!) >= 0) { - return false; // reflex, can't be an ear - } - - // now make sure we don't have other points inside the potential ear - _Node? p = ear.next!.next; - - while (p != ear.prev) { - if (_pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, p!.x, p.y) && - _area(p.prev!, p, p.next!) >= 0) { - return false; - } - p = p.next; - } - - return true; - } - - static bool _isEarHashed( - _Node ear, - double minX, - double minY, - double invSize, - ) { - final a = ear.prev!; - final b = ear; - final c = ear.next!; - - if (_area(a, b, c) >= 0) { - return false; // reflex, can't be an ear - } - - // triangle bbox; min & max are calculated like this for speed - - final minTX = a.x < b.x ? (a.x < c.x ? a.x : c.x) : (b.x < c.x ? b.x : c.x); - final minTY = a.y < b.y ? (a.y < c.y ? a.y : c.y) : (b.y < c.y ? b.y : c.y); - final maxTX = a.x > b.x ? (a.x > c.x ? a.x : c.x) : (b.x > c.x ? b.x : c.x); - final maxTY = a.y > b.y ? (a.y > c.y ? a.y : c.y) : (b.y > c.y ? b.y : c.y); - - // z-order range for the current triangle bbox; - final minZ = _zOrder(minTX, minTY, minX, minY, invSize); - final maxZ = _zOrder(maxTX, maxTY, minX, minY, invSize); - - // first look for points inside the triangle in increasing z-order - _Node? p = ear.prevZ; - _Node? n = ear.nextZ; - - while (p != null && p.z >= minZ && n != null && n.z <= maxZ) { - if (p != ear.prev && - p != ear.next && - _pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y) && - _area(p.prev!, p, p.next!) >= 0) return false; - p = p.prevZ; - - if (n != ear.prev && - n != ear.next && - _pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, n.x, n.y) && - _area(n.prev!, n, n.next!) >= 0) return false; - n = n.nextZ; - } - - // look for remaining points in decreasing z-order - while (p != null && p.z >= minZ) { - if (p != ear.prev && - p != ear.next && - _pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y) && - _area(p.prev!, p, p.next!) >= 0) return false; - p = p.prevZ; - } - - // look for remaining points in increasing z-order - while (n != null && n.z <= maxZ) { - if (n != ear.prev && - n != ear.next && - _pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, n.x, n.y) && - _area(n.prev!, n, n.next!) >= 0) return false; - n = n.nextZ; - } - - return true; - } - - // z-order of a point given coords and inverse of the longer side of data bbox - static int _zOrder( - double x, - double y, - double minX, - double minY, - double invSize, - ) { - // coords are transformed into non-negative 15-bit int range - int lx = (32767 * (x - minX) * invSize).toInt(); - int ly = (32767 * (y - minY) * invSize).toInt(); - - lx = (lx | (lx << 8)) & 0x00FF00FF; - lx = (lx | (lx << 4)) & 0x0F0F0F0F; - lx = (lx | (lx << 2)) & 0x33333333; - lx = (lx | (lx << 1)) & 0x55555555; - - ly = (ly | (ly << 8)) & 0x00FF00FF; - ly = (ly | (ly << 4)) & 0x0F0F0F0F; - ly = (ly | (ly << 2)) & 0x33333333; - ly = (ly | (ly << 1)) & 0x55555555; - - return lx | (ly << 1); - } - - static void _indexCurve( - _Node start, - double minX, - double minY, - double invSize, - ) { - _Node p = start; - do { - if (p.z == 4.9E-324) { - p.z = _zOrder(p.x, p.y, minX, minY, invSize).toDouble(); - } - p - ..prevZ = p.prev - ..nextZ = p.next; - p = p.next!; - } while (p != start); - - p.prevZ!.nextZ = null; - p.prevZ = null; - - _sortLinked(p); - } - - static _Node? _sortLinked(_Node? list) { - int inSize = 1; - - int numMerges; - do { - _Node? p = list; - list = null; - _Node? tail; - numMerges = 0; - - while (p != null) { - numMerges++; - _Node? q = p; - int pSize = 0; - for (int i = 0; i < inSize; i++) { - pSize++; - q = q?.nextZ; - if (q == null) { - break; - } - } - - int qSize = inSize; - - while (pSize > 0 || (qSize > 0 && q != null)) { - _Node? e; - if (pSize == 0) { - e = q; - q = q!.nextZ; - qSize--; - } else if (qSize == 0 || q == null) { - e = p; - p = p!.nextZ; - pSize--; - } else if (p!.z <= q.z) { - e = p; - p = p.nextZ; - pSize--; - } else { - e = q; - q = q.nextZ; - qSize--; - } - - if (tail != null) { - tail.nextZ = e; - } else { - list = e; - } - - e!.prevZ = tail; - tail = e; - } - - p = q; - } - - tail!.nextZ = null; - inSize *= 2; - } while (numMerges > 1); - - return list; - } - - static _Node? _eliminateHoles( - List data, - List holeIndices, - _Node outerNode, - int dim, - ) { - final queue = <_Node>[]; - - final int len = holeIndices.length; - for (int i = 0; i < len; i++) { - final int start = holeIndices[i] * dim; - final int end = i < len - 1 ? holeIndices[i + 1] * dim : data.length; - final _Node? list = _linkedList(data, start, end, dim, clockwise: false); - if (list == list?.next) list?.steiner = true; - queue.add(_getLeftmost(list!)); - } - - queue.sort((a, b) { - if (a.x - b.x > 0) { - return 1; - } else if (a.x - b.x < 0) { - return -1; - } - return 0; - }); - - for (final _Node node in queue) { - _eliminateHole(node, outerNode); - outerNode = _filterPoints(outerNode, outerNode.next)!; - } - - return outerNode; - } - - static _Node? _filterPoints(_Node? start, _Node? end) { - if (start == null) return start; - end ??= start; - - _Node p = start; - bool again; - - do { - again = false; - - if (!p.steiner && _equals(p, p.next!) || - _area(p.prev!, p, p.next!) == 0) { - _removeNode(p); - p = (end = p.prev)!; - if (p == p.next) { - break; - } - again = true; - } else { - p = p.next!; - } - } while (again || p != end); - - return end; - } - - static bool _equals(_Node p1, _Node p2) => p1.x == p2.x && p1.y == p2.y; - - static double _area(_Node p, _Node q, _Node r) => - (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y); - - static void _eliminateHole(_Node hole, _Node? outerNode) { - outerNode = _findHoleBridge(hole, outerNode!); - if (outerNode != null) { - final _Node b = _splitPolygon(outerNode, hole); - - // filter collinear points around the cuts - _filterPoints(outerNode, outerNode.next); - _filterPoints(b, b.next); - } - } - - static _Node _splitPolygon(_Node a, _Node b) { - final a2 = _Node(a.i, a.x, a.y); - final b2 = _Node(b.i, b.x, b.y); - final an = a.next!; - final bp = b.prev!; - - a.next = b; - b.prev = a; - - a2.next = an; - an.prev = a2; - - b2.next = a2; - a2.prev = b2; - - bp.next = b2; - b2.prev = bp; - - return b2; - } - - // David Eberly's algorithm for finding a bridge between hole and outer - // polygon - static _Node? _findHoleBridge(_Node hole, _Node outerNode) { - _Node p = outerNode; - final double hx = hole.x; - final double hy = hole.y; - double qx = -1.7976931348623157E308; - _Node? m; - - // find a segment intersected by a ray from the hole's leftmost point to - // the left; - // segment's endpoint with lesser x will be potential connection point - do { - if (hy <= p.y && hy >= p.next!.y) { - final double x = - p.x + (hy - p.y) * (p.next!.x - p.x) / (p.next!.y - p.y); - if (x <= hx && x > qx) { - qx = x; - if (x == hx) { - if (hy == p.y) { - return p; - } - if (hy == p.next!.y) { - return p.next; - } - } - m = p.x < p.next!.x ? p : p.next; - } - } - p = p.next!; - } while (p != outerNode); - - if (m == null) { - return null; - } - - if (hx == qx) { - return m; // hole touches outer segment; pick leftmost endpoint - } - - // look for points inside the triangle of hole point, segment - // intersection and endpoint; - // if there are no points found, we have a valid connection; - // otherwise choose the point of the minimum angle with the ray as - // connection point - - final _Node stop = m; - final double mx = m.x; - final double my = m.y; - double tanMin = 1.7976931348623157E308; - double tan; - - p = m; - - do { - if (hx >= p.x && - p.x >= mx && - _pointInTriangle( - hy < my ? hx : qx, - hy, - mx, - my, - hy < my ? qx : hx, - hy, - p.x, - p.y, - )) { - tan = (hy - p.y).abs() / (hx - p.x); // tangential - - if (_locallyInside(p, hole) && - (tan < tanMin || - (tan == tanMin && - (p.x > m!.x || - (p.x == m.x && _sectorContainsSector(m, p)))))) { - m = p; - tanMin = tan; - } - } - - p = p.next!; - } while (p != stop); - - return m; - } - - static bool _locallyInside(_Node a, _Node? b) => - _area(a.prev!, a, a.next!) < 0 - ? _area(a, b!, a.next!) >= 0 && _area(a, a.prev!, b) >= 0 - : _area(a, b!, a.prev!) < 0 || _area(a, a.next!, b) < 0; - - // whether sector in vertex m contains sector in vertex p in the same - // coordinates - static bool _sectorContainsSector(_Node m, _Node p) => - _area(m.prev!, m, p.prev!) < 0 && _area(p.next!, m, m.next!) < 0; - - static bool _pointInTriangle( - double ax, - double ay, - double bx, - double by, - double cx, - double cy, - double px, - double py, - ) => - (cx - px) * (ay - py) - (ax - px) * (cy - py) >= 0 && - (ax - px) * (by - py) - (bx - px) * (ay - py) >= 0 && - (bx - px) * (cy - py) - (cx - px) * (by - py) >= 0; - - static _Node _getLeftmost(_Node start) { - _Node p = start; - _Node leftmost = start; - do { - if (p.x < leftmost.x || (p.x == leftmost.x && p.y < leftmost.y)) { - leftmost = p; - } - p = p.next!; - } while (p != start); - return leftmost; - } - - static _Node? _linkedList( - List data, - int start, - int end, - int dim, { - required bool clockwise, - }) { - _Node? last; - if (clockwise == (_signedArea(data, start, end, dim) > 0)) { - for (int i = start; i < end; i += dim) { - last = _insertNode(i, data[i], data[i + 1], last); - } - } else { - for (int i = end - dim; i >= start; i -= dim) { - last = _insertNode(i, data[i], data[i + 1], last); - } - } - - if (last != null && _equals(last, last.next!)) { - _removeNode(last); - last = last.next; - } - - return last; - } - - static void _removeNode(_Node p) { - p.next!.prev = p.prev; - p.prev!.next = p.next; - - p.prevZ?.nextZ = p.nextZ; - - p.nextZ?.prevZ = p.prevZ; - } - - static _Node _insertNode(int i, double x, double y, _Node? last) { - final _Node p = _Node(i, x, y); - - if (last == null) { - p - ..prev = p - ..next = p; - } else { - p - ..next = last.next - ..prev = last; - last.next!.prev = p; - last.next = p; - } - return p; - } - - static double _signedArea(List data, int start, int end, int dim) { - double sum = 0; - int j = end - dim; - for (int i = start; i < end; i += dim) { - sum += (data[j] - data[i]) * (data[i + 1] + data[j + 1]); - j = i; - } - return sum; - } -} - -class _Node { - int i; - double x; - double y; - double z; - bool steiner; - _Node? prev; - _Node? next; - _Node? prevZ; - _Node? nextZ; - - _Node(this.i, this.x, this.y) - : z = 4.9E-324, - steiner = false; - - @override - String toString() => 'i: $i, x: $x, y: $y, prev: $prev, next: $next'; -} diff --git a/lib/src/bulk_download/tile_loops/generate.dart b/lib/src/bulk_download/tile_loops/generate.dart index eac496dd..05744a65 100644 --- a/lib/src/bulk_download/tile_loops/generate.dart +++ b/lib/src/bulk_download/tile_loops/generate.dart @@ -261,17 +261,17 @@ class TilesGenerator { final vertex3 = region.crs.latLngToPoint(triangle[2], zoomLvl).round(); final outlineTiles = { - ...bresenhamsLGA( + ..._bresenhamsLGA( Point(vertex1.x, vertex1.y), Point(vertex2.x, vertex2.y), unscaleBy: region.options.tileSize, ), - ...bresenhamsLGA( + ..._bresenhamsLGA( Point(vertex2.x, vertex2.y), Point(vertex3.x, vertex3.y), unscaleBy: region.options.tileSize, ), - ...bresenhamsLGA( + ..._bresenhamsLGA( Point(vertex3.x, vertex3.y), Point(vertex1.x, vertex1.y), unscaleBy: region.options.tileSize, diff --git a/lib/src/bulk_download/tile_loops/shared.dart b/lib/src/bulk_download/tile_loops/shared.dart index 8685eae7..a4f3fb9c 100644 --- a/lib/src/bulk_download/tile_loops/shared.dart +++ b/lib/src/bulk_download/tile_loops/shared.dart @@ -6,14 +6,13 @@ import 'dart:math'; import 'package:async/async.dart'; import 'package:collection/collection.dart'; +import 'package:dart_earcut/dart_earcut.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/flutter_map.dart' hide Polygon; import 'package:latlong2/latlong.dart'; import '../../../flutter_map_tile_caching.dart'; import '../../misc/int_extremes.dart'; -import 'custom_polygon_tools/bres.dart'; -import 'custom_polygon_tools/earcut.dart'; part 'count.dart'; part 'generate.dart'; @@ -34,3 +33,49 @@ class _Polygon { identical(this, other) || (other is _Polygon && hashCode == other.hashCode); } + +/// Bresenham’s line generation algorithm, ported (with minor API differences) +/// from [anushaihalapathirana/Bresenham-line-drawing-algorithm](https://github.com/anushaihalapathirana/Bresenham-line-drawing-algorithm). +Iterable> _bresenhamsLGA( + Point start, + Point end, { + double unscaleBy = 1, +}) sync* { + final dx = end.x - start.x; + final dy = end.y - start.y; + final absdx = dx.abs(); + final absdy = dy.abs(); + + var x = start.x; + var y = start.y; + yield Point((x / unscaleBy).floor(), (y / unscaleBy).floor()); + + if (absdx > absdy) { + var d = 2 * absdy - absdx; + + for (var i = 0; i < absdx; i++) { + x = dx < 0 ? x - 1 : x + 1; + if (d < 0) { + d = d + 2 * absdy; + } else { + y = dy < 0 ? y - 1 : y + 1; + d = d + (2 * absdy - 2 * absdx); + } + yield Point((x / unscaleBy).floor(), (y / unscaleBy).floor()); + } + } else { + // case when slope is greater than or equals to 1 + var d = 2 * absdx - absdy; + + for (var i = 0; i < absdy; i++) { + y = dy < 0 ? y - 1 : y + 1; + if (d < 0) { + d = d + 2 * absdx; + } else { + x = dx < 0 ? x - 1 : x + 1; + d = d + (2 * absdx) - (2 * absdy); + } + yield Point((x / unscaleBy).floor(), (y / unscaleBy).floor()); + } + } +} diff --git a/pubspec.yaml b/pubspec.yaml index d9753982..ab0deb65 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -28,6 +28,7 @@ environment: dependencies: async: ^2.9.0 collection: ^1.16.0 + dart_earcut: ^1.0.0 flutter: sdk: flutter flutter_map: ^6.0.0-dev.2 diff --git a/test/earcut_test.dart b/test/earcut_test.dart deleted file mode 100644 index fd3a71c3..00000000 --- a/test/earcut_test.dart +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright © Luka S (JaffaKetchup) under GPL-v3 -// A full license can be found at .\LICENSE - -// ignore_for_file: avoid_print - -import 'package:flutter_map_tile_caching/src/bulk_download/tile_loops/custom_polygon_tools/earcut.dart'; -import 'package:test/test.dart'; - -void main() { - test( - 'Simple Triangle', - () => expect(Earcut.triangulateRaw([0, 0, 0, 50, 50, 00]), [1, 0, 2]), - ); - - test( - 'Complex Triangle', - () => expect( - Earcut.triangulateRaw([0, 0, 0, 25, 0, 50, 25, 25, 50, 0, 25, 0]), - [1, 0, 5, 5, 4, 3, 3, 2, 1, 1, 5, 3], - ), - ); - - test( - 'L Shape', - () => expect( - Earcut.triangulateRaw([0, 0, 10, 0, 10, 5, 5, 5, 5, 15, 0, 15]), - [4, 5, 0, 0, 1, 2, 3, 4, 0, 0, 2, 3], - ), - ); - - test( - 'Simple Polygon', - () => expect( - Earcut.triangulateRaw([10, 0, 0, 50, 60, 60, 70, 10]), - [1, 0, 3, 3, 2, 1], - ), - ); - - test( - 'Polygon With Hole', - () => expect( - Earcut.triangulateRaw( - [0, 0, 100, 0, 100, 100, 0, 100, 20, 20, 80, 20, 80, 80, 20, 80], - holeIndices: [4], - ), - [3, 0, 4, 5, 4, 0, 3, 4, 7, 5, 0, 1, 2, 3, 7, 6, 5, 1, 2, 7, 6, 6, 1, 2], - ), - ); - - test( - 'Polygon With 3D Coords', - () => expect( - Earcut.triangulateRaw( - [10, 0, 1, 0, 50, 2, 60, 60, 3, 70, 10, 4], - dimensions: 3, - ), - [1, 0, 3, 3, 2, 1], - ), - ); -} From db826d9fe9332b3c2384ba582400ddd08b924669 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Fri, 4 Aug 2023 12:51:28 +0100 Subject: [PATCH 059/168] Improved example application's state handling Improved example application's performance Reorganized example application structure Former-commit-id: 29b19e64561f533daef7a23e2588573af1b2adc9 [formerly e019650aa60091992e9830f71712625a82bd01ed] Former-commit-id: 534ba4a80b8cb6c235f5631994dd0cf61e432053 --- example/lib/main.dart | 28 +- .../components/numerical_input_row.dart | 94 +++++ .../components/region_information.dart | 0 .../components/section_separator.dart | 0 .../components/store_selector.dart | 4 +- .../configure_download.dart | 263 ++++++++++++++ .../state/configure_download_provider.dart | 45 +++ .../download_region/download_region.dart | 341 ------------------ example/lib/screens/main/main.dart | 26 +- .../components/download_layout.dart | 22 +- .../components/main_statistics.dart | 9 +- .../main/pages/downloading/downloading.dart | 86 +++-- .../state/downloading_provider.dart | 60 +++ .../lib/screens/main/pages/map/map_view.dart | 4 +- .../main/pages/map}/state/map_provider.dart | 2 +- .../pages/recovery/components/header.dart | 2 +- .../components/crosshairs.dart | 0 .../custom_polygon_snapping_indicator.dart | 8 +- .../components/region_shape.dart | 4 +- .../additional_panes/additional_pane.dart | 2 +- .../adjust_zoom_lvls_pane.dart | 8 +- .../additional_panes/line_region_pane.dart | 4 +- .../additional_panes/slider_panel_base.dart | 0 .../side_panel/custom_slider_track_shape.dart | 0 .../components/side_panel/parent.dart | 10 +- .../components/side_panel/primary_pane.dart | 28 +- .../side_panel/region_shape_button.dart | 2 +- .../components/usage_instructions.dart | 4 +- .../map_view.dart | 90 +++-- .../region_selection.dart} | 12 +- .../state/region_selection_provider.dart} | 69 +--- .../main/pages/stores/components/header.dart | 2 +- .../pages/stores/components/store_tile.dart | 2 +- .../screens/store_editor/store_editor.dart | 4 +- .../components}/build_attribution.dart | 0 example/pubspec.yaml | 2 +- pubspec.yaml | 2 +- 37 files changed, 680 insertions(+), 559 deletions(-) create mode 100644 example/lib/screens/configure_download/components/numerical_input_row.dart rename example/lib/screens/{download_region => configure_download}/components/region_information.dart (100%) rename example/lib/screens/{download_region => configure_download}/components/section_separator.dart (100%) rename example/lib/screens/{download_region => configure_download}/components/store_selector.dart (92%) create mode 100644 example/lib/screens/configure_download/configure_download.dart create mode 100644 example/lib/screens/configure_download/state/configure_download_provider.dart delete mode 100644 example/lib/screens/download_region/download_region.dart create mode 100644 example/lib/screens/main/pages/downloading/state/downloading_provider.dart rename example/lib/{shared => screens/main/pages/map}/state/map_provider.dart (95%) rename example/lib/screens/main/pages/{downloader => region_selection}/components/crosshairs.dart (100%) rename example/lib/screens/main/pages/{downloader => region_selection}/components/custom_polygon_snapping_indicator.dart (82%) rename example/lib/screens/main/pages/{downloader => region_selection}/components/region_shape.dart (96%) rename example/lib/screens/main/pages/{downloader => region_selection}/components/side_panel/additional_panes/additional_pane.dart (94%) rename example/lib/screens/main/pages/{downloader => region_selection}/components/side_panel/additional_panes/adjust_zoom_lvls_pane.dart (88%) rename example/lib/screens/main/pages/{downloader => region_selection}/components/side_panel/additional_panes/line_region_pane.dart (95%) rename example/lib/screens/main/pages/{downloader => region_selection}/components/side_panel/additional_panes/slider_panel_base.dart (100%) rename example/lib/screens/main/pages/{downloader => region_selection}/components/side_panel/custom_slider_track_shape.dart (100%) rename example/lib/screens/main/pages/{downloader => region_selection}/components/side_panel/parent.dart (87%) rename example/lib/screens/main/pages/{downloader => region_selection}/components/side_panel/primary_pane.dart (87%) rename example/lib/screens/main/pages/{downloader => region_selection}/components/side_panel/region_shape_button.dart (89%) rename example/lib/screens/main/pages/{downloader => region_selection}/components/usage_instructions.dart (97%) rename example/lib/screens/main/pages/{downloader => region_selection}/map_view.dart (76%) rename example/lib/screens/main/pages/{downloader/downloader.dart => region_selection/region_selection.dart} (84%) rename example/lib/{shared/state/download_provider.dart => screens/main/pages/region_selection/state/region_selection_provider.dart} (64%) rename example/lib/{screens/main/pages/map => shared/components}/build_attribution.dart (100%) diff --git a/example/lib/main.dart b/example/lib/main.dart index 467b00c8..1d518719 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -7,10 +7,12 @@ import 'package:google_fonts/google_fonts.dart'; import 'package:path/path.dart' as p; import 'package:provider/provider.dart'; +import 'screens/configure_download/state/configure_download_provider.dart'; import 'screens/main/main.dart'; -import 'shared/state/download_provider.dart'; +import 'screens/main/pages/downloading/state/downloading_provider.dart'; +import 'screens/main/pages/map/state/map_provider.dart'; +import 'screens/main/pages/region_selection/state/region_selection_provider.dart'; import 'shared/state/general_provider.dart'; -import 'shared/state/map_provider.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -53,16 +55,30 @@ class AppContainer extends StatelessWidget { @override Widget build(BuildContext context) => MultiProvider( providers: [ - ChangeNotifierProvider(create: (context) => GeneralProvider()), - ChangeNotifierProvider(create: (context) => DownloaderProvider()), - ChangeNotifierProvider(create: (context) => MapProvider()), + ChangeNotifierProvider(create: (_) => GeneralProvider()), + ChangeNotifierProvider( + create: (_) => MapProvider(), + lazy: true, + ), + ChangeNotifierProvider( + create: (_) => RegionSelectionProvider(), + lazy: true, + ), + ChangeNotifierProvider( + create: (_) => ConfigureDownloadProvider(), + lazy: true, + ), + ChangeNotifierProvider( + create: (_) => DownloadingProvider(), + lazy: true, + ), ], child: MaterialApp( title: 'FMTC Demo', theme: ThemeData( brightness: Brightness.dark, useMaterial3: true, - textTheme: GoogleFonts.openSansTextTheme(const TextTheme()), + textTheme: GoogleFonts.ubuntuTextTheme(const TextTheme()), colorSchemeSeed: Colors.red, switchTheme: SwitchThemeData( thumbIcon: MaterialStateProperty.resolveWith( diff --git a/example/lib/screens/configure_download/components/numerical_input_row.dart b/example/lib/screens/configure_download/components/numerical_input_row.dart new file mode 100644 index 00000000..46f43aa2 --- /dev/null +++ b/example/lib/screens/configure_download/components/numerical_input_row.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:provider/provider.dart'; + +import '../state/configure_download_provider.dart'; + +class NumericalInputRow extends StatelessWidget { + const NumericalInputRow({ + super.key, + required this.label, + required this.suffixText, + required this.value, + required this.min, + required this.max, + required this.onChanged, + }); + + final String label; + final String suffixText; + final int Function(ConfigureDownloadProvider provider) value; + final int min; + final int max; + final void Function(ConfigureDownloadProvider provider, int value) onChanged; + + @override + Widget build(BuildContext context) { + final currentValue = context.select(value); + + return Row( + children: [ + Text(label), + const Spacer(), + Icon( + Icons.lock, + color: currentValue == max + ? Colors.amber + : Colors.white.withOpacity(0.2), + ), + const SizedBox(width: 16), + IntrinsicWidth( + child: TextFormField( + initialValue: currentValue.toString(), + textAlign: TextAlign.end, + keyboardType: TextInputType.number, + decoration: InputDecoration( + isDense: true, + counterText: '', + suffixText: ' $suffixText', + ), + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + _NumericalRangeFormatter(min: min, max: max), + ], + onChanged: (newVal) => onChanged( + context.read(), + int.tryParse(newVal) ?? currentValue, + ), + ), + ), + ], + ); + } +} + +class _NumericalRangeFormatter extends TextInputFormatter { + const _NumericalRangeFormatter({required this.min, required this.max}); + final int min; + final int max; + + @override + TextEditingValue formatEditUpdate( + TextEditingValue oldValue, + TextEditingValue newValue, + ) { + if (newValue.text.isEmpty) return newValue; + + final int parsed = int.parse(newValue.text); + + if (parsed < min) { + return TextEditingValue.empty.copyWith( + text: min.toString(), + selection: TextSelection.collapsed(offset: min.toString().length), + ); + } + if (parsed > max) { + return TextEditingValue.empty.copyWith( + text: max.toString(), + selection: TextSelection.collapsed(offset: max.toString().length), + ); + } + + return newValue; + } +} diff --git a/example/lib/screens/download_region/components/region_information.dart b/example/lib/screens/configure_download/components/region_information.dart similarity index 100% rename from example/lib/screens/download_region/components/region_information.dart rename to example/lib/screens/configure_download/components/region_information.dart diff --git a/example/lib/screens/download_region/components/section_separator.dart b/example/lib/screens/configure_download/components/section_separator.dart similarity index 100% rename from example/lib/screens/download_region/components/section_separator.dart rename to example/lib/screens/configure_download/components/section_separator.dart diff --git a/example/lib/screens/download_region/components/store_selector.dart b/example/lib/screens/configure_download/components/store_selector.dart similarity index 92% rename from example/lib/screens/download_region/components/store_selector.dart rename to example/lib/screens/configure_download/components/store_selector.dart index 66a50b78..06107003 100644 --- a/example/lib/screens/download_region/components/store_selector.dart +++ b/example/lib/screens/configure_download/components/store_selector.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; import 'package:provider/provider.dart'; -import '../../../shared/state/download_provider.dart'; import '../../../shared/state/general_provider.dart'; +import '../../main/pages/region_selection/state/region_selection_provider.dart'; class StoreSelector extends StatefulWidget { const StoreSelector({super.key}); @@ -18,7 +18,7 @@ class _StoreSelectorState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('CHOOSE A STORE'), - Consumer2( + Consumer2( builder: (context, downloadProvider, generalProvider, _) => FutureBuilder>( future: FMTC.instance.rootDirectory.stats.storesAvailableAsync, diff --git a/example/lib/screens/configure_download/configure_download.dart b/example/lib/screens/configure_download/configure_download.dart new file mode 100644 index 00000000..dcb4dd7f --- /dev/null +++ b/example/lib/screens/configure_download/configure_download.dart @@ -0,0 +1,263 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; +import 'package:provider/provider.dart'; + +import '../main/pages/downloading/state/downloading_provider.dart'; +import '../main/pages/region_selection/state/region_selection_provider.dart'; +import 'components/numerical_input_row.dart'; +import 'components/region_information.dart'; +import 'components/section_separator.dart'; +import 'components/store_selector.dart'; +import 'state/configure_download_provider.dart'; + +class ConfigureDownloadPopup extends StatelessWidget { + const ConfigureDownloadPopup({ + super.key, + required this.region, + required this.minZoom, + required this.maxZoom, + }); + + final BaseRegion region; + final int minZoom; + final int maxZoom; + + @override + Widget build(BuildContext context) => + Selector( + selector: (context, provider) => provider.isReady, + builder: (context, isReady, _) => Scaffold( + appBar: AppBar(title: const Text('Configure Bulk Download')), + floatingActionButton: + Selector( + selector: (context, provider) => provider.selectedStore, + builder: (context, selectedStore, child) => + selectedStore == null ? const SizedBox.shrink() : child!, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + AnimatedScale( + scale: isReady ? 1 : 0, + duration: const Duration(milliseconds: 200), + curve: Curves.easeInCubic, + alignment: Alignment.bottomRight, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.onBackground, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(12), + topRight: Radius.circular(12), + bottomLeft: Radius.circular(12), + ), + ), + margin: const EdgeInsets.only(right: 12, left: 32), + padding: const EdgeInsets.all(12), + constraints: const BoxConstraints(maxWidth: 500), + child: const Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + "You must abide by your tile server's Terms of Service when bulk downloading. Many servers will forbid or heavily restrict this action, as it places extra strain on resources. Be respectful, and note that you use this functionality at your own risk.", + textAlign: TextAlign.end, + style: TextStyle(color: Colors.black), + ), + SizedBox(height: 8), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'CAUTION', + style: TextStyle( + color: Colors.red, + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + SizedBox(width: 8), + Icon( + Icons.report, + color: Colors.red, + size: 32, + ), + ], + ), + ], + ), + ), + ), + const SizedBox(height: 16), + FloatingActionButton.extended( + onPressed: () async { + final configureDownloadProvider = + context.read(); + + if (!isReady) { + configureDownloadProvider.isReady = true; + return; + } + + final regionSelectionProvider = + context.read(); + final downloadingProvider = + context.read(); + + final navigator = Navigator.of(context); + + final metadata = await regionSelectionProvider + .selectedStore!.metadata.readAsync; + + downloadingProvider.setDownloadProgress( + regionSelectionProvider.selectedStore!.download + .startForeground( + region: region.toDownloadable( + minZoom: minZoom, + maxZoom: maxZoom, + options: TileLayer( + urlTemplate: metadata['sourceURL'], + userAgentPackageName: + 'dev.jaffaketchup.fmtc.demo', + ), + ), + parallelThreads: + configureDownloadProvider.parallelThreads, + maxBufferLength: + configureDownloadProvider.maxBufferLength, + skipExistingTiles: + configureDownloadProvider.skipExistingTiles, + skipSeaTiles: + configureDownloadProvider.skipSeaTiles, + rateLimit: configureDownloadProvider.rateLimit, + ) + .asBroadcastStream(), + ); + configureDownloadProvider.isReady = false; + + navigator.pop(); + }, + label: const Text('Start Download'), + icon: Icon(isReady ? Icons.save : Icons.arrow_forward), + ), + ], + ), + ), + body: Stack( + children: [ + Positioned.fill( + left: 12, + top: 12, + right: 12, + bottom: 12, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RegionInformation( + region: region, + minZoom: minZoom, + maxZoom: maxZoom, + ), + const SectionSeparator(), + const StoreSelector(), + const SectionSeparator(), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('CONFIGURE DOWNLOAD OPTIONS'), + const SizedBox(height: 16), + NumericalInputRow( + label: 'Parallel Threads', + suffixText: 'threads', + value: (provider) => provider.parallelThreads, + min: 1, + max: 10, + onChanged: (provider, value) => + provider.parallelThreads = value, + ), + const SizedBox(height: 8), + NumericalInputRow( + label: 'Rate Limit', + suffixText: 'max. tiles/second', + value: (provider) => provider.rateLimit, + min: 5, + max: 200, + onChanged: (provider, value) => + provider.rateLimit = value, + ), + const SizedBox(height: 8), + NumericalInputRow( + label: 'Tile Buffer Length', + suffixText: 'max. tiles', + value: (provider) => provider.maxBufferLength, + min: 0, + max: 2000, + onChanged: (provider, value) => + provider.maxBufferLength = value, + ), + const SizedBox(height: 16), + Row( + children: [ + const Text('Skip Existing Tiles'), + const Spacer(), + Switch( + value: context + .select( + (provider) => provider.skipExistingTiles, + ), + onChanged: (val) => context + .read() + .skipExistingTiles = val, + activeColor: + Theme.of(context).colorScheme.primary, + ) + ], + ), + const SizedBox(height: 6), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Skip Sea Tiles'), + const Spacer(), + Switch( + value: context.select((provider) => provider.skipSeaTiles), + onChanged: (val) => context + .read() + .skipSeaTiles = val, + activeColor: + Theme.of(context).colorScheme.primary, + ) + ], + ), + ], + ), + ], + ), + ), + ), + Positioned.fill( + child: IgnorePointer( + ignoring: !isReady, + child: GestureDetector( + onTap: isReady + ? () => context + .read() + .isReady = false + : null, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + curve: Curves.easeInCubic, + color: isReady + ? Colors.black.withOpacity(2 / 3) + : Colors.transparent, + ), + ), + ), + ), + ], + ), + ), + ); +} diff --git a/example/lib/screens/configure_download/state/configure_download_provider.dart b/example/lib/screens/configure_download/state/configure_download_provider.dart new file mode 100644 index 00000000..bb93ba9d --- /dev/null +++ b/example/lib/screens/configure_download/state/configure_download_provider.dart @@ -0,0 +1,45 @@ +import 'package:flutter/foundation.dart'; + +class ConfigureDownloadProvider extends ChangeNotifier { + int _parallelThreads = 5; + int get parallelThreads => _parallelThreads; + set parallelThreads(int newNum) { + _parallelThreads = newNum; + notifyListeners(); + } + + int _rateLimit = 200; + int get rateLimit => _rateLimit; + set rateLimit(int newNum) { + _rateLimit = newNum; + notifyListeners(); + } + + int _maxBufferLength = 500; + int get maxBufferLength => _maxBufferLength; + set maxBufferLength(int newNum) { + _maxBufferLength = newNum; + notifyListeners(); + } + + bool _skipExistingTiles = true; + bool get skipExistingTiles => _skipExistingTiles; + set skipExistingTiles(bool newState) { + _skipExistingTiles = newState; + notifyListeners(); + } + + bool _skipSeaTiles = true; + bool get skipSeaTiles => _skipSeaTiles; + set skipSeaTiles(bool newState) { + _skipSeaTiles = newState; + notifyListeners(); + } + + bool _isReady = false; + bool get isReady => _isReady; + set isReady(bool newState) { + _isReady = newState; + notifyListeners(); + } +} diff --git a/example/lib/screens/download_region/download_region.dart b/example/lib/screens/download_region/download_region.dart deleted file mode 100644 index 02ef7e60..00000000 --- a/example/lib/screens/download_region/download_region.dart +++ /dev/null @@ -1,341 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; -import 'package:provider/provider.dart'; - -import '../../shared/state/download_provider.dart'; -import 'components/region_information.dart'; -import 'components/section_separator.dart'; -import 'components/store_selector.dart'; - -class DownloadRegionPopup extends StatefulWidget { - const DownloadRegionPopup({ - super.key, - required this.region, - required this.minZoom, - required this.maxZoom, - }); - - final BaseRegion region; - final int minZoom; - final int maxZoom; - - @override - State createState() => _DownloadRegionPopupState(); -} - -class _DownloadRegionPopupState extends State { - bool isReady = false; - - @override - Widget build(BuildContext context) => Consumer( - builder: (context, provider, _) => Scaffold( - appBar: AppBar(title: const Text('Configure Bulk Download')), - floatingActionButton: provider.selectedStore == null - ? null - : Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - AnimatedScale( - scale: isReady ? 1 : 0, - duration: const Duration(milliseconds: 200), - curve: Curves.easeInCubic, - alignment: Alignment.bottomRight, - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.onBackground, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(12), - topRight: Radius.circular(12), - bottomLeft: Radius.circular(12), - ), - ), - margin: const EdgeInsets.only(right: 12, left: 32), - padding: const EdgeInsets.all(12), - constraints: const BoxConstraints(maxWidth: 500), - child: const Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - "You must abide by your tile server's Terms of Service when bulk downloading. Many servers will forbid or heavily restrict this action, as it places extra strain on resources. Be respectful, and note that you use this functionality at your own risk.", - textAlign: TextAlign.end, - style: TextStyle(color: Colors.black), - ), - SizedBox(height: 8), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - 'CAUTION', - style: TextStyle( - color: Colors.red, - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - SizedBox(width: 8), - Icon( - Icons.report, - color: Colors.red, - size: 32, - ), - ], - ), - ], - ), - ), - ), - const SizedBox(height: 16), - FloatingActionButton.extended( - onPressed: () async { - if (!isReady) { - setState(() => isReady = true); - return; - } - final Map metadata = - await provider.selectedStore!.metadata.readAsync; - - provider.setDownloadProgress( - provider.selectedStore!.download - .startForeground( - region: widget.region.toDownloadable( - minZoom: provider.minZoom, - maxZoom: provider.maxZoom, - options: TileLayer( - urlTemplate: metadata['sourceURL'], - userAgentPackageName: - 'dev.jaffaketchup.fmtc.demo', - ), - ), - parallelThreads: provider.parallelThreads, - maxBufferLength: provider.bufferingAmount, - skipExistingTiles: provider.skipExistingTiles, - skipSeaTiles: provider.skipSeaTiles, - rateLimit: provider.rateLimit, - disableRecovery: provider.disableRecovery, - ) - .asBroadcastStream(), - ); - - if (mounted) Navigator.of(context).pop(); - }, - label: const Text('Start Download'), - icon: Icon(isReady ? Icons.save : Icons.arrow_forward), - ), - ], - ), - body: Stack( - children: [ - Positioned.fill( - left: 12, - top: 12, - right: 12, - bottom: 12, - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - RegionInformation( - region: widget.region, - minZoom: widget.minZoom, - maxZoom: widget.maxZoom, - ), - const SectionSeparator(), - const StoreSelector(), - const SectionSeparator(), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text('CONFIGURE DOWNLOAD OPTIONS'), - const SizedBox(height: 16), - Row( - children: [ - const Text('Parallel Threads'), - const Spacer(), - Icon( - Icons.lock, - color: provider.parallelThreads == 10 - ? Colors.amber - : Colors.white.withOpacity(0.2), - ), - const SizedBox(width: 16), - IntrinsicWidth( - child: TextFormField( - initialValue: - provider.parallelThreads.toString(), - textAlign: TextAlign.end, - keyboardType: TextInputType.number, - decoration: const InputDecoration( - isDense: true, - counterText: '', - suffixText: ' threads', - ), - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly, - const NumericalRangeFormatter( - min: 1, - max: 10, - ), - ], - onChanged: (value) => - provider.parallelThreads = - int.tryParse(value) ?? - provider.parallelThreads, - ), - ), - ], - ), - const SizedBox(height: 8), - Row( - children: [ - const Text('Rate Limit'), - const Spacer(), - Icon( - Icons.lock, - color: provider.rateLimit == 200 - ? Colors.amber - : Colors.white.withOpacity(0.2), - ), - const SizedBox(width: 16), - IntrinsicWidth( - child: TextFormField( - initialValue: provider.rateLimit.toString(), - textAlign: TextAlign.end, - keyboardType: TextInputType.number, - decoration: const InputDecoration( - isDense: true, - counterText: '', - suffixText: ' max. tiles/second', - ), - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly, - const NumericalRangeFormatter( - min: 1, - max: 200, - ), - ], - onChanged: (value) => provider.rateLimit = - int.tryParse(value) ?? provider.rateLimit, - ), - ), - ], - ), - const SizedBox(height: 8), - Row( - children: [ - const Text('Tile Buffer Length'), - const Spacer(), - IntrinsicWidth( - child: TextFormField( - initialValue: '200', - textAlign: TextAlign.end, - keyboardType: TextInputType.number, - decoration: const InputDecoration( - isDense: true, - counterText: '', - suffixText: ' tiles', - ), - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly, - const NumericalRangeFormatter( - min: 0, - max: 9223372036854775807, - ), - ], - onChanged: (value) => - provider.bufferingAmount = - int.tryParse(value) ?? - provider.bufferingAmount, - ), - ), - ], - ), - const SizedBox(height: 16), - Row( - children: [ - const Text('Skip Existing Tiles'), - const Spacer(), - Switch( - value: provider.skipExistingTiles, - onChanged: (val) => - provider.skipExistingTiles = val, - activeColor: - Theme.of(context).colorScheme.primary, - ) - ], - ), - const SizedBox(height: 6), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text('Skip Sea Tiles'), - const Spacer(), - Switch( - value: provider.skipSeaTiles, - onChanged: (val) => provider.skipSeaTiles = val, - activeColor: - Theme.of(context).colorScheme.primary, - ) - ], - ), - ], - ), - ], - ), - ), - ), - Positioned.fill( - child: IgnorePointer( - ignoring: !isReady, - child: GestureDetector( - onTap: - isReady ? () => setState(() => isReady = false) : null, - child: AnimatedContainer( - duration: const Duration(milliseconds: 200), - curve: Curves.easeInCubic, - color: isReady - ? Colors.black.withOpacity(2 / 3) - : Colors.transparent, - ), - ), - ), - ), - ], - ), - ), - ); -} - -class NumericalRangeFormatter extends TextInputFormatter { - const NumericalRangeFormatter({required this.min, required this.max}); - final int min; - final int max; - - @override - TextEditingValue formatEditUpdate( - TextEditingValue oldValue, - TextEditingValue newValue, - ) { - if (newValue.text.isEmpty) return newValue; - - final int parsed = int.parse(newValue.text); - - if (parsed < min) { - return TextEditingValue.empty.copyWith( - text: min.toString(), - selection: TextSelection.collapsed(offset: min.toString().length), - ); - } - if (parsed > max) { - return TextEditingValue.empty.copyWith( - text: max.toString(), - selection: TextSelection.collapsed(offset: max.toString().length), - ); - } - - return newValue; - } -} diff --git a/example/lib/screens/main/main.dart b/example/lib/screens/main/main.dart index 9b235fbb..0ef6a984 100644 --- a/example/lib/screens/main/main.dart +++ b/example/lib/screens/main/main.dart @@ -3,12 +3,12 @@ import 'package:flutter/material.dart' hide Badge; import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; import 'package:provider/provider.dart'; -//import '../../shared/state/download_provider.dart'; -import '../../shared/state/map_provider.dart'; -import 'pages/downloader/downloader.dart'; -//import 'pages/downloading/downloading.dart'; +import 'pages/downloading/downloading.dart'; +import 'pages/downloading/state/downloading_provider.dart'; import 'pages/map/map_view.dart'; +import 'pages/map/state/map_provider.dart'; import 'pages/recovery/recovery.dart'; +import 'pages/region_selection/region_selection.dart'; import 'pages/stores/stores.dart'; class MainScreen extends StatefulWidget { @@ -66,12 +66,12 @@ class _MainScreenState extends State { List get _pages => [ const MapPage(), const StoresPage(), - const DownloaderPage(), - /*Consumer( - builder: (context, provider, _) => provider.downloadProgress == null - ? const DownloaderPage() + Selector?>( + selector: (context, provider) => provider.downloadProgress, + builder: (context, downloadProgress, _) => downloadProgress == null + ? const RegionSelectionPage() : const DownloadingPage(), - ),*/ + ), RecoveryPage(moveToDownloadPage: () => _onDestinationSelected(2)), ]; @@ -109,7 +109,7 @@ class _MainScreenState extends State { @override Widget build(BuildContext context) => Scaffold( - bottomNavigationBar: MediaQuery.of(context).size.width > 950 + bottomNavigationBar: MediaQuery.sizeOf(context).width > 950 ? null : NavigationBar( backgroundColor: @@ -157,7 +157,7 @@ class _MainScreenState extends State { ), body: Row( children: [ - if (MediaQuery.of(context).size.width > 950) + if (MediaQuery.sizeOf(context).width > 950) NavigationRail( onDestinationSelected: _onDestinationSelected, selectedIndex: _currentPageIndex, @@ -198,10 +198,10 @@ class _MainScreenState extends State { Expanded( child: ClipRRect( borderRadius: BorderRadius.only( - topLeft: MediaQuery.of(context).size.width > 950 + topLeft: MediaQuery.sizeOf(context).width > 950 ? const Radius.circular(16) : Radius.zero, - bottomLeft: MediaQuery.of(context).size.width > 950 + bottomLeft: MediaQuery.sizeOf(context).width > 950 ? const Radius.circular(16) : Radius.zero, ), diff --git a/example/lib/screens/main/pages/downloading/components/download_layout.dart b/example/lib/screens/main/pages/downloading/components/download_layout.dart index d5cd855b..0fc93a7b 100644 --- a/example/lib/screens/main/pages/downloading/components/download_layout.dart +++ b/example/lib/screens/main/pages/downloading/components/download_layout.dart @@ -4,7 +4,7 @@ import 'package:google_fonts/google_fonts.dart'; import 'package:provider/provider.dart'; import '../../../../../shared/misc/size_formatter.dart'; -import '../../../../../shared/state/download_provider.dart'; +import '../state/downloading_provider.dart'; import 'main_statistics.dart'; import 'multi_linear_progress_indicator.dart'; import 'stat_display.dart'; @@ -189,9 +189,9 @@ class DownloadLayout extends StatelessWidget { ), Expanded( child: RepaintBoundary( - child: Consumer( - builder: (context, provider, _) => provider - .failedTiles.isEmpty + child: Selector>( + selector: (context, provider) => provider.failedTiles, + builder: (context, failedTiles, _) => failedTiles.isEmpty ? const Column( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -203,10 +203,10 @@ class DownloadLayout extends StatelessWidget { : ListView.builder( reverse: true, addRepaintBoundaries: false, - itemCount: provider.failedTiles.length, + itemCount: failedTiles.length, itemBuilder: (context, index) => ListTile( leading: Icon( - switch (provider.failedTiles[index].result) { + switch (failedTiles[index].result) { TileEventResult.noConnectionDuringFetch => Icons.wifi_off, TileEventResult.unknownFetchException => @@ -216,16 +216,16 @@ class DownloadLayout extends StatelessWidget { _ => Icons.abc, }, ), - title: Text(provider.failedTiles[index].url), + title: Text(failedTiles[index].url), subtitle: Text( - switch (provider.failedTiles[index].result) { + switch (failedTiles[index].result) { TileEventResult.noConnectionDuringFetch => 'Failed to establish a connection to the network. Check your Internet connection!', TileEventResult.unknownFetchException => - 'There was an unknown error when trying to download this tile, of type ${provider.failedTiles[index].fetchError.runtimeType}', + 'There was an unknown error when trying to download this tile, of type ${failedTiles[index].fetchError.runtimeType}', TileEventResult.negativeFetchResponse => - 'The tile server responded with an HTTP status code of ${provider.failedTiles[index].fetchResponse!.statusCode} (${provider.failedTiles[index].fetchResponse!.reasonPhrase})', - _ => '', + 'The tile server responded with an HTTP status code of ${failedTiles[index].fetchResponse!.statusCode} (${failedTiles[index].fetchResponse!.reasonPhrase})', + _ => throw Error(), }, ), ), diff --git a/example/lib/screens/main/pages/downloading/components/main_statistics.dart b/example/lib/screens/main/pages/downloading/components/main_statistics.dart index f3d922db..56cee139 100644 --- a/example/lib/screens/main/pages/downloading/components/main_statistics.dart +++ b/example/lib/screens/main/pages/downloading/components/main_statistics.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; import 'package:provider/provider.dart'; -import '../../../../../shared/state/download_provider.dart'; +import '../state/downloading_provider.dart'; import 'stat_display.dart'; class MainStatistics extends StatefulWidget { @@ -114,10 +114,9 @@ class _MainStatisticsState extends State { child: OutlinedButton( onPressed: () { WidgetsBinding.instance.addPostFrameCallback( - (_) => Provider.of( - context, - listen: false, - ).setDownloadProgress(null), + (_) => context + .read() + .setDownloadProgress(null), ); }, child: const Padding( diff --git a/example/lib/screens/main/pages/downloading/downloading.dart b/example/lib/screens/main/pages/downloading/downloading.dart index fb9ec165..8642c893 100644 --- a/example/lib/screens/main/pages/downloading/downloading.dart +++ b/example/lib/screens/main/pages/downloading/downloading.dart @@ -3,8 +3,9 @@ import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:provider/provider.dart'; -import '../../../../shared/state/download_provider.dart'; +import '../region_selection/state/region_selection_provider.dart'; import 'components/download_layout.dart'; +import 'state/downloading_provider.dart'; class DownloadingPage extends StatefulWidget { const DownloadingPage({super.key}); @@ -25,19 +26,20 @@ class _DownloadingPageState extends State child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Consumer( - builder: (context, provider, _) => Column( + Selector( + selector: (context, provider) => provider.selectedStore, + builder: (context, selectedStore, _) => Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Downloading', - style: GoogleFonts.openSans( + style: GoogleFonts.ubuntu( fontWeight: FontWeight.bold, fontSize: 24, ), ), Text( - 'Downloading To: ${provider.selectedStore!.storeName}', + 'Downloading To: ${selectedStore!.storeName}', overflow: TextOverflow.fade, softWrap: false, ), @@ -48,44 +50,48 @@ class _DownloadingPageState extends State Expanded( child: Padding( padding: const EdgeInsets.all(6), - child: Consumer( - builder: (context, provider, _) => - StreamBuilder( - stream: provider.downloadProgress, - builder: (context, snapshot) { - if (snapshot.data == null) { - return const Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - CircularProgressIndicator(), - SizedBox(height: 16), - Text( - 'Taking a while?', - style: TextStyle(fontWeight: FontWeight.bold), - ), - Text( - 'Please wait for the download to start...', - ), - ], - ), - ); - } + child: StreamBuilder( + stream: context + .select?>( + (provider) => provider.downloadProgress,), + builder: (context, snapshot) { + if (snapshot.data == null) { + return const Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + CircularProgressIndicator(), + SizedBox(height: 16), + Text( + 'Taking a while?', + style: TextStyle(fontWeight: FontWeight.bold), + ), + Text( + 'Please wait for the download to start...', + ), + ], + ), + ); + } - final latestTileEvent = snapshot.data!.latestTileEvent; + final latestTileEvent = snapshot.data!.latestTileEvent; - if (latestTileEvent.result.category == - TileEventResultCategory.failed && - !latestTileEvent.isRepeat) { - provider.addFailedTile(latestTileEvent); - } + if (latestTileEvent.result.category == + TileEventResultCategory.failed && + !latestTileEvent.isRepeat) { + context + .read() + .addFailedTile(latestTileEvent); + } - return DownloadLayout( - storeDirectory: provider.selectedStore!, - download: snapshot.data!, - ); - }, - ), + return DownloadLayout( + storeDirectory: context + .select( + (provider) => provider.selectedStore, + )!, + download: snapshot.data!, + ); + }, ), ), ), diff --git a/example/lib/screens/main/pages/downloading/state/downloading_provider.dart b/example/lib/screens/main/pages/downloading/state/downloading_provider.dart new file mode 100644 index 00000000..96d93a95 --- /dev/null +++ b/example/lib/screens/main/pages/downloading/state/downloading_provider.dart @@ -0,0 +1,60 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; + +class DownloadingProvider extends ChangeNotifier { + Stream? _downloadProgress; + Stream? get downloadProgress => _downloadProgress; + void setDownloadProgress( + Stream? newStream, { + bool notify = true, + }) { + _downloadProgress = newStream; + if (notify) notifyListeners(); + } + + int _parallelThreads = 5; + int get parallelThreads => _parallelThreads; + set parallelThreads(int newNum) { + _parallelThreads = newNum; + notifyListeners(); + } + + int _bufferingAmount = 100; + int get bufferingAmount => _bufferingAmount; + set bufferingAmount(int newNum) { + _bufferingAmount = newNum; + notifyListeners(); + } + + bool _skipExistingTiles = true; + bool get skipExistingTiles => _skipExistingTiles; + set skipExistingTiles(bool newBool) { + _skipExistingTiles = newBool; + notifyListeners(); + } + + bool _skipSeaTiles = true; + bool get skipSeaTiles => _skipSeaTiles; + set skipSeaTiles(bool newBool) { + _skipSeaTiles = newBool; + notifyListeners(); + } + + int? _rateLimit = 200; + int? get rateLimit => _rateLimit; + set rateLimit(int? newNum) { + _rateLimit = newNum; + notifyListeners(); + } + + bool _disableRecovery = false; + bool get disableRecovery => _disableRecovery; + set disableRecovery(bool newBool) { + _disableRecovery = newBool; + notifyListeners(); + } + + final List _failedTiles = []; + List get failedTiles => _failedTiles; + void addFailedTile(TileEvent e) => _failedTiles.add(e); +} diff --git a/example/lib/screens/main/pages/map/map_view.dart b/example/lib/screens/main/pages/map/map_view.dart index 810f68d2..d0feab31 100644 --- a/example/lib/screens/main/pages/map/map_view.dart +++ b/example/lib/screens/main/pages/map/map_view.dart @@ -8,10 +8,10 @@ import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; import 'package:latlong2/latlong.dart'; import 'package:provider/provider.dart'; +import '../../../../shared/components/build_attribution.dart'; import '../../../../shared/components/loading_indicator.dart'; import '../../../../shared/state/general_provider.dart'; -import '../../../../shared/state/map_provider.dart'; -import 'build_attribution.dart'; +import 'state/map_provider.dart'; enum UserLocationFollowState { off, diff --git a/example/lib/shared/state/map_provider.dart b/example/lib/screens/main/pages/map/state/map_provider.dart similarity index 95% rename from example/lib/shared/state/map_provider.dart rename to example/lib/screens/main/pages/map/state/map_provider.dart index a13c36d0..45c28499 100644 --- a/example/lib/shared/state/map_provider.dart +++ b/example/lib/screens/main/pages/map/state/map_provider.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter_map/flutter_map.dart'; -import '../../screens/main/pages/map/map_view.dart'; +import '../map_view.dart'; class MapProvider extends ChangeNotifier { UserLocationFollowState _followState = UserLocationFollowState.standard; diff --git a/example/lib/screens/main/pages/recovery/components/header.dart b/example/lib/screens/main/pages/recovery/components/header.dart index d1a37e7e..a5bd75cf 100644 --- a/example/lib/screens/main/pages/recovery/components/header.dart +++ b/example/lib/screens/main/pages/recovery/components/header.dart @@ -9,7 +9,7 @@ class Header extends StatelessWidget { @override Widget build(BuildContext context) => Text( 'Recovery', - style: GoogleFonts.openSans( + style: GoogleFonts.ubuntu( fontWeight: FontWeight.bold, fontSize: 24, ), diff --git a/example/lib/screens/main/pages/downloader/components/crosshairs.dart b/example/lib/screens/main/pages/region_selection/components/crosshairs.dart similarity index 100% rename from example/lib/screens/main/pages/downloader/components/crosshairs.dart rename to example/lib/screens/main/pages/region_selection/components/crosshairs.dart diff --git a/example/lib/screens/main/pages/downloader/components/custom_polygon_snapping_indicator.dart b/example/lib/screens/main/pages/region_selection/components/custom_polygon_snapping_indicator.dart similarity index 82% rename from example/lib/screens/main/pages/downloader/components/custom_polygon_snapping_indicator.dart rename to example/lib/screens/main/pages/region_selection/components/custom_polygon_snapping_indicator.dart index 260048bb..69420469 100644 --- a/example/lib/screens/main/pages/downloader/components/custom_polygon_snapping_indicator.dart +++ b/example/lib/screens/main/pages/region_selection/components/custom_polygon_snapping_indicator.dart @@ -3,7 +3,7 @@ import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; import 'package:provider/provider.dart'; -import '../../../../../shared/state/download_provider.dart'; +import '../state/region_selection_provider.dart'; class CustomPolygonSnappingIndicator extends StatelessWidget { const CustomPolygonSnappingIndicator({ @@ -14,18 +14,18 @@ class CustomPolygonSnappingIndicator extends StatelessWidget { Widget build(BuildContext context) => MarkerLayer( markers: [ if (context - .select>( + .select>( (p) => p.coordinates, ) .isNotEmpty && - context.select( + context.select( (p) => p.customPolygonSnap, )) Marker( height: 25, width: 25, point: context - .select>( + .select>( (p) => p.coordinates, ) .first, diff --git a/example/lib/screens/main/pages/downloader/components/region_shape.dart b/example/lib/screens/main/pages/region_selection/components/region_shape.dart similarity index 96% rename from example/lib/screens/main/pages/downloader/components/region_shape.dart rename to example/lib/screens/main/pages/region_selection/components/region_shape.dart index c404b912..22abb9b7 100644 --- a/example/lib/screens/main/pages/downloader/components/region_shape.dart +++ b/example/lib/screens/main/pages/region_selection/components/region_shape.dart @@ -6,7 +6,7 @@ import 'package:latlong2/latlong.dart'; import 'package:provider/provider.dart'; import '../../../../../shared/misc/region_type.dart'; -import '../../../../../shared/state/download_provider.dart'; +import '../state/region_selection_provider.dart'; class RegionShape extends StatelessWidget { const RegionShape({ @@ -14,7 +14,7 @@ class RegionShape extends StatelessWidget { }); @override - Widget build(BuildContext context) => Consumer( + Widget build(BuildContext context) => Consumer( builder: (context, provider, _) { if (provider.regionType == RegionType.line) { if (provider.coordinates.isEmpty) return const SizedBox.shrink(); diff --git a/example/lib/screens/main/pages/downloader/components/side_panel/additional_panes/additional_pane.dart b/example/lib/screens/main/pages/region_selection/components/side_panel/additional_panes/additional_pane.dart similarity index 94% rename from example/lib/screens/main/pages/downloader/components/side_panel/additional_panes/additional_pane.dart rename to example/lib/screens/main/pages/region_selection/components/side_panel/additional_panes/additional_pane.dart index 788ed94b..87033919 100644 --- a/example/lib/screens/main/pages/downloader/components/side_panel/additional_panes/additional_pane.dart +++ b/example/lib/screens/main/pages/region_selection/components/side_panel/additional_panes/additional_pane.dart @@ -10,7 +10,7 @@ class _AdditionalPane extends StatelessWidget { final Axis layoutDirection; @override - Widget build(BuildContext context) => Consumer( + Widget build(BuildContext context) => Consumer( builder: (context, provider, _) => Stack( fit: StackFit.passthrough, children: [ diff --git a/example/lib/screens/main/pages/downloader/components/side_panel/additional_panes/adjust_zoom_lvls_pane.dart b/example/lib/screens/main/pages/region_selection/components/side_panel/additional_panes/adjust_zoom_lvls_pane.dart similarity index 88% rename from example/lib/screens/main/pages/downloader/components/side_panel/additional_panes/adjust_zoom_lvls_pane.dart rename to example/lib/screens/main/pages/region_selection/components/side_panel/additional_panes/adjust_zoom_lvls_pane.dart index ca2f4a73..c7458ea7 100644 --- a/example/lib/screens/main/pages/downloader/components/side_panel/additional_panes/adjust_zoom_lvls_pane.dart +++ b/example/lib/screens/main/pages/region_selection/components/side_panel/additional_panes/adjust_zoom_lvls_pane.dart @@ -9,7 +9,7 @@ class AdjustZoomLvlsPane extends StatelessWidget { final Axis layoutDirection; @override - Widget build(BuildContext context) => Consumer( + Widget build(BuildContext context) => Consumer( builder: (context, provider, _) => Flex( direction: layoutDirection, mainAxisAlignment: MainAxisAlignment.center, @@ -37,11 +37,11 @@ class AdjustZoomLvlsPane extends StatelessWidget { ), onChanged: (v) { provider - ..minZoom = v.start.toInt() - ..maxZoom = v.end.toInt(); + ..minZoom = v.start.round() + ..maxZoom = v.end.round(); }, max: 22, - divisions: 23, + divisions: 22, ), ), ), diff --git a/example/lib/screens/main/pages/downloader/components/side_panel/additional_panes/line_region_pane.dart b/example/lib/screens/main/pages/region_selection/components/side_panel/additional_panes/line_region_pane.dart similarity index 95% rename from example/lib/screens/main/pages/downloader/components/side_panel/additional_panes/line_region_pane.dart rename to example/lib/screens/main/pages/region_selection/components/side_panel/additional_panes/line_region_pane.dart index 07b84456..475ded7d 100644 --- a/example/lib/screens/main/pages/downloader/components/side_panel/additional_panes/line_region_pane.dart +++ b/example/lib/screens/main/pages/region_selection/components/side_panel/additional_panes/line_region_pane.dart @@ -9,7 +9,7 @@ class LineRegionPane extends StatelessWidget { final Axis layoutDirection; @override - Widget build(BuildContext context) => Consumer( + Widget build(BuildContext context) => Consumer( builder: (context, provider, _) => Flex( direction: layoutDirection, mainAxisAlignment: MainAxisAlignment.center, @@ -17,7 +17,7 @@ class LineRegionPane extends StatelessWidget { children: [ IconButton( onPressed: () async { - final provider = context.read(); + final provider = context.read(); if (Platform.isAndroid || Platform.isIOS) { await FilePicker.platform.clearTemporaryFiles(); diff --git a/example/lib/screens/main/pages/downloader/components/side_panel/additional_panes/slider_panel_base.dart b/example/lib/screens/main/pages/region_selection/components/side_panel/additional_panes/slider_panel_base.dart similarity index 100% rename from example/lib/screens/main/pages/downloader/components/side_panel/additional_panes/slider_panel_base.dart rename to example/lib/screens/main/pages/region_selection/components/side_panel/additional_panes/slider_panel_base.dart diff --git a/example/lib/screens/main/pages/downloader/components/side_panel/custom_slider_track_shape.dart b/example/lib/screens/main/pages/region_selection/components/side_panel/custom_slider_track_shape.dart similarity index 100% rename from example/lib/screens/main/pages/downloader/components/side_panel/custom_slider_track_shape.dart rename to example/lib/screens/main/pages/region_selection/components/side_panel/custom_slider_track_shape.dart diff --git a/example/lib/screens/main/pages/downloader/components/side_panel/parent.dart b/example/lib/screens/main/pages/region_selection/components/side_panel/parent.dart similarity index 87% rename from example/lib/screens/main/pages/downloader/components/side_panel/parent.dart rename to example/lib/screens/main/pages/region_selection/components/side_panel/parent.dart index 803014ac..bdd8f061 100644 --- a/example/lib/screens/main/pages/downloader/components/side_panel/parent.dart +++ b/example/lib/screens/main/pages/region_selection/components/side_panel/parent.dart @@ -9,25 +9,27 @@ import 'package:provider/provider.dart'; import '../../../../../../shared/misc/region_selection_method.dart'; import '../../../../../../shared/misc/region_type.dart'; -import '../../../../../../shared/state/download_provider.dart'; -import '../../../../../download_region/download_region.dart'; +import '../../state/region_selection_provider.dart'; part 'additional_panes/additional_pane.dart'; part 'additional_panes/adjust_zoom_lvls_pane.dart'; part 'additional_panes/line_region_pane.dart'; +part 'additional_panes/slider_panel_base.dart'; part 'custom_slider_track_shape.dart'; part 'primary_pane.dart'; part 'region_shape_button.dart'; -part 'additional_panes/slider_panel_base.dart'; class SidePanel extends StatelessWidget { SidePanel({ super.key, required this.constraints, + required this.pushToConfigureDownload, }) : layoutDirection = constraints.maxWidth > 850 ? Axis.vertical : Axis.horizontal; final BoxConstraints constraints; + final void Function() pushToConfigureDownload; + final Axis layoutDirection; @override @@ -43,12 +45,14 @@ class SidePanel extends StatelessWidget { child: _PrimaryPane( constraints: constraints, layoutDirection: layoutDirection, + pushToConfigureDownload: pushToConfigureDownload, ), ) : IntrinsicWidth( child: _PrimaryPane( constraints: constraints, layoutDirection: layoutDirection, + pushToConfigureDownload: pushToConfigureDownload, ), ), ), diff --git a/example/lib/screens/main/pages/downloader/components/side_panel/primary_pane.dart b/example/lib/screens/main/pages/region_selection/components/side_panel/primary_pane.dart similarity index 87% rename from example/lib/screens/main/pages/downloader/components/side_panel/primary_pane.dart rename to example/lib/screens/main/pages/region_selection/components/side_panel/primary_pane.dart index 385b2a80..56a0804c 100644 --- a/example/lib/screens/main/pages/downloader/components/side_panel/primary_pane.dart +++ b/example/lib/screens/main/pages/region_selection/components/side_panel/primary_pane.dart @@ -4,9 +4,12 @@ class _PrimaryPane extends StatelessWidget { const _PrimaryPane({ required this.constraints, required this.layoutDirection, + required this.pushToConfigureDownload, }); final BoxConstraints constraints; + final void Function() pushToConfigureDownload; + final Axis layoutDirection; static const regionShapes = { @@ -56,7 +59,7 @@ class _PrimaryPane extends StatelessWidget { ? constraints.maxHeight : constraints.maxWidth) < 500 - ? Consumer( + ? Consumer( builder: (context, provider, _) => IconButton( icon: Icon( regionShapes[provider.regionType]!.selectedIcon, @@ -100,7 +103,7 @@ class _PrimaryPane extends StatelessWidget { direction: layoutDirection, mainAxisSize: MainAxisSize.min, children: [ - Selector( + Selector( selector: (context, provider) => provider.regionSelectionMethod, builder: (context, method, _) => IconButton( @@ -110,7 +113,7 @@ class _PrimaryPane extends StatelessWidget { : Icons.ads_click, ), onPressed: () => context - .read() + .read() .regionSelectionMethod = method == RegionSelectionMethod.useMapCenter ? RegionSelectionMethod.usePointer @@ -121,9 +124,9 @@ class _PrimaryPane extends StatelessWidget { const SizedBox.square(dimension: 12), IconButton( icon: const Icon(Icons.delete_forever), - onPressed: () => context.read() - ..clearCoordinates() - ..region = null, + onPressed: () => context + .read() + .clearCoordinates(), tooltip: 'Remove All Points', ), ], @@ -136,7 +139,7 @@ class _PrimaryPane extends StatelessWidget { borderRadius: BorderRadius.circular(1028), ), padding: const EdgeInsets.all(12), - child: Consumer( + child: Consumer( builder: (context, provider, _) => Flex( direction: layoutDirection, mainAxisSize: MainAxisSize.min, @@ -160,16 +163,7 @@ class _PrimaryPane extends StatelessWidget { IconButton.filled( icon: const Icon(Icons.done), onPressed: provider.region != null - ? () => Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => DownloadRegionPopup( - region: provider.region!, - minZoom: provider.minZoom, - maxZoom: provider.maxZoom, - ), - fullscreenDialog: true, - ), - ) + ? pushToConfigureDownload : null, ), ], diff --git a/example/lib/screens/main/pages/downloader/components/side_panel/region_shape_button.dart b/example/lib/screens/main/pages/region_selection/components/side_panel/region_shape_button.dart similarity index 89% rename from example/lib/screens/main/pages/downloader/components/side_panel/region_shape_button.dart rename to example/lib/screens/main/pages/region_selection/components/side_panel/region_shape_button.dart index 544c5416..7c9763f5 100644 --- a/example/lib/screens/main/pages/downloader/components/side_panel/region_shape_button.dart +++ b/example/lib/screens/main/pages/region_selection/components/side_panel/region_shape_button.dart @@ -14,7 +14,7 @@ class _RegionShapeButton extends StatelessWidget { final String tooltip; @override - Widget build(BuildContext context) => Consumer( + Widget build(BuildContext context) => Consumer( builder: (context, provider, _) => IconButton( icon: unselectedIcon, selectedIcon: selectedIcon, diff --git a/example/lib/screens/main/pages/downloader/components/usage_instructions.dart b/example/lib/screens/main/pages/region_selection/components/usage_instructions.dart similarity index 97% rename from example/lib/screens/main/pages/downloader/components/usage_instructions.dart rename to example/lib/screens/main/pages/region_selection/components/usage_instructions.dart index d9cb96a2..e3e90476 100644 --- a/example/lib/screens/main/pages/downloader/components/usage_instructions.dart +++ b/example/lib/screens/main/pages/region_selection/components/usage_instructions.dart @@ -5,7 +5,7 @@ import 'package:provider/provider.dart'; import '../../../../../shared/misc/region_selection_method.dart'; import '../../../../../shared/misc/region_type.dart'; -import '../../../../../shared/state/download_provider.dart'; +import '../state/region_selection_provider.dart'; class UsageInstructions extends StatelessWidget { UsageInstructions({ @@ -36,7 +36,7 @@ class UsageInstructions extends StatelessWidget { fontSize: 20, color: Colors.white, ), - child: Consumer( + child: Consumer( builder: (context, provider, _) => AnimatedOpacity( duration: const Duration(milliseconds: 250), curve: Curves.easeInOut, diff --git a/example/lib/screens/main/pages/downloader/map_view.dart b/example/lib/screens/main/pages/region_selection/map_view.dart similarity index 76% rename from example/lib/screens/main/pages/downloader/map_view.dart rename to example/lib/screens/main/pages/region_selection/map_view.dart index fba63a0a..6e189cf0 100644 --- a/example/lib/screens/main/pages/downloader/map_view.dart +++ b/example/lib/screens/main/pages/region_selection/map_view.dart @@ -8,18 +8,18 @@ import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; import 'package:latlong2/latlong.dart'; import 'package:provider/provider.dart'; +import '../../../../shared/components/build_attribution.dart'; import '../../../../shared/components/loading_indicator.dart'; import '../../../../shared/misc/region_selection_method.dart'; import '../../../../shared/misc/region_type.dart'; -import '../../../../shared/state/download_provider.dart'; import '../../../../shared/state/general_provider.dart'; -import '../../../download_region/download_region.dart'; -import '../map/build_attribution.dart'; +import '../../../configure_download/configure_download.dart'; import 'components/crosshairs.dart'; import 'components/custom_polygon_snapping_indicator.dart'; import 'components/region_shape.dart'; import 'components/side_panel/parent.dart'; import 'components/usage_instructions.dart'; +import 'state/region_selection_provider.dart'; class MapView extends StatefulWidget { const MapView({super.key}); @@ -51,7 +51,7 @@ class _MapViewState extends State { ), keepAlive: true, onTap: (_, __) { - final provider = context.read(); + final provider = context.read(); if (provider.isCustomPolygonComplete) return; @@ -102,11 +102,11 @@ class _MapViewState extends State { } }, onSecondaryTap: (_, __) => - context.read().removeLastCoordinate(), + context.read().removeLastCoordinate(), onLongPress: (_, __) => - context.read().removeLastCoordinate(), + context.read().removeLastCoordinate(), onPointerHover: (evt, point) { - final provider = context.read(); + final provider = context.read(); if (provider.regionSelectionMethod == RegionSelectionMethod.usePointer) { provider.currentNewPointPos = point; @@ -128,7 +128,7 @@ class _MapViewState extends State { } }, onPositionChanged: (position, _) { - final provider = context.read(); + final provider = context.read(); if (provider.regionSelectionMethod == RegionSelectionMethod.useMapCenter) { @@ -156,25 +156,62 @@ class _MapViewState extends State { ); bool keyboardHandler(KeyEvent event) { - final provider = context.read(); + if (event is! KeyDownEvent) return false; + + final provider = context.read(); + if (provider.region != null && - event is KeyDownEvent && event.logicalKey == LogicalKeyboardKey.enter) { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => DownloadRegionPopup( - region: provider.region!, - minZoom: provider.minZoom, - maxZoom: provider.maxZoom, - ), - fullscreenDialog: true, - ), - ); + pushToConfigureDownload(); + } else if (event.logicalKey == LogicalKeyboardKey.escape || + event.logicalKey == LogicalKeyboardKey.delete) { + provider.clearCoordinates(); + } else if (event.logicalKey == LogicalKeyboardKey.backspace) { + provider.removeLastCoordinate(); + } else if (provider.regionType != RegionType.square && + event.logicalKey == LogicalKeyboardKey.keyZ) { + provider + ..regionType = RegionType.square + ..clearCoordinates(); + } else if (provider.regionType != RegionType.circle && + event.logicalKey == LogicalKeyboardKey.keyX) { + provider + ..regionType = RegionType.circle + ..clearCoordinates(); + } else if (provider.regionType != RegionType.line && + event.logicalKey == LogicalKeyboardKey.keyC) { + provider + ..regionType = RegionType.line + ..clearCoordinates(); + } else if (provider.regionType != RegionType.customPolygon && + event.logicalKey == LogicalKeyboardKey.keyV) { + provider + ..regionType = RegionType.customPolygon + ..clearCoordinates(); } return false; } + void pushToConfigureDownload() { + final provider = context.read(); + ServicesBinding.instance.keyboard.removeHandler(keyboardHandler); + Navigator.of(context) + .push( + MaterialPageRoute( + builder: (context) => ConfigureDownloadPopup( + region: provider.region!, + minZoom: provider.minZoom, + maxZoom: provider.maxZoom, + ), + fullscreenDialog: true, + ), + ) + .then( + (_) => ServicesBinding.instance.keyboard.addHandler(keyboardHandler), + ); + } + @override void initState() { super.initState(); @@ -208,13 +245,13 @@ class _MapViewState extends State { return MouseRegion( opaque: false, - cursor: context.select( (p) => p.regionSelectionMethod, ) == RegionSelectionMethod.useMapCenter ? MouseCursor.defer - : context.select( + : context.select( (p) => p.customPolygonSnap, ) ? SystemMouseCursors.none @@ -260,12 +297,15 @@ class _MapViewState extends State { }, ), ), - SidePanel(constraints: constraints), - if (context.select( + SidePanel( + constraints: constraints, + pushToConfigureDownload: pushToConfigureDownload, + ), + if (context.select( (p) => p.regionSelectionMethod, ) == RegionSelectionMethod.useMapCenter && - !context.select( + !context.select( (p) => p.customPolygonSnap, )) const Center(child: Crosshairs()), diff --git a/example/lib/screens/main/pages/downloader/downloader.dart b/example/lib/screens/main/pages/region_selection/region_selection.dart similarity index 84% rename from example/lib/screens/main/pages/downloader/downloader.dart rename to example/lib/screens/main/pages/region_selection/region_selection.dart index 6eaa83f9..c52fbf61 100644 --- a/example/lib/screens/main/pages/downloader/downloader.dart +++ b/example/lib/screens/main/pages/region_selection/region_selection.dart @@ -5,14 +5,14 @@ import 'package:provider/provider.dart'; import '../../../../shared/state/general_provider.dart'; import 'map_view.dart'; -class DownloaderPage extends StatefulWidget { - const DownloaderPage({super.key}); +class RegionSelectionPage extends StatefulWidget { + const RegionSelectionPage({super.key}); @override - State createState() => _DownloaderPageState(); + State createState() => _RegionSelectionPageState(); } -class _DownloaderPageState extends State { +class _RegionSelectionPageState extends State { @override Widget build(BuildContext context) => Scaffold( body: Column( @@ -28,7 +28,7 @@ class _DownloaderPageState extends State { children: [ Text( 'Downloader', - style: GoogleFonts.openSans( + style: GoogleFonts.ubuntu( fontWeight: FontWeight.bold, fontSize: 24, ), @@ -54,7 +54,7 @@ class _DownloaderPageState extends State { child: ClipRRect( borderRadius: BorderRadius.only( topLeft: const Radius.circular(20), - topRight: MediaQuery.of(context).size.width <= 950 + topRight: MediaQuery.sizeOf(context).width <= 950 ? const Radius.circular(20) : Radius.zero, ), diff --git a/example/lib/shared/state/download_provider.dart b/example/lib/screens/main/pages/region_selection/state/region_selection_provider.dart similarity index 64% rename from example/lib/shared/state/download_provider.dart rename to example/lib/screens/main/pages/region_selection/state/region_selection_provider.dart index 06063fbc..ff28fbbd 100644 --- a/example/lib/shared/state/download_provider.dart +++ b/example/lib/screens/main/pages/region_selection/state/region_selection_provider.dart @@ -1,14 +1,13 @@ -import 'dart:async'; import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; import 'package:latlong2/latlong.dart'; -import '../misc/region_selection_method.dart'; -import '../misc/region_type.dart'; +import '../../../../../shared/misc/region_selection_method.dart'; +import '../../../../../shared/misc/region_type.dart'; -class DownloaderProvider extends ChangeNotifier { +class RegionSelectionProvider extends ChangeNotifier { RegionSelectionMethod _regionSelectionMethod = Platform.isAndroid || Platform.isIOS ? RegionSelectionMethod.useMapCenter @@ -61,7 +60,7 @@ class DownloaderProvider extends ChangeNotifier { } void removeLastCoordinate() { - _coordinates.removeLast(); + if (_coordinates.isNotEmpty) _coordinates.removeLast(); if (_regionType == RegionType.customPolygon ? !isCustomPolygonComplete : _coordinates.length < 2) _region = null; @@ -94,7 +93,7 @@ class DownloaderProvider extends ChangeNotifier { notifyListeners(); } - int _minZoom = 1; + int _minZoom = 0; int get minZoom => _minZoom; set minZoom(int newNum) { _minZoom = newNum; @@ -108,68 +107,10 @@ class DownloaderProvider extends ChangeNotifier { notifyListeners(); } - // OLD - StoreDirectory? _selectedStore; StoreDirectory? get selectedStore => _selectedStore; void setSelectedStore(StoreDirectory? newStore, {bool notify = true}) { _selectedStore = newStore; if (notify) notifyListeners(); } - - Stream? _downloadProgress; - Stream? get downloadProgress => _downloadProgress; - void setDownloadProgress( - Stream? newStream, { - bool notify = true, - }) { - _downloadProgress = newStream; - if (notify) notifyListeners(); - } - - int _parallelThreads = 5; - int get parallelThreads => _parallelThreads; - set parallelThreads(int newNum) { - _parallelThreads = newNum; - notifyListeners(); - } - - int _bufferingAmount = 100; - int get bufferingAmount => _bufferingAmount; - set bufferingAmount(int newNum) { - _bufferingAmount = newNum; - notifyListeners(); - } - - bool _skipExistingTiles = true; - bool get skipExistingTiles => _skipExistingTiles; - set skipExistingTiles(bool newBool) { - _skipExistingTiles = newBool; - notifyListeners(); - } - - bool _skipSeaTiles = true; - bool get skipSeaTiles => _skipSeaTiles; - set skipSeaTiles(bool newBool) { - _skipSeaTiles = newBool; - notifyListeners(); - } - - int? _rateLimit = 200; - int? get rateLimit => _rateLimit; - set rateLimit(int? newNum) { - _rateLimit = newNum; - notifyListeners(); - } - - bool _disableRecovery = false; - bool get disableRecovery => _disableRecovery; - set disableRecovery(bool newBool) { - _disableRecovery = newBool; - notifyListeners(); - } - - final List _failedTiles = []; - List get failedTiles => _failedTiles; - void addFailedTile(TileEvent e) => _failedTiles.add(e); } diff --git a/example/lib/screens/main/pages/stores/components/header.dart b/example/lib/screens/main/pages/stores/components/header.dart index 4e7b8146..cb1877bc 100644 --- a/example/lib/screens/main/pages/stores/components/header.dart +++ b/example/lib/screens/main/pages/stores/components/header.dart @@ -18,7 +18,7 @@ class Header extends StatelessWidget { children: [ Text( 'Stores', - style: GoogleFonts.openSans( + style: GoogleFonts.ubuntu( fontWeight: FontWeight.bold, fontSize: 24, ), diff --git a/example/lib/screens/main/pages/stores/components/store_tile.dart b/example/lib/screens/main/pages/stores/components/store_tile.dart index 88b622ef..f2bc5356 100644 --- a/example/lib/screens/main/pages/stores/components/store_tile.dart +++ b/example/lib/screens/main/pages/stores/components/store_tile.dart @@ -120,7 +120,7 @@ class _StoreTileState extends State { ) : snapshot.data!, ), - if (MediaQuery.of(context).size.width > 675) + if (MediaQuery.sizeOf(context).width > 675) ...stats else Column(children: stats), diff --git a/example/lib/screens/store_editor/store_editor.dart b/example/lib/screens/store_editor/store_editor.dart index 07314010..80b9d4cd 100644 --- a/example/lib/screens/store_editor/store_editor.dart +++ b/example/lib/screens/store_editor/store_editor.dart @@ -7,8 +7,8 @@ import 'package:provider/provider.dart'; import 'package:validators/validators.dart' as validators; import '../../shared/components/loading_indicator.dart'; -import '../../shared/state/download_provider.dart'; import '../../shared/state/general_provider.dart'; +import '../main/pages/region_selection/state/region_selection_provider.dart'; import 'components/header.dart'; class StoreEditorPopup extends StatefulWidget { @@ -44,7 +44,7 @@ class _StoreEditorPopupState extends State { } @override - Widget build(BuildContext context) => Consumer( + Widget build(BuildContext context) => Consumer( builder: (context, downloadProvider, _) => WillPopScope( onWillPop: () async { scaffoldMessenger.showSnackBar( diff --git a/example/lib/screens/main/pages/map/build_attribution.dart b/example/lib/shared/components/build_attribution.dart similarity index 100% rename from example/lib/screens/main/pages/map/build_attribution.dart rename to example/lib/shared/components/build_attribution.dart diff --git a/example/pubspec.yaml b/example/pubspec.yaml index d8de93dc..57551592 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -14,7 +14,7 @@ dependencies: badges: ^3.0.2 better_open_file: ^3.6.4 collection: ^1.17.1 - dart_earcut: ^1.0.0 + dart_earcut: ^1.0.1 file_picker: ^5.2.10 flutter: sdk: flutter diff --git a/pubspec.yaml b/pubspec.yaml index ab0deb65..89f8a3c2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -28,7 +28,7 @@ environment: dependencies: async: ^2.9.0 collection: ^1.16.0 - dart_earcut: ^1.0.0 + dart_earcut: ^1.0.1 flutter: sdk: flutter flutter_map: ^6.0.0-dev.2 From 0c019a7e0bffc7a3cf97c793c0fa9b5241e96ce9 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Fri, 4 Aug 2023 12:55:26 +0100 Subject: [PATCH 060/168] Fixed incorrect dependency location Former-commit-id: 2e653709103b99f498560a56baabe64b6eef2d46 [formerly acb3071700862e7c0b7ddc819f41a24a2de11986] Former-commit-id: c922e8b5a1945eeb9a51eb964ce26be4aa560c2f --- example/pubspec.yaml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 57551592..59741514 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -20,7 +20,7 @@ dependencies: sdk: flutter flutter_foreground_task: ^6.0.0+1 flutter_map: ^6.0.0-dev.2 - flutter_map_location_marker: ^7.0.1 + flutter_map_location_marker: ^8.0.0-dev.1 flutter_map_tile_caching: flutter_speed_dial: ^7.0.0 fmtc_plus_sharing: ^8.0.0 @@ -37,10 +37,6 @@ dependencies: version: ^3.0.2 dependency_overrides: - flutter_map_location_marker: - git: - url: https://github.com/JaffaKetchup/_fmlm - ref: 'fm-v6' flutter_map_tile_caching: path: ../ From a72cec6cdba339c31e1fedc7c12aa9a3d2179cb7 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Fri, 4 Aug 2023 12:59:47 +0100 Subject: [PATCH 061/168] Fixed formatting Former-commit-id: e41bb7e9d6a8e28294af066e0c5c2ebf0611a11f [formerly f91e4e3ae86143a7df995203c203abf1180bf410] Former-commit-id: d3b09da675c41c038796403d4fbbd41c2f0355dc --- example/lib/screens/main/pages/downloading/downloading.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/example/lib/screens/main/pages/downloading/downloading.dart b/example/lib/screens/main/pages/downloading/downloading.dart index 8642c893..601fafc0 100644 --- a/example/lib/screens/main/pages/downloading/downloading.dart +++ b/example/lib/screens/main/pages/downloading/downloading.dart @@ -53,7 +53,8 @@ class _DownloadingPageState extends State child: StreamBuilder( stream: context .select?>( - (provider) => provider.downloadProgress,), + (provider) => provider.downloadProgress, + ), builder: (context, snapshot) { if (snapshot.data == null) { return const Center( From 5af51e54b230f9842c77b9a7b4814112b955db83 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Fri, 4 Aug 2023 23:02:43 +0100 Subject: [PATCH 062/168] Fixed bugs Improved example application Updated CHANGELOG Former-commit-id: 3328e4e564e4049e91daaf4c117366e075369eaf [formerly c0f4e378e0c271c612b285b79007c91a0f6bcd65] Former-commit-id: cc3069220ffd80c82c4e97a6e7f034eae63592cc --- CHANGELOG.md | 11 ++-- .../components/download_layout.dart | 58 +++++++++++++++++- .../downloading/components/stat_display.dart | 2 + .../main/pages/downloading/downloading.dart | 41 ++++++++++--- .../state/downloading_provider.dart | 6 ++ .../recovery/components/recovery_list.dart | 6 +- example/lib/shared/misc/circular_buffer.dart | 61 +++++++++++++++++++ lib/src/bulk_download/download_progress.dart | 13 ++-- lib/src/bulk_download/tile_loops/count.dart | 2 +- .../bulk_download/tile_loops/generate.dart | 2 +- lib/src/db/defs/recovery.dart | 2 +- lib/src/regions/recovered_region.dart | 13 +++- lib/src/root/recovery.dart | 24 +++++--- lib/src/store/download.dart | 9 +-- test/fmtc_test.dart | 11 ++-- 15 files changed, 215 insertions(+), 46 deletions(-) create mode 100644 example/lib/shared/misc/circular_buffer.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 204cfc6a..c4e62982 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,13 +18,14 @@ Many thanks to my sponsors, no matter how much or how little they donated. Spons * Migrated to Flutter 3.10 and Dart 3 * Migrated to flutter_map v5 -* Added support for Isar v4 (bug fixes & stability improvements) +* Migrated to Isar v4 (bug fixes & stability improvements) * Bulk downloading reimplementation - * Improved developer experience by re-organizing scope of `DownloadableRegion`s and `startForeground` - * Improved download speed significantly - * Added pause and resume functionality (to accompany existing cancel functionality) - * Added support for rate limiting + * Added `CustomPolygonRegion`, a `BaseRegion` that is formed of any* outline + * Added pause and resume functionality + * Added rate limiting functionality * Added support for multiple simultaneous downloads + * Improved developer experience by refactoring `DownloadableRegion` and `startForeground` + * Improved download speed significantly * Fixed instability and bugs when cancelling buffering downloads * Fixed generation of `LineRegion` tiles by reducing number of redundant duplicate tiles * Fixed usage of `obscuredQueryParams` diff --git a/example/lib/screens/main/pages/downloading/components/download_layout.dart b/example/lib/screens/main/pages/downloading/components/download_layout.dart index 0fc93a7b..af25d46e 100644 --- a/example/lib/screens/main/pages/downloading/components/download_layout.dart +++ b/example/lib/screens/main/pages/downloading/components/download_layout.dart @@ -29,7 +29,7 @@ class DownloadLayout extends StatelessWidget { child: ClipRRect( borderRadius: BorderRadius.circular(16), child: SizedBox.square( - dimension: 256, + dimension: 216, child: download.latestTileEvent.tileImage != null ? Image.memory( download.latestTileEvent.tileImage!, @@ -220,7 +220,7 @@ class DownloadLayout extends StatelessWidget { subtitle: Text( switch (failedTiles[index].result) { TileEventResult.noConnectionDuringFetch => - 'Failed to establish a connection to the network. Check your Internet connection!', + 'Failed to establish a connection to the network', TileEventResult.unknownFetchException => 'There was an unknown error when trying to download this tile, of type ${failedTiles[index].fetchError.runtimeType}', TileEventResult.negativeFetchResponse => @@ -233,6 +233,60 @@ class DownloadLayout extends StatelessWidget { ), ), ), + const SizedBox(width: 8), + RotatedBox( + quarterTurns: 3, + child: Text( + 'SKIPPED TILES', + style: GoogleFonts.ubuntu( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + ), + Expanded( + child: RepaintBoundary( + child: Selector>( + selector: (context, provider) => provider.skippedTiles, + builder: (context, skippedTiles, _) => + skippedTiles.isEmpty + ? const Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.task_alt, size: 48), + SizedBox(height: 10), + Text('Any skipped tiles will appear here'), + ], + ) + : ListView.builder( + reverse: true, + addRepaintBoundaries: false, + itemCount: skippedTiles.length, + itemBuilder: (context, index) => ListTile( + leading: Icon( + switch (skippedTiles[index].result) { + TileEventResult.alreadyExisting => + Icons.disabled_visible, + TileEventResult.isSeaTile => + Icons.water_drop, + _ => Icons.abc, + }, + ), + title: Text(skippedTiles[index].url), + subtitle: Text( + switch (skippedTiles[index].result) { + TileEventResult.alreadyExisting => + 'Tile already exists', + TileEventResult.isSeaTile => + 'Tile is a sea tile', + _ => throw Error(), + }, + ), + ), + ), + ), + ), + ), ], ), ), diff --git a/example/lib/screens/main/pages/downloading/components/stat_display.dart b/example/lib/screens/main/pages/downloading/components/stat_display.dart index 6ce752c0..3592c850 100644 --- a/example/lib/screens/main/pages/downloading/components/stat_display.dart +++ b/example/lib/screens/main/pages/downloading/components/stat_display.dart @@ -20,10 +20,12 @@ class StatDisplay extends StatelessWidget { fontSize: 24, fontWeight: FontWeight.bold, ), + textAlign: TextAlign.center, ), Text( description, style: const TextStyle(fontSize: 16), + textAlign: TextAlign.center, ), ], ), diff --git a/example/lib/screens/main/pages/downloading/downloading.dart b/example/lib/screens/main/pages/downloading/downloading.dart index 601fafc0..37d8ddd3 100644 --- a/example/lib/screens/main/pages/downloading/downloading.dart +++ b/example/lib/screens/main/pages/downloading/downloading.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; import 'package:google_fonts/google_fonts.dart'; @@ -16,6 +18,35 @@ class DownloadingPage extends StatefulWidget { class _DownloadingPageState extends State with AutomaticKeepAliveClientMixin { + StreamSubscription? downloadProgressStreamSubscription; + + @override + void didChangeDependencies() { + final provider = context.read(); + + downloadProgressStreamSubscription?.cancel(); + downloadProgressStreamSubscription = + provider.downloadProgress!.listen((event) { + final latestTileEvent = event.latestTileEvent; + if (latestTileEvent.isRepeat) return; + + if (latestTileEvent.result.category == TileEventResultCategory.failed) { + provider.addFailedTile(latestTileEvent); + } + if (latestTileEvent.result.category == TileEventResultCategory.skipped) { + provider.addSkippedTile(latestTileEvent); + } + }); + + super.didChangeDependencies(); + } + + @override + void dispose() { + downloadProgressStreamSubscription?.cancel(); + super.dispose(); + } + @override Widget build(BuildContext context) { super.build(context); @@ -75,16 +106,6 @@ class _DownloadingPageState extends State ); } - final latestTileEvent = snapshot.data!.latestTileEvent; - - if (latestTileEvent.result.category == - TileEventResultCategory.failed && - !latestTileEvent.isRepeat) { - context - .read() - .addFailedTile(latestTileEvent); - } - return DownloadLayout( storeDirectory: context .select( diff --git a/example/lib/screens/main/pages/downloading/state/downloading_provider.dart b/example/lib/screens/main/pages/downloading/state/downloading_provider.dart index 96d93a95..44937193 100644 --- a/example/lib/screens/main/pages/downloading/state/downloading_provider.dart +++ b/example/lib/screens/main/pages/downloading/state/downloading_provider.dart @@ -1,6 +1,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; +import '../../../../../shared/misc/circular_buffer.dart'; + class DownloadingProvider extends ChangeNotifier { Stream? _downloadProgress; Stream? get downloadProgress => _downloadProgress; @@ -57,4 +59,8 @@ class DownloadingProvider extends ChangeNotifier { final List _failedTiles = []; List get failedTiles => _failedTiles; void addFailedTile(TileEvent e) => _failedTiles.add(e); + + final CircularBuffer _skippedTiles = CircularBuffer(50); + CircularBuffer get skippedTiles => _skippedTiles; + void addSkippedTile(TileEvent e) => _skippedTiles.add(e); } diff --git a/example/lib/screens/main/pages/recovery/components/recovery_list.dart b/example/lib/screens/main/pages/recovery/components/recovery_list.dart index 9dbbff67..9ac622e8 100644 --- a/example/lib/screens/main/pages/recovery/components/recovery_list.dart +++ b/example/lib/screens/main/pages/recovery/components/recovery_list.dart @@ -33,9 +33,10 @@ class _RecoveryListState extends State { isFailed.data != null ? Icons.warning : region.toRegion( - rectangle: (_) => Icons.rectangle_outlined, + rectangle: (_) => Icons.square_outlined, circle: (_) => Icons.circle_outlined, - line: (_) => Icons.timeline, + line: (_) => Icons.polyline_outlined, + customPolygon: (_) => Icons.pentagon_outlined, ), color: isFailed.data != null ? Colors.red : null, ), @@ -45,6 +46,7 @@ class _RecoveryListState extends State { rectangle: (_) => 'Rectangle', circle: (_) => 'Circle', line: (_) => 'Line', + customPolygon: (_) => 'Custom Polygon', )} Type', ), subtitle: FutureBuilder( diff --git a/example/lib/shared/misc/circular_buffer.dart b/example/lib/shared/misc/circular_buffer.dart new file mode 100644 index 00000000..212ed111 --- /dev/null +++ b/example/lib/shared/misc/circular_buffer.dart @@ -0,0 +1,61 @@ +// Adapted from https://github.com/kranfix/dart-circularbuffer under MIT license + +import 'dart:collection'; + +class CircularBuffer with ListMixin { + CircularBuffer(this.capacity) + : assert(capacity > 1, 'CircularBuffer must have a positive capacity'), + _buf = []; + + final List _buf; + int _start = 0; + + final int capacity; + bool get isFilled => _buf.length == capacity; + bool get isUnfilled => _buf.length < capacity; + + @override + T operator [](int index) { + if (index >= 0 && index < _buf.length) { + return _buf[(_start + index) % _buf.length]; + } + throw RangeError.index(index, this); + } + + @override + void operator []=(int index, T value) { + if (index >= 0 && index < _buf.length) { + _buf[(_start + index) % _buf.length] = value; + } else { + throw RangeError.index(index, this); + } + } + + @override + void add(T element) { + if (isUnfilled) { + assert(_start == 0, 'Internal buffer grown from a bad state'); + _buf.add(element); + return; + } + + _buf[_start] = element; + _start++; + if (_start == capacity) { + _start = 0; + } + } + + @override + void clear() { + _start = 0; + _buf.clear(); + } + + @override + int get length => _buf.length; + + @override + set length(int newLength) => + throw UnsupportedError('Cannot resize a CircularBuffer.'); +} diff --git a/lib/src/bulk_download/download_progress.dart b/lib/src/bulk_download/download_progress.dart index c4ee986c..a71f5a61 100644 --- a/lib/src/bulk_download/download_progress.dart +++ b/lib/src/bulk_download/download_progress.dart @@ -5,16 +5,19 @@ part of flutter_map_tile_caching; /// Statistics and information about the current progress of the download /// +/// Note that there a number of things to keep in mind when tracking the progress +/// of a download. See https://fmtc.jaffaketchup.dev/bulk-downloading/foreground +/// for more information. +/// /// See the documentation on each individual property for more information. @immutable class DownloadProgress { /// The result of the latest attempted tile /// - /// May be used for UI display, error handling, or debugging purposes. - /// - /// It is not recommended to construct or keep a list of all these results, as - /// that may consume memory in large quanities. Instead, prefer counting events - /// that are of importance only. + /// Note that there a number of things to keep in mind when tracking the + /// progress of a download. See + /// https://fmtc.jaffaketchup.dev/bulk-downloading/foreground for more + /// information. TileEvent get latestTileEvent => _latestTileEvent!; final TileEvent? _latestTileEvent; diff --git a/lib/src/bulk_download/tile_loops/count.dart b/lib/src/bulk_download/tile_loops/count.dart index dd51b286..cf97b1e8 100644 --- a/lib/src/bulk_download/tile_loops/count.dart +++ b/lib/src/bulk_download/tile_loops/count.dart @@ -261,7 +261,7 @@ class TilesCounter { final byY = >{}; for (final Point(:x, :y) in outlineTiles) { - (byY[y] ?? (byY[y] = {})).add(x); + (byY[y] ??= {}).add(x); } for (final xs in byY.values) { diff --git a/lib/src/bulk_download/tile_loops/generate.dart b/lib/src/bulk_download/tile_loops/generate.dart index 05744a65..b8f24891 100644 --- a/lib/src/bulk_download/tile_loops/generate.dart +++ b/lib/src/bulk_download/tile_loops/generate.dart @@ -281,7 +281,7 @@ class TilesGenerator { final byY = >{}; for (final Point(:x, :y) in outlineTiles) { - (byY[y] ?? (byY[y] = {})).add(x); + (byY[y] ??= {}).add(x); } for (final MapEntry(key: y, value: xs) in byY.entries) { diff --git a/lib/src/db/defs/recovery.dart b/lib/src/db/defs/recovery.dart index dfda92da..708730c1 100644 --- a/lib/src/db/defs/recovery.dart +++ b/lib/src/db/defs/recovery.dart @@ -7,7 +7,7 @@ import 'package:meta/meta.dart'; part 'recovery.g.dart'; @internal -enum RegionType { rectangle, circle, line } +enum RegionType { rectangle, circle, line, customPolygon } @internal @Collection(accessor: 'recovery') diff --git a/lib/src/regions/recovered_region.dart b/lib/src/regions/recovered_region.dart index f491bd99..aa48326d 100644 --- a/lib/src/regions/recovered_region.dart +++ b/lib/src/regions/recovered_region.dart @@ -39,7 +39,8 @@ class RecoveredRegion { /// The bounds for a rectangular region final LatLngBounds? bounds; - /// The line making a line-based region + /// The line making a line-based region or the outline making a custom polygon + /// region final List? line; /// The center of a circular region @@ -81,11 +82,14 @@ class RecoveredRegion { required T Function(RectangleRegion rectangle) rectangle, required T Function(CircleRegion circle) circle, required T Function(LineRegion line) line, + required T Function(CustomPolygonRegion customPolygon) customPolygon, }) => switch (_type) { RegionType.rectangle => rectangle(RectangleRegion(bounds!)), RegionType.circle => circle(CircleRegion(center!, radius!)), RegionType.line => line(LineRegion(this.line!, radius!)), + RegionType.customPolygon => + customPolygon(CustomPolygonRegion(this.line!)), }; /// Convert this region into a [DownloadableRegion] @@ -94,7 +98,12 @@ class RecoveredRegion { Crs crs = const Epsg3857(), }) => DownloadableRegion._( - toRegion(rectangle: (r) => r, circle: (c) => c, line: (l) => l), + toRegion( + rectangle: (r) => r, + circle: (c) => c, + line: (l) => l, + customPolygon: (p) => p, + ), minZoom: minZoom, maxZoom: maxZoom, options: options, diff --git a/lib/src/root/recovery.dart b/lib/src/root/recovery.dart index c0a92d7d..88a0cae4 100644 --- a/lib/src/root/recovery.dart +++ b/lib/src/root/recovery.dart @@ -42,13 +42,11 @@ class RootRecovery { center: r.type == RegionType.circle ? LatLng(r.centerLat!, r.centerLng!) : null, - line: r.type == RegionType.line + line: r.type == RegionType.line || + r.type == RegionType.customPolygon ? List.generate( r.linePointsLat!.length, - (i) => LatLng( - r.linePointsLat![i], - r.linePointsLng![i], - ), + (i) => LatLng(r.linePointsLat![i], r.linePointsLng![i]), ) : null, radius: r.type != RegionType.rectangle @@ -100,7 +98,7 @@ class RootRecovery { rectangle: (_) => RegionType.rectangle, circle: (_) => RegionType.circle, line: (_) => RegionType.line, - customPolygon: (_) => throw UnimplementedError(), + customPolygon: (_) => RegionType.customPolygon, ), minZoom: region.minZoom, maxZoom: region.maxZoom, @@ -141,13 +139,23 @@ class RootRecovery { .line .map((c) => c.latitude) .toList() - : null, + : region.originalRegion is CustomPolygonRegion + ? (region.originalRegion as CustomPolygonRegion) + .outline + .map((c) => c.latitude) + .toList() + : null, linePointsLng: region.originalRegion is LineRegion ? (region.originalRegion as LineRegion) .line .map((c) => c.longitude) .toList() - : null, + : region.originalRegion is CustomPolygonRegion + ? (region.originalRegion as CustomPolygonRegion) + .outline + .map((c) => c.longitude) + .toList() + : null, circleRadius: region.originalRegion is CircleRegion ? (region.originalRegion as CircleRegion).radius : null, diff --git a/lib/src/store/download.dart b/lib/src/store/download.dart index eaa2909b..13c496c1 100644 --- a/lib/src/store/download.dart +++ b/lib/src/store/download.dart @@ -38,8 +38,7 @@ class DownloadManagement { /// /// 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). This must be - /// listened to, otherwise the download will not start. + /// of no longer than [maxReportInterval] (after the first tile). /// /// --- /// @@ -54,8 +53,7 @@ class DownloadManagement { /// - [skipExistingTiles] (defaults to `true`): whether to skip downloading /// tiles that are already cached /// - [skipSeaTiles] (defaults to `true`): whether to skip caching tiles that - /// are entirely sea (this is decided based on a comparison to the tile at - /// x0y0z17) + /// are entirely sea (based on a comparison to the tile at x0,y0,z17) /// /// Using too many parallel threads may place significant strain on the tile /// server, so check your tile server's ToS for more information. @@ -65,6 +63,9 @@ class DownloadManagement { /// currently in the buffer. It will also increase the memory (RAM) required. /// The output stream's statistics do not account for buffering. /// + /// Note that skipping sea tiles will not reduce the number of downloads - + /// tiles must be downloaded to be compared against the sample sea tile. + /// /// --- /// /// Although disabled `null` by default, [rateLimit] can be used to impose a diff --git a/test/fmtc_test.dart b/test/fmtc_test.dart index 3de74f13..172af88d 100644 --- a/test/fmtc_test.dart +++ b/test/fmtc_test.dart @@ -127,18 +127,19 @@ void main() { }); group('Line Region', () { - final lineRegion = - LineRegion([const LatLng(-1, -1), const LatLng(1, 1)], 5000) - .toDownloadable(minZoom: 1, maxZoom: 15, options: TileLayer()); + final lineRegion = LineRegion( + [const LatLng(-1, -1), const LatLng(1, 1), const LatLng(1, -1)], + 5000, + ).toDownloadable(minZoom: 1, maxZoom: 15, options: TileLayer()); test( 'Count By Counter', - () => expect(TilesCounter.lineTiles(lineRegion), 3131), + () => expect(TilesCounter.lineTiles(lineRegion), 5040), ); test( 'Count By Generator', - () async => expect(await countByGenerator(lineRegion), 3131), + () async => expect(await countByGenerator(lineRegion), 5040), ); test( From a238833f54c700e7de518e54a8b08fab8927c096 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sat, 5 Aug 2023 21:13:35 +0100 Subject: [PATCH 063/168] Improved example application Former-commit-id: e96954ea47f1a75c888da36fbfb1a92a81526140 [formerly bff4d1f916cb6b95f07274e8d7cad8c01467ea5b] Former-commit-id: 884ab680c7a7117633352d40dc1523a0232036c7 --- .../components/numerical_input_row.dart | 26 +- .../components/options_pane.dart | 44 +++ .../components/section_separator.dart | 14 - .../components/start_download_button.dart | 129 +++++++ .../components/store_selector.dart | 59 ++-- .../configure_download.dart | 334 ++++++------------ example/lib/screens/main/main.dart | 61 ++-- .../components/download_layout.dart | 2 +- .../components/side_panel/parent.dart | 10 +- .../main/pages/region_selection/map_view.dart | 316 ----------------- .../region_selection/region_selection.dart | 330 ++++++++++++++--- .../pages/stores/components/store_tile.dart | 2 +- .../lib/screens/main/pages/stores/stores.dart | 4 +- example/lib/shared/misc/exts/interleave.dart | 8 + .../misc/{ => exts}/size_formatter.dart | 0 lib/src/bulk_download/thread.dart | 6 +- 16 files changed, 659 insertions(+), 686 deletions(-) create mode 100644 example/lib/screens/configure_download/components/options_pane.dart delete mode 100644 example/lib/screens/configure_download/components/section_separator.dart create mode 100644 example/lib/screens/configure_download/components/start_download_button.dart delete mode 100644 example/lib/screens/main/pages/region_selection/map_view.dart create mode 100644 example/lib/shared/misc/exts/interleave.dart rename example/lib/shared/misc/{ => exts}/size_formatter.dart (100%) diff --git a/example/lib/screens/configure_download/components/numerical_input_row.dart b/example/lib/screens/configure_download/components/numerical_input_row.dart index 46f43aa2..7c36ec45 100644 --- a/example/lib/screens/configure_download/components/numerical_input_row.dart +++ b/example/lib/screens/configure_download/components/numerical_input_row.dart @@ -19,7 +19,7 @@ class NumericalInputRow extends StatelessWidget { final String suffixText; final int Function(ConfigureDownloadProvider provider) value; final int min; - final int max; + final int? max; final void Function(ConfigureDownloadProvider provider, int value) onChanged; @override @@ -30,13 +30,18 @@ class NumericalInputRow extends StatelessWidget { children: [ Text(label), const Spacer(), - Icon( - Icons.lock, - color: currentValue == max - ? Colors.amber - : Colors.white.withOpacity(0.2), - ), - const SizedBox(width: 16), + if (max != null) ...[ + Tooltip( + message: currentValue == max ? 'Limited in the example app' : '', + child: Icon( + Icons.lock, + color: currentValue == max + ? Colors.amber + : Colors.white.withOpacity(0.2), + ), + ), + const SizedBox(width: 16), + ], IntrinsicWidth( child: TextFormField( initialValue: currentValue.toString(), @@ -49,7 +54,10 @@ class NumericalInputRow extends StatelessWidget { ), inputFormatters: [ FilteringTextInputFormatter.digitsOnly, - _NumericalRangeFormatter(min: min, max: max), + _NumericalRangeFormatter( + min: min, + max: max ?? 9223372036854775807, + ), ], onChanged: (newVal) => onChanged( context.read(), diff --git a/example/lib/screens/configure_download/components/options_pane.dart b/example/lib/screens/configure_download/components/options_pane.dart new file mode 100644 index 00000000..3318bd9e --- /dev/null +++ b/example/lib/screens/configure_download/components/options_pane.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; + +import '../../../shared/misc/exts/interleave.dart'; + +class OptionsPane extends StatelessWidget { + const OptionsPane({ + super.key, + required this.label, + required this.children, + this.interPadding = 8, + }); + + final String label; + final Iterable children; + final double interPadding; + + @override + Widget build(BuildContext context) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(left: 14), + child: Text(label), + ), + const SizedBox.square(dimension: 4), + DecoratedBox( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceVariant, + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), + child: children.singleOrNull ?? + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: children + .interleave(SizedBox.square(dimension: interPadding)) + .toList(), + ), + ), + ), + ], + ); +} diff --git a/example/lib/screens/configure_download/components/section_separator.dart b/example/lib/screens/configure_download/components/section_separator.dart deleted file mode 100644 index 9a0969c8..00000000 --- a/example/lib/screens/configure_download/components/section_separator.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:flutter/material.dart'; - -class SectionSeparator extends StatelessWidget { - const SectionSeparator({super.key}); - - @override - Widget build(BuildContext context) => const Column( - children: [ - SizedBox(height: 5), - Divider(), - SizedBox(height: 5), - ], - ); -} diff --git a/example/lib/screens/configure_download/components/start_download_button.dart b/example/lib/screens/configure_download/components/start_download_button.dart new file mode 100644 index 00000000..782aa00c --- /dev/null +++ b/example/lib/screens/configure_download/components/start_download_button.dart @@ -0,0 +1,129 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; +import 'package:provider/provider.dart'; + +import '../../main/pages/downloading/state/downloading_provider.dart'; +import '../../main/pages/region_selection/state/region_selection_provider.dart'; +import '../state/configure_download_provider.dart'; + +class StartDownloadButton extends StatelessWidget { + const StartDownloadButton({ + super.key, + required this.region, + required this.minZoom, + required this.maxZoom, + }); + + final BaseRegion region; + final int minZoom; + final int maxZoom; + + @override + Widget build(BuildContext context) => + Selector( + selector: (context, provider) => provider.isReady, + builder: (context, isReady, _) => + Selector( + selector: (context, provider) => provider.selectedStore, + builder: (context, selectedStore, child) => IgnorePointer( + ignoring: selectedStore == null, + child: AnimatedOpacity( + opacity: selectedStore == null ? 0 : 1, + duration: const Duration(milliseconds: 200), + curve: Curves.easeInOut, + child: child, + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + AnimatedScale( + scale: isReady ? 1 : 0, + duration: const Duration(milliseconds: 200), + curve: Curves.easeInCubic, + alignment: Alignment.bottomRight, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.onBackground, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(12), + topRight: Radius.circular(12), + bottomLeft: Radius.circular(12), + ), + ), + margin: const EdgeInsets.only(right: 12, left: 32), + padding: const EdgeInsets.all(12), + constraints: const BoxConstraints(maxWidth: 500), + child: const Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + "You must abide by your tile server's Terms of Service when bulk downloading. Many servers will forbid or heavily restrict this action, as it places extra strain on resources. Be respectful, and note that you use this functionality at your own risk.", + textAlign: TextAlign.end, + style: TextStyle(color: Colors.black), + ), + SizedBox(height: 8), + Icon(Icons.report, color: Colors.red, size: 32), + ], + ), + ), + ), + const SizedBox(height: 16), + FloatingActionButton.extended( + onPressed: () async { + final configureDownloadProvider = + context.read(); + + if (!isReady) { + configureDownloadProvider.isReady = true; + return; + } + + final regionSelectionProvider = + context.read(); + final downloadingProvider = + context.read(); + + final navigator = Navigator.of(context); + + final metadata = await regionSelectionProvider + .selectedStore!.metadata.readAsync; + + downloadingProvider.setDownloadProgress( + regionSelectionProvider.selectedStore!.download + .startForeground( + region: region.toDownloadable( + minZoom: minZoom, + maxZoom: maxZoom, + options: TileLayer( + urlTemplate: metadata['sourceURL'], + userAgentPackageName: + 'dev.jaffaketchup.fmtc.demo', + ), + ), + parallelThreads: + configureDownloadProvider.parallelThreads, + maxBufferLength: + configureDownloadProvider.maxBufferLength, + skipExistingTiles: + configureDownloadProvider.skipExistingTiles, + skipSeaTiles: configureDownloadProvider.skipSeaTiles, + rateLimit: configureDownloadProvider.rateLimit, + ) + .asBroadcastStream(), + ); + configureDownloadProvider.isReady = false; + + navigator.pop(); + }, + label: const Text('Start Download'), + icon: Icon(isReady ? Icons.save : Icons.arrow_forward), + ), + ], + ), + ), + ); +} diff --git a/example/lib/screens/configure_download/components/store_selector.dart b/example/lib/screens/configure_download/components/store_selector.dart index 06107003..50c78560 100644 --- a/example/lib/screens/configure_download/components/store_selector.dart +++ b/example/lib/screens/configure_download/components/store_selector.dart @@ -14,35 +14,38 @@ class StoreSelector extends StatefulWidget { class _StoreSelectorState extends State { @override - Widget build(BuildContext context) => Column( - crossAxisAlignment: CrossAxisAlignment.start, + Widget build(BuildContext context) => Row( children: [ - const Text('CHOOSE A STORE'), - Consumer2( - builder: (context, downloadProvider, generalProvider, _) => - FutureBuilder>( - future: FMTC.instance.rootDirectory.stats.storesAvailableAsync, - builder: (context, snapshot) => DropdownButton( - items: snapshot.data - ?.map( - (e) => DropdownMenuItem( - value: e, - child: Text(e.storeName), - ), - ) - .toList(), - onChanged: (store) => downloadProvider.setSelectedStore(store), - value: downloadProvider.selectedStore ?? - (generalProvider.currentStore == null - ? null - : FMTC.instance(generalProvider.currentStore!)), - isExpanded: true, - hint: Text( - snapshot.data == null - ? 'Loading...' - : snapshot.data!.isEmpty - ? 'None Available' - : 'None Selected', + const Text('Store'), + const Spacer(), + IntrinsicWidth( + child: Consumer2( + builder: (context, downloadProvider, generalProvider, _) => + FutureBuilder>( + future: FMTC.instance.rootDirectory.stats.storesAvailableAsync, + builder: (context, snapshot) => DropdownButton( + items: snapshot.data + ?.map( + (e) => DropdownMenuItem( + value: e, + child: Text(e.storeName), + ), + ) + .toList(), + onChanged: (store) => + downloadProvider.setSelectedStore(store), + value: downloadProvider.selectedStore ?? + (generalProvider.currentStore == null + ? null + : FMTC.instance(generalProvider.currentStore!)), + hint: Text( + snapshot.data == null + ? 'Loading...' + : snapshot.data!.isEmpty + ? 'None Available' + : 'None Selected', + ), + padding: const EdgeInsets.only(left: 12), ), ), ), diff --git a/example/lib/screens/configure_download/configure_download.dart b/example/lib/screens/configure_download/configure_download.dart index dcb4dd7f..34115620 100644 --- a/example/lib/screens/configure_download/configure_download.dart +++ b/example/lib/screens/configure_download/configure_download.dart @@ -1,13 +1,12 @@ import 'package:flutter/material.dart'; -import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; import 'package:provider/provider.dart'; -import '../main/pages/downloading/state/downloading_provider.dart'; -import '../main/pages/region_selection/state/region_selection_provider.dart'; +import '../../shared/misc/exts/interleave.dart'; import 'components/numerical_input_row.dart'; +import 'components/options_pane.dart'; import 'components/region_information.dart'; -import 'components/section_separator.dart'; +import 'components/start_download_button.dart'; import 'components/store_selector.dart'; import 'state/configure_download_provider.dart'; @@ -24,240 +23,129 @@ class ConfigureDownloadPopup extends StatelessWidget { final int maxZoom; @override - Widget build(BuildContext context) => - Selector( - selector: (context, provider) => provider.isReady, - builder: (context, isReady, _) => Scaffold( - appBar: AppBar(title: const Text('Configure Bulk Download')), - floatingActionButton: - Selector( - selector: (context, provider) => provider.selectedStore, - builder: (context, selectedStore, child) => - selectedStore == null ? const SizedBox.shrink() : child!, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - AnimatedScale( - scale: isReady ? 1 : 0, - duration: const Duration(milliseconds: 200), - curve: Curves.easeInCubic, - alignment: Alignment.bottomRight, - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.onBackground, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(12), - topRight: Radius.circular(12), - bottomLeft: Radius.circular(12), - ), + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text('Configure Bulk Download')), + floatingActionButton: StartDownloadButton( + region: region, + minZoom: minZoom, + maxZoom: maxZoom, + ), + body: Stack( + fit: StackFit.expand, + children: [ + SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox.shrink(), + RegionInformation( + region: region, + minZoom: minZoom, + maxZoom: maxZoom, + ), + const Divider(thickness: 2, height: 8), + const OptionsPane( + label: 'STORE DIRECTORY', + children: [StoreSelector()], ), - margin: const EdgeInsets.only(right: 12, left: 32), - padding: const EdgeInsets.all(12), - constraints: const BoxConstraints(maxWidth: 500), - child: const Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.end, + OptionsPane( + label: 'PERFORMANCE FACTORS', children: [ - Text( - "You must abide by your tile server's Terms of Service when bulk downloading. Many servers will forbid or heavily restrict this action, as it places extra strain on resources. Be respectful, and note that you use this functionality at your own risk.", - textAlign: TextAlign.end, - style: TextStyle(color: Colors.black), + NumericalInputRow( + label: 'Parallel Threads', + suffixText: 'threads', + value: (provider) => provider.parallelThreads, + min: 1, + max: 10, + onChanged: (provider, value) => + provider.parallelThreads = value, + ), + NumericalInputRow( + label: 'Rate Limit', + suffixText: 'max. tps', + value: (provider) => provider.rateLimit, + min: 5, + max: 500, + onChanged: (provider, value) => + provider.rateLimit = value, + ), + NumericalInputRow( + label: 'Tile Buffer Length', + suffixText: 'max. tiles', + value: (provider) => provider.maxBufferLength, + min: 0, + max: null, + onChanged: (provider, value) => + provider.maxBufferLength = value, ), - SizedBox(height: 8), + ], + ), + OptionsPane( + label: 'SKIP TILES', + children: [ Row( - mainAxisSize: MainAxisSize.min, children: [ - Text( - 'CAUTION', - style: TextStyle( - color: Colors.red, - fontWeight: FontWeight.bold, - fontSize: 16, + const Text('Skip Existing Tiles'), + const Spacer(), + Switch.adaptive( + value: context + .select( + (provider) => provider.skipExistingTiles, ), - ), - SizedBox(width: 8), - Icon( - Icons.report, - color: Colors.red, - size: 32, - ), + onChanged: (val) => context + .read() + .skipExistingTiles = val, + activeColor: + Theme.of(context).colorScheme.primary, + ) + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Skip Sea Tiles'), + const Spacer(), + Switch.adaptive( + value: context.select((provider) => provider.skipSeaTiles), + onChanged: (val) => context + .read() + .skipSeaTiles = val, + activeColor: + Theme.of(context).colorScheme.primary, + ) ], ), ], ), - ), - ), - const SizedBox(height: 16), - FloatingActionButton.extended( - onPressed: () async { - final configureDownloadProvider = - context.read(); - - if (!isReady) { - configureDownloadProvider.isReady = true; - return; - } - - final regionSelectionProvider = - context.read(); - final downloadingProvider = - context.read(); - - final navigator = Navigator.of(context); - - final metadata = await regionSelectionProvider - .selectedStore!.metadata.readAsync; - - downloadingProvider.setDownloadProgress( - regionSelectionProvider.selectedStore!.download - .startForeground( - region: region.toDownloadable( - minZoom: minZoom, - maxZoom: maxZoom, - options: TileLayer( - urlTemplate: metadata['sourceURL'], - userAgentPackageName: - 'dev.jaffaketchup.fmtc.demo', - ), - ), - parallelThreads: - configureDownloadProvider.parallelThreads, - maxBufferLength: - configureDownloadProvider.maxBufferLength, - skipExistingTiles: - configureDownloadProvider.skipExistingTiles, - skipSeaTiles: - configureDownloadProvider.skipSeaTiles, - rateLimit: configureDownloadProvider.rateLimit, - ) - .asBroadcastStream(), - ); - configureDownloadProvider.isReady = false; - - navigator.pop(); - }, - label: const Text('Start Download'), - icon: Icon(isReady ? Icons.save : Icons.arrow_forward), - ), - ], - ), - ), - body: Stack( - children: [ - Positioned.fill( - left: 12, - top: 12, - right: 12, - bottom: 12, - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - RegionInformation( - region: region, - minZoom: minZoom, - maxZoom: maxZoom, - ), - const SectionSeparator(), - const StoreSelector(), - const SectionSeparator(), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text('CONFIGURE DOWNLOAD OPTIONS'), - const SizedBox(height: 16), - NumericalInputRow( - label: 'Parallel Threads', - suffixText: 'threads', - value: (provider) => provider.parallelThreads, - min: 1, - max: 10, - onChanged: (provider, value) => - provider.parallelThreads = value, - ), - const SizedBox(height: 8), - NumericalInputRow( - label: 'Rate Limit', - suffixText: 'max. tiles/second', - value: (provider) => provider.rateLimit, - min: 5, - max: 200, - onChanged: (provider, value) => - provider.rateLimit = value, - ), - const SizedBox(height: 8), - NumericalInputRow( - label: 'Tile Buffer Length', - suffixText: 'max. tiles', - value: (provider) => provider.maxBufferLength, - min: 0, - max: 2000, - onChanged: (provider, value) => - provider.maxBufferLength = value, - ), - const SizedBox(height: 16), - Row( - children: [ - const Text('Skip Existing Tiles'), - const Spacer(), - Switch( - value: context - .select( - (provider) => provider.skipExistingTiles, - ), - onChanged: (val) => context - .read() - .skipExistingTiles = val, - activeColor: - Theme.of(context).colorScheme.primary, - ) - ], - ), - const SizedBox(height: 6), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text('Skip Sea Tiles'), - const Spacer(), - Switch( - value: context.select((provider) => provider.skipSeaTiles), - onChanged: (val) => context - .read() - .skipSeaTiles = val, - activeColor: - Theme.of(context).colorScheme.primary, - ) - ], - ), - ], - ), - ], - ), + const SizedBox(height: 72), + ].interleave(const SizedBox.square(dimension: 16)).toList(), ), ), - Positioned.fill( - child: IgnorePointer( - ignoring: !isReady, - child: GestureDetector( - onTap: isReady - ? () => context - .read() - .isReady = false - : null, - child: AnimatedContainer( - duration: const Duration(milliseconds: 200), - curve: Curves.easeInCubic, - color: isReady - ? Colors.black.withOpacity(2 / 3) - : Colors.transparent, - ), + ), + Selector( + selector: (context, provider) => provider.isReady, + builder: (context, isReady, _) => IgnorePointer( + ignoring: !isReady, + child: GestureDetector( + onTap: isReady + ? () => context + .read() + .isReady = false + : null, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + curve: Curves.easeInCubic, + color: isReady + ? Colors.black.withOpacity(2 / 3) + : Colors.transparent, ), ), ), - ], - ), + ), + ], ), ); } diff --git a/example/lib/screens/main/main.dart b/example/lib/screens/main/main.dart index 0ef6a984..b349b8b3 100644 --- a/example/lib/screens/main/main.dart +++ b/example/lib/screens/main/main.dart @@ -30,18 +30,22 @@ class _MainScreenState extends State { List get _destinations => [ const NavigationDestination( - icon: Icon(Icons.map), label: 'Map', + icon: Icon(Icons.map_outlined), + selectedIcon: Icon(Icons.map), ), const NavigationDestination( - icon: Icon(Icons.folder), label: 'Stores', + icon: Icon(Icons.inventory_2_outlined), + selectedIcon: Icon(Icons.inventory_2), ), const NavigationDestination( - icon: Icon(Icons.download), label: 'Download', + icon: Icon(Icons.download_outlined), + selectedIcon: Icon(Icons.download), ), NavigationDestination( + label: 'Recover', icon: StreamBuilder( stream: FMTC.instance.rootDirectory.stats .watchChanges() @@ -55,11 +59,10 @@ class _MainScreenState extends State { ), showBadge: _currentPageIndex != 3 && (snapshot.data?.isNotEmpty ?? false), - child: const Icon(Icons.running_with_errors), + child: const Icon(Icons.support), ), ), ), - label: 'Recover', ), ]; @@ -161,50 +164,32 @@ class _MainScreenState extends State { NavigationRail( onDestinationSelected: _onDestinationSelected, selectedIndex: _currentPageIndex, + labelType: NavigationRailLabelType.all, groupAlignment: 0, - extended: extended, destinations: _destinations .map( (d) => NavigationRailDestination( - icon: d.icon, label: Text(d.label), - padding: const EdgeInsets.all(10), + icon: d.icon, + selectedIcon: d.selectedIcon, + padding: const EdgeInsets.symmetric(vertical: 6), ), ) .toList(), - leading: Row( - children: [ - AnimatedContainer( - width: extended ? 205 : 0, - duration: kThemeAnimationDuration, - curve: Curves.easeInOut, - ), - IconButton( - icon: AnimatedSwitcher( - duration: kThemeAnimationDuration, - switchInCurve: Curves.easeInOut, - switchOutCurve: Curves.easeInOut, - child: Icon( - key: UniqueKey(), - extended ? Icons.menu_open : Icons.menu, - ), - ), - onPressed: () => setState(() => extended = !extended), - tooltip: !extended ? 'Extend Menu' : 'Collapse Menu', - ), - ], - ), ), Expanded( - child: ClipRRect( - borderRadius: BorderRadius.only( - topLeft: MediaQuery.sizeOf(context).width > 950 - ? const Radius.circular(16) - : Radius.zero, - bottomLeft: MediaQuery.sizeOf(context).width > 950 - ? const Radius.circular(16) - : Radius.zero, + child: DecoratedBox( + decoration: BoxDecoration( + border: Border( + left: MediaQuery.sizeOf(context).width > 950 + ? BorderSide(color: Theme.of(context).dividerColor) + : BorderSide.none, + bottom: MediaQuery.sizeOf(context).width <= 950 + ? BorderSide(color: Theme.of(context).dividerColor) + : BorderSide.none, + ), ), + position: DecorationPosition.foreground, child: PageView( controller: _pageController, physics: const NeverScrollableScrollPhysics(), diff --git a/example/lib/screens/main/pages/downloading/components/download_layout.dart b/example/lib/screens/main/pages/downloading/components/download_layout.dart index af25d46e..5cf9b445 100644 --- a/example/lib/screens/main/pages/downloading/components/download_layout.dart +++ b/example/lib/screens/main/pages/downloading/components/download_layout.dart @@ -3,7 +3,7 @@ import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:provider/provider.dart'; -import '../../../../../shared/misc/size_formatter.dart'; +import '../../../../../shared/misc/exts/size_formatter.dart'; import '../state/downloading_provider.dart'; import 'main_statistics.dart'; import 'multi_linear_progress_indicator.dart'; diff --git a/example/lib/screens/main/pages/region_selection/components/side_panel/parent.dart b/example/lib/screens/main/pages/region_selection/components/side_panel/parent.dart index bdd8f061..6024fe60 100644 --- a/example/lib/screens/main/pages/region_selection/components/side_panel/parent.dart +++ b/example/lib/screens/main/pages/region_selection/components/side_panel/parent.dart @@ -7,6 +7,7 @@ import 'package:gpx/gpx.dart'; import 'package:latlong2/latlong.dart'; import 'package:provider/provider.dart'; +import '../../../../../../shared/misc/exts/interleave.dart'; import '../../../../../../shared/misc/region_selection_method.dart'; import '../../../../../../shared/misc/region_type.dart'; import '../../state/region_selection_provider.dart'; @@ -59,12 +60,3 @@ class SidePanel extends StatelessWidget { ), ); } - -extension _IterableExt on Iterable { - Iterable interleave(E separator) sync* { - for (int i = 0; i < length; i++) { - yield elementAt(i); - if (i < length - 1) yield separator; - } - } -} diff --git a/example/lib/screens/main/pages/region_selection/map_view.dart b/example/lib/screens/main/pages/region_selection/map_view.dart deleted file mode 100644 index 6e189cf0..00000000 --- a/example/lib/screens/main/pages/region_selection/map_view.dart +++ /dev/null @@ -1,316 +0,0 @@ -import 'dart:math'; - -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/plugin_api.dart'; -import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; -import 'package:latlong2/latlong.dart'; -import 'package:provider/provider.dart'; - -import '../../../../shared/components/build_attribution.dart'; -import '../../../../shared/components/loading_indicator.dart'; -import '../../../../shared/misc/region_selection_method.dart'; -import '../../../../shared/misc/region_type.dart'; -import '../../../../shared/state/general_provider.dart'; -import '../../../configure_download/configure_download.dart'; -import 'components/crosshairs.dart'; -import 'components/custom_polygon_snapping_indicator.dart'; -import 'components/region_shape.dart'; -import 'components/side_panel/parent.dart'; -import 'components/usage_instructions.dart'; -import 'state/region_selection_provider.dart'; - -class MapView extends StatefulWidget { - const MapView({super.key}); - - @override - State createState() => _MapViewState(); -} - -class _MapViewState extends State { - final mapController = MapController(); - - late final mapOptions = MapOptions( - initialCenter: const LatLng(51.509364, -0.128928), - initialZoom: 11, - maxZoom: 22, - cameraConstraint: CameraConstraint.contain( - bounds: LatLngBounds.fromPoints([ - const LatLng(-90, 180), - const LatLng(90, 180), - const LatLng(90, -180), - const LatLng(-90, -180), - ]), - ), - interactionOptions: const InteractionOptions( - flags: InteractiveFlag.all & - ~InteractiveFlag.rotate & - ~InteractiveFlag.doubleTapZoom, - scrollWheelVelocity: 0.002, - ), - keepAlive: true, - onTap: (_, __) { - final provider = context.read(); - - if (provider.isCustomPolygonComplete) return; - - final List coords; - if (provider.customPolygonSnap && - provider.regionType == RegionType.customPolygon) { - coords = provider.addCoordinate(provider.coordinates.first); - provider.customPolygonSnap = false; - } else { - coords = provider.addCoordinate(provider.currentNewPointPos); - } - - if (coords.length < 2) return; - - switch (provider.regionType) { - case RegionType.square: - if (coords.length == 2) { - provider.region = RectangleRegion(LatLngBounds.fromPoints(coords)); - break; - } - provider - ..clearCoordinates() - ..addCoordinate(provider.currentNewPointPos); - - break; - case RegionType.circle: - if (coords.length == 2) { - provider.region = CircleRegion( - coords[0], - const Distance(roundResult: false) - .distance(coords[0], coords[1]) / - 1000, - ); - break; - } - provider - ..clearCoordinates() - ..addCoordinate(provider.currentNewPointPos); - - break; - case RegionType.line: - provider.region = LineRegion(coords, provider.lineRadius); - break; - case RegionType.customPolygon: - if (!provider.isCustomPolygonComplete) break; - provider.region = CustomPolygonRegion(coords); - break; - } - }, - onSecondaryTap: (_, __) => - context.read().removeLastCoordinate(), - onLongPress: (_, __) => - context.read().removeLastCoordinate(), - onPointerHover: (evt, point) { - final provider = context.read(); - - if (provider.regionSelectionMethod == RegionSelectionMethod.usePointer) { - provider.currentNewPointPos = point; - - if (provider.regionType == RegionType.customPolygon) { - final coords = provider.coordinates; - if (coords.length > 1) { - final newPointPos = mapController.camera - .latLngToScreenPoint(coords.first) - .toOffset(); - provider.customPolygonSnap = coords.first != coords.last && - sqrt( - pow(newPointPos.dx - evt.localPosition.dx, 2) + - pow(newPointPos.dy - evt.localPosition.dy, 2), - ) < - 15; - } - } - } - }, - onPositionChanged: (position, _) { - final provider = context.read(); - - if (provider.regionSelectionMethod == - RegionSelectionMethod.useMapCenter) { - provider.currentNewPointPos = position.center!; - - if (provider.regionType == RegionType.customPolygon) { - final coords = provider.coordinates; - if (coords.length > 1) { - final newPointPos = mapController.camera - .latLngToScreenPoint(coords.first) - .toOffset(); - final centerPos = mapController.camera - .latLngToScreenPoint(provider.currentNewPointPos) - .toOffset(); - provider.customPolygonSnap = coords.first != coords.last && - sqrt( - pow(newPointPos.dx - centerPos.dx, 2) + - pow(newPointPos.dy - centerPos.dy, 2), - ) < - 30; - } - } - } - }, - ); - - bool keyboardHandler(KeyEvent event) { - if (event is! KeyDownEvent) return false; - - final provider = context.read(); - - if (provider.region != null && - event.logicalKey == LogicalKeyboardKey.enter) { - pushToConfigureDownload(); - } else if (event.logicalKey == LogicalKeyboardKey.escape || - event.logicalKey == LogicalKeyboardKey.delete) { - provider.clearCoordinates(); - } else if (event.logicalKey == LogicalKeyboardKey.backspace) { - provider.removeLastCoordinate(); - } else if (provider.regionType != RegionType.square && - event.logicalKey == LogicalKeyboardKey.keyZ) { - provider - ..regionType = RegionType.square - ..clearCoordinates(); - } else if (provider.regionType != RegionType.circle && - event.logicalKey == LogicalKeyboardKey.keyX) { - provider - ..regionType = RegionType.circle - ..clearCoordinates(); - } else if (provider.regionType != RegionType.line && - event.logicalKey == LogicalKeyboardKey.keyC) { - provider - ..regionType = RegionType.line - ..clearCoordinates(); - } else if (provider.regionType != RegionType.customPolygon && - event.logicalKey == LogicalKeyboardKey.keyV) { - provider - ..regionType = RegionType.customPolygon - ..clearCoordinates(); - } - - return false; - } - - void pushToConfigureDownload() { - final provider = context.read(); - ServicesBinding.instance.keyboard.removeHandler(keyboardHandler); - Navigator.of(context) - .push( - MaterialPageRoute( - builder: (context) => ConfigureDownloadPopup( - region: provider.region!, - minZoom: provider.minZoom, - maxZoom: provider.maxZoom, - ), - fullscreenDialog: true, - ), - ) - .then( - (_) => ServicesBinding.instance.keyboard.addHandler(keyboardHandler), - ); - } - - @override - void initState() { - super.initState(); - ServicesBinding.instance.keyboard.addHandler(keyboardHandler); - } - - @override - void dispose() { - ServicesBinding.instance.keyboard.removeHandler(keyboardHandler); - super.dispose(); - } - - @override - Widget build(BuildContext context) => LayoutBuilder( - builder: (context, constraints) => Stack( - children: [ - Selector( - selector: (context, provider) => provider.currentStore, - builder: (context, currentStore, _) => - FutureBuilder?>( - future: currentStore == null - ? Future.value() - : FMTC.instance(currentStore).metadata.readAsync, - builder: (context, metadata) { - if (currentStore != null && metadata.data == null) { - return const LoadingIndicator('Preparing Map'); - } - - final urlTemplate = metadata.data?['sourceURL'] ?? - 'https://tile.openstreetmap.org/{z}/{x}/{y}.png'; - - return MouseRegion( - opaque: false, - cursor: context.select( - (p) => p.regionSelectionMethod, - ) == - RegionSelectionMethod.useMapCenter - ? MouseCursor.defer - : context.select( - (p) => p.customPolygonSnap, - ) - ? SystemMouseCursors.none - : SystemMouseCursors.precise, - child: FlutterMap( - mapController: mapController, - options: mapOptions, - nonRotatedChildren: buildStdAttribution(urlTemplate), - children: [ - TileLayer( - urlTemplate: urlTemplate, - userAgentPackageName: 'dev.jaffaketchup.fmtc.demo', - maxNativeZoom: 20, - backgroundColor: const Color(0xFFaad3df), - tileBuilder: (context, widget, tile) => - FutureBuilder( - future: currentStore == null - ? Future.value() - : FMTC - .instance(currentStore) - .getTileProvider() - .checkTileCachedAsync( - coords: tile.coordinates, - options: - TileLayer(urlTemplate: urlTemplate), - ), - builder: (context, snapshot) => DecoratedBox( - position: DecorationPosition.foreground, - decoration: BoxDecoration( - color: (snapshot.data ?? false) - ? Colors.deepOrange.withOpacity(1 / 3) - : Colors.transparent, - ), - child: widget, - ), - ), - ), - const RegionShape(), - const CustomPolygonSnappingIndicator(), - ], - ), - ); - }, - ), - ), - SidePanel( - constraints: constraints, - pushToConfigureDownload: pushToConfigureDownload, - ), - if (context.select( - (p) => p.regionSelectionMethod, - ) == - RegionSelectionMethod.useMapCenter && - !context.select( - (p) => p.customPolygonSnap, - )) - const Center(child: Crosshairs()), - UsageInstructions(constraints: constraints), - ], - ), - ); -} diff --git a/example/lib/screens/main/pages/region_selection/region_selection.dart b/example/lib/screens/main/pages/region_selection/region_selection.dart index c52fbf61..615a3202 100644 --- a/example/lib/screens/main/pages/region_selection/region_selection.dart +++ b/example/lib/screens/main/pages/region_selection/region_selection.dart @@ -1,9 +1,25 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; -import 'package:google_fonts/google_fonts.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map/plugin_api.dart'; +import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; +import 'package:latlong2/latlong.dart'; import 'package:provider/provider.dart'; +import '../../../../shared/components/build_attribution.dart'; +import '../../../../shared/components/loading_indicator.dart'; +import '../../../../shared/misc/region_selection_method.dart'; +import '../../../../shared/misc/region_type.dart'; import '../../../../shared/state/general_provider.dart'; -import 'map_view.dart'; +import '../../../configure_download/configure_download.dart'; +import 'components/crosshairs.dart'; +import 'components/custom_polygon_snapping_indicator.dart'; +import 'components/region_shape.dart'; +import 'components/side_panel/parent.dart'; +import 'components/usage_instructions.dart'; +import 'state/region_selection_provider.dart'; class RegionSelectionPage extends StatefulWidget { const RegionSelectionPage({super.key}); @@ -13,55 +29,287 @@ class RegionSelectionPage extends StatefulWidget { } class _RegionSelectionPageState extends State { + final mapController = MapController(); + + late final mapOptions = MapOptions( + initialCenter: const LatLng(51.509364, -0.128928), + initialZoom: 11, + maxZoom: 22, + cameraConstraint: CameraConstraint.contain( + bounds: LatLngBounds.fromPoints([ + const LatLng(-90, 180), + const LatLng(90, 180), + const LatLng(90, -180), + const LatLng(-90, -180), + ]), + ), + interactionOptions: const InteractionOptions( + flags: InteractiveFlag.all & + ~InteractiveFlag.rotate & + ~InteractiveFlag.doubleTapZoom, + scrollWheelVelocity: 0.002, + ), + keepAlive: true, + onTap: (_, __) { + final provider = context.read(); + + if (provider.isCustomPolygonComplete) return; + + final List coords; + if (provider.customPolygonSnap && + provider.regionType == RegionType.customPolygon) { + coords = provider.addCoordinate(provider.coordinates.first); + provider.customPolygonSnap = false; + } else { + coords = provider.addCoordinate(provider.currentNewPointPos); + } + + if (coords.length < 2) return; + + switch (provider.regionType) { + case RegionType.square: + if (coords.length == 2) { + provider.region = RectangleRegion(LatLngBounds.fromPoints(coords)); + break; + } + provider + ..clearCoordinates() + ..addCoordinate(provider.currentNewPointPos); + + break; + case RegionType.circle: + if (coords.length == 2) { + provider.region = CircleRegion( + coords[0], + const Distance(roundResult: false) + .distance(coords[0], coords[1]) / + 1000, + ); + break; + } + provider + ..clearCoordinates() + ..addCoordinate(provider.currentNewPointPos); + + break; + case RegionType.line: + provider.region = LineRegion(coords, provider.lineRadius); + break; + case RegionType.customPolygon: + if (!provider.isCustomPolygonComplete) break; + provider.region = CustomPolygonRegion(coords); + break; + } + }, + onSecondaryTap: (_, __) => + context.read().removeLastCoordinate(), + onLongPress: (_, __) => + context.read().removeLastCoordinate(), + onPointerHover: (evt, point) { + final provider = context.read(); + + if (provider.regionSelectionMethod == RegionSelectionMethod.usePointer) { + provider.currentNewPointPos = point; + + if (provider.regionType == RegionType.customPolygon) { + final coords = provider.coordinates; + if (coords.length > 1) { + final newPointPos = mapController.camera + .latLngToScreenPoint(coords.first) + .toOffset(); + provider.customPolygonSnap = coords.first != coords.last && + sqrt( + pow(newPointPos.dx - evt.localPosition.dx, 2) + + pow(newPointPos.dy - evt.localPosition.dy, 2), + ) < + 15; + } + } + } + }, + onPositionChanged: (position, _) { + final provider = context.read(); + + if (provider.regionSelectionMethod == + RegionSelectionMethod.useMapCenter) { + provider.currentNewPointPos = position.center!; + + if (provider.regionType == RegionType.customPolygon) { + final coords = provider.coordinates; + if (coords.length > 1) { + final newPointPos = mapController.camera + .latLngToScreenPoint(coords.first) + .toOffset(); + final centerPos = mapController.camera + .latLngToScreenPoint(provider.currentNewPointPos) + .toOffset(); + provider.customPolygonSnap = coords.first != coords.last && + sqrt( + pow(newPointPos.dx - centerPos.dx, 2) + + pow(newPointPos.dy - centerPos.dy, 2), + ) < + 30; + } + } + } + }, + ); + + bool keyboardHandler(KeyEvent event) { + if (event is! KeyDownEvent) return false; + + final provider = context.read(); + + if (provider.region != null && + event.logicalKey == LogicalKeyboardKey.enter) { + pushToConfigureDownload(); + } else if (event.logicalKey == LogicalKeyboardKey.escape || + event.logicalKey == LogicalKeyboardKey.delete) { + provider.clearCoordinates(); + } else if (event.logicalKey == LogicalKeyboardKey.backspace) { + provider.removeLastCoordinate(); + } else if (provider.regionType != RegionType.square && + event.logicalKey == LogicalKeyboardKey.keyZ) { + provider + ..regionType = RegionType.square + ..clearCoordinates(); + } else if (provider.regionType != RegionType.circle && + event.logicalKey == LogicalKeyboardKey.keyX) { + provider + ..regionType = RegionType.circle + ..clearCoordinates(); + } else if (provider.regionType != RegionType.line && + event.logicalKey == LogicalKeyboardKey.keyC) { + provider + ..regionType = RegionType.line + ..clearCoordinates(); + } else if (provider.regionType != RegionType.customPolygon && + event.logicalKey == LogicalKeyboardKey.keyV) { + provider + ..regionType = RegionType.customPolygon + ..clearCoordinates(); + } + + return false; + } + + void pushToConfigureDownload() { + final provider = context.read(); + ServicesBinding.instance.keyboard.removeHandler(keyboardHandler); + Navigator.of(context) + .push( + MaterialPageRoute( + builder: (context) => ConfigureDownloadPopup( + region: provider.region!, + minZoom: provider.minZoom, + maxZoom: provider.maxZoom, + ), + fullscreenDialog: true, + ), + ) + .then( + (_) => ServicesBinding.instance.keyboard.addHandler(keyboardHandler), + ); + } + + @override + void initState() { + super.initState(); + ServicesBinding.instance.keyboard.addHandler(keyboardHandler); + } + @override - Widget build(BuildContext context) => Scaffold( - body: Column( + void dispose() { + ServicesBinding.instance.keyboard.removeHandler(keyboardHandler); + super.dispose(); + } + + @override + Widget build(BuildContext context) => LayoutBuilder( + builder: (context, constraints) => Stack( children: [ - SafeArea( - child: Padding( - padding: const EdgeInsets.all(12), - child: Row( - children: [ - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, + Selector( + selector: (context, provider) => provider.currentStore, + builder: (context, currentStore, _) => + FutureBuilder?>( + future: currentStore == null + ? Future.value() + : FMTC.instance(currentStore).metadata.readAsync, + builder: (context, metadata) { + if (currentStore != null && metadata.data == null) { + return const LoadingIndicator('Preparing Map'); + } + + final urlTemplate = metadata.data?['sourceURL'] ?? + 'https://tile.openstreetmap.org/{z}/{x}/{y}.png'; + + return MouseRegion( + opaque: false, + cursor: context.select( + (p) => p.regionSelectionMethod, + ) == + RegionSelectionMethod.useMapCenter + ? MouseCursor.defer + : context.select( + (p) => p.customPolygonSnap, + ) + ? SystemMouseCursors.none + : SystemMouseCursors.precise, + child: FlutterMap( + mapController: mapController, + options: mapOptions, + nonRotatedChildren: buildStdAttribution(urlTemplate), children: [ - Text( - 'Downloader', - style: GoogleFonts.ubuntu( - fontWeight: FontWeight.bold, - fontSize: 24, + TileLayer( + urlTemplate: urlTemplate, + userAgentPackageName: 'dev.jaffaketchup.fmtc.demo', + maxNativeZoom: 20, + backgroundColor: const Color(0xFFaad3df), + tileBuilder: (context, widget, tile) => + FutureBuilder( + future: currentStore == null + ? Future.value() + : FMTC + .instance(currentStore) + .getTileProvider() + .checkTileCachedAsync( + coords: tile.coordinates, + options: + TileLayer(urlTemplate: urlTemplate), + ), + builder: (context, snapshot) => DecoratedBox( + position: DecorationPosition.foreground, + decoration: BoxDecoration( + color: (snapshot.data ?? false) + ? Colors.deepOrange.withOpacity(1 / 3) + : Colors.transparent, + ), + child: widget, + ), ), ), - Consumer( - builder: (context, provider, _) => provider - .currentStore == - null - ? const SizedBox.shrink() - : const Text( - 'Existing tiles will appear in red', - style: TextStyle(fontStyle: FontStyle.italic), - ), - ), + const RegionShape(), + const CustomPolygonSnappingIndicator(), ], ), - ], - ), + ); + }, ), ), - Expanded( - child: SizedBox.expand( - child: ClipRRect( - borderRadius: BorderRadius.only( - topLeft: const Radius.circular(20), - topRight: MediaQuery.sizeOf(context).width <= 950 - ? const Radius.circular(20) - : Radius.zero, - ), - child: const MapView(), - ), - ), + SidePanel( + constraints: constraints, + pushToConfigureDownload: pushToConfigureDownload, ), + if (context.select( + (p) => p.regionSelectionMethod, + ) == + RegionSelectionMethod.useMapCenter && + !context.select( + (p) => p.customPolygonSnap, + )) + const Center(child: Crosshairs()), + UsageInstructions(constraints: constraints), ], ), ); diff --git a/example/lib/screens/main/pages/stores/components/store_tile.dart b/example/lib/screens/main/pages/stores/components/store_tile.dart index f2bc5356..30a729bf 100644 --- a/example/lib/screens/main/pages/stores/components/store_tile.dart +++ b/example/lib/screens/main/pages/stores/components/store_tile.dart @@ -3,7 +3,7 @@ import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; import 'package:fmtc_plus_sharing/fmtc_plus_sharing.dart'; import 'package:provider/provider.dart'; -import '../../../../../shared/misc/size_formatter.dart'; +import '../../../../../shared/misc/exts/size_formatter.dart'; import '../../../../../shared/state/general_provider.dart'; import '../../../../store_editor/store_editor.dart'; import 'stat_display.dart'; diff --git a/example/lib/screens/main/pages/stores/stores.dart b/example/lib/screens/main/pages/stores/stores.dart index 7e29accd..2c7b1fbf 100644 --- a/example/lib/screens/main/pages/stores/stores.dart +++ b/example/lib/screens/main/pages/stores/stores.dart @@ -72,7 +72,7 @@ class _StoresPageState extends State { ), ), floatingActionButton: SpeedDial( - icon: Icons.create_new_folder, + icon: Icons.add, activeIcon: Icons.close, children: [ SpeedDialChild( @@ -85,7 +85,7 @@ class _StoresPageState extends State { fullscreenDialog: true, ), ), - child: const Icon(Icons.add), + child: const Icon(Icons.add_box), label: 'Create New Store', ), SpeedDialChild( diff --git a/example/lib/shared/misc/exts/interleave.dart b/example/lib/shared/misc/exts/interleave.dart new file mode 100644 index 00000000..1b92b856 --- /dev/null +++ b/example/lib/shared/misc/exts/interleave.dart @@ -0,0 +1,8 @@ +extension IterableExt on Iterable { + Iterable interleave(E separator) sync* { + for (int i = 0; i < length; i++) { + yield elementAt(i); + if (i < length - 1) yield separator; + } + } +} diff --git a/example/lib/shared/misc/size_formatter.dart b/example/lib/shared/misc/exts/size_formatter.dart similarity index 100% rename from example/lib/shared/misc/size_formatter.dart rename to example/lib/shared/misc/exts/size_formatter.dart diff --git a/lib/src/bulk_download/thread.dart b/lib/src/bulk_download/thread.dart index 1350893e..1eeb5e37 100644 --- a/lib/src/bulk_download/thread.dart +++ b/lib/src/bulk_download/thread.dart @@ -84,10 +84,8 @@ Future _singleDownloadThread( // Fetch new tile from URL final http.Response response; try { - response = await httpClient.get( - Uri.parse(networkUrl), - headers: input.headers, - ); + response = + await httpClient.get(Uri.parse(networkUrl), headers: input.headers); } catch (e) { send( TileEvent._( From 1286676ff332ef1708f8cb08e40d3cec2deb4bfe Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Mon, 7 Aug 2023 19:33:17 +0100 Subject: [PATCH 064/168] Improved tests & testing tile server Improved `CustomPolygonRegion` tile generation performance Former-commit-id: 6d5a09191cc5ee7c861f103c2ec8593aa68efec4 [formerly 5ef2cb54f4affe2637fcc94c92d2532b866a7b06] Former-commit-id: 57dfbbe14054edb3e233eccc5a33d43fa160f64d --- lib/src/bulk_download/tile_loops/count.dart | 23 +++--- .../bulk_download/tile_loops/generate.dart | 23 +++--- test/fmtc_test.dart | 72 ++++++++++++++++--- tile_server/bin/tile_server.dart | 24 +++++-- tile_server/start_dev.bat | 2 +- 5 files changed, 107 insertions(+), 37 deletions(-) diff --git a/lib/src/bulk_download/tile_loops/count.dart b/lib/src/bulk_download/tile_loops/count.dart index cf97b1e8..ddd8e703 100644 --- a/lib/src/bulk_download/tile_loops/count.dart +++ b/lib/src/bulk_download/tile_loops/count.dart @@ -233,27 +233,26 @@ class TilesCounter { zoomLvl++) { final allOutlineTiles = >{}; - for (final triangle in Earcut.triangulateFromPoints( - customPolygonOutline.map(region.crs.projection.project), - ).map(customPolygonOutline.elementAt).slices(3)) { - final vertex1 = region.crs.latLngToPoint(triangle[0], zoomLvl).round(); - final vertex2 = region.crs.latLngToPoint(triangle[1], zoomLvl).round(); - final vertex3 = region.crs.latLngToPoint(triangle[2], zoomLvl).round(); + final pointsOutline = customPolygonOutline + .map((e) => region.crs.latLngToPoint(e, zoomLvl).floor()); + for (final triangle in Earcut.triangulateFromPoints( + pointsOutline.map((e) => e.toDoublePoint()), + ).map(pointsOutline.elementAt).slices(3)) { final outlineTiles = { ..._bresenhamsLGA( - Point(vertex1.x, vertex1.y), - Point(vertex2.x, vertex2.y), + Point(triangle[0].x, triangle[0].y), + Point(triangle[1].x, triangle[1].y), unscaleBy: region.options.tileSize, ), ..._bresenhamsLGA( - Point(vertex2.x, vertex2.y), - Point(vertex3.x, vertex3.y), + Point(triangle[1].x, triangle[1].y), + Point(triangle[2].x, triangle[2].y), unscaleBy: region.options.tileSize, ), ..._bresenhamsLGA( - Point(vertex3.x, vertex3.y), - Point(vertex1.x, vertex1.y), + Point(triangle[2].x, triangle[2].y), + Point(triangle[0].x, triangle[0].y), unscaleBy: region.options.tileSize, ), }; diff --git a/lib/src/bulk_download/tile_loops/generate.dart b/lib/src/bulk_download/tile_loops/generate.dart index b8f24891..a56c1b63 100644 --- a/lib/src/bulk_download/tile_loops/generate.dart +++ b/lib/src/bulk_download/tile_loops/generate.dart @@ -253,27 +253,26 @@ class TilesGenerator { zoomLvl++) { final allOutlineTiles = >{}; - for (final triangle in Earcut.triangulateFromPoints( - customPolygonOutline.map(region.crs.projection.project), - ).map(customPolygonOutline.elementAt).slices(3)) { - final vertex1 = region.crs.latLngToPoint(triangle[0], zoomLvl).round(); - final vertex2 = region.crs.latLngToPoint(triangle[1], zoomLvl).round(); - final vertex3 = region.crs.latLngToPoint(triangle[2], zoomLvl).round(); + final pointsOutline = customPolygonOutline + .map((e) => region.crs.latLngToPoint(e, zoomLvl).floor()); + for (final triangle in Earcut.triangulateFromPoints( + pointsOutline.map((e) => e.toDoublePoint()), + ).map(pointsOutline.elementAt).slices(3)) { final outlineTiles = { ..._bresenhamsLGA( - Point(vertex1.x, vertex1.y), - Point(vertex2.x, vertex2.y), + Point(triangle[0].x, triangle[0].y), + Point(triangle[1].x, triangle[1].y), unscaleBy: region.options.tileSize, ), ..._bresenhamsLGA( - Point(vertex2.x, vertex2.y), - Point(vertex3.x, vertex3.y), + Point(triangle[1].x, triangle[1].y), + Point(triangle[2].x, triangle[2].y), unscaleBy: region.options.tileSize, ), ..._bresenhamsLGA( - Point(vertex3.x, vertex3.y), - Point(vertex1.x, vertex1.y), + Point(triangle[2].x, triangle[2].y), + Point(triangle[0].x, triangle[0].y), unscaleBy: region.options.tileSize, ), }; diff --git a/test/fmtc_test.dart b/test/fmtc_test.dart index 172af88d..48eddec1 100644 --- a/test/fmtc_test.dart +++ b/test/fmtc_test.dart @@ -46,16 +46,16 @@ void main() { group('Rectangle Region', () { final rectRegion = RectangleRegion(LatLngBounds(const LatLng(-1, -1), const LatLng(1, 1))) - .toDownloadable(minZoom: 1, maxZoom: 15, options: TileLayer()); + .toDownloadable(minZoom: 1, maxZoom: 16, options: TileLayer()); test( 'Count By Counter', - () => expect(TilesCounter.rectangleTiles(rectRegion), 45240), + () => expect(TilesCounter.rectangleTiles(rectRegion), 179196), ); test( 'Count By Generator', - () async => expect(await countByGenerator(rectRegion), 45240), + () async => expect(await countByGenerator(rectRegion), 179196), ); test( @@ -170,7 +170,7 @@ void main() { }); group('Custom Polygon Region', () { - final customPolygonRegion = CustomPolygonRegion([ + final customPolygonRegion1 = CustomPolygonRegion([ const LatLng(51.45818683312154, -0.9674646220840917), const LatLng(51.55859639937614, -0.9185366064186982), const LatLng(51.476641197796724, -0.7494743298246318), @@ -179,14 +179,33 @@ void main() { const LatLng(51.38781341753136, -0.6779891095601829), ]).toDownloadable(minZoom: 1, maxZoom: 17, options: TileLayer()); + final customPolygonRegion2 = CustomPolygonRegion([ + const LatLng(-1, -1), + const LatLng(1, -1), + const LatLng(1, 1), + const LatLng(-1, 1), + ]).toDownloadable(minZoom: 1, maxZoom: 17, options: TileLayer()); + test( 'Count By Counter', - () => expect(TilesCounter.customPolygonTiles(customPolygonRegion), 15961), + () => + expect(TilesCounter.customPolygonTiles(customPolygonRegion1), 15962), ); test( 'Count By Generator', - () async => expect(await countByGenerator(customPolygonRegion), 15961), + () async => expect(await countByGenerator(customPolygonRegion1), 15962), + ); + + test( + 'Count By Counter (Compare to Rectangle Region)', + () => + expect(TilesCounter.customPolygonTiles(customPolygonRegion2), 712096), + ); + + test( + 'Count By Generator (Compare to Rectangle Region)', + () async => expect(await countByGenerator(customPolygonRegion2), 712096), ); test( @@ -196,7 +215,7 @@ void main() { 500, (index) { final clock = Stopwatch()..start(); - TilesCounter.customPolygonTiles(customPolygonRegion); + TilesCounter.customPolygonTiles(customPolygonRegion1); clock.stop(); return clock.elapsedMilliseconds; }, @@ -209,10 +228,47 @@ void main() { 'Generator Duration', () async { final clock = Stopwatch()..start(); - await countByGenerator(customPolygonRegion); + await countByGenerator(customPolygonRegion1); clock.stop(); print('${clock.elapsedMilliseconds / 1000} s'); }, ); }); } + +/* + Future> listGenerator( + DownloadableRegion region, + ) async { + final tileRecievePort = ReceivePort(); + final tileIsolate = await Isolate.spawn( + region.when( + rectangle: (_) => TilesGenerator.rectangleTiles, + circle: (_) => TilesGenerator.circleTiles, + line: (_) => TilesGenerator.lineTiles, + customPolygon: (_) => TilesGenerator.customPolygonTiles, + ), + (sendPort: tileRecievePort.sendPort, region: region), + onExit: tileRecievePort.sendPort, + debugName: '[FMTC] Tile Coords Generator Thread', + ); + late final SendPort requestTilePort; + + final Set<(int, int, int)> evts = {}; + + await for (final evt in tileRecievePort) { + if (evt == null) break; + if (evt is SendPort) { + requestTilePort = evt..send(null); + continue; + } + requestTilePort.send(null); + evts.add(evt); + } + + tileIsolate.kill(priority: Isolate.immediate); + tileRecievePort.close(); + + return evts; + } +*/ diff --git a/tile_server/bin/tile_server.dart b/tile_server/bin/tile_server.dart index 7e693337..6ee36d9a 100644 --- a/tile_server/bin/tile_server.dart +++ b/tile_server/bin/tile_server.dart @@ -47,7 +47,7 @@ Future main(List _) async { final requestTime = ctx.at; requestTimestamps.add(requestTime); console.write( - '[$requestTime] ${ctx.method} ${ctx.path}\t\t$servedSeaTiles sea tiles\t\t\t$lastRate tps - ${currentArtificialDelay.inMilliseconds} ms delay\n', + '[$requestTime] ${ctx.method} ${ctx.path}: ${ctx.response.statusCode}\t\t$servedSeaTiles sea tiles this session\t\t\t$lastRate tps - ${currentArtificialDelay.inMilliseconds} ms delay\n', ); }, ); @@ -106,15 +106,31 @@ Future main(List _) async { ..get('/favicon.ico', (_) => faviconReponse) // Serve tiles to all other requests ..get( - '*', + '/:z/:x/:y', (ctx) async { + // Get tile request segments + final z = ctx.pathParams.getInt('z', -1)!; + final x = ctx.pathParams.getInt('x', -1)!; + final y = ctx.pathParams.getInt('y', -1)!; + + // Check if tile request is in valid format + if (z == -1 || x == -1 || y == -1) { + return Response(statusCode: 400); + } + + // Check if tile request is inside valid range + final maxTileNum = sqrt(pow(4, z)) - 1; + if (x > maxTileNum || y > maxTileNum) { + return Response(statusCode: 400); + } + // Create artificial delay if applicable if (currentArtificialDelay > Duration.zero) { await Future.delayed(currentArtificialDelay); } // Serve either sea or land tile - if (ctx.path == '/17/0/0.png' || random.nextInt(10) == 0) { + if (ctx.path == '/17/0/0' || random.nextInt(10) == 0) { servedSeaTiles += 1; return seaTileResponse; } @@ -125,7 +141,7 @@ Future main(List _) async { // Output basic console instructions console ..setTextStyle(italic: true) - ..write('Now serving tiles to all requests to 127.0.0.1:8080\n\n') + ..write('Now serving tiles at 127.0.0.1:8080/{z}/{x}/{y}\n\n') ..write("Press 'q' to kill server\n") ..write( 'Press UP or DOWN to manipulate artificial delay by ${artificialDelayChangeAmount.inMilliseconds} ms\n\n', diff --git a/tile_server/start_dev.bat b/tile_server/start_dev.bat index 6a690c31..0d81c0ca 100644 --- a/tile_server/start_dev.bat +++ b/tile_server/start_dev.bat @@ -1,2 +1,2 @@ @echo off -start cmd /C "dart compile exe tile_server/bin/tile_server.dart && start /b tile_server/bin/tile_server.exe" \ No newline at end of file +start cmd /C "dart compile exe bin/tile_server.dart && start /b bin/tile_server.exe" \ No newline at end of file From e699eb277b75e553dde218a20dd619c8cd91b17c Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Tue, 8 Aug 2023 16:11:36 +0100 Subject: [PATCH 065/168] Empty commit Former-commit-id: 69836ce749131aafb22430200cb2735b2c7c8ef0 [formerly 1cd6d955d42e455285cca110911ff2b5a3042fa3] Former-commit-id: 5f11376257d55a0b9ee3f560e39014638c022374 From a76c358b37e884e62d87b204c19d927668a4f3e7 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Wed, 23 Aug 2023 09:38:32 +0100 Subject: [PATCH 066/168] Fixed lints Former-commit-id: 4191a99b384548853ad8ca5398928135c2650758 [formerly 8c78524f621092a873f22378e116ffc61e65f7ff] Former-commit-id: 31427ee570dcadf8a732b20f5d23862609171128 --- .../lib/screens/configure_download/configure_download.dart | 4 ++-- .../pages/region_selection/components/region_shape.dart | 2 +- example/lib/screens/store_editor/components/header.dart | 2 +- example/lib/screens/store_editor/store_editor.dart | 6 +++--- lib/src/regions/circle.dart | 2 +- lib/src/regions/custom_polygon.dart | 2 +- lib/src/regions/rectangle.dart | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/example/lib/screens/configure_download/configure_download.dart b/example/lib/screens/configure_download/configure_download.dart index 34115620..fdb9d725 100644 --- a/example/lib/screens/configure_download/configure_download.dart +++ b/example/lib/screens/configure_download/configure_download.dart @@ -99,7 +99,7 @@ class ConfigureDownloadPopup extends StatelessWidget { .skipExistingTiles = val, activeColor: Theme.of(context).colorScheme.primary, - ) + ), ], ), Row( @@ -115,7 +115,7 @@ class ConfigureDownloadPopup extends StatelessWidget { .skipSeaTiles = val, activeColor: Theme.of(context).colorScheme.primary, - ) + ), ], ), ], diff --git a/example/lib/screens/main/pages/region_selection/components/region_shape.dart b/example/lib/screens/main/pages/region_selection/components/region_shape.dart index 22abb9b7..434e09ce 100644 --- a/example/lib/screens/main/pages/region_selection/components/region_shape.dart +++ b/example/lib/screens/main/pages/region_selection/components/region_shape.dart @@ -23,7 +23,7 @@ class RegionShape extends StatelessWidget { Polyline( points: [ ...provider.coordinates, - provider.currentNewPointPos + provider.currentNewPointPos, ], borderColor: Colors.black, borderStrokeWidth: 2, diff --git a/example/lib/screens/store_editor/components/header.dart b/example/lib/screens/store_editor/components/header.dart index 9625b02c..0326f3af 100644 --- a/example/lib/screens/store_editor/components/header.dart +++ b/example/lib/screens/store_editor/components/header.dart @@ -101,6 +101,6 @@ AppBar buildHeader({ ); } }, - ) + ), ], ); diff --git a/example/lib/screens/store_editor/store_editor.dart b/example/lib/screens/store_editor/store_editor.dart index 80b9d4cd..bb003326 100644 --- a/example/lib/screens/store_editor/store_editor.dart +++ b/example/lib/screens/store_editor/store_editor.dart @@ -193,7 +193,7 @@ class _StoreEditorPopupState extends State { AutovalidateMode.onUserInteraction, keyboardType: TextInputType.number, inputFormatters: [ - FilteringTextInputFormatter.digitsOnly + FilteringTextInputFormatter.digitsOnly, ], initialValue: metadata.data!.isEmpty ? '14' @@ -223,7 +223,7 @@ class _StoreEditorPopupState extends State { AutovalidateMode.onUserInteraction, keyboardType: TextInputType.number, inputFormatters: [ - FilteringTextInputFormatter.digitsOnly + FilteringTextInputFormatter.digitsOnly, ], initialValue: metadata.data!.isEmpty ? '20000' @@ -251,7 +251,7 @@ class _StoreEditorPopupState extends State { items: [ 'cacheFirst', 'onlineFirst', - 'cacheOnly' + 'cacheOnly', ] .map>( (v) => DropdownMenuItem( diff --git a/lib/src/regions/circle.dart b/lib/src/regions/circle.dart index ca0f15b0..c34bf44c 100644 --- a/lib/src/regions/circle.dart +++ b/lib/src/regions/circle.dart @@ -65,7 +65,7 @@ class CircleRegion extends BaseRegion { label: label, labelStyle: labelStyle, labelPlacement: labelPlacement, - ) + ), ], ); diff --git a/lib/src/regions/custom_polygon.dart b/lib/src/regions/custom_polygon.dart index 1fe3ad7a..ba187932 100644 --- a/lib/src/regions/custom_polygon.dart +++ b/lib/src/regions/custom_polygon.dart @@ -62,7 +62,7 @@ class CustomPolygonRegion extends BaseRegion { label: label, labelStyle: labelStyle, labelPlacement: labelPlacement, - ) + ), ], ); diff --git a/lib/src/regions/rectangle.dart b/lib/src/regions/rectangle.dart index 06b8eba4..ae39dda1 100644 --- a/lib/src/regions/rectangle.dart +++ b/lib/src/regions/rectangle.dart @@ -64,7 +64,7 @@ class RectangleRegion extends BaseRegion { labelStyle: labelStyle, labelPlacement: labelPlacement, points: toOutline(), - ) + ), ], ); From eaa0fc3d4a65943352dedbe8717fb8b7066e188a Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Wed, 23 Aug 2023 09:44:58 +0100 Subject: [PATCH 067/168] Increased timeouts on tests Former-commit-id: 3342095157985e80bef1687dc8016430b28a7743 [formerly 4febb6dcc04d2dae739ee0135ebb743c02a05b48] Former-commit-id: 08b8f1bffe1a546057725e5c3bd783aea61e54e6 --- test/fmtc_test.dart | 403 +++++++++++++++++++++++--------------------- 1 file changed, 212 insertions(+), 191 deletions(-) diff --git a/test/fmtc_test.dart b/test/fmtc_test.dart index 48eddec1..cd054cf4 100644 --- a/test/fmtc_test.dart +++ b/test/fmtc_test.dart @@ -43,197 +43,218 @@ void main() { return evts; } - group('Rectangle Region', () { - final rectRegion = - RectangleRegion(LatLngBounds(const LatLng(-1, -1), const LatLng(1, 1))) - .toDownloadable(minZoom: 1, maxZoom: 16, options: TileLayer()); - - test( - 'Count By Counter', - () => expect(TilesCounter.rectangleTiles(rectRegion), 179196), - ); - - test( - 'Count By Generator', - () async => expect(await countByGenerator(rectRegion), 179196), - ); - - test( - 'Counter Duration', - () => print( - '${List.generate( - 2000, - (index) { - final clock = Stopwatch()..start(); - TilesCounter.rectangleTiles(rectRegion); - clock.stop(); - return clock.elapsedMilliseconds; - }, - growable: false, - ).average} ms', - ), - ); - - test( - 'Generator Duration', - () async { - final clock = Stopwatch()..start(); - await countByGenerator(rectRegion); - clock.stop(); - print('${clock.elapsedMilliseconds / 1000} s'); - }, - ); - }); - - group('Circle Region', () { - final circleRegion = CircleRegion(const LatLng(0, 0), 200) - .toDownloadable(minZoom: 1, maxZoom: 15, options: TileLayer()); - - test( - 'Count By Counter', - () => expect(TilesCounter.circleTiles(circleRegion), 61564), - ); - - test( - 'Count By Generator', - () async => expect(await countByGenerator(circleRegion), 61564), - ); - - test( - 'Counter Duration', - () => print( - '${List.generate( - 500, - (index) { - final clock = Stopwatch()..start(); - TilesCounter.circleTiles(circleRegion); - clock.stop(); - return clock.elapsedMilliseconds; - }, - growable: false, - ).average} ms', - ), - ); - - test( - 'Generator Duration', - () async { - final clock = Stopwatch()..start(); - await countByGenerator(circleRegion); - clock.stop(); - print('${clock.elapsedMilliseconds / 1000} s'); - }, - ); - }); - - group('Line Region', () { - final lineRegion = LineRegion( - [const LatLng(-1, -1), const LatLng(1, 1), const LatLng(1, -1)], - 5000, - ).toDownloadable(minZoom: 1, maxZoom: 15, options: TileLayer()); - - test( - 'Count By Counter', - () => expect(TilesCounter.lineTiles(lineRegion), 5040), - ); - - test( - 'Count By Generator', - () async => expect(await countByGenerator(lineRegion), 5040), - ); - - test( - 'Counter Duration', - () => print( - '${List.generate( - 300, - (index) { - final clock = Stopwatch()..start(); - TilesCounter.lineTiles(lineRegion); - clock.stop(); - return clock.elapsedMilliseconds; - }, - growable: false, - ).average} ms', - ), - ); - - test( - 'Generator Duration', - () async { - final clock = Stopwatch()..start(); - await countByGenerator(lineRegion); - clock.stop(); - print('${clock.elapsedMilliseconds / 1000} s'); - }, - ); - }); - - group('Custom Polygon Region', () { - final customPolygonRegion1 = CustomPolygonRegion([ - const LatLng(51.45818683312154, -0.9674646220840917), - const LatLng(51.55859639937614, -0.9185366064186982), - const LatLng(51.476641197796724, -0.7494743298246318), - const LatLng(51.56029831737391, -0.5322770067805148), - const LatLng(51.235701626195365, -0.5746290119276093), - const LatLng(51.38781341753136, -0.6779891095601829), - ]).toDownloadable(minZoom: 1, maxZoom: 17, options: TileLayer()); - - final customPolygonRegion2 = CustomPolygonRegion([ - const LatLng(-1, -1), - const LatLng(1, -1), - const LatLng(1, 1), - const LatLng(-1, 1), - ]).toDownloadable(minZoom: 1, maxZoom: 17, options: TileLayer()); - - test( - 'Count By Counter', - () => - expect(TilesCounter.customPolygonTiles(customPolygonRegion1), 15962), - ); - - test( - 'Count By Generator', - () async => expect(await countByGenerator(customPolygonRegion1), 15962), - ); - - test( - 'Count By Counter (Compare to Rectangle Region)', - () => - expect(TilesCounter.customPolygonTiles(customPolygonRegion2), 712096), - ); - - test( - 'Count By Generator (Compare to Rectangle Region)', - () async => expect(await countByGenerator(customPolygonRegion2), 712096), - ); - - test( - 'Counter Duration', - () => print( - '${List.generate( - 500, - (index) { - final clock = Stopwatch()..start(); - TilesCounter.customPolygonTiles(customPolygonRegion1); - clock.stop(); - return clock.elapsedMilliseconds; - }, - growable: false, - ).average} ms', - ), - ); - - test( - 'Generator Duration', - () async { - final clock = Stopwatch()..start(); - await countByGenerator(customPolygonRegion1); - clock.stop(); - print('${clock.elapsedMilliseconds / 1000} s'); - }, - ); - }); + group( + 'Rectangle Region', + () { + final rectRegion = RectangleRegion( + LatLngBounds(const LatLng(-1, -1), const LatLng(1, 1)), + ).toDownloadable(minZoom: 1, maxZoom: 16, options: TileLayer()); + + test( + 'Count By Counter', + () => expect(TilesCounter.rectangleTiles(rectRegion), 179196), + ); + + test( + 'Count By Generator', + () async => expect(await countByGenerator(rectRegion), 179196), + ); + + test( + 'Counter Duration', + () => print( + '${List.generate( + 2000, + (index) { + final clock = Stopwatch()..start(); + TilesCounter.rectangleTiles(rectRegion); + clock.stop(); + return clock.elapsedMilliseconds; + }, + growable: false, + ).average} ms', + ), + ); + + test( + 'Generator Duration', + () async { + final clock = Stopwatch()..start(); + await countByGenerator(rectRegion); + clock.stop(); + print('${clock.elapsedMilliseconds / 1000} s'); + }, + ); + }, + timeout: const Timeout(Duration(minutes: 1)), + ); + + group( + 'Circle Region', + () { + final circleRegion = CircleRegion(const LatLng(0, 0), 200) + .toDownloadable(minZoom: 1, maxZoom: 15, options: TileLayer()); + + test( + 'Count By Counter', + () => expect(TilesCounter.circleTiles(circleRegion), 61564), + ); + + test( + 'Count By Generator', + () async => expect(await countByGenerator(circleRegion), 61564), + ); + + test( + 'Counter Duration', + () => print( + '${List.generate( + 500, + (index) { + final clock = Stopwatch()..start(); + TilesCounter.circleTiles(circleRegion); + clock.stop(); + return clock.elapsedMilliseconds; + }, + growable: false, + ).average} ms', + ), + ); + + test( + 'Generator Duration', + () async { + final clock = Stopwatch()..start(); + await countByGenerator(circleRegion); + clock.stop(); + print('${clock.elapsedMilliseconds / 1000} s'); + }, + ); + }, + timeout: const Timeout(Duration(minutes: 1)), + ); + + group( + 'Line Region', + () { + final lineRegion = LineRegion( + [const LatLng(-1, -1), const LatLng(1, 1), const LatLng(1, -1)], + 5000, + ).toDownloadable(minZoom: 1, maxZoom: 15, options: TileLayer()); + + test( + 'Count By Counter', + () => expect(TilesCounter.lineTiles(lineRegion), 5040), + ); + + test( + 'Count By Generator', + () async => expect(await countByGenerator(lineRegion), 5040), + ); + + test( + 'Counter Duration', + () => print( + '${List.generate( + 300, + (index) { + final clock = Stopwatch()..start(); + TilesCounter.lineTiles(lineRegion); + clock.stop(); + return clock.elapsedMilliseconds; + }, + growable: false, + ).average} ms', + ), + ); + + test( + 'Generator Duration', + () async { + final clock = Stopwatch()..start(); + await countByGenerator(lineRegion); + clock.stop(); + print('${clock.elapsedMilliseconds / 1000} s'); + }, + ); + }, + timeout: const Timeout(Duration(minutes: 1)), + ); + + group( + 'Custom Polygon Region', + () { + final customPolygonRegion1 = CustomPolygonRegion([ + const LatLng(51.45818683312154, -0.9674646220840917), + const LatLng(51.55859639937614, -0.9185366064186982), + const LatLng(51.476641197796724, -0.7494743298246318), + const LatLng(51.56029831737391, -0.5322770067805148), + const LatLng(51.235701626195365, -0.5746290119276093), + const LatLng(51.38781341753136, -0.6779891095601829), + ]).toDownloadable(minZoom: 1, maxZoom: 17, options: TileLayer()); + + final customPolygonRegion2 = CustomPolygonRegion([ + const LatLng(-1, -1), + const LatLng(1, -1), + const LatLng(1, 1), + const LatLng(-1, 1), + ]).toDownloadable(minZoom: 1, maxZoom: 17, options: TileLayer()); + + test( + 'Count By Counter', + () => expect( + TilesCounter.customPolygonTiles(customPolygonRegion1), + 15962, + ), + ); + + test( + 'Count By Generator', + () async => expect(await countByGenerator(customPolygonRegion1), 15962), + ); + + test( + 'Count By Counter (Compare to Rectangle Region)', + () => expect( + TilesCounter.customPolygonTiles(customPolygonRegion2), + 712096, + ), + ); + + test( + 'Count By Generator (Compare to Rectangle Region)', + () async => + expect(await countByGenerator(customPolygonRegion2), 712096), + ); + + test( + 'Counter Duration', + () => print( + '${List.generate( + 500, + (index) { + final clock = Stopwatch()..start(); + TilesCounter.customPolygonTiles(customPolygonRegion1); + clock.stop(); + return clock.elapsedMilliseconds; + }, + growable: false, + ).average} ms', + ), + ); + + test( + 'Generator Duration', + () async { + final clock = Stopwatch()..start(); + await countByGenerator(customPolygonRegion1); + clock.stop(); + print('${clock.elapsedMilliseconds / 1000} s'); + }, + ); + }, + timeout: const Timeout(Duration(minutes: 1)), + ); } /* From 3f5edc9033c0b549902a25ecd62ae94437db6b13 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sun, 10 Sep 2023 15:47:45 +0100 Subject: [PATCH 068/168] Migrate to flutter_map v6.0.0-dev.3 Removed user location marker from map to improve performance Recreated example app's Windows config files Former-commit-id: 5d5a490ad12f41c63fe179d0af1160705fee7c1c [formerly c571c566ecb67f3d211e042e284030cbd29bd5fe] Former-commit-id: 2990c9e00ae2895c92a17c9ff6bcc7e5aa66f49c --- example/.metadata | 14 +++++++------- example/lib/screens/main/pages/map/map_view.dart | 8 ++++---- example/windows/CMakeLists.txt | 3 ++- example/windows/runner/flutter_window.cpp | 5 +++++ example/windows/runner/utils.cpp | 9 +++++---- example/windows/runner/win32_window.cpp | 2 +- example/windows/runner/win32_window.h | 2 +- lib/src/root/migrator.dart | 12 ++++++++++-- pubspec.yaml | 2 +- 9 files changed, 36 insertions(+), 21 deletions(-) diff --git a/example/.metadata b/example/.metadata index 05fece53..bead5eef 100644 --- a/example/.metadata +++ b/example/.metadata @@ -1,11 +1,11 @@ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # -# This file should be version controlled. +# This file should be version controlled and should not be manually edited. version: - revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff - channel: stable + revision: "2524052335ec76bb03e04ede244b071f1b86d190" + channel: "stable" project_type: app @@ -13,11 +13,11 @@ project_type: app migration: platforms: - platform: root - create_revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff - base_revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff + create_revision: 2524052335ec76bb03e04ede244b071f1b86d190 + base_revision: 2524052335ec76bb03e04ede244b071f1b86d190 - platform: windows - create_revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff - base_revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff + create_revision: 2524052335ec76bb03e04ede244b071f1b86d190 + base_revision: 2524052335ec76bb03e04ede244b071f1b86d190 # User provided section diff --git a/example/lib/screens/main/pages/map/map_view.dart b/example/lib/screens/main/pages/map/map_view.dart index d0feab31..1c28ba55 100644 --- a/example/lib/screens/main/pages/map/map_view.dart +++ b/example/lib/screens/main/pages/map/map_view.dart @@ -1,9 +1,9 @@ import 'dart:async'; -import 'dart:io'; +//import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map_location_marker/flutter_map_location_marker.dart'; +//import 'package:flutter_map_location_marker/flutter_map_location_marker.dart'; import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; import 'package:latlong2/latlong.dart'; import 'package:provider/provider.dart'; @@ -96,7 +96,7 @@ class MapPage extends StatelessWidget { ) : NetworkTileProvider(), ), - Consumer( + /*Consumer( builder: (context, mapProvider, _) => CurrentLocationLayer( followCurrentLocationStream: mapProvider.trackLocationStream, @@ -126,7 +126,7 @@ class MapPage extends StatelessWidget { markerDirection: MarkerDirection.heading, ), ), - ), + ),*/ ], ); }, diff --git a/example/windows/CMakeLists.txt b/example/windows/CMakeLists.txt index c0270746..c09389c5 100644 --- a/example/windows/CMakeLists.txt +++ b/example/windows/CMakeLists.txt @@ -8,7 +8,7 @@ set(BINARY_NAME "example") # Explicitly opt in to modern CMake behaviors to avoid warnings with recent # versions of CMake. -cmake_policy(SET CMP0063 NEW) +cmake_policy(VERSION 3.14...3.25) # Define build configuration option. get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) @@ -52,6 +52,7 @@ add_subdirectory(${FLUTTER_MANAGED_DIR}) # Application build; see runner/CMakeLists.txt. add_subdirectory("runner") + # Generated plugin build rules, which manage building the plugins and adding # them to the application. include(flutter/generated_plugins.cmake) diff --git a/example/windows/runner/flutter_window.cpp b/example/windows/runner/flutter_window.cpp index b25e363e..955ee303 100644 --- a/example/windows/runner/flutter_window.cpp +++ b/example/windows/runner/flutter_window.cpp @@ -31,6 +31,11 @@ bool FlutterWindow::OnCreate() { this->Show(); }); + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + return true; } diff --git a/example/windows/runner/utils.cpp b/example/windows/runner/utils.cpp index f5bf9fa0..b2b08734 100644 --- a/example/windows/runner/utils.cpp +++ b/example/windows/runner/utils.cpp @@ -47,16 +47,17 @@ std::string Utf8FromUtf16(const wchar_t* utf16_string) { } int target_length = ::WideCharToMultiByte( CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, - -1, nullptr, 0, nullptr, nullptr); + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); std::string utf8_string; - if (target_length == 0 || target_length > utf8_string.max_size()) { + if (target_length <= 0 || target_length > utf8_string.max_size()) { return utf8_string; } utf8_string.resize(target_length); int converted_length = ::WideCharToMultiByte( CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, - -1, utf8_string.data(), - target_length, nullptr, nullptr); + input_length, utf8_string.data(), target_length, nullptr, nullptr); if (converted_length == 0) { return std::string(); } diff --git a/example/windows/runner/win32_window.cpp b/example/windows/runner/win32_window.cpp index 041a3855..60608d0f 100644 --- a/example/windows/runner/win32_window.cpp +++ b/example/windows/runner/win32_window.cpp @@ -60,7 +60,7 @@ class WindowClassRegistrar { public: ~WindowClassRegistrar() = default; - // Returns the singleton registar instance. + // Returns the singleton registrar instance. static WindowClassRegistrar* GetInstance() { if (!instance_) { instance_ = new WindowClassRegistrar(); diff --git a/example/windows/runner/win32_window.h b/example/windows/runner/win32_window.h index c86632d8..e901dde6 100644 --- a/example/windows/runner/win32_window.h +++ b/example/windows/runner/win32_window.h @@ -77,7 +77,7 @@ class Win32Window { // OS callback called by message pump. Handles the WM_NCCREATE message which // is passed when the non-client area is being created and enables automatic // non-client DPI scaling so that the non-client area automatically - // responsponds to changes in DPI. All other messages are handled by + // responds to changes in DPI. All other messages are handled by // MessageHandler. static LRESULT CALLBACK WndProc(HWND const window, UINT const message, diff --git a/lib/src/root/migrator.dart b/lib/src/root/migrator.dart index ab9eb231..68a93fd0 100644 --- a/lib/src/root/migrator.dart +++ b/lib/src/root/migrator.dart @@ -150,8 +150,16 @@ class RootMigrator { } return DbTile( - url: - TileLayer().templateFunction(e[2], placeholderValues), + url: e[2].replaceAllMapped( + TileProvider.templatePlaceholderElement, + (match) { + final value = placeholderValues[match.group(1)!]; + if (value != null) return value; + throw ArgumentError( + 'Missing value for placeholder: {${match.group(1)}}', + ); + }, + ), bytes: await f.readAsBytes(), ); } diff --git a/pubspec.yaml b/pubspec.yaml index 89f8a3c2..f1360cc9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -31,7 +31,7 @@ dependencies: dart_earcut: ^1.0.1 flutter: sdk: flutter - flutter_map: ^6.0.0-dev.2 + flutter_map: 6.0.0-dev.3 http: ^1.1.0 isar: ^3.1.0+1 isar_flutter_libs: ^3.1.0+1 From 328c142ac409eb6f1f5ca1d23f25066a705834f6 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sat, 7 Oct 2023 22:47:17 +0100 Subject: [PATCH 069/168] Upgrade to FM v6 Former-commit-id: 9d28d3aa501884e8a688e606940b6c2021f7bf21 [formerly 00bb4ae738fc41f1ae3d839edd04677f7af94ccf] Former-commit-id: 33ff4a426e9bc7a6d98e927d7fbb48826aa7d2de --- example/lib/screens/main/main.dart | 35 ------------ .../lib/screens/main/pages/map/map_view.dart | 38 ++++--------- .../main/pages/map/state/map_provider.dart | 24 --------- .../custom_polygon_snapping_indicator.dart | 53 +++++++++---------- .../components/region_shape.dart | 1 - .../region_selection/region_selection.dart | 14 +---- .../shared/components/build_attribution.dart | 22 +++++--- example/pubspec.yaml | 5 +- .../flutter/generated_plugin_registrant.cc | 3 -- .../windows/flutter/generated_plugins.cmake | 1 - lib/flutter_map_tile_caching.dart | 2 - lib/src/providers/image_provider.dart | 2 +- pubspec.yaml | 7 ++- 13 files changed, 59 insertions(+), 148 deletions(-) diff --git a/example/lib/screens/main/main.dart b/example/lib/screens/main/main.dart index b349b8b3..ab14b926 100644 --- a/example/lib/screens/main/main.dart +++ b/example/lib/screens/main/main.dart @@ -6,7 +6,6 @@ import 'package:provider/provider.dart'; import 'pages/downloading/downloading.dart'; import 'pages/downloading/state/downloading_provider.dart'; import 'pages/map/map_view.dart'; -import 'pages/map/state/map_provider.dart'; import 'pages/recovery/recovery.dart'; import 'pages/region_selection/region_selection.dart'; import 'pages/stores/stores.dart'; @@ -124,40 +123,6 @@ class _MainScreenState extends State { NavigationDestinationLabelBehavior.onlyShowSelected, height: 70, ), - floatingActionButton: _currentPageIndex != 0 - ? null - : Consumer( - builder: (context, mapProvider, _) => FloatingActionButton( - onPressed: () { - switch (mapProvider.followState) { - case UserLocationFollowState.off: - mapProvider.followState = - UserLocationFollowState.standard; - mapProvider.trackLocation(navigation: false); - mapProvider.mapController.rotate(0); - break; - case UserLocationFollowState.standard: - mapProvider.followState = - UserLocationFollowState.navigation; - mapProvider.trackLocation(navigation: true); - mapProvider.trackHeading(); - break; - case UserLocationFollowState.navigation: - mapProvider.followState = UserLocationFollowState.off; - mapProvider.mapController.rotate(0); - break; - } - setState(() {}); - }, - child: Icon( - switch (mapProvider.followState) { - UserLocationFollowState.off => Icons.gps_off, - UserLocationFollowState.standard => Icons.gps_fixed, - UserLocationFollowState.navigation => Icons.navigation, - }, - ), - ), - ), body: Row( children: [ if (MediaQuery.sizeOf(context).width > 950) diff --git a/example/lib/screens/main/pages/map/map_view.dart b/example/lib/screens/main/pages/map/map_view.dart index 1c28ba55..7c7fd824 100644 --- a/example/lib/screens/main/pages/map/map_view.dart +++ b/example/lib/screens/main/pages/map/map_view.dart @@ -1,9 +1,7 @@ import 'dart:async'; -//import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; -//import 'package:flutter_map_location_marker/flutter_map_location_marker.dart'; import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; import 'package:latlong2/latlong.dart'; import 'package:provider/provider.dart'; @@ -13,12 +11,6 @@ import '../../../../shared/components/loading_indicator.dart'; import '../../../../shared/state/general_provider.dart'; import 'state/map_provider.dart'; -enum UserLocationFollowState { - off, - standard, - navigation, -} - class MapPage extends StatelessWidget { const MapPage({super.key}); @@ -42,36 +34,20 @@ class MapPage extends StatelessWidget { return FlutterMap( mapController: Provider.of(context).mapController, - options: MapOptions( - initialCenter: const LatLng(51.509364, -0.128928), + options: const MapOptions( + initialCenter: LatLng(51.509364, -0.128928), initialZoom: 16, - maxZoom: 22, - cameraConstraint: CameraConstraint.contain( - bounds: LatLngBounds.fromPoints([ - const LatLng(-90, 180), - const LatLng(90, 180), - const LatLng(90, -180), - const LatLng(-90, -180), - ]), - ), - interactionOptions: const InteractionOptions( - // flags: InteractiveFlag.all & ~InteractiveFlag.rotate, + interactionOptions: InteractionOptions( + flags: InteractiveFlag.all & ~InteractiveFlag.rotate, scrollWheelVelocity: 0.002, ), keepAlive: true, - onPointerDown: (_, __) => - Provider.of(context, listen: false) - .followState = UserLocationFollowState.off, - ), - nonRotatedChildren: buildStdAttribution( - urlTemplate, - alignment: AttributionAlignment.bottomLeft, + backgroundColor: Color(0xFFaad3df), ), children: [ TileLayer( urlTemplate: urlTemplate, userAgentPackageName: 'dev.jaffaketchup.fmtc.demo', - backgroundColor: const Color(0xFFaad3df), maxNativeZoom: 20, panBuffer: 5, tileProvider: provider.currentStore != null @@ -96,6 +72,10 @@ class MapPage extends StatelessWidget { ) : NetworkTileProvider(), ), + StandardAttribution( + urlTemplate: urlTemplate, + alignment: AttributionAlignment.bottomLeft, + ), /*Consumer( builder: (context, mapProvider, _) => CurrentLocationLayer( followCurrentLocationStream: diff --git a/example/lib/screens/main/pages/map/state/map_provider.dart b/example/lib/screens/main/pages/map/state/map_provider.dart index 45c28499..a76cdb2a 100644 --- a/example/lib/screens/main/pages/map/state/map_provider.dart +++ b/example/lib/screens/main/pages/map/state/map_provider.dart @@ -1,35 +1,11 @@ -import 'dart:async'; - import 'package:flutter/foundation.dart'; import 'package:flutter_map/flutter_map.dart'; -import '../map_view.dart'; - class MapProvider extends ChangeNotifier { - UserLocationFollowState _followState = UserLocationFollowState.standard; - UserLocationFollowState get followState => _followState; - set followState(UserLocationFollowState newState) { - _followState = newState; - notifyListeners(); - } - MapController _mapController = MapController(); MapController get mapController => _mapController; set mapController(MapController newController) { _mapController = newController; notifyListeners(); } - - // ignore: close_sinks - final _trackLocationStreamController = StreamController()..add(16); - late final trackLocationStream = - _trackLocationStreamController.stream.asBroadcastStream(); - void trackLocation({required bool navigation}) => - _trackLocationStreamController.add(navigation ? 18 : 16); - - // ignore: close_sinks - final _trackHeadingStreamController = StreamController()..add(null); - late final trackHeadingStream = - _trackHeadingStreamController.stream.asBroadcastStream(); - void trackHeading() => _trackHeadingStreamController.add(null); } diff --git a/example/lib/screens/main/pages/region_selection/components/custom_polygon_snapping_indicator.dart b/example/lib/screens/main/pages/region_selection/components/custom_polygon_snapping_indicator.dart index 69420469..cffb227a 100644 --- a/example/lib/screens/main/pages/region_selection/components/custom_polygon_snapping_indicator.dart +++ b/example/lib/screens/main/pages/region_selection/components/custom_polygon_snapping_indicator.dart @@ -11,34 +11,31 @@ class CustomPolygonSnappingIndicator extends StatelessWidget { }); @override - Widget build(BuildContext context) => MarkerLayer( - markers: [ - if (context - .select>( - (p) => p.coordinates, - ) - .isNotEmpty && - context.select( - (p) => p.customPolygonSnap, - )) - Marker( - height: 25, - width: 25, - point: context - .select>( - (p) => p.coordinates, - ) - .first, - builder: (context) => DecoratedBox( - decoration: BoxDecoration( - color: Colors.green, - borderRadius: BorderRadius.circular(1028), - ), - child: const Center( - child: Icon(Icons.auto_awesome, size: 15), - ), + Widget build(BuildContext context) { + final coords = context + .select>((p) => p.coordinates); + + return MarkerLayer( + markers: [ + if (coords.isNotEmpty && + context.select( + (p) => p.customPolygonSnap, + )) + Marker( + height: 25, + width: 25, + point: coords.first, + child: DecoratedBox( + decoration: BoxDecoration( + color: Colors.green, + borderRadius: BorderRadius.circular(1028), + ), + child: const Center( + child: Icon(Icons.auto_awesome, size: 15), ), ), - ], - ); + ), + ], + ); + } } diff --git a/example/lib/screens/main/pages/region_selection/components/region_shape.dart b/example/lib/screens/main/pages/region_selection/components/region_shape.dart index 434e09ce..8056f045 100644 --- a/example/lib/screens/main/pages/region_selection/components/region_shape.dart +++ b/example/lib/screens/main/pages/region_selection/components/region_shape.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/plugin_api.dart'; import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; import 'package:latlong2/latlong.dart'; import 'package:provider/provider.dart'; diff --git a/example/lib/screens/main/pages/region_selection/region_selection.dart b/example/lib/screens/main/pages/region_selection/region_selection.dart index 615a3202..d961ae18 100644 --- a/example/lib/screens/main/pages/region_selection/region_selection.dart +++ b/example/lib/screens/main/pages/region_selection/region_selection.dart @@ -3,7 +3,6 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/plugin_api.dart'; import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; import 'package:latlong2/latlong.dart'; import 'package:provider/provider.dart'; @@ -34,15 +33,6 @@ class _RegionSelectionPageState extends State { late final mapOptions = MapOptions( initialCenter: const LatLng(51.509364, -0.128928), initialZoom: 11, - maxZoom: 22, - cameraConstraint: CameraConstraint.contain( - bounds: LatLngBounds.fromPoints([ - const LatLng(-90, 180), - const LatLng(90, 180), - const LatLng(90, -180), - const LatLng(-90, -180), - ]), - ), interactionOptions: const InteractionOptions( flags: InteractiveFlag.all & ~InteractiveFlag.rotate & @@ -50,6 +40,7 @@ class _RegionSelectionPageState extends State { scrollWheelVelocity: 0.002, ), keepAlive: true, + backgroundColor: const Color(0xFFaad3df), onTap: (_, __) { final provider = context.read(); @@ -259,13 +250,11 @@ class _RegionSelectionPageState extends State { child: FlutterMap( mapController: mapController, options: mapOptions, - nonRotatedChildren: buildStdAttribution(urlTemplate), children: [ TileLayer( urlTemplate: urlTemplate, userAgentPackageName: 'dev.jaffaketchup.fmtc.demo', maxNativeZoom: 20, - backgroundColor: const Color(0xFFaad3df), tileBuilder: (context, widget, tile) => FutureBuilder( future: currentStore == null @@ -291,6 +280,7 @@ class _RegionSelectionPageState extends State { ), const RegionShape(), const CustomPolygonSnappingIndicator(), + StandardAttribution(urlTemplate: urlTemplate), ], ), ); diff --git a/example/lib/shared/components/build_attribution.dart b/example/lib/shared/components/build_attribution.dart index 3837fe69..02f3ef1f 100644 --- a/example/lib/shared/components/build_attribution.dart +++ b/example/lib/shared/components/build_attribution.dart @@ -1,12 +1,18 @@ import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; -List buildStdAttribution( - String urlTemplate, { - AttributionAlignment alignment = AttributionAlignment.bottomRight, -}) => - [ - RichAttributionWidget( +class StandardAttribution extends StatelessWidget { + const StandardAttribution({ + super.key, + required this.urlTemplate, + this.alignment = AttributionAlignment.bottomRight, + }); + + final String urlTemplate; + final AttributionAlignment alignment; + + @override + Widget build(BuildContext context) => RichAttributionWidget( alignment: alignment, popupInitialDisplayDuration: const Duration(seconds: 3), popupBorderRadius: alignment == AttributionAlignment.bottomRight @@ -29,5 +35,5 @@ List buildStdAttribution( tooltip: 'flutter_map_tile_caching', ), ], - ), - ]; + ); +} diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 59741514..09850e43 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -19,8 +19,9 @@ dependencies: flutter: sdk: flutter flutter_foreground_task: ^6.0.0+1 - flutter_map: ^6.0.0-dev.2 - flutter_map_location_marker: ^8.0.0-dev.1 + flutter_map: + git: + url: https://github.com/fleaflet/flutter_map.git flutter_map_tile_caching: flutter_speed_dial: ^7.0.0 fmtc_plus_sharing: ^8.0.0 diff --git a/example/windows/flutter/generated_plugin_registrant.cc b/example/windows/flutter/generated_plugin_registrant.cc index f84e82cf..6d420e2b 100644 --- a/example/windows/flutter/generated_plugin_registrant.cc +++ b/example/windows/flutter/generated_plugin_registrant.cc @@ -6,14 +6,11 @@ #include "generated_plugin_registrant.h" -#include #include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { - GeolocatorWindowsRegisterWithRegistrar( - registry->GetRegistrarForPlugin("GeolocatorWindows")); IsarFlutterLibsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("IsarFlutterLibsPlugin")); SharePlusWindowsPluginCApiRegisterWithRegistrar( diff --git a/example/windows/flutter/generated_plugins.cmake b/example/windows/flutter/generated_plugins.cmake index 1f658706..759d9b5d 100644 --- a/example/windows/flutter/generated_plugins.cmake +++ b/example/windows/flutter/generated_plugins.cmake @@ -3,7 +3,6 @@ # list(APPEND FLUTTER_PLUGIN_LIST - geolocator_windows isar_flutter_libs share_plus url_launcher_windows diff --git a/lib/flutter_map_tile_caching.dart b/lib/flutter_map_tile_caching.dart index 94c4ce05..342bd60f 100644 --- a/lib/flutter_map_tile_caching.dart +++ b/lib/flutter_map_tile_caching.dart @@ -23,7 +23,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/plugin_api.dart'; import 'package:http/http.dart' as http; import 'package:http/io_client.dart'; import 'package:isar/isar.dart'; @@ -35,7 +34,6 @@ import 'package:stream_transform/stream_transform.dart'; import 'package:watcher/watcher.dart'; import 'src/bulk_download/instance.dart'; - import 'src/bulk_download/rate_limited_stream.dart'; import 'src/bulk_download/tile_loops/shared.dart'; import 'src/db/defs/metadata.dart'; diff --git a/lib/src/providers/image_provider.dart b/lib/src/providers/image_provider.dart index 9f6300cf..0a43f81c 100644 --- a/lib/src/providers/image_provider.dart +++ b/lib/src/providers/image_provider.dart @@ -8,7 +8,7 @@ import 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; -import 'package:flutter_map/plugin_api.dart'; +import 'package:flutter_map/flutter_map.dart'; import 'package:http/http.dart'; import 'package:isar/isar.dart'; import 'package:queue/queue.dart'; diff --git a/pubspec.yaml b/pubspec.yaml index f1360cc9..b49bb6e6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,8 @@ name: flutter_map_tile_caching description: Plugin for 'flutter_map' providing advanced caching functionality, with ability to download map regions for offline use. -version: 9.0.0-dev.4 +version: 9.0.0-dev.5 + repository: https://github.com/JaffaKetchup/flutter_map_tile_caching issue_tracker: https://github.com/JaffaKetchup/flutter_map_tile_caching/issues documentation: https://fmtc.jaffaketchup.dev @@ -31,7 +32,9 @@ dependencies: dart_earcut: ^1.0.1 flutter: sdk: flutter - flutter_map: 6.0.0-dev.3 + flutter_map: + git: + url: https://github.com/fleaflet/flutter_map.git http: ^1.1.0 isar: ^3.1.0+1 isar_flutter_libs: ^3.1.0+1 From 9d0f60716e75ccd21da0720678c67e9865b59047 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Mon, 9 Oct 2023 19:42:44 +0100 Subject: [PATCH 070/168] Updated pubspec Former-commit-id: 114f3e81177ca3458ab604c5658da1410f2fccac [formerly c3aaab7bd9e88a8be3d615f6e1cbf66139745ff3] Former-commit-id: b8e46c5a5d530c9e83d3efa93e52ff06ce2f10c2 --- example/pubspec.yaml | 4 +--- pubspec.yaml | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 09850e43..6c77bf99 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -19,9 +19,7 @@ dependencies: flutter: sdk: flutter flutter_foreground_task: ^6.0.0+1 - flutter_map: - git: - url: https://github.com/fleaflet/flutter_map.git + flutter_map: ^6.0.0 flutter_map_tile_caching: flutter_speed_dial: ^7.0.0 fmtc_plus_sharing: ^8.0.0 diff --git a/pubspec.yaml b/pubspec.yaml index b49bb6e6..40c7f2d8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -32,9 +32,7 @@ dependencies: dart_earcut: ^1.0.1 flutter: sdk: flutter - flutter_map: - git: - url: https://github.com/fleaflet/flutter_map.git + flutter_map: ^6.0.0 http: ^1.1.0 isar: ^3.1.0+1 isar_flutter_libs: ^3.1.0+1 From d43c3483b361d2af5991d7b9356eb27560585cf2 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sat, 21 Oct 2023 23:14:34 +0100 Subject: [PATCH 071/168] Added download follow/tiles preview functionality to example app Added tile coordinates to `TileEvent` Former-commit-id: 0ea7d247f053f2cdb51b88662e5dfecb0bd36c6c [formerly 2d898a65a285a5ca141255891e5d47ca2e6982e8] Former-commit-id: 25580b2c5f44c17853af206959fad83cd49ff2ad --- example/lib/screens/main/main.dart | 29 +++-- .../components/download_layout.dart | 3 + .../components/main_statistics.dart | 50 ++++++++ .../main/pages/downloading/downloading.dart | 5 +- .../state/downloading_provider.dart | 32 +++++ .../map/components/bubble_arrow_painter.dart | 38 ++++++ .../download_progress_indicator.dart | 99 +++++++++++++++ .../map/components/empty_tile_provider.dart | 11 ++ .../main/pages/map/components/map_view.dart | 106 ++++++++++++++++ .../quit_tiles_preview_indicator.dart | 72 +++++++++++ .../components/side_indicator_painter.dart | 52 ++++++++ .../lib/screens/main/pages/map/map_page.dart | 48 ++++++++ .../lib/screens/main/pages/map/map_view.dart | 115 ------------------ .../main/pages/map/state/map_provider.dart | 19 ++- example/pubspec.yaml | 4 + lib/src/bulk_download/thread.dart | 19 ++- lib/src/bulk_download/tile_event.dart | 9 ++ tile_server/static/source/images/land.png | Bin 18689 -> 18627 bytes 18 files changed, 581 insertions(+), 130 deletions(-) create mode 100644 example/lib/screens/main/pages/map/components/bubble_arrow_painter.dart create mode 100644 example/lib/screens/main/pages/map/components/download_progress_indicator.dart create mode 100644 example/lib/screens/main/pages/map/components/empty_tile_provider.dart create mode 100644 example/lib/screens/main/pages/map/components/map_view.dart create mode 100644 example/lib/screens/main/pages/map/components/quit_tiles_preview_indicator.dart create mode 100644 example/lib/screens/main/pages/map/components/side_indicator_painter.dart create mode 100644 example/lib/screens/main/pages/map/map_page.dart delete mode 100644 example/lib/screens/main/pages/map/map_view.dart diff --git a/example/lib/screens/main/main.dart b/example/lib/screens/main/main.dart index ab14b926..7ad63f69 100644 --- a/example/lib/screens/main/main.dart +++ b/example/lib/screens/main/main.dart @@ -5,7 +5,7 @@ import 'package:provider/provider.dart'; import 'pages/downloading/downloading.dart'; import 'pages/downloading/state/downloading_provider.dart'; -import 'pages/map/map_view.dart'; +import 'pages/map/map_page.dart'; import 'pages/recovery/recovery.dart'; import 'pages/region_selection/region_selection.dart'; import 'pages/stores/stores.dart'; @@ -72,17 +72,31 @@ class _MainScreenState extends State { selector: (context, provider) => provider.downloadProgress, builder: (context, downloadProgress, _) => downloadProgress == null ? const RegionSelectionPage() - : const DownloadingPage(), + : DownloadingPage( + moveToMapPage: () => + _onDestinationSelected(0, cancelTilesPreview: false), + ), ), RecoveryPage(moveToDownloadPage: () => _onDestinationSelected(2)), ]; - void _onDestinationSelected(int index) { + void _onDestinationSelected(int index, {bool cancelTilesPreview = true}) { setState(() => _currentPageIndex = index); - _pageController.animateToPage( + _pageController + .animateToPage( _currentPageIndex, duration: const Duration(milliseconds: 250), curve: Curves.easeInOut, + ) + .then( + (_) { + if (cancelTilesPreview) { + final dp = context.read(); + dp.tilesPreviewStreamSub + ?.cancel() + .then((_) => dp.tilesPreviewStreamSub = null); + } + }, ); } @@ -114,8 +128,6 @@ class _MainScreenState extends State { bottomNavigationBar: MediaQuery.sizeOf(context).width > 950 ? null : NavigationBar( - backgroundColor: - Theme.of(context).navigationBarTheme.backgroundColor, onDestinationSelected: _onDestinationSelected, selectedIndex: _currentPageIndex, destinations: _destinations, @@ -137,7 +149,10 @@ class _MainScreenState extends State { label: Text(d.label), icon: d.icon, selectedIcon: d.selectedIcon, - padding: const EdgeInsets.symmetric(vertical: 6), + padding: const EdgeInsets.symmetric( + vertical: 6, + horizontal: 3, + ), ), ) .toList(), diff --git a/example/lib/screens/main/pages/downloading/components/download_layout.dart b/example/lib/screens/main/pages/downloading/components/download_layout.dart index 5cf9b445..54de03f4 100644 --- a/example/lib/screens/main/pages/downloading/components/download_layout.dart +++ b/example/lib/screens/main/pages/downloading/components/download_layout.dart @@ -14,10 +14,12 @@ class DownloadLayout extends StatelessWidget { super.key, required this.storeDirectory, required this.download, + required this.moveToMapPage, }); final StoreDirectory storeDirectory; final DownloadProgress download; + final void Function() moveToMapPage; @override Widget build(BuildContext context) => Column( @@ -45,6 +47,7 @@ class DownloadLayout extends StatelessWidget { MainStatistics( download: download, storeDirectory: storeDirectory, + moveToMapPage: moveToMapPage, ), const SizedBox(width: 32), const VerticalDivider(), diff --git a/example/lib/screens/main/pages/downloading/components/main_statistics.dart b/example/lib/screens/main/pages/downloading/components/main_statistics.dart index 56cee139..54b2dfa5 100644 --- a/example/lib/screens/main/pages/downloading/components/main_statistics.dart +++ b/example/lib/screens/main/pages/downloading/components/main_statistics.dart @@ -1,19 +1,26 @@ import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; import 'package:provider/provider.dart'; +import '../../map/state/map_provider.dart'; import '../state/downloading_provider.dart'; import 'stat_display.dart'; +const _tileSize = 256; +const _offset = Offset(-(_tileSize / 2), -(_tileSize / 2)); + class MainStatistics extends StatefulWidget { const MainStatistics({ super.key, required this.download, required this.storeDirectory, + required this.moveToMapPage, }); final DownloadProgress download; final StoreDirectory storeDirectory; + final void Function() moveToMapPage; @override State createState() => _MainStatisticsState(); @@ -85,6 +92,49 @@ class _MainStatisticsState extends State { child: Row( mainAxisSize: MainAxisSize.min, children: [ + IconButton.filled( + onPressed: () { + final mp = context.read(); + + final dp = context.read(); + + dp + ..tilesPreviewStreamSub = + dp.downloadProgress?.listen((prog) { + final lte = prog.latestTileEvent; + if (!lte.isRepeat) { + if (dp.tilesPreview.isNotEmpty && + lte.coordinates.z != + dp.tilesPreview.keys.first.z) { + dp.clearTilesPreview(); + } + dp.addTilePreview(lte.coordinates, lte.tileImage); + } + + final zoom = lte.coordinates.z.toDouble(); + + mp.animateTo( + dest: mp.mapController.camera.unproject( + lte.coordinates.toIntPoint() * _tileSize, + zoom, + ), + zoom: zoom, + offset: _offset, + ); + }) + ..showQuitTilesPreviewIndicator = true; + + Future.delayed( + const Duration(seconds: 3), + () => dp.showQuitTilesPreviewIndicator = false, + ); + + widget.moveToMapPage(); + }, + icon: const Icon(Icons.visibility), + tooltip: 'Follow Download On Map', + ), + const SizedBox(width: 24), IconButton.outlined( onPressed: () async { if (widget.storeDirectory.download.isPaused()) { diff --git a/example/lib/screens/main/pages/downloading/downloading.dart b/example/lib/screens/main/pages/downloading/downloading.dart index 37d8ddd3..176daad0 100644 --- a/example/lib/screens/main/pages/downloading/downloading.dart +++ b/example/lib/screens/main/pages/downloading/downloading.dart @@ -10,7 +10,9 @@ import 'components/download_layout.dart'; import 'state/downloading_provider.dart'; class DownloadingPage extends StatefulWidget { - const DownloadingPage({super.key}); + const DownloadingPage({super.key, required this.moveToMapPage}); + + final void Function() moveToMapPage; @override State createState() => _DownloadingPageState(); @@ -112,6 +114,7 @@ class _DownloadingPageState extends State (provider) => provider.selectedStore, )!, download: snapshot.data!, + moveToMapPage: widget.moveToMapPage, ); }, ), diff --git a/example/lib/screens/main/pages/downloading/state/downloading_provider.dart b/example/lib/screens/main/pages/downloading/state/downloading_provider.dart index 44937193..32009d60 100644 --- a/example/lib/screens/main/pages/downloading/state/downloading_provider.dart +++ b/example/lib/screens/main/pages/downloading/state/downloading_provider.dart @@ -1,4 +1,7 @@ +import 'dart:async'; + import 'package:flutter/foundation.dart'; +import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; import '../../../../../shared/misc/circular_buffer.dart'; @@ -56,6 +59,35 @@ class DownloadingProvider extends ChangeNotifier { notifyListeners(); } + bool _showQuitTilesPreviewIndicator = false; + bool get showQuitTilesPreviewIndicator => _showQuitTilesPreviewIndicator; + set showQuitTilesPreviewIndicator(bool newBool) { + _showQuitTilesPreviewIndicator = newBool; + notifyListeners(); + } + + StreamSubscription? _tilesPreviewStreamSub; + StreamSubscription? get tilesPreviewStreamSub => + _tilesPreviewStreamSub; + set tilesPreviewStreamSub( + StreamSubscription? newStreamSub, + ) { + _tilesPreviewStreamSub = newStreamSub; + notifyListeners(); + } + + final _tilesPreview = {}; + Map get tilesPreview => _tilesPreview; + void addTilePreview(TileCoordinates coords, Uint8List? image) { + _tilesPreview[coords] = image; + notifyListeners(); + } + + void clearTilesPreview() { + _tilesPreview.clear(); + notifyListeners(); + } + final List _failedTiles = []; List get failedTiles => _failedTiles; void addFailedTile(TileEvent e) => _failedTiles.add(e); diff --git a/example/lib/screens/main/pages/map/components/bubble_arrow_painter.dart b/example/lib/screens/main/pages/map/components/bubble_arrow_painter.dart new file mode 100644 index 00000000..0491b3c7 --- /dev/null +++ b/example/lib/screens/main/pages/map/components/bubble_arrow_painter.dart @@ -0,0 +1,38 @@ +import 'package:flutter/widgets.dart'; + +class BubbleArrowIndicator extends CustomPainter { + const BubbleArrowIndicator({ + this.borderRadius = BorderRadius.zero, + this.triangleSize = const Size(25, 10), + this.color, + }); + + final BorderRadius borderRadius; + final Size triangleSize; + final Color? color; + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = color ?? const Color(0xFF000000) + ..strokeCap = StrokeCap.round + ..style = PaintingStyle.fill; + + canvas + ..drawPath( + Path() + ..moveTo(size.width / 2 - triangleSize.width / 2, size.height) + ..lineTo(size.width / 2, triangleSize.height + size.height) + ..lineTo(size.width / 2 + triangleSize.width / 2, size.height) + ..lineTo(size.width / 2 - triangleSize.width / 2, size.height), + paint, + ) + ..drawRRect( + borderRadius.toRRect(Rect.fromLTRB(0, 0, size.width, size.height)), + paint, + ); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} diff --git a/example/lib/screens/main/pages/map/components/download_progress_indicator.dart b/example/lib/screens/main/pages/map/components/download_progress_indicator.dart new file mode 100644 index 00000000..b5fba889 --- /dev/null +++ b/example/lib/screens/main/pages/map/components/download_progress_indicator.dart @@ -0,0 +1,99 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; +import 'package:provider/provider.dart'; + +import '../../downloading/state/downloading_provider.dart'; +import 'bubble_arrow_painter.dart'; +import 'side_indicator_painter.dart'; + +class DownloadProgressIndicator extends StatelessWidget { + const DownloadProgressIndicator({ + super.key, + required this.constraints, + }); + + final BoxConstraints constraints; + + @override + Widget build(BuildContext context) { + final isNarrow = MediaQuery.sizeOf(context).width <= 950; + + return Selector?>( + selector: (context, provider) => provider.tilesPreviewStreamSub, + builder: (context, tpss, child) => isNarrow + ? AnimatedPositioned( + duration: const Duration(milliseconds: 1200), + curve: Curves.elasticOut, + bottom: tpss != null ? 20 : -55, + left: constraints.maxWidth / 2 + constraints.maxWidth / 8 - 85, + height: 50, + width: 170, + child: CustomPaint( + painter: BubbleArrowIndicator( + borderRadius: BorderRadius.circular(12), + color: Theme.of(context).colorScheme.background, + ), + child: child, + ), + ) + : AnimatedPositioned( + duration: const Duration(milliseconds: 1200), + curve: Curves.elasticOut, + top: constraints.maxHeight / 2 + 12, + left: tpss != null ? 8 : -200, + height: 50, + width: 180, + child: CustomPaint( + painter: SideIndicatorPainter( + startRadius: const Radius.circular(8), + endRadius: const Radius.circular(25), + color: Theme.of(context).colorScheme.background, + ), + child: Padding( + padding: const EdgeInsets.only(left: 20), + child: child, + ), + ), + ), + child: StreamBuilder( + stream: context.select?>( + (provider) => provider.downloadProgress, + ), + builder: (context, snapshot) { + if (snapshot.data == null) { + return const Center( + child: SizedBox.square( + dimension: 24, + child: CircularProgressIndicator.adaptive(), + ), + ); + } + + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + '${snapshot.data!.percentageProgress.toStringAsFixed(0)}%', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 22, + color: Colors.white, + ), + ), + const SizedBox.square(dimension: 12), + Text( + '${snapshot.data!.tilesPerSecond.toStringAsPrecision(3)} tps', + style: const TextStyle( + fontSize: 20, + color: Colors.white, + ), + ), + ], + ); + }, + ), + ); + } +} diff --git a/example/lib/screens/main/pages/map/components/empty_tile_provider.dart b/example/lib/screens/main/pages/map/components/empty_tile_provider.dart new file mode 100644 index 00000000..9bc23269 --- /dev/null +++ b/example/lib/screens/main/pages/map/components/empty_tile_provider.dart @@ -0,0 +1,11 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_map/flutter_map.dart'; + +class EmptyTileProvider extends TileProvider { + @override + ImageProvider getImage( + TileCoordinates coordinates, + TileLayer options, + ) => + MemoryImage(TileProvider.transparentImage); +} diff --git a/example/lib/screens/main/pages/map/components/map_view.dart b/example/lib/screens/main/pages/map/components/map_view.dart new file mode 100644 index 00000000..af7cac5b --- /dev/null +++ b/example/lib/screens/main/pages/map/components/map_view.dart @@ -0,0 +1,106 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:provider/provider.dart'; + +import '../../../../../shared/components/build_attribution.dart'; +import '../../../../../shared/components/loading_indicator.dart'; +import '../../../../../shared/state/general_provider.dart'; +import '../../downloading/state/downloading_provider.dart'; +import '../../region_selection/components/region_shape.dart'; +import '../state/map_provider.dart'; +import 'empty_tile_provider.dart'; + +class MapView extends StatelessWidget { + const MapView({super.key}); + + @override + Widget build(BuildContext context) => Selector( + selector: (context, provider) => provider.currentStore, + builder: (context, currentStore, _) => + FutureBuilder?>( + future: currentStore == null + ? Future.sync(() => {}) + : FMTC.instance(currentStore).metadata.readAsync, + builder: (context, metadata) { + if (!metadata.hasData || + metadata.data == null || + (currentStore != null && metadata.data!.isEmpty)) { + return const LoadingIndicator('Preparing Map'); + } + + final urlTemplate = currentStore != null && metadata.data != null + ? metadata.data!['sourceURL']! + : 'https://tile.openstreetmap.org/{z}/{x}/{y}.png'; + + return FlutterMap( + mapController: Provider.of(context).mapController, + options: const MapOptions( + initialCenter: LatLng(51.509364, -0.128928), + initialZoom: 12, + interactionOptions: InteractionOptions( + flags: InteractiveFlag.all & ~InteractiveFlag.rotate, + scrollWheelVelocity: 0.002, + ), + keepAlive: true, + backgroundColor: Color(0xFFaad3df), + ), + children: [ + if (context.select?>( + (provider) => provider.tilesPreviewStreamSub, + ) == + null) + TileLayer( + urlTemplate: urlTemplate, + userAgentPackageName: 'dev.jaffaketchup.fmtc.demo', + maxNativeZoom: 20, + panBuffer: 5, + tileProvider: currentStore != null + ? FMTC.instance(currentStore).getTileProvider( + settings: FMTCTileProviderSettings( + behavior: CacheBehavior.values + .byName(metadata.data!['behaviour']!), + cachedValidDuration: int.parse( + metadata.data!['validDuration']!, + ) == + 0 + ? Duration.zero + : Duration( + days: int.parse( + metadata.data!['validDuration']!, + ), + ), + maxStoreLength: int.parse( + metadata.data!['maxLength']!, + ), + ), + ) + : NetworkTileProvider(), + ) + else ...[ + const SizedBox.expand( + child: ColoredBox(color: Colors.grey), + ), + TileLayer( + tileBuilder: (context, widget, tile) { + final bytes = context + .read() + .tilesPreview[tile.coordinates]; + if (bytes == null) return const SizedBox.shrink(); + return Image.memory(bytes); + }, + tileProvider: EmptyTileProvider(), + ), + const RegionShape(), + ], + StandardAttribution(urlTemplate: urlTemplate), + ], + ); + }, + ), + ); +} diff --git a/example/lib/screens/main/pages/map/components/quit_tiles_preview_indicator.dart b/example/lib/screens/main/pages/map/components/quit_tiles_preview_indicator.dart new file mode 100644 index 00000000..a40e4502 --- /dev/null +++ b/example/lib/screens/main/pages/map/components/quit_tiles_preview_indicator.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:provider/provider.dart'; + +import '../../downloading/state/downloading_provider.dart'; +import 'side_indicator_painter.dart'; + +class QuitTilesPreviewIndicator extends StatelessWidget { + const QuitTilesPreviewIndicator({ + super.key, + required this.constraints, + }); + + final BoxConstraints constraints; + + @override + Widget build(BuildContext context) { + final isNarrow = MediaQuery.sizeOf(context).width <= 950; + + return Selector( + selector: (context, provider) => provider.showQuitTilesPreviewIndicator, + builder: (context, sqtpi, child) => AnimatedPositioned( + duration: const Duration(milliseconds: 1200), + curve: Curves.elasticOut, + top: isNarrow ? null : constraints.maxHeight / 2 - 139, + left: isNarrow + ? constraints.maxWidth / 2 - + 55 - + constraints.maxWidth / 4 - + constraints.maxWidth / 8 + : sqtpi + ? 8 + : -120, + bottom: isNarrow + ? sqtpi + ? 38 + : -90 + : null, + height: 50, + width: 110, + child: child!, + ), + child: Transform.rotate( + angle: isNarrow ? 270 * pi / 180 : 0, + child: CustomPaint( + painter: SideIndicatorPainter( + startRadius: const Radius.circular(8), + endRadius: const Radius.circular(25), + color: Theme.of(context).colorScheme.background, + ), + child: Padding( + padding: const EdgeInsets.only(left: 20), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + RotatedBox( + quarterTurns: isNarrow ? 1 : 0, + child: const Icon(Icons.touch_app, size: 32), + ), + const SizedBox.square(dimension: 6), + RotatedBox( + quarterTurns: isNarrow ? 1 : 0, + child: const Icon(Icons.disabled_visible, size: 32), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/example/lib/screens/main/pages/map/components/side_indicator_painter.dart b/example/lib/screens/main/pages/map/components/side_indicator_painter.dart new file mode 100644 index 00000000..399b0857 --- /dev/null +++ b/example/lib/screens/main/pages/map/components/side_indicator_painter.dart @@ -0,0 +1,52 @@ +import 'package:flutter/widgets.dart'; + +class SideIndicatorPainter extends CustomPainter { + const SideIndicatorPainter({ + this.startRadius = Radius.zero, + this.endRadius = Radius.zero, + this.color, + }); + + final Radius startRadius; + final Radius endRadius; + final Color? color; + + @override + void paint(Canvas canvas, Size size) => canvas.drawPath( + Path() + ..moveTo(0, size.height / 2) + ..lineTo((size.height / 2) - startRadius.x, startRadius.y) + ..quadraticBezierTo( + size.height / 2, + 0, + (size.height / 2) + startRadius.x, + 0, + ) + ..lineTo(size.width - endRadius.x, 0) + ..arcToPoint( + Offset(size.width, endRadius.y), + radius: endRadius, + ) + ..lineTo(size.width, size.height - endRadius.y) + ..arcToPoint( + Offset(size.width - endRadius.x, size.height), + radius: endRadius, + ) + ..lineTo((size.height / 2) + startRadius.x, size.height) + ..quadraticBezierTo( + size.height / 2, + size.height, + (size.height / 2) - startRadius.x, + size.height - startRadius.y, + ) + ..lineTo(0, size.height / 2) + ..close(), + Paint() + ..color = color ?? const Color(0xFF000000) + ..strokeCap = StrokeCap.round + ..style = PaintingStyle.fill, + ); + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} diff --git a/example/lib/screens/main/pages/map/map_page.dart b/example/lib/screens/main/pages/map/map_page.dart new file mode 100644 index 00000000..94114889 --- /dev/null +++ b/example/lib/screens/main/pages/map/map_page.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_map_animations/flutter_map_animations.dart'; +import 'package:provider/provider.dart'; + +import 'components/download_progress_indicator.dart'; +import 'components/map_view.dart'; +import 'components/quit_tiles_preview_indicator.dart'; +import 'state/map_provider.dart'; + +class MapPage extends StatefulWidget { + const MapPage({super.key}); + + @override + State createState() => _MapPageState(); +} + +class _MapPageState extends State with TickerProviderStateMixin { + late final _animatedMapController = AnimatedMapController( + vsync: this, + duration: const Duration(milliseconds: 80), + curve: Curves.linear, + ); + + @override + void initState() { + super.initState(); + + // Setup animated map controller + WidgetsBinding.instance.addPostFrameCallback( + (_) { + context.read() + ..mapController = _animatedMapController.mapController + ..animateTo = _animatedMapController.animateTo; + }, + ); + } + + @override + Widget build(BuildContext context) => LayoutBuilder( + builder: (context, constraints) => Stack( + children: [ + const MapView(), + QuitTilesPreviewIndicator(constraints: constraints), + DownloadProgressIndicator(constraints: constraints), + ], + ), + ); +} diff --git a/example/lib/screens/main/pages/map/map_view.dart b/example/lib/screens/main/pages/map/map_view.dart deleted file mode 100644 index 7c7fd824..00000000 --- a/example/lib/screens/main/pages/map/map_view.dart +++ /dev/null @@ -1,115 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; -import 'package:latlong2/latlong.dart'; -import 'package:provider/provider.dart'; - -import '../../../../shared/components/build_attribution.dart'; -import '../../../../shared/components/loading_indicator.dart'; -import '../../../../shared/state/general_provider.dart'; -import 'state/map_provider.dart'; - -class MapPage extends StatelessWidget { - const MapPage({super.key}); - - @override - Widget build(BuildContext context) => Consumer( - builder: (context, provider, _) => FutureBuilder?>( - future: provider.currentStore == null - ? Future.sync(() => {}) - : FMTC.instance(provider.currentStore!).metadata.readAsync, - builder: (context, metadata) { - if (!metadata.hasData || - metadata.data == null || - (provider.currentStore != null && metadata.data!.isEmpty)) { - return const LoadingIndicator('Preparing Map'); - } - - final String urlTemplate = - provider.currentStore != null && metadata.data != null - ? metadata.data!['sourceURL']! - : 'https://tile.openstreetmap.org/{z}/{x}/{y}.png'; - - return FlutterMap( - mapController: Provider.of(context).mapController, - options: const MapOptions( - initialCenter: LatLng(51.509364, -0.128928), - initialZoom: 16, - interactionOptions: InteractionOptions( - flags: InteractiveFlag.all & ~InteractiveFlag.rotate, - scrollWheelVelocity: 0.002, - ), - keepAlive: true, - backgroundColor: Color(0xFFaad3df), - ), - children: [ - TileLayer( - urlTemplate: urlTemplate, - userAgentPackageName: 'dev.jaffaketchup.fmtc.demo', - maxNativeZoom: 20, - panBuffer: 5, - tileProvider: provider.currentStore != null - ? FMTC.instance(provider.currentStore!).getTileProvider( - settings: FMTCTileProviderSettings( - behavior: CacheBehavior.values - .byName(metadata.data!['behaviour']!), - cachedValidDuration: int.parse( - metadata.data!['validDuration']!, - ) == - 0 - ? Duration.zero - : Duration( - days: int.parse( - metadata.data!['validDuration']!, - ), - ), - maxStoreLength: int.parse( - metadata.data!['maxLength']!, - ), - ), - ) - : NetworkTileProvider(), - ), - StandardAttribution( - urlTemplate: urlTemplate, - alignment: AttributionAlignment.bottomLeft, - ), - /*Consumer( - builder: (context, mapProvider, _) => CurrentLocationLayer( - followCurrentLocationStream: - mapProvider.trackLocationStream, - turnHeadingUpLocationStream: mapProvider.trackHeadingStream, - followOnLocationUpdate: - mapProvider.followState != UserLocationFollowState.off - ? FollowOnLocationUpdate.always - : FollowOnLocationUpdate.never, - turnOnHeadingUpdate: mapProvider.followState == - UserLocationFollowState.navigation - ? TurnOnHeadingUpdate.always - : TurnOnHeadingUpdate.never, - headingStream: Platform.isAndroid || Platform.isIOS - ? null - : const Stream.empty(), - style: LocationMarkerStyle( - marker: DefaultLocationMarker( - child: Platform.isAndroid || Platform.isIOS - ? const Icon( - Icons.navigation, - color: Colors.white, - size: 18, - ) - : null, - ), - markerSize: const Size(30, 30), - markerDirection: MarkerDirection.heading, - ), - ), - ),*/ - ], - ); - }, - ), - ); -} diff --git a/example/lib/screens/main/pages/map/state/map_provider.dart b/example/lib/screens/main/pages/map/state/map_provider.dart index a76cdb2a..0228b98e 100644 --- a/example/lib/screens/main/pages/map/state/map_provider.dart +++ b/example/lib/screens/main/pages/map/state/map_provider.dart @@ -1,5 +1,15 @@ -import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; +import 'package:latlong2/latlong.dart'; + +typedef AnimateToSignature = Future Function({ + LatLng? dest, + double? zoom, + Offset offset, + double? rotation, + Curve? curve, + String? customId, +}); class MapProvider extends ChangeNotifier { MapController _mapController = MapController(); @@ -8,4 +18,11 @@ class MapProvider extends ChangeNotifier { _mapController = newController; notifyListeners(); } + + late AnimateToSignature? _animateTo; + AnimateToSignature get animateTo => _animateTo!; + set animateTo(AnimateToSignature newMethod) { + _animateTo = newMethod; + notifyListeners(); + } } diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 6c77bf99..70c0b433 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -20,6 +20,10 @@ dependencies: sdk: flutter flutter_foreground_task: ^6.0.0+1 flutter_map: ^6.0.0 + flutter_map_animations: + git: + url: https://github.com/JaffaKetchup/_flutter_map_animations.git + ref: offset flutter_map_tile_caching: flutter_speed_dial: ^7.0.0 fmtc_plus_sharing: ^8.0.0 diff --git a/lib/src/bulk_download/thread.dart b/lib/src/bulk_download/thread.dart index 1eeb5e37..ed42d00b 100644 --- a/lib/src/bulk_download/thread.dart +++ b/lib/src/bulk_download/thread.dart @@ -41,10 +41,10 @@ Future _singleDownloadThread( while (true) { // Request new tile coords send(0); - final coords = (await tileQueue.next) as (int, int, int)?; + final rawCoords = (await tileQueue.next) as (int, int, int)?; // Cleanup resources and shutdown if no more coords available - if (coords == null) { + if (rawCoords == null) { recievePort.close(); await tileQueue.cancel(immediate: true); @@ -58,11 +58,13 @@ Future _singleDownloadThread( Isolate.exit(); } + // Generate `TileCoordinates` + final coordinates = + TileCoordinates(rawCoords.$1, rawCoords.$2, rawCoords.$3); + // Get new tile URL & any existing tile - final networkUrl = input.options.tileProvider.getTileUrl( - TileCoordinates(coords.$1, coords.$2, coords.$3), - input.options, - ); + final networkUrl = + input.options.tileProvider.getTileUrl(coordinates, input.options); final matcherUrl = obscureQueryParams( url: networkUrl, obscuredQueryParams: input.obscuredQueryParams, @@ -75,6 +77,7 @@ Future _singleDownloadThread( TileEvent._( TileEventResult.alreadyExisting, url: networkUrl, + coordinates: coordinates, tileImage: Uint8List.fromList(existingTile.bytes), ), ); @@ -93,6 +96,7 @@ Future _singleDownloadThread( ? TileEventResult.noConnectionDuringFetch : TileEventResult.unknownFetchException, url: networkUrl, + coordinates: coordinates, fetchError: e, ), ); @@ -104,6 +108,7 @@ Future _singleDownloadThread( TileEvent._( TileEventResult.negativeFetchResponse, url: networkUrl, + coordinates: coordinates, fetchResponse: response, ), ); @@ -116,6 +121,7 @@ Future _singleDownloadThread( TileEvent._( TileEventResult.isSeaTile, url: networkUrl, + coordinates: coordinates, tileImage: response.bodyBytes, fetchResponse: response, ), @@ -143,6 +149,7 @@ Future _singleDownloadThread( TileEvent._( TileEventResult.success, url: networkUrl, + coordinates: coordinates, tileImage: response.bodyBytes, fetchResponse: response, wasBufferReset: wasBufferReset, diff --git a/lib/src/bulk_download/tile_event.dart b/lib/src/bulk_download/tile_event.dart index de63a203..39c9d3c9 100644 --- a/lib/src/bulk_download/tile_event.dart +++ b/lib/src/bulk_download/tile_event.dart @@ -83,6 +83,11 @@ class TileEvent { /// Remember to check [isRepeat] before keeping track of this value. final String url; + /// The (x, y, z) coordinates of this tile + /// + /// Remember to check [isRepeat] before keeping track of this value. + final TileCoordinates coordinates; + /// The raw bytes that were fetched from the [url], if available /// /// Not available if the result category is [TileEventResultCategory.failed]. @@ -119,6 +124,7 @@ class TileEvent { const TileEvent._( this.result, { required this.url, + required this.coordinates, this.tileImage, this.fetchResponse, this.fetchError, @@ -129,6 +135,7 @@ class TileEvent { TileEvent _repeat() => TileEvent._( result, url: url, + coordinates: coordinates, tileImage: tileImage, fetchResponse: fetchResponse, fetchError: fetchError, @@ -142,6 +149,7 @@ class TileEvent { (other is TileEvent && result == other.result && url == other.url && + coordinates == other.coordinates && tileImage == other.tileImage && fetchResponse == other.fetchResponse && fetchError == other.fetchError && @@ -152,6 +160,7 @@ class TileEvent { int get hashCode => Object.hashAllUnordered([ result, url, + coordinates, tileImage, fetchResponse, fetchError, diff --git a/tile_server/static/source/images/land.png b/tile_server/static/source/images/land.png index 80f9961d825b9a82cd358d919469aac7bcd4dd25..6239c7a2445429d1ec868697517970dd455bf490 100644 GIT binary patch literal 18627 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 From 5a24b46f34c4722cd5ba3faec8b6c8b2733e3f82 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sat, 21 Oct 2023 23:23:47 +0100 Subject: [PATCH 072/168] Fix failing Windows build Former-commit-id: d111a388f602d01366590fd12e66008baa8654d0 [formerly 6810bfa323d0191e64273c114667d1943e797d53] Former-commit-id: e11ca7e82960178cff413102bfb1c481fe1c67b6 --- windowsApplicationInstallerSetup.iss | 1 - 1 file changed, 1 deletion(-) diff --git a/windowsApplicationInstallerSetup.iss b/windowsApplicationInstallerSetup.iss index 3b2284cc..d8fbeadb 100644 --- a/windowsApplicationInstallerSetup.iss +++ b/windowsApplicationInstallerSetup.iss @@ -67,7 +67,6 @@ Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{ [Files] Source: "example\build\windows\runner\Release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion Source: "example\build\windows\runner\Release\flutter_windows.dll"; DestDir: "{app}"; Flags: ignoreversion -Source: "example\build\windows\runner\Release\geolocator_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "example\build\windows\runner\Release\isar_flutter_libs_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "example\build\windows\runner\Release\isar.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "example\build\windows\runner\Release\share_plus_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion From c779107a1be6440e2a6c7ebac6ca9e6785d6906c Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sun, 22 Oct 2023 13:40:54 +0100 Subject: [PATCH 073/168] Improved follow download live feature in example app Former-commit-id: f4c4a05632fff8048b6261b9cc6540a677987003 [formerly 6309659f10606a1d19de1c408b660cdec0c0be83] Former-commit-id: 10a6fc112cc35986c1776f9c5e6c9f134210265b --- CHANGELOG.md | 2 +- .../components/numerical_input_row.dart | 120 +++++++++++------- .../configure_download.dart | 3 +- .../state/configure_download_provider.dart | 12 +- .../quit_tiles_preview_indicator.dart | 2 +- 5 files changed, 90 insertions(+), 49 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4e62982..43aabfcf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,7 +44,7 @@ Also: * Created automated tests for tile generation * Improved & simplified example application * Removed update mechanism - * Added user location tracking + * Added tile-by-tile download following ## [8.0.1] - 2023/07/29 diff --git a/example/lib/screens/configure_download/components/numerical_input_row.dart b/example/lib/screens/configure_download/components/numerical_input_row.dart index 7c36ec45..a3fbf60d 100644 --- a/example/lib/screens/configure_download/components/numerical_input_row.dart +++ b/example/lib/screens/configure_download/components/numerical_input_row.dart @@ -4,7 +4,7 @@ import 'package:provider/provider.dart'; import '../state/configure_download_provider.dart'; -class NumericalInputRow extends StatelessWidget { +class NumericalInputRow extends StatefulWidget { const NumericalInputRow({ super.key, required this.label, @@ -12,6 +12,7 @@ class NumericalInputRow extends StatelessWidget { required this.value, required this.min, required this.max, + this.maxEligibleTilesPreview, required this.onChanged, }); @@ -20,54 +21,87 @@ class NumericalInputRow extends StatelessWidget { final int Function(ConfigureDownloadProvider provider) value; final int min; final int? max; + final int? maxEligibleTilesPreview; final void Function(ConfigureDownloadProvider provider, int value) onChanged; @override - Widget build(BuildContext context) { - final currentValue = context.select(value); + State createState() => _NumericalInputRowState(); +} + +class _NumericalInputRowState extends State { + TextEditingController? tec; + + @override + Widget build(BuildContext context) => + Selector( + selector: (context, provider) => widget.value(provider), + builder: (context, currentValue, _) { + tec ??= TextEditingController(text: currentValue.toString()); - return Row( - children: [ - Text(label), - const Spacer(), - if (max != null) ...[ - Tooltip( - message: currentValue == max ? 'Limited in the example app' : '', - child: Icon( - Icons.lock, - color: currentValue == max - ? Colors.amber - : Colors.white.withOpacity(0.2), - ), - ), - const SizedBox(width: 16), - ], - IntrinsicWidth( - child: TextFormField( - initialValue: currentValue.toString(), - textAlign: TextAlign.end, - keyboardType: TextInputType.number, - decoration: InputDecoration( - isDense: true, - counterText: '', - suffixText: ' $suffixText', - ), - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly, - _NumericalRangeFormatter( - min: min, - max: max ?? 9223372036854775807, + return Row( + children: [ + Text(widget.label), + const Spacer(), + if (widget.maxEligibleTilesPreview != null) ...[ + IconButton( + icon: const Icon(Icons.visibility), + disabledColor: Colors.green, + tooltip: currentValue > widget.maxEligibleTilesPreview! + ? 'Tap to enable following download live' + : 'Eligible to follow download live', + onPressed: currentValue > widget.maxEligibleTilesPreview! + ? () { + widget.onChanged( + context.read(), + widget.maxEligibleTilesPreview!, + ); + tec!.text = widget.maxEligibleTilesPreview.toString(); + } + : null, + ), + const SizedBox(width: 8), + ], + if (widget.max != null) ...[ + Tooltip( + message: currentValue == widget.max + ? 'Limited in the example app' + : '', + child: Icon( + Icons.lock, + color: currentValue == widget.max + ? Colors.amber + : Colors.white.withOpacity(0.2), + ), + ), + const SizedBox(width: 16), + ], + IntrinsicWidth( + child: TextFormField( + controller: tec, + textAlign: TextAlign.end, + keyboardType: TextInputType.number, + decoration: InputDecoration( + isDense: true, + counterText: '', + suffixText: ' ${widget.suffixText}', + ), + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + _NumericalRangeFormatter( + min: widget.min, + max: widget.max ?? 9223372036854775807, + ), + ], + onChanged: (newVal) => widget.onChanged( + context.read(), + int.tryParse(newVal) ?? currentValue, + ), + ), ), ], - onChanged: (newVal) => onChanged( - context.read(), - int.tryParse(newVal) ?? currentValue, - ), - ), - ), - ], - ); - } + ); + }, + ); } class _NumericalRangeFormatter extends TextInputFormatter { diff --git a/example/lib/screens/configure_download/configure_download.dart b/example/lib/screens/configure_download/configure_download.dart index fdb9d725..624450f1 100644 --- a/example/lib/screens/configure_download/configure_download.dart +++ b/example/lib/screens/configure_download/configure_download.dart @@ -66,8 +66,9 @@ class ConfigureDownloadPopup extends StatelessWidget { label: 'Rate Limit', suffixText: 'max. tps', value: (provider) => provider.rateLimit, - min: 5, + min: 1, max: 500, + maxEligibleTilesPreview: 20, onChanged: (provider, value) => provider.rateLimit = value, ), diff --git a/example/lib/screens/configure_download/state/configure_download_provider.dart b/example/lib/screens/configure_download/state/configure_download_provider.dart index bb93ba9d..d7ce1387 100644 --- a/example/lib/screens/configure_download/state/configure_download_provider.dart +++ b/example/lib/screens/configure_download/state/configure_download_provider.dart @@ -1,21 +1,27 @@ import 'package:flutter/foundation.dart'; class ConfigureDownloadProvider extends ChangeNotifier { - int _parallelThreads = 5; + static const defaultValues = { + 'parallelThreads': 5, + 'rateLimit': 200, + 'maxBufferLength': 500, + }; + + int _parallelThreads = defaultValues['parallelThreads']!; int get parallelThreads => _parallelThreads; set parallelThreads(int newNum) { _parallelThreads = newNum; notifyListeners(); } - int _rateLimit = 200; + int _rateLimit = defaultValues['rateLimit']!; int get rateLimit => _rateLimit; set rateLimit(int newNum) { _rateLimit = newNum; notifyListeners(); } - int _maxBufferLength = 500; + int _maxBufferLength = defaultValues['maxBufferLength']!; int get maxBufferLength => _maxBufferLength; set maxBufferLength(int newNum) { _maxBufferLength = newNum; diff --git a/example/lib/screens/main/pages/map/components/quit_tiles_preview_indicator.dart b/example/lib/screens/main/pages/map/components/quit_tiles_preview_indicator.dart index a40e4502..bf39beec 100644 --- a/example/lib/screens/main/pages/map/components/quit_tiles_preview_indicator.dart +++ b/example/lib/screens/main/pages/map/components/quit_tiles_preview_indicator.dart @@ -60,7 +60,7 @@ class QuitTilesPreviewIndicator extends StatelessWidget { const SizedBox.square(dimension: 6), RotatedBox( quarterTurns: isNarrow ? 1 : 0, - child: const Icon(Icons.disabled_visible, size: 32), + child: const Icon(Icons.visibility_off, size: 32), ), ], ), From e91c0c8220a03254c6271b5d9dcc0c8afd565414 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Mon, 23 Oct 2023 11:36:20 +0100 Subject: [PATCH 074/168] Updated CHANGELOG Former-commit-id: 1262b9e806594695d407e6787c697c0c28a4df6a [formerly 42eed0e0d7f408665c2e4eeaa26cd3c4c51896ee] Former-commit-id: 2da397f0dcbb4bfdef8479c1d5e387fc3c51d2b6 --- CHANGELOG.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 43aabfcf..50ca081b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,9 +16,8 @@ Many thanks to my sponsors, no matter how much or how little they donated. Spons ## [9.0.0] - 2023/XX/XX -* Migrated to Flutter 3.10 and Dart 3 -* Migrated to flutter_map v5 -* Migrated to Isar v4 (bug fixes & stability improvements) +* Migrated to Flutter 3.13 and Dart 3.1 +* Migrated to flutter_map v6 * Bulk downloading reimplementation * Added `CustomPolygonRegion`, a `BaseRegion` that is formed of any* outline * Added pause and resume functionality @@ -44,7 +43,7 @@ Also: * Created automated tests for tile generation * Improved & simplified example application * Removed update mechanism - * Added tile-by-tile download following + * Added tile-by-tile/live download following ## [8.0.1] - 2023/07/29 From a008d166153baca1385c0477651327b7dedcd1b5 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Fri, 3 Nov 2023 18:36:33 +0000 Subject: [PATCH 075/168] Initial transition work to ObjectBox from Isar Improved maintainability Former-commit-id: 92ae5fc0f9617a2cf0ea8120a441b9ee1c19572c [formerly 4ee03e7d2d4e8970b2d860ec610dee6370d5d60f] Former-commit-id: bdc056921fef975c5d5f97020d3abff1f045e6c8 --- analysis_options.yaml | 2 +- example/pubspec.yaml | 5 +- .../flutter/generated_plugin_registrant.cc | 3 + .../windows/flutter/generated_plugins.cmake | 1 + lib/src/backend/errors.dart | 27 + lib/src/backend/impls/objectbox/backend.dart | 168 ++ .../models/generated/objectbox-model.json | 88 + .../models/generated/objectbox.g.dart | 230 ++ .../impls/objectbox/models/models.dart | 49 + lib/src/backend/interfaces/backend.dart | 68 + lib/src/backend/interfaces/models.dart | 11 + lib/src/db/defs/metadata.g.dart | 606 ---- lib/src/db/defs/recovery.g.dart | 2602 ----------------- lib/src/db/defs/store_descriptor.g.dart | 660 ----- lib/src/db/defs/tile.g.dart | 785 ----- lib/src/settings/fmtc_settings.dart | 6 +- pubspec.yaml | 10 +- 17 files changed, 660 insertions(+), 4661 deletions(-) create mode 100644 lib/src/backend/errors.dart create mode 100644 lib/src/backend/impls/objectbox/backend.dart create mode 100644 lib/src/backend/impls/objectbox/models/generated/objectbox-model.json create mode 100644 lib/src/backend/impls/objectbox/models/generated/objectbox.g.dart create mode 100644 lib/src/backend/impls/objectbox/models/models.dart create mode 100644 lib/src/backend/interfaces/backend.dart create mode 100644 lib/src/backend/interfaces/models.dart delete mode 100644 lib/src/db/defs/metadata.g.dart delete mode 100644 lib/src/db/defs/recovery.g.dart delete mode 100644 lib/src/db/defs/store_descriptor.g.dart delete mode 100644 lib/src/db/defs/tile.g.dart diff --git a/analysis_options.yaml b/analysis_options.yaml index d16bc85b..59d8a4a8 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -2,7 +2,7 @@ include: jaffa_lints.yaml analyzer: exclude: - - lib/src/db/defs/*.g.dart + - lib/**.g.dart linter: rules: diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 70c0b433..a6c8648f 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -20,10 +20,7 @@ dependencies: sdk: flutter flutter_foreground_task: ^6.0.0+1 flutter_map: ^6.0.0 - flutter_map_animations: - git: - url: https://github.com/JaffaKetchup/_flutter_map_animations.git - ref: offset + flutter_map_animations: ^0.5.3 flutter_map_tile_caching: flutter_speed_dial: ^7.0.0 fmtc_plus_sharing: ^8.0.0 diff --git a/example/windows/flutter/generated_plugin_registrant.cc b/example/windows/flutter/generated_plugin_registrant.cc index 6d420e2b..8700c13c 100644 --- a/example/windows/flutter/generated_plugin_registrant.cc +++ b/example/windows/flutter/generated_plugin_registrant.cc @@ -7,12 +7,15 @@ #include "generated_plugin_registrant.h" #include +#include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { IsarFlutterLibsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("IsarFlutterLibsPlugin")); + ObjectboxFlutterLibsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("ObjectboxFlutterLibsPlugin")); SharePlusWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); UrlLauncherWindowsRegisterWithRegistrar( diff --git a/example/windows/flutter/generated_plugins.cmake b/example/windows/flutter/generated_plugins.cmake index 759d9b5d..57dfe6f1 100644 --- a/example/windows/flutter/generated_plugins.cmake +++ b/example/windows/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST isar_flutter_libs + objectbox_flutter_libs share_plus url_launcher_windows ) diff --git a/lib/src/backend/errors.dart b/lib/src/backend/errors.dart new file mode 100644 index 00000000..e409d6bb --- /dev/null +++ b/lib/src/backend/errors.dart @@ -0,0 +1,27 @@ +/// Indicates that the backend/root structure (ie. database and/or directory) was +/// not available for use in operations, because either: +/// * it was already closed +/// * it was never created +/// * it was invalid/corrupt +/// * ... or it was otherwise unavailable +/// +/// To be thrown by backend implementations. +class RootUnavailable extends Error { + @override + String toString() => + 'RootUnavailable: The requested backend/root was unavailable'; +} + +/// Indicates that the specified store structure was not available for use in +/// operations, likely because it didn't exist +/// +/// To be thrown by backend implementations. +class StoreUnavailable extends Error { + final String storeName; + + StoreUnavailable({required this.storeName}); + + @override + String toString() => + 'StoreUnavailable: The requested store "$storeName" was unavailable'; +} diff --git a/lib/src/backend/impls/objectbox/backend.dart b/lib/src/backend/impls/objectbox/backend.dart new file mode 100644 index 00000000..045ca8e3 --- /dev/null +++ b/lib/src/backend/impls/objectbox/backend.dart @@ -0,0 +1,168 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:meta/meta.dart'; +import 'package:path_provider/path_provider.dart'; + +import '../../../misc/exts.dart'; +import '../../errors.dart'; +import '../../interfaces/backend.dart'; +import 'models/generated/objectbox.g.dart'; +import 'models/models.dart'; + +/// Implementation of [FMTCBackend] that uses ObjectBox as the storage database +/// +/// Only the factory constructor ([ObjectBoxBackend.new]), and +/// [friendlyIdentifier], should be used in end-applications. Other methods are +/// for internal use only. +class ObjectBoxBackend implements FMTCBackend { + factory ObjectBoxBackend() => _instance; + static final ObjectBoxBackend _instance = ObjectBoxBackend._(); + ObjectBoxBackend._(); + + Store? _root; // Must not be closed if not `null` + Store get _expectRoot => _root ?? (throw RootUnavailable()); + + Future _getStore(String name) async => + (await _expectRoot + .box() + .query(ObjectBoxStore_.name.equals(name)) + .build() + .findFirstAsync()) ?? + (throw StoreUnavailable(storeName: name)); + + @override + String get friendlyIdentifier => 'ObjectBox'; + + @override + @internal + bool get supportsSharing => false; + + @override + @internal + Future initialise({ + String? rootDirectory, + }) async { + final dir = await ((rootDirectory == null + ? await getApplicationDocumentsDirectory() + : Directory(rootDirectory)) >> + 'fmtc') + .create(recursive: true); + _root = await openStore(directory: dir.absolute.path); + } + + @override + @internal + Future destroy({ + bool deleteRoot = false, + }) async { + _expectRoot; + + await Directory((_root!..close()).directoryPath).delete(recursive: true); + _root = null; + } + + @override + @internal + Future createStore({ + required String storeName, + }) async { + await _expectRoot + .box() + .putAsync(ObjectBoxStore(name: storeName), mode: PutMode.insert); + } + + @override + @internal + Future resetStore({ + required String storeName, + }) async { + _expectRoot; + + await _root!.runInTransactionAsync( + TxMode.write, + (store, storeName) { + final tiles = _root!.box(); + + final removeIds = []; + + final tilesBelongingToStore = (tiles.query() + ..linkMany(ObjectBoxTile_.stores, + ObjectBoxStore_.name.equals(storeName))) + .build(); + tiles.putMany( + tilesBelongingToStore + .find() + .map((tile) { + tile.stores.removeWhere((store) => store.name == storeName); + if (tile.stores.isNotEmpty) return tile; + removeIds.add(tile.id); + return null; + }) + .whereNotNull() + .toList(), + mode: PutMode.update, + ); + tilesBelongingToStore.close(); + + tiles.query(ObjectBoxTile_.id.oneOf(removeIds)).build() + ..remove() + ..close(); + }, + storeName, + ); + } + + @override + @internal + Future renameStore({ + required String currentStoreName, + required String newStoreName, + }) async => + _expectRoot + .box() + .putAsync((await _getStore(currentStoreName))..name = newStoreName); + + @override + @internal + Future deleteStore({ + required String storeName, + }) async { + //await resetStore(storeName: storeName); + await _expectRoot + .box() + .query(ObjectBoxStore_.name.equals(storeName)) + .build() + .removeAsync(); + } + + @override + @internal + Future> readTile({required String url}) async { + final query = _expectRoot + .box() + .query(ObjectBoxTile_.url.equals(url)) + .build(); + final tiles = await query.findAsync(); + query.close(); + return tiles; + } + + @override + @internal + FutureOr createTile() {} + @override + @internal + FutureOr updateTile() {} + @override + @internal + FutureOr deleteTile() {} + + @override + @internal + FutureOr readLatestTile() {} + @override + @internal + FutureOr pruneTilesOlderThan({required DateTime expiry}) {} +} diff --git a/lib/src/backend/impls/objectbox/models/generated/objectbox-model.json b/lib/src/backend/impls/objectbox/models/generated/objectbox-model.json new file mode 100644 index 00000000..9df75546 --- /dev/null +++ b/lib/src/backend/impls/objectbox/models/generated/objectbox-model.json @@ -0,0 +1,88 @@ +{ + "_note1": "KEEP THIS FILE! Check it into a version control system (VCS) like git.", + "_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.", + "_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.", + "entities": [ + { + "id": "3:3564285316728157354", + "lastPropertyId": "2:5839091915824466165", + "name": "ObjectBoxStore", + "properties": [ + { + "id": "1:5129628855357846983", + "name": "id", + "type": 6, + "flags": 1 + }, + { + "id": "2:5839091915824466165", + "name": "name", + "type": 9, + "flags": 2048, + "indexId": "4:4070553972642232057" + } + ], + "relations": [] + }, + { + "id": "4:1221776040269555016", + "lastPropertyId": "4:325740197791906264", + "name": "ObjectBoxTile", + "properties": [ + { + "id": "1:6708383205929458485", + "name": "id", + "type": 6, + "flags": 1 + }, + { + "id": "2:7632311904162794182", + "name": "url", + "type": 9, + "flags": 34848, + "indexId": "5:6190682863606461840" + }, + { + "id": "3:4021600588433010488", + "name": "lastModified", + "type": 10, + "flags": 8, + "indexId": "6:7105980852148339018" + }, + { + "id": "4:325740197791906264", + "name": "bytes", + "type": 23 + } + ], + "relations": [ + { + "id": "2:7253865609688140673", + "name": "stores", + "targetId": "3:3564285316728157354" + } + ] + } + ], + "lastEntityId": "4:1221776040269555016", + "lastIndexId": "6:7105980852148339018", + "lastRelationId": "2:7253865609688140673", + "lastSequenceId": "0:0", + "modelVersion": 5, + "modelVersionParserMinimum": 5, + "retiredEntityUids": [ + 6351682704282101940, + 3030016702019813306 + ], + "retiredIndexUids": [], + "retiredPropertyUids": [ + 4615710800892160360, + 7634309479420249012, + 1360422513191232937, + 2679707907762759593, + 4242260097954056817, + 5886094580441791807 + ], + "retiredRelationUids": [], + "version": 1 +} \ No newline at end of file diff --git a/lib/src/backend/impls/objectbox/models/generated/objectbox.g.dart b/lib/src/backend/impls/objectbox/models/generated/objectbox.g.dart new file mode 100644 index 00000000..e9818d90 --- /dev/null +++ b/lib/src/backend/impls/objectbox/models/generated/objectbox.g.dart @@ -0,0 +1,230 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// This code was generated by ObjectBox. To update it run the generator again: +// With a Flutter package, run `flutter pub run build_runner build`. +// With a Dart package, run `dart run build_runner build`. +// See also https://docs.objectbox.io/getting-started#generate-objectbox-code + +// ignore_for_file: camel_case_types, depend_on_referenced_packages +// coverage:ignore-file + +import 'dart:typed_data'; + +import 'package:flat_buffers/flat_buffers.dart' as fb; +import 'package:objectbox/internal.dart'; // generated code can access "internal" functionality +import 'package:objectbox/objectbox.dart'; +import 'package:objectbox_flutter_libs/objectbox_flutter_libs.dart'; + +import '../../../../../../src/backend/impls/objectbox/models/models.dart'; + +export 'package:objectbox/objectbox.dart'; // so that callers only have to import this file + +final _entities = [ + ModelEntity( + id: const IdUid(3, 3564285316728157354), + name: 'ObjectBoxStore', + lastPropertyId: const IdUid(2, 5839091915824466165), + flags: 0, + properties: [ + ModelProperty( + id: const IdUid(1, 5129628855357846983), + name: 'id', + type: 6, + flags: 1), + ModelProperty( + id: const IdUid(2, 5839091915824466165), + name: 'name', + type: 9, + flags: 2048, + indexId: const IdUid(4, 4070553972642232057)) + ], + relations: [], + backlinks: [ + ModelBacklink(name: 'tiles', srcEntity: 'ObjectBoxTile', srcField: '') + ]), + ModelEntity( + id: const IdUid(4, 1221776040269555016), + name: 'ObjectBoxTile', + lastPropertyId: const IdUid(4, 325740197791906264), + flags: 0, + properties: [ + ModelProperty( + id: const IdUid(1, 6708383205929458485), + name: 'id', + type: 6, + flags: 1), + ModelProperty( + id: const IdUid(2, 7632311904162794182), + name: 'url', + type: 9, + flags: 34848, + indexId: const IdUid(5, 6190682863606461840)), + ModelProperty( + id: const IdUid(3, 4021600588433010488), + name: 'lastModified', + type: 10, + flags: 8, + indexId: const IdUid(6, 7105980852148339018)), + ModelProperty( + id: const IdUid(4, 325740197791906264), + name: 'bytes', + type: 23, + flags: 0) + ], + relations: [ + ModelRelation( + id: const IdUid(2, 7253865609688140673), + name: 'stores', + targetId: const IdUid(3, 3564285316728157354)) + ], + backlinks: []) +]; + +/// Shortcut for [Store.new] that passes [getObjectBoxModel] and for Flutter +/// apps by default a [directory] using `defaultStoreDirectory()` from the +/// ObjectBox Flutter library. +/// +/// Note: for desktop apps it is recommended to specify a unique [directory]. +/// +/// See [Store.new] for an explanation of all parameters. +Future openStore( + {String? directory, + int? maxDBSizeInKB, + int? fileMode, + int? maxReaders, + bool queriesCaseSensitiveDefault = true, + String? macosApplicationGroup}) async => + Store(getObjectBoxModel(), + directory: directory ?? (await defaultStoreDirectory()).path, + maxDBSizeInKB: maxDBSizeInKB, + fileMode: fileMode, + maxReaders: maxReaders, + queriesCaseSensitiveDefault: queriesCaseSensitiveDefault, + macosApplicationGroup: macosApplicationGroup); + +/// Returns the ObjectBox model definition for this project for use with +/// [Store.new]. +ModelDefinition getObjectBoxModel() { + final model = ModelInfo( + entities: _entities, + lastEntityId: const IdUid(4, 1221776040269555016), + lastIndexId: const IdUid(6, 7105980852148339018), + lastRelationId: const IdUid(2, 7253865609688140673), + lastSequenceId: const IdUid(0, 0), + retiredEntityUids: const [6351682704282101940, 3030016702019813306], + retiredIndexUids: const [], + retiredPropertyUids: const [ + 4615710800892160360, + 7634309479420249012, + 1360422513191232937, + 2679707907762759593, + 4242260097954056817, + 5886094580441791807 + ], + retiredRelationUids: const [], + modelVersion: 5, + modelVersionParserMinimum: 5, + version: 1); + + final bindings = { + ObjectBoxStore: EntityDefinition( + model: _entities[0], + toOneRelations: (ObjectBoxStore object) => [], + toManyRelations: (ObjectBoxStore object) => + {RelInfo.toManyBacklink(2, object.id): object.tiles}, + getId: (ObjectBoxStore object) => object.id, + setId: (ObjectBoxStore object, int id) { + object.id = id; + }, + objectToFB: (ObjectBoxStore object, fb.Builder fbb) { + final nameOffset = fbb.writeString(object.name); + fbb.startTable(3); + fbb.addInt64(0, object.id); + fbb.addOffset(1, nameOffset); + fbb.finish(fbb.endTable()); + return object.id; + }, + objectFromFB: (Store store, ByteData fbData) { + final buffer = fb.BufferContext(fbData); + final rootOffset = buffer.derefObject(0); + final nameParam = const fb.StringReader(asciiOptimization: true) + .vTableGet(buffer, rootOffset, 6, ''); + final object = ObjectBoxStore(name: nameParam) + ..id = const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0); + InternalToManyAccess.setRelInfo(object.tiles, store, + RelInfo.toManyBacklink(2, object.id)); + return object; + }), + ObjectBoxTile: EntityDefinition( + model: _entities[1], + toOneRelations: (ObjectBoxTile object) => [], + toManyRelations: (ObjectBoxTile object) => + {RelInfo.toMany(2, object.id): object.stores}, + getId: (ObjectBoxTile object) => object.id, + setId: (ObjectBoxTile object, int id) { + object.id = id; + }, + objectToFB: (ObjectBoxTile object, fb.Builder fbb) { + final urlOffset = fbb.writeString(object.url); + final bytesOffset = fbb.writeListInt8(object.bytes); + fbb.startTable(5); + fbb.addInt64(0, object.id); + fbb.addOffset(1, urlOffset); + fbb.addInt64(2, object.lastModified.millisecondsSinceEpoch); + fbb.addOffset(3, bytesOffset); + fbb.finish(fbb.endTable()); + return object.id; + }, + objectFromFB: (Store store, ByteData fbData) { + final buffer = fb.BufferContext(fbData); + final rootOffset = buffer.derefObject(0); + final urlParam = const fb.StringReader(asciiOptimization: true) + .vTableGet(buffer, rootOffset, 6, ''); + final lastModifiedParam = DateTime.fromMillisecondsSinceEpoch( + const fb.Int64Reader().vTableGet(buffer, rootOffset, 8, 0)); + final bytesParam = const fb.Uint8ListReader(lazy: false) + .vTableGet(buffer, rootOffset, 10, Uint8List(0)) as Uint8List; + final object = ObjectBoxTile( + url: urlParam, lastModified: lastModifiedParam, bytes: bytesParam) + ..id = const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0); + InternalToManyAccess.setRelInfo(object.stores, store, + RelInfo.toMany(2, object.id)); + return object; + }) + }; + + return ModelDefinition(model, bindings); +} + +/// [ObjectBoxStore] entity fields to define ObjectBox queries. +class ObjectBoxStore_ { + /// see [ObjectBoxStore.id] + static final id = + QueryIntegerProperty(_entities[0].properties[0]); + + /// see [ObjectBoxStore.name] + static final name = + QueryStringProperty(_entities[0].properties[1]); +} + +/// [ObjectBoxTile] entity fields to define ObjectBox queries. +class ObjectBoxTile_ { + /// see [ObjectBoxTile.id] + static final id = + QueryIntegerProperty(_entities[1].properties[0]); + + /// see [ObjectBoxTile.url] + static final url = + QueryStringProperty(_entities[1].properties[1]); + + /// see [ObjectBoxTile.lastModified] + static final lastModified = + QueryIntegerProperty(_entities[1].properties[2]); + + /// see [ObjectBoxTile.bytes] + static final bytes = + QueryByteVectorProperty(_entities[1].properties[3]); + + /// see [ObjectBoxTile.stores] + static final stores = QueryRelationToMany( + _entities[1].relations[0]); +} diff --git a/lib/src/backend/impls/objectbox/models/models.dart b/lib/src/backend/impls/objectbox/models/models.dart new file mode 100644 index 00000000..dfdafd65 --- /dev/null +++ b/lib/src/backend/impls/objectbox/models/models.dart @@ -0,0 +1,49 @@ +import 'dart:typed_data'; + +import 'package:objectbox/objectbox.dart'; + +import '../../../interfaces/models.dart'; + +@Entity() +class ObjectBoxStore implements BackendStore { + @Id() + int id = 0; + + @override + @Index() + String name; + + @Index() + @Backlink() + final tiles = ToMany(); + + ObjectBoxStore({required this.name}); +} + +@Entity() +class ObjectBoxTile implements BackendTile { + @Id() + int id = 0; + + @override + @Index() + @Unique(onConflict: ConflictStrategy.replace) + String url; + + @override + @Index() + @Property(type: PropertyType.date) + DateTime lastModified; + + @override + Uint8List bytes; + + @Index() + final stores = ToMany(); + + ObjectBoxTile({ + required this.url, + required this.lastModified, + required this.bytes, + }); +} diff --git a/lib/src/backend/interfaces/backend.dart b/lib/src/backend/interfaces/backend.dart new file mode 100644 index 00000000..e19fa624 --- /dev/null +++ b/lib/src/backend/interfaces/backend.dart @@ -0,0 +1,68 @@ +import 'dart:async'; + +import 'package:meta/meta.dart'; + +import '../../../flutter_map_tile_caching.dart'; +import 'models.dart'; + +/// An abstract interface that FMTC will use to communicate with a storage +/// 'backend' (usually one root) +/// +/// To implementers: +/// * Use singletons (with a factory) to ensure consistent state management +/// * Prefer throwing included implementation-generic errors/exceptions +/// * Mark all attributes/methods `@internal`, except the factory +/// +/// To end-users: +/// * Use [FMTCSettings.backend] to set a custom backend +/// * Only the constructor ([FMTCBackend.new]) of an implementation should be +/// used in end applications +abstract interface class FMTCBackend { + const FMTCBackend(); + + abstract final String friendlyIdentifier; + + @internal + abstract final bool supportsSharing; + + @internal + FutureOr initialise({ + String? rootDirectory, + }); + @internal + FutureOr destroy({ + bool deleteRoot = false, + }); + + @internal + FutureOr createStore({ + required String storeName, + }); + @internal + FutureOr resetStore({ + required String storeName, + }); + @internal + FutureOr renameStore({ + required String currentStoreName, + required String newStoreName, + }); + @internal + FutureOr deleteStore({ + required String storeName, + }); + + @internal + Future> readTile({required String url}); + @internal + FutureOr createTile(); + @internal + FutureOr updateTile(); + @internal + FutureOr deleteTile(); + + @internal + FutureOr readLatestTile(); + @internal + FutureOr pruneTilesOlderThan({required DateTime expiry}); +} diff --git a/lib/src/backend/interfaces/models.dart b/lib/src/backend/interfaces/models.dart new file mode 100644 index 00000000..ff6dc95e --- /dev/null +++ b/lib/src/backend/interfaces/models.dart @@ -0,0 +1,11 @@ +import 'dart:typed_data'; + +abstract interface class BackendStore { + abstract String name; +} + +abstract interface class BackendTile { + abstract String url; + abstract DateTime lastModified; + abstract Uint8List bytes; +} diff --git a/lib/src/db/defs/metadata.g.dart b/lib/src/db/defs/metadata.g.dart deleted file mode 100644 index 7275c698..00000000 --- a/lib/src/db/defs/metadata.g.dart +++ /dev/null @@ -1,606 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'metadata.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetDbMetadataCollection on Isar { - IsarCollection get metadata => this.collection(); -} - -const DbMetadataSchema = CollectionSchema( - name: r'DbMetadata', - id: -8585861370448577604, - properties: { - r'data': PropertySchema( - id: 0, - name: r'data', - type: IsarType.string, - ), - r'name': PropertySchema( - id: 1, - name: r'name', - type: IsarType.string, - ) - }, - estimateSize: _dbMetadataEstimateSize, - serialize: _dbMetadataSerialize, - deserialize: _dbMetadataDeserialize, - deserializeProp: _dbMetadataDeserializeProp, - idName: r'id', - indexes: {}, - links: {}, - embeddedSchemas: {}, - getId: _dbMetadataGetId, - getLinks: _dbMetadataGetLinks, - attach: _dbMetadataAttach, - version: '3.1.0+1', -); - -int _dbMetadataEstimateSize( - DbMetadata object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - bytesCount += 3 + object.data.length * 3; - bytesCount += 3 + object.name.length * 3; - return bytesCount; -} - -void _dbMetadataSerialize( - DbMetadata object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeString(offsets[0], object.data); - writer.writeString(offsets[1], object.name); -} - -DbMetadata _dbMetadataDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = DbMetadata( - data: reader.readString(offsets[0]), - name: reader.readString(offsets[1]), - ); - return object; -} - -P _dbMetadataDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readString(offset)) as P; - case 1: - return (reader.readString(offset)) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -Id _dbMetadataGetId(DbMetadata object) { - return object.id; -} - -List> _dbMetadataGetLinks(DbMetadata object) { - return []; -} - -void _dbMetadataAttach(IsarCollection col, Id id, DbMetadata object) {} - -extension DbMetadataQueryWhereSort - on QueryBuilder { - QueryBuilder anyId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension DbMetadataQueryWhere - on QueryBuilder { - QueryBuilder idEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IdWhereClause.between( - lower: id, - upper: id, - )); - }); - } - - QueryBuilder idNotEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ); - } - }); - } - - QueryBuilder idGreaterThan(Id id, - {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: include), - ); - }); - } - - QueryBuilder idLessThan(Id id, - {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: include), - ); - }); - } - - QueryBuilder idBetween( - Id lowerId, - Id upperId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IdWhereClause.between( - lower: lowerId, - includeLower: includeLower, - upper: upperId, - includeUpper: includeUpper, - )); - }); - } -} - -extension DbMetadataQueryFilter - on QueryBuilder { - QueryBuilder dataEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'data', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder dataGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'data', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder dataLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'data', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder dataBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'data', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder dataStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.startsWith( - property: r'data', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder dataEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.endsWith( - property: r'data', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder dataContains( - String value, - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.contains( - property: r'data', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder dataMatches( - String pattern, - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.matches( - property: r'data', - wildcard: pattern, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder dataIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'data', - value: '', - )); - }); - } - - QueryBuilder dataIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - property: r'data', - value: '', - )); - }); - } - - QueryBuilder idEqualTo( - Id value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'id', - value: value, - )); - }); - } - - QueryBuilder idGreaterThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - )); - }); - } - - QueryBuilder idLessThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - )); - }); - } - - QueryBuilder idBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - )); - }); - } - - QueryBuilder nameEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'name', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder nameGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'name', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder nameLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'name', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder nameBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'name', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder nameStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.startsWith( - property: r'name', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder nameEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.endsWith( - property: r'name', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder nameContains( - String value, - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.contains( - property: r'name', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder nameMatches( - String pattern, - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.matches( - property: r'name', - wildcard: pattern, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder nameIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'name', - value: '', - )); - }); - } - - QueryBuilder nameIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - property: r'name', - value: '', - )); - }); - } -} - -extension DbMetadataQueryObject - on QueryBuilder {} - -extension DbMetadataQueryLinks - on QueryBuilder {} - -extension DbMetadataQuerySortBy - on QueryBuilder { - QueryBuilder sortByData() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'data', Sort.asc); - }); - } - - QueryBuilder sortByDataDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'data', Sort.desc); - }); - } - - QueryBuilder sortByName() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'name', Sort.asc); - }); - } - - QueryBuilder sortByNameDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'name', Sort.desc); - }); - } -} - -extension DbMetadataQuerySortThenBy - on QueryBuilder { - QueryBuilder thenByData() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'data', Sort.asc); - }); - } - - QueryBuilder thenByDataDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'data', Sort.desc); - }); - } - - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder thenByName() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'name', Sort.asc); - }); - } - - QueryBuilder thenByNameDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'name', Sort.desc); - }); - } -} - -extension DbMetadataQueryWhereDistinct - on QueryBuilder { - QueryBuilder distinctByData( - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'data', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByName( - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'name', caseSensitive: caseSensitive); - }); - } -} - -extension DbMetadataQueryProperty - on QueryBuilder { - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } - - QueryBuilder dataProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'data'); - }); - } - - QueryBuilder nameProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'name'); - }); - } -} diff --git a/lib/src/db/defs/recovery.g.dart b/lib/src/db/defs/recovery.g.dart deleted file mode 100644 index 6122c7cc..00000000 --- a/lib/src/db/defs/recovery.g.dart +++ /dev/null @@ -1,2602 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'recovery.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetDbRecoverableRegionCollection on Isar { - IsarCollection get recovery => this.collection(); -} - -const DbRecoverableRegionSchema = CollectionSchema( - name: r'DbRecoverableRegion', - id: -8117814053675000476, - properties: { - r'centerLat': PropertySchema( - id: 0, - name: r'centerLat', - type: IsarType.float, - ), - r'centerLng': PropertySchema( - id: 1, - name: r'centerLng', - type: IsarType.float, - ), - r'circleRadius': PropertySchema( - id: 2, - name: r'circleRadius', - type: IsarType.float, - ), - r'end': PropertySchema( - id: 3, - name: r'end', - type: IsarType.int, - ), - r'linePointsLat': PropertySchema( - id: 4, - name: r'linePointsLat', - type: IsarType.floatList, - ), - r'linePointsLng': PropertySchema( - id: 5, - name: r'linePointsLng', - type: IsarType.floatList, - ), - r'lineRadius': PropertySchema( - id: 6, - name: r'lineRadius', - type: IsarType.float, - ), - r'maxZoom': PropertySchema( - id: 7, - name: r'maxZoom', - type: IsarType.byte, - ), - r'minZoom': PropertySchema( - id: 8, - name: r'minZoom', - type: IsarType.byte, - ), - r'nwLat': PropertySchema( - id: 9, - name: r'nwLat', - type: IsarType.float, - ), - r'nwLng': PropertySchema( - id: 10, - name: r'nwLng', - type: IsarType.float, - ), - r'seLat': PropertySchema( - id: 11, - name: r'seLat', - type: IsarType.float, - ), - r'seLng': PropertySchema( - id: 12, - name: r'seLng', - type: IsarType.float, - ), - r'start': PropertySchema( - id: 13, - name: r'start', - type: IsarType.int, - ), - r'storeName': PropertySchema( - id: 14, - name: r'storeName', - type: IsarType.string, - ), - r'time': PropertySchema( - id: 15, - name: r'time', - type: IsarType.dateTime, - ), - r'type': PropertySchema( - id: 16, - name: r'type', - type: IsarType.byte, - enumMap: _DbRecoverableRegiontypeEnumValueMap, - ) - }, - estimateSize: _dbRecoverableRegionEstimateSize, - serialize: _dbRecoverableRegionSerialize, - deserialize: _dbRecoverableRegionDeserialize, - deserializeProp: _dbRecoverableRegionDeserializeProp, - idName: r'id', - indexes: {}, - links: {}, - embeddedSchemas: {}, - getId: _dbRecoverableRegionGetId, - getLinks: _dbRecoverableRegionGetLinks, - attach: _dbRecoverableRegionAttach, - version: '3.1.0+1', -); - -int _dbRecoverableRegionEstimateSize( - DbRecoverableRegion object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - { - final value = object.linePointsLat; - if (value != null) { - bytesCount += 3 + value.length * 4; - } - } - { - final value = object.linePointsLng; - if (value != null) { - bytesCount += 3 + value.length * 4; - } - } - bytesCount += 3 + object.storeName.length * 3; - return bytesCount; -} - -void _dbRecoverableRegionSerialize( - DbRecoverableRegion object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeFloat(offsets[0], object.centerLat); - writer.writeFloat(offsets[1], object.centerLng); - writer.writeFloat(offsets[2], object.circleRadius); - writer.writeInt(offsets[3], object.end); - writer.writeFloatList(offsets[4], object.linePointsLat); - writer.writeFloatList(offsets[5], object.linePointsLng); - writer.writeFloat(offsets[6], object.lineRadius); - writer.writeByte(offsets[7], object.maxZoom); - writer.writeByte(offsets[8], object.minZoom); - writer.writeFloat(offsets[9], object.nwLat); - writer.writeFloat(offsets[10], object.nwLng); - writer.writeFloat(offsets[11], object.seLat); - writer.writeFloat(offsets[12], object.seLng); - writer.writeInt(offsets[13], object.start); - writer.writeString(offsets[14], object.storeName); - writer.writeDateTime(offsets[15], object.time); - writer.writeByte(offsets[16], object.type.index); -} - -DbRecoverableRegion _dbRecoverableRegionDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = DbRecoverableRegion( - centerLat: reader.readFloatOrNull(offsets[0]), - centerLng: reader.readFloatOrNull(offsets[1]), - circleRadius: reader.readFloatOrNull(offsets[2]), - end: reader.readIntOrNull(offsets[3]), - id: id, - linePointsLat: reader.readFloatList(offsets[4]), - linePointsLng: reader.readFloatList(offsets[5]), - lineRadius: reader.readFloatOrNull(offsets[6]), - maxZoom: reader.readByte(offsets[7]), - minZoom: reader.readByte(offsets[8]), - nwLat: reader.readFloatOrNull(offsets[9]), - nwLng: reader.readFloatOrNull(offsets[10]), - seLat: reader.readFloatOrNull(offsets[11]), - seLng: reader.readFloatOrNull(offsets[12]), - start: reader.readInt(offsets[13]), - storeName: reader.readString(offsets[14]), - time: reader.readDateTime(offsets[15]), - type: _DbRecoverableRegiontypeValueEnumMap[ - reader.readByteOrNull(offsets[16])] ?? - RegionType.rectangle, - ); - return object; -} - -P _dbRecoverableRegionDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readFloatOrNull(offset)) as P; - case 1: - return (reader.readFloatOrNull(offset)) as P; - case 2: - return (reader.readFloatOrNull(offset)) as P; - case 3: - return (reader.readIntOrNull(offset)) as P; - case 4: - return (reader.readFloatList(offset)) as P; - case 5: - return (reader.readFloatList(offset)) as P; - case 6: - return (reader.readFloatOrNull(offset)) as P; - case 7: - return (reader.readByte(offset)) as P; - case 8: - return (reader.readByte(offset)) as P; - case 9: - return (reader.readFloatOrNull(offset)) as P; - case 10: - return (reader.readFloatOrNull(offset)) as P; - case 11: - return (reader.readFloatOrNull(offset)) as P; - case 12: - return (reader.readFloatOrNull(offset)) as P; - case 13: - return (reader.readInt(offset)) as P; - case 14: - return (reader.readString(offset)) as P; - case 15: - return (reader.readDateTime(offset)) as P; - case 16: - return (_DbRecoverableRegiontypeValueEnumMap[ - reader.readByteOrNull(offset)] ?? - RegionType.rectangle) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -const _DbRecoverableRegiontypeEnumValueMap = { - 'rectangle': 0, - 'circle': 1, - 'line': 2, -}; -const _DbRecoverableRegiontypeValueEnumMap = { - 0: RegionType.rectangle, - 1: RegionType.circle, - 2: RegionType.line, -}; - -Id _dbRecoverableRegionGetId(DbRecoverableRegion object) { - return object.id; -} - -List> _dbRecoverableRegionGetLinks( - DbRecoverableRegion object) { - return []; -} - -void _dbRecoverableRegionAttach( - IsarCollection col, Id id, DbRecoverableRegion object) {} - -extension DbRecoverableRegionQueryWhereSort - on QueryBuilder { - QueryBuilder anyId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension DbRecoverableRegionQueryWhere - on QueryBuilder { - QueryBuilder - idEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IdWhereClause.between( - lower: id, - upper: id, - )); - }); - } - - QueryBuilder - idNotEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ); - } - }); - } - - QueryBuilder - idGreaterThan(Id id, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: include), - ); - }); - } - - QueryBuilder - idLessThan(Id id, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: include), - ); - }); - } - - QueryBuilder - idBetween( - Id lowerId, - Id upperId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IdWhereClause.between( - lower: lowerId, - includeLower: includeLower, - upper: upperId, - includeUpper: includeUpper, - )); - }); - } -} - -extension DbRecoverableRegionQueryFilter on QueryBuilder { - QueryBuilder - centerLatIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNull( - property: r'centerLat', - )); - }); - } - - QueryBuilder - centerLatIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNotNull( - property: r'centerLat', - )); - }); - } - - QueryBuilder - centerLatEqualTo( - double? value, { - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'centerLat', - value: value, - epsilon: epsilon, - )); - }); - } - - QueryBuilder - centerLatGreaterThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'centerLat', - value: value, - epsilon: epsilon, - )); - }); - } - - QueryBuilder - centerLatLessThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'centerLat', - value: value, - epsilon: epsilon, - )); - }); - } - - QueryBuilder - centerLatBetween( - double? lower, - double? upper, { - bool includeLower = true, - bool includeUpper = true, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'centerLat', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - epsilon: epsilon, - )); - }); - } - - QueryBuilder - centerLngIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNull( - property: r'centerLng', - )); - }); - } - - QueryBuilder - centerLngIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNotNull( - property: r'centerLng', - )); - }); - } - - QueryBuilder - centerLngEqualTo( - double? value, { - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'centerLng', - value: value, - epsilon: epsilon, - )); - }); - } - - QueryBuilder - centerLngGreaterThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'centerLng', - value: value, - epsilon: epsilon, - )); - }); - } - - QueryBuilder - centerLngLessThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'centerLng', - value: value, - epsilon: epsilon, - )); - }); - } - - QueryBuilder - centerLngBetween( - double? lower, - double? upper, { - bool includeLower = true, - bool includeUpper = true, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'centerLng', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - epsilon: epsilon, - )); - }); - } - - QueryBuilder - circleRadiusIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNull( - property: r'circleRadius', - )); - }); - } - - QueryBuilder - circleRadiusIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNotNull( - property: r'circleRadius', - )); - }); - } - - QueryBuilder - circleRadiusEqualTo( - double? value, { - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'circleRadius', - value: value, - epsilon: epsilon, - )); - }); - } - - QueryBuilder - circleRadiusGreaterThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'circleRadius', - value: value, - epsilon: epsilon, - )); - }); - } - - QueryBuilder - circleRadiusLessThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'circleRadius', - value: value, - epsilon: epsilon, - )); - }); - } - - QueryBuilder - circleRadiusBetween( - double? lower, - double? upper, { - bool includeLower = true, - bool includeUpper = true, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'circleRadius', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - epsilon: epsilon, - )); - }); - } - - QueryBuilder - endIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNull( - property: r'end', - )); - }); - } - - QueryBuilder - endIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNotNull( - property: r'end', - )); - }); - } - - QueryBuilder - endEqualTo(int? value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'end', - value: value, - )); - }); - } - - QueryBuilder - endGreaterThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'end', - value: value, - )); - }); - } - - QueryBuilder - endLessThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'end', - value: value, - )); - }); - } - - QueryBuilder - endBetween( - int? lower, - int? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'end', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - )); - }); - } - - QueryBuilder - idEqualTo(Id value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'id', - value: value, - )); - }); - } - - QueryBuilder - idGreaterThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - )); - }); - } - - QueryBuilder - idLessThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - )); - }); - } - - QueryBuilder - idBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - )); - }); - } - - QueryBuilder - linePointsLatIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNull( - property: r'linePointsLat', - )); - }); - } - - QueryBuilder - linePointsLatIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNotNull( - property: r'linePointsLat', - )); - }); - } - - QueryBuilder - linePointsLatElementEqualTo( - double value, { - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'linePointsLat', - value: value, - epsilon: epsilon, - )); - }); - } - - QueryBuilder - linePointsLatElementGreaterThan( - double value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'linePointsLat', - value: value, - epsilon: epsilon, - )); - }); - } - - QueryBuilder - linePointsLatElementLessThan( - double value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'linePointsLat', - value: value, - epsilon: epsilon, - )); - }); - } - - QueryBuilder - linePointsLatElementBetween( - double lower, - double upper, { - bool includeLower = true, - bool includeUpper = true, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'linePointsLat', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - epsilon: epsilon, - )); - }); - } - - QueryBuilder - linePointsLatLengthEqualTo(int length) { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'linePointsLat', - length, - true, - length, - true, - ); - }); - } - - QueryBuilder - linePointsLatIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'linePointsLat', - 0, - true, - 0, - true, - ); - }); - } - - QueryBuilder - linePointsLatIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'linePointsLat', - 0, - false, - 999999, - true, - ); - }); - } - - QueryBuilder - linePointsLatLengthLessThan( - int length, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'linePointsLat', - 0, - true, - length, - include, - ); - }); - } - - QueryBuilder - linePointsLatLengthGreaterThan( - int length, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'linePointsLat', - length, - include, - 999999, - true, - ); - }); - } - - QueryBuilder - linePointsLatLengthBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'linePointsLat', - lower, - includeLower, - upper, - includeUpper, - ); - }); - } - - QueryBuilder - linePointsLngIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNull( - property: r'linePointsLng', - )); - }); - } - - QueryBuilder - linePointsLngIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNotNull( - property: r'linePointsLng', - )); - }); - } - - QueryBuilder - linePointsLngElementEqualTo( - double value, { - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'linePointsLng', - value: value, - epsilon: epsilon, - )); - }); - } - - QueryBuilder - linePointsLngElementGreaterThan( - double value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'linePointsLng', - value: value, - epsilon: epsilon, - )); - }); - } - - QueryBuilder - linePointsLngElementLessThan( - double value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'linePointsLng', - value: value, - epsilon: epsilon, - )); - }); - } - - QueryBuilder - linePointsLngElementBetween( - double lower, - double upper, { - bool includeLower = true, - bool includeUpper = true, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'linePointsLng', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - epsilon: epsilon, - )); - }); - } - - QueryBuilder - linePointsLngLengthEqualTo(int length) { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'linePointsLng', - length, - true, - length, - true, - ); - }); - } - - QueryBuilder - linePointsLngIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'linePointsLng', - 0, - true, - 0, - true, - ); - }); - } - - QueryBuilder - linePointsLngIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'linePointsLng', - 0, - false, - 999999, - true, - ); - }); - } - - QueryBuilder - linePointsLngLengthLessThan( - int length, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'linePointsLng', - 0, - true, - length, - include, - ); - }); - } - - QueryBuilder - linePointsLngLengthGreaterThan( - int length, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'linePointsLng', - length, - include, - 999999, - true, - ); - }); - } - - QueryBuilder - linePointsLngLengthBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'linePointsLng', - lower, - includeLower, - upper, - includeUpper, - ); - }); - } - - QueryBuilder - lineRadiusIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNull( - property: r'lineRadius', - )); - }); - } - - QueryBuilder - lineRadiusIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNotNull( - property: r'lineRadius', - )); - }); - } - - QueryBuilder - lineRadiusEqualTo( - double? value, { - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'lineRadius', - value: value, - epsilon: epsilon, - )); - }); - } - - QueryBuilder - lineRadiusGreaterThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'lineRadius', - value: value, - epsilon: epsilon, - )); - }); - } - - QueryBuilder - lineRadiusLessThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'lineRadius', - value: value, - epsilon: epsilon, - )); - }); - } - - QueryBuilder - lineRadiusBetween( - double? lower, - double? upper, { - bool includeLower = true, - bool includeUpper = true, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'lineRadius', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - epsilon: epsilon, - )); - }); - } - - QueryBuilder - maxZoomEqualTo(int value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'maxZoom', - value: value, - )); - }); - } - - QueryBuilder - maxZoomGreaterThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'maxZoom', - value: value, - )); - }); - } - - QueryBuilder - maxZoomLessThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'maxZoom', - value: value, - )); - }); - } - - QueryBuilder - maxZoomBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'maxZoom', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - )); - }); - } - - QueryBuilder - minZoomEqualTo(int value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'minZoom', - value: value, - )); - }); - } - - QueryBuilder - minZoomGreaterThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'minZoom', - value: value, - )); - }); - } - - QueryBuilder - minZoomLessThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'minZoom', - value: value, - )); - }); - } - - QueryBuilder - minZoomBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'minZoom', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - )); - }); - } - - QueryBuilder - nwLatIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNull( - property: r'nwLat', - )); - }); - } - - QueryBuilder - nwLatIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNotNull( - property: r'nwLat', - )); - }); - } - - QueryBuilder - nwLatEqualTo( - double? value, { - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'nwLat', - value: value, - epsilon: epsilon, - )); - }); - } - - QueryBuilder - nwLatGreaterThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'nwLat', - value: value, - epsilon: epsilon, - )); - }); - } - - QueryBuilder - nwLatLessThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'nwLat', - value: value, - epsilon: epsilon, - )); - }); - } - - QueryBuilder - nwLatBetween( - double? lower, - double? upper, { - bool includeLower = true, - bool includeUpper = true, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'nwLat', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - epsilon: epsilon, - )); - }); - } - - QueryBuilder - nwLngIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNull( - property: r'nwLng', - )); - }); - } - - QueryBuilder - nwLngIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNotNull( - property: r'nwLng', - )); - }); - } - - QueryBuilder - nwLngEqualTo( - double? value, { - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'nwLng', - value: value, - epsilon: epsilon, - )); - }); - } - - QueryBuilder - nwLngGreaterThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'nwLng', - value: value, - epsilon: epsilon, - )); - }); - } - - QueryBuilder - nwLngLessThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'nwLng', - value: value, - epsilon: epsilon, - )); - }); - } - - QueryBuilder - nwLngBetween( - double? lower, - double? upper, { - bool includeLower = true, - bool includeUpper = true, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'nwLng', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - epsilon: epsilon, - )); - }); - } - - QueryBuilder - seLatIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNull( - property: r'seLat', - )); - }); - } - - QueryBuilder - seLatIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNotNull( - property: r'seLat', - )); - }); - } - - QueryBuilder - seLatEqualTo( - double? value, { - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'seLat', - value: value, - epsilon: epsilon, - )); - }); - } - - QueryBuilder - seLatGreaterThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'seLat', - value: value, - epsilon: epsilon, - )); - }); - } - - QueryBuilder - seLatLessThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'seLat', - value: value, - epsilon: epsilon, - )); - }); - } - - QueryBuilder - seLatBetween( - double? lower, - double? upper, { - bool includeLower = true, - bool includeUpper = true, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'seLat', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - epsilon: epsilon, - )); - }); - } - - QueryBuilder - seLngIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNull( - property: r'seLng', - )); - }); - } - - QueryBuilder - seLngIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNotNull( - property: r'seLng', - )); - }); - } - - QueryBuilder - seLngEqualTo( - double? value, { - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'seLng', - value: value, - epsilon: epsilon, - )); - }); - } - - QueryBuilder - seLngGreaterThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'seLng', - value: value, - epsilon: epsilon, - )); - }); - } - - QueryBuilder - seLngLessThan( - double? value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'seLng', - value: value, - epsilon: epsilon, - )); - }); - } - - QueryBuilder - seLngBetween( - double? lower, - double? upper, { - bool includeLower = true, - bool includeUpper = true, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'seLng', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - epsilon: epsilon, - )); - }); - } - - QueryBuilder - startEqualTo(int value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'start', - value: value, - )); - }); - } - - QueryBuilder - startGreaterThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'start', - value: value, - )); - }); - } - - QueryBuilder - startLessThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'start', - value: value, - )); - }); - } - - QueryBuilder - startBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'start', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - )); - }); - } - - QueryBuilder - storeNameEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'storeName', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - storeNameGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'storeName', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - storeNameLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'storeName', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - storeNameBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'storeName', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - storeNameStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.startsWith( - property: r'storeName', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - storeNameEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.endsWith( - property: r'storeName', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - storeNameContains(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.contains( - property: r'storeName', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - storeNameMatches(String pattern, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.matches( - property: r'storeName', - wildcard: pattern, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - storeNameIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'storeName', - value: '', - )); - }); - } - - QueryBuilder - storeNameIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - property: r'storeName', - value: '', - )); - }); - } - - QueryBuilder - timeEqualTo(DateTime value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'time', - value: value, - )); - }); - } - - QueryBuilder - timeGreaterThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'time', - value: value, - )); - }); - } - - QueryBuilder - timeLessThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'time', - value: value, - )); - }); - } - - QueryBuilder - timeBetween( - DateTime lower, - DateTime upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'time', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - )); - }); - } - - QueryBuilder - typeEqualTo(RegionType value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'type', - value: value, - )); - }); - } - - QueryBuilder - typeGreaterThan( - RegionType value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'type', - value: value, - )); - }); - } - - QueryBuilder - typeLessThan( - RegionType value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'type', - value: value, - )); - }); - } - - QueryBuilder - typeBetween( - RegionType lower, - RegionType upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'type', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - )); - }); - } -} - -extension DbRecoverableRegionQueryObject on QueryBuilder {} - -extension DbRecoverableRegionQueryLinks on QueryBuilder {} - -extension DbRecoverableRegionQuerySortBy - on QueryBuilder { - QueryBuilder - sortByCenterLat() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'centerLat', Sort.asc); - }); - } - - QueryBuilder - sortByCenterLatDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'centerLat', Sort.desc); - }); - } - - QueryBuilder - sortByCenterLng() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'centerLng', Sort.asc); - }); - } - - QueryBuilder - sortByCenterLngDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'centerLng', Sort.desc); - }); - } - - QueryBuilder - sortByCircleRadius() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'circleRadius', Sort.asc); - }); - } - - QueryBuilder - sortByCircleRadiusDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'circleRadius', Sort.desc); - }); - } - - QueryBuilder - sortByEnd() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'end', Sort.asc); - }); - } - - QueryBuilder - sortByEndDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'end', Sort.desc); - }); - } - - QueryBuilder - sortByLineRadius() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lineRadius', Sort.asc); - }); - } - - QueryBuilder - sortByLineRadiusDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lineRadius', Sort.desc); - }); - } - - QueryBuilder - sortByMaxZoom() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'maxZoom', Sort.asc); - }); - } - - QueryBuilder - sortByMaxZoomDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'maxZoom', Sort.desc); - }); - } - - QueryBuilder - sortByMinZoom() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'minZoom', Sort.asc); - }); - } - - QueryBuilder - sortByMinZoomDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'minZoom', Sort.desc); - }); - } - - QueryBuilder - sortByNwLat() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'nwLat', Sort.asc); - }); - } - - QueryBuilder - sortByNwLatDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'nwLat', Sort.desc); - }); - } - - QueryBuilder - sortByNwLng() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'nwLng', Sort.asc); - }); - } - - QueryBuilder - sortByNwLngDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'nwLng', Sort.desc); - }); - } - - QueryBuilder - sortBySeLat() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'seLat', Sort.asc); - }); - } - - QueryBuilder - sortBySeLatDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'seLat', Sort.desc); - }); - } - - QueryBuilder - sortBySeLng() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'seLng', Sort.asc); - }); - } - - QueryBuilder - sortBySeLngDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'seLng', Sort.desc); - }); - } - - QueryBuilder - sortByStart() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'start', Sort.asc); - }); - } - - QueryBuilder - sortByStartDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'start', Sort.desc); - }); - } - - QueryBuilder - sortByStoreName() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'storeName', Sort.asc); - }); - } - - QueryBuilder - sortByStoreNameDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'storeName', Sort.desc); - }); - } - - QueryBuilder - sortByTime() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'time', Sort.asc); - }); - } - - QueryBuilder - sortByTimeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'time', Sort.desc); - }); - } - - QueryBuilder - sortByType() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'type', Sort.asc); - }); - } - - QueryBuilder - sortByTypeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'type', Sort.desc); - }); - } -} - -extension DbRecoverableRegionQuerySortThenBy - on QueryBuilder { - QueryBuilder - thenByCenterLat() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'centerLat', Sort.asc); - }); - } - - QueryBuilder - thenByCenterLatDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'centerLat', Sort.desc); - }); - } - - QueryBuilder - thenByCenterLng() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'centerLng', Sort.asc); - }); - } - - QueryBuilder - thenByCenterLngDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'centerLng', Sort.desc); - }); - } - - QueryBuilder - thenByCircleRadius() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'circleRadius', Sort.asc); - }); - } - - QueryBuilder - thenByCircleRadiusDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'circleRadius', Sort.desc); - }); - } - - QueryBuilder - thenByEnd() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'end', Sort.asc); - }); - } - - QueryBuilder - thenByEndDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'end', Sort.desc); - }); - } - - QueryBuilder - thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder - thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder - thenByLineRadius() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lineRadius', Sort.asc); - }); - } - - QueryBuilder - thenByLineRadiusDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lineRadius', Sort.desc); - }); - } - - QueryBuilder - thenByMaxZoom() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'maxZoom', Sort.asc); - }); - } - - QueryBuilder - thenByMaxZoomDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'maxZoom', Sort.desc); - }); - } - - QueryBuilder - thenByMinZoom() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'minZoom', Sort.asc); - }); - } - - QueryBuilder - thenByMinZoomDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'minZoom', Sort.desc); - }); - } - - QueryBuilder - thenByNwLat() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'nwLat', Sort.asc); - }); - } - - QueryBuilder - thenByNwLatDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'nwLat', Sort.desc); - }); - } - - QueryBuilder - thenByNwLng() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'nwLng', Sort.asc); - }); - } - - QueryBuilder - thenByNwLngDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'nwLng', Sort.desc); - }); - } - - QueryBuilder - thenBySeLat() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'seLat', Sort.asc); - }); - } - - QueryBuilder - thenBySeLatDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'seLat', Sort.desc); - }); - } - - QueryBuilder - thenBySeLng() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'seLng', Sort.asc); - }); - } - - QueryBuilder - thenBySeLngDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'seLng', Sort.desc); - }); - } - - QueryBuilder - thenByStart() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'start', Sort.asc); - }); - } - - QueryBuilder - thenByStartDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'start', Sort.desc); - }); - } - - QueryBuilder - thenByStoreName() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'storeName', Sort.asc); - }); - } - - QueryBuilder - thenByStoreNameDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'storeName', Sort.desc); - }); - } - - QueryBuilder - thenByTime() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'time', Sort.asc); - }); - } - - QueryBuilder - thenByTimeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'time', Sort.desc); - }); - } - - QueryBuilder - thenByType() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'type', Sort.asc); - }); - } - - QueryBuilder - thenByTypeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'type', Sort.desc); - }); - } -} - -extension DbRecoverableRegionQueryWhereDistinct - on QueryBuilder { - QueryBuilder - distinctByCenterLat() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'centerLat'); - }); - } - - QueryBuilder - distinctByCenterLng() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'centerLng'); - }); - } - - QueryBuilder - distinctByCircleRadius() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'circleRadius'); - }); - } - - QueryBuilder - distinctByEnd() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'end'); - }); - } - - QueryBuilder - distinctByLinePointsLat() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'linePointsLat'); - }); - } - - QueryBuilder - distinctByLinePointsLng() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'linePointsLng'); - }); - } - - QueryBuilder - distinctByLineRadius() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'lineRadius'); - }); - } - - QueryBuilder - distinctByMaxZoom() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'maxZoom'); - }); - } - - QueryBuilder - distinctByMinZoom() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'minZoom'); - }); - } - - QueryBuilder - distinctByNwLat() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'nwLat'); - }); - } - - QueryBuilder - distinctByNwLng() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'nwLng'); - }); - } - - QueryBuilder - distinctBySeLat() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'seLat'); - }); - } - - QueryBuilder - distinctBySeLng() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'seLng'); - }); - } - - QueryBuilder - distinctByStart() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'start'); - }); - } - - QueryBuilder - distinctByStoreName({bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'storeName', caseSensitive: caseSensitive); - }); - } - - QueryBuilder - distinctByTime() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'time'); - }); - } - - QueryBuilder - distinctByType() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'type'); - }); - } -} - -extension DbRecoverableRegionQueryProperty - on QueryBuilder { - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } - - QueryBuilder - centerLatProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'centerLat'); - }); - } - - QueryBuilder - centerLngProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'centerLng'); - }); - } - - QueryBuilder - circleRadiusProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'circleRadius'); - }); - } - - QueryBuilder endProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'end'); - }); - } - - QueryBuilder?, QQueryOperations> - linePointsLatProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'linePointsLat'); - }); - } - - QueryBuilder?, QQueryOperations> - linePointsLngProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'linePointsLng'); - }); - } - - QueryBuilder - lineRadiusProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'lineRadius'); - }); - } - - QueryBuilder maxZoomProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'maxZoom'); - }); - } - - QueryBuilder minZoomProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'minZoom'); - }); - } - - QueryBuilder nwLatProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'nwLat'); - }); - } - - QueryBuilder nwLngProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'nwLng'); - }); - } - - QueryBuilder seLatProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'seLat'); - }); - } - - QueryBuilder seLngProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'seLng'); - }); - } - - QueryBuilder startProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'start'); - }); - } - - QueryBuilder - storeNameProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'storeName'); - }); - } - - QueryBuilder timeProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'time'); - }); - } - - QueryBuilder - typeProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'type'); - }); - } -} diff --git a/lib/src/db/defs/store_descriptor.g.dart b/lib/src/db/defs/store_descriptor.g.dart deleted file mode 100644 index baff9402..00000000 --- a/lib/src/db/defs/store_descriptor.g.dart +++ /dev/null @@ -1,660 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'store_descriptor.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetDbStoreDescriptorCollection on Isar { - IsarCollection get storeDescriptor => this.collection(); -} - -const DbStoreDescriptorSchema = CollectionSchema( - name: r'DbStoreDescriptor', - id: 1365152130637522244, - properties: { - r'hits': PropertySchema( - id: 0, - name: r'hits', - type: IsarType.long, - ), - r'misses': PropertySchema( - id: 1, - name: r'misses', - type: IsarType.long, - ), - r'name': PropertySchema( - id: 2, - name: r'name', - type: IsarType.string, - ) - }, - estimateSize: _dbStoreDescriptorEstimateSize, - serialize: _dbStoreDescriptorSerialize, - deserialize: _dbStoreDescriptorDeserialize, - deserializeProp: _dbStoreDescriptorDeserializeProp, - idName: r'id', - indexes: {}, - links: {}, - embeddedSchemas: {}, - getId: _dbStoreDescriptorGetId, - getLinks: _dbStoreDescriptorGetLinks, - attach: _dbStoreDescriptorAttach, - version: '3.1.0+1', -); - -int _dbStoreDescriptorEstimateSize( - DbStoreDescriptor object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - bytesCount += 3 + object.name.length * 3; - return bytesCount; -} - -void _dbStoreDescriptorSerialize( - DbStoreDescriptor object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeLong(offsets[0], object.hits); - writer.writeLong(offsets[1], object.misses); - writer.writeString(offsets[2], object.name); -} - -DbStoreDescriptor _dbStoreDescriptorDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = DbStoreDescriptor( - name: reader.readString(offsets[2]), - ); - object.hits = reader.readLong(offsets[0]); - object.misses = reader.readLong(offsets[1]); - return object; -} - -P _dbStoreDescriptorDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readLong(offset)) as P; - case 1: - return (reader.readLong(offset)) as P; - case 2: - return (reader.readString(offset)) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -Id _dbStoreDescriptorGetId(DbStoreDescriptor object) { - return object.id; -} - -List> _dbStoreDescriptorGetLinks( - DbStoreDescriptor object) { - return []; -} - -void _dbStoreDescriptorAttach( - IsarCollection col, Id id, DbStoreDescriptor object) {} - -extension DbStoreDescriptorQueryWhereSort - on QueryBuilder { - QueryBuilder anyId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension DbStoreDescriptorQueryWhere - on QueryBuilder { - QueryBuilder - idEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IdWhereClause.between( - lower: id, - upper: id, - )); - }); - } - - QueryBuilder - idNotEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ); - } - }); - } - - QueryBuilder - idGreaterThan(Id id, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: include), - ); - }); - } - - QueryBuilder - idLessThan(Id id, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: include), - ); - }); - } - - QueryBuilder - idBetween( - Id lowerId, - Id upperId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IdWhereClause.between( - lower: lowerId, - includeLower: includeLower, - upper: upperId, - includeUpper: includeUpper, - )); - }); - } -} - -extension DbStoreDescriptorQueryFilter - on QueryBuilder { - QueryBuilder - hitsEqualTo(int value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'hits', - value: value, - )); - }); - } - - QueryBuilder - hitsGreaterThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'hits', - value: value, - )); - }); - } - - QueryBuilder - hitsLessThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'hits', - value: value, - )); - }); - } - - QueryBuilder - hitsBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'hits', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - )); - }); - } - - QueryBuilder - idEqualTo(Id value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'id', - value: value, - )); - }); - } - - QueryBuilder - idGreaterThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - )); - }); - } - - QueryBuilder - idLessThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - )); - }); - } - - QueryBuilder - idBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - )); - }); - } - - QueryBuilder - missesEqualTo(int value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'misses', - value: value, - )); - }); - } - - QueryBuilder - missesGreaterThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'misses', - value: value, - )); - }); - } - - QueryBuilder - missesLessThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'misses', - value: value, - )); - }); - } - - QueryBuilder - missesBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'misses', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - )); - }); - } - - QueryBuilder - nameEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'name', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - nameGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'name', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - nameLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'name', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - nameBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'name', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - nameStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.startsWith( - property: r'name', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - nameEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.endsWith( - property: r'name', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - nameContains(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.contains( - property: r'name', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - nameMatches(String pattern, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.matches( - property: r'name', - wildcard: pattern, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - nameIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'name', - value: '', - )); - }); - } - - QueryBuilder - nameIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - property: r'name', - value: '', - )); - }); - } -} - -extension DbStoreDescriptorQueryObject - on QueryBuilder {} - -extension DbStoreDescriptorQueryLinks - on QueryBuilder {} - -extension DbStoreDescriptorQuerySortBy - on QueryBuilder { - QueryBuilder - sortByHits() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'hits', Sort.asc); - }); - } - - QueryBuilder - sortByHitsDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'hits', Sort.desc); - }); - } - - QueryBuilder - sortByMisses() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'misses', Sort.asc); - }); - } - - QueryBuilder - sortByMissesDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'misses', Sort.desc); - }); - } - - QueryBuilder - sortByName() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'name', Sort.asc); - }); - } - - QueryBuilder - sortByNameDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'name', Sort.desc); - }); - } -} - -extension DbStoreDescriptorQuerySortThenBy - on QueryBuilder { - QueryBuilder - thenByHits() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'hits', Sort.asc); - }); - } - - QueryBuilder - thenByHitsDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'hits', Sort.desc); - }); - } - - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder - thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder - thenByMisses() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'misses', Sort.asc); - }); - } - - QueryBuilder - thenByMissesDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'misses', Sort.desc); - }); - } - - QueryBuilder - thenByName() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'name', Sort.asc); - }); - } - - QueryBuilder - thenByNameDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'name', Sort.desc); - }); - } -} - -extension DbStoreDescriptorQueryWhereDistinct - on QueryBuilder { - QueryBuilder - distinctByHits() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'hits'); - }); - } - - QueryBuilder - distinctByMisses() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'misses'); - }); - } - - QueryBuilder distinctByName( - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'name', caseSensitive: caseSensitive); - }); - } -} - -extension DbStoreDescriptorQueryProperty - on QueryBuilder { - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } - - QueryBuilder hitsProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'hits'); - }); - } - - QueryBuilder missesProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'misses'); - }); - } - - QueryBuilder nameProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'name'); - }); - } -} diff --git a/lib/src/db/defs/tile.g.dart b/lib/src/db/defs/tile.g.dart deleted file mode 100644 index f50c76b8..00000000 --- a/lib/src/db/defs/tile.g.dart +++ /dev/null @@ -1,785 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'tile.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetDbTileCollection on Isar { - IsarCollection get tiles => this.collection(); -} - -const DbTileSchema = CollectionSchema( - name: r'DbTile', - id: -5030120948284417748, - properties: { - r'bytes': PropertySchema( - id: 0, - name: r'bytes', - type: IsarType.byteList, - ), - r'lastModified': PropertySchema( - id: 1, - name: r'lastModified', - type: IsarType.dateTime, - ), - r'url': PropertySchema( - id: 2, - name: r'url', - type: IsarType.string, - ) - }, - estimateSize: _dbTileEstimateSize, - serialize: _dbTileSerialize, - deserialize: _dbTileDeserialize, - deserializeProp: _dbTileDeserializeProp, - idName: r'id', - indexes: { - r'lastModified': IndexSchema( - id: 5953778071269117195, - name: r'lastModified', - unique: false, - replace: false, - properties: [ - IndexPropertySchema( - name: r'lastModified', - type: IndexType.value, - caseSensitive: false, - ) - ], - ) - }, - links: {}, - embeddedSchemas: {}, - getId: _dbTileGetId, - getLinks: _dbTileGetLinks, - attach: _dbTileAttach, - version: '3.1.0+1', -); - -int _dbTileEstimateSize( - DbTile object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - bytesCount += 3 + object.bytes.length; - bytesCount += 3 + object.url.length * 3; - return bytesCount; -} - -void _dbTileSerialize( - DbTile object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeByteList(offsets[0], object.bytes); - writer.writeDateTime(offsets[1], object.lastModified); - writer.writeString(offsets[2], object.url); -} - -DbTile _dbTileDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = DbTile( - bytes: reader.readByteList(offsets[0]) ?? [], - url: reader.readString(offsets[2]), - ); - return object; -} - -P _dbTileDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readByteList(offset) ?? []) as P; - case 1: - return (reader.readDateTime(offset)) as P; - case 2: - return (reader.readString(offset)) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -Id _dbTileGetId(DbTile object) { - return object.id; -} - -List> _dbTileGetLinks(DbTile object) { - return []; -} - -void _dbTileAttach(IsarCollection col, Id id, DbTile object) {} - -extension DbTileQueryWhereSort on QueryBuilder { - QueryBuilder anyId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } - - QueryBuilder anyLastModified() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - const IndexWhereClause.any(indexName: r'lastModified'), - ); - }); - } -} - -extension DbTileQueryWhere on QueryBuilder { - QueryBuilder idEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IdWhereClause.between( - lower: id, - upper: id, - )); - }); - } - - QueryBuilder idNotEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ); - } - }); - } - - QueryBuilder idGreaterThan(Id id, - {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: include), - ); - }); - } - - QueryBuilder idLessThan(Id id, - {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: include), - ); - }); - } - - QueryBuilder idBetween( - Id lowerId, - Id upperId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IdWhereClause.between( - lower: lowerId, - includeLower: includeLower, - upper: upperId, - includeUpper: includeUpper, - )); - }); - } - - QueryBuilder lastModifiedEqualTo( - DateTime lastModified) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IndexWhereClause.equalTo( - indexName: r'lastModified', - value: [lastModified], - )); - }); - } - - QueryBuilder lastModifiedNotEqualTo( - DateTime lastModified) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause(IndexWhereClause.between( - indexName: r'lastModified', - lower: [], - upper: [lastModified], - includeUpper: false, - )) - .addWhereClause(IndexWhereClause.between( - indexName: r'lastModified', - lower: [lastModified], - includeLower: false, - upper: [], - )); - } else { - return query - .addWhereClause(IndexWhereClause.between( - indexName: r'lastModified', - lower: [lastModified], - includeLower: false, - upper: [], - )) - .addWhereClause(IndexWhereClause.between( - indexName: r'lastModified', - lower: [], - upper: [lastModified], - includeUpper: false, - )); - } - }); - } - - QueryBuilder lastModifiedGreaterThan( - DateTime lastModified, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IndexWhereClause.between( - indexName: r'lastModified', - lower: [lastModified], - includeLower: include, - upper: [], - )); - }); - } - - QueryBuilder lastModifiedLessThan( - DateTime lastModified, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IndexWhereClause.between( - indexName: r'lastModified', - lower: [], - upper: [lastModified], - includeUpper: include, - )); - }); - } - - QueryBuilder lastModifiedBetween( - DateTime lowerLastModified, - DateTime upperLastModified, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IndexWhereClause.between( - indexName: r'lastModified', - lower: [lowerLastModified], - includeLower: includeLower, - upper: [upperLastModified], - includeUpper: includeUpper, - )); - }); - } -} - -extension DbTileQueryFilter on QueryBuilder { - QueryBuilder bytesElementEqualTo( - int value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'bytes', - value: value, - )); - }); - } - - QueryBuilder bytesElementGreaterThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'bytes', - value: value, - )); - }); - } - - QueryBuilder bytesElementLessThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'bytes', - value: value, - )); - }); - } - - QueryBuilder bytesElementBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'bytes', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - )); - }); - } - - QueryBuilder bytesLengthEqualTo( - int length) { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'bytes', - length, - true, - length, - true, - ); - }); - } - - QueryBuilder bytesIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'bytes', - 0, - true, - 0, - true, - ); - }); - } - - QueryBuilder bytesIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'bytes', - 0, - false, - 999999, - true, - ); - }); - } - - QueryBuilder bytesLengthLessThan( - int length, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'bytes', - 0, - true, - length, - include, - ); - }); - } - - QueryBuilder bytesLengthGreaterThan( - int length, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'bytes', - length, - include, - 999999, - true, - ); - }); - } - - QueryBuilder bytesLengthBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'bytes', - lower, - includeLower, - upper, - includeUpper, - ); - }); - } - - QueryBuilder idEqualTo(Id value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'id', - value: value, - )); - }); - } - - QueryBuilder idGreaterThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - )); - }); - } - - QueryBuilder idLessThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - )); - }); - } - - QueryBuilder idBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - )); - }); - } - - QueryBuilder lastModifiedEqualTo( - DateTime value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'lastModified', - value: value, - )); - }); - } - - QueryBuilder lastModifiedGreaterThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'lastModified', - value: value, - )); - }); - } - - QueryBuilder lastModifiedLessThan( - DateTime value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'lastModified', - value: value, - )); - }); - } - - QueryBuilder lastModifiedBetween( - DateTime lower, - DateTime upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'lastModified', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - )); - }); - } - - QueryBuilder urlEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'url', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder urlGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'url', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder urlLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'url', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder urlBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'url', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder urlStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.startsWith( - property: r'url', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder urlEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.endsWith( - property: r'url', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder urlContains(String value, - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.contains( - property: r'url', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder urlMatches(String pattern, - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.matches( - property: r'url', - wildcard: pattern, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder urlIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'url', - value: '', - )); - }); - } - - QueryBuilder urlIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - property: r'url', - value: '', - )); - }); - } -} - -extension DbTileQueryObject on QueryBuilder {} - -extension DbTileQueryLinks on QueryBuilder {} - -extension DbTileQuerySortBy on QueryBuilder { - QueryBuilder sortByLastModified() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lastModified', Sort.asc); - }); - } - - QueryBuilder sortByLastModifiedDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lastModified', Sort.desc); - }); - } - - QueryBuilder sortByUrl() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'url', Sort.asc); - }); - } - - QueryBuilder sortByUrlDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'url', Sort.desc); - }); - } -} - -extension DbTileQuerySortThenBy on QueryBuilder { - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder thenByLastModified() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lastModified', Sort.asc); - }); - } - - QueryBuilder thenByLastModifiedDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lastModified', Sort.desc); - }); - } - - QueryBuilder thenByUrl() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'url', Sort.asc); - }); - } - - QueryBuilder thenByUrlDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'url', Sort.desc); - }); - } -} - -extension DbTileQueryWhereDistinct on QueryBuilder { - QueryBuilder distinctByBytes() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'bytes'); - }); - } - - QueryBuilder distinctByLastModified() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'lastModified'); - }); - } - - QueryBuilder distinctByUrl( - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'url', caseSensitive: caseSensitive); - }); - } -} - -extension DbTileQueryProperty on QueryBuilder { - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } - - QueryBuilder, QQueryOperations> bytesProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'bytes'); - }); - } - - QueryBuilder lastModifiedProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'lastModified'); - }); - } - - QueryBuilder urlProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'url'); - }); - } -} diff --git a/lib/src/settings/fmtc_settings.dart b/lib/src/settings/fmtc_settings.dart index 53c84357..3d10dc59 100644 --- a/lib/src/settings/fmtc_settings.dart +++ b/lib/src/settings/fmtc_settings.dart @@ -5,6 +5,8 @@ part of flutter_map_tile_caching; /// Global FMTC settings class FMTCSettings { + final FMTCBackend backend; + /// Default settings used when creating an [FMTCTileProvider] /// /// Can be overridden on a case-to-case basis when actually creating the tile @@ -42,9 +44,11 @@ class FMTCSettings { /// Create custom global FMTC settings FMTCSettings({ + FMTCBackend? backend, FMTCTileProviderSettings? defaultTileProviderSettings, this.databaseMaxSize = 2048, this.databaseCompactCondition = const CompactCondition(minRatio: 2), - }) : defaultTileProviderSettings = + }) : backend = backend ?? ObjectBoxBackend(), + defaultTileProviderSettings = defaultTileProviderSettings ?? FMTCTileProviderSettings(); } diff --git a/pubspec.yaml b/pubspec.yaml index 40c7f2d8..2de9d4a9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,6 +38,8 @@ dependencies: isar_flutter_libs: ^3.1.0+1 latlong2: ^0.9.0 meta: ^1.7.0 + objectbox: ^2.3.1 + objectbox_flutter_libs: any path: ^1.8.2 path_provider: ^2.0.7 queue: ^3.1.0+1 @@ -45,8 +47,12 @@ dependencies: watcher: ^1.0.2 dev_dependencies: - build_runner: ^2.3.2 - isar_generator: ^3.1.0+1 + build_runner: ^2.4.6 + #isar_generator: ^3.1.0+1 + objectbox_generator: any test: ^1.24.4 flutter: null + +objectbox: + output_dir: src/backend/impls/objectbox/models/generated From 58fe6a3b02c694bcbe968b26cf62c83c781adfa5 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sat, 4 Nov 2023 19:00:27 +0000 Subject: [PATCH 076/168] Finished initial uber-basic implementation of `ObjectBoxBackend` Changed exporting/visibility to avoid exporting internals Former-commit-id: ffc82d9de8de6b327c3e11124d4ffb880ff41ccc [formerly 7d159761d20eafff11801353809a49c91da38acb] Former-commit-id: db29376dca5b8f2ac270cd921ccb26af6d7edea4 --- lib/flutter_map_tile_caching.dart | 2 + lib/fmtc_module_api.dart | 1 + lib/src/backend/export_plus.dart | 2 + lib/src/backend/export_std.dart | 3 + lib/src/backend/impls/objectbox/backend.dart | 160 +++++++++++------- .../models/generated/objectbox-model.json | 2 +- .../models/generated/objectbox.g.dart | 2 +- .../impls/objectbox/models/models.dart | 5 +- lib/src/backend/interfaces/backend.dart | 41 ++--- lib/src/backend/interfaces/models.dart | 22 ++- 10 files changed, 147 insertions(+), 93 deletions(-) create mode 100644 lib/src/backend/export_plus.dart create mode 100644 lib/src/backend/export_std.dart diff --git a/lib/flutter_map_tile_caching.dart b/lib/flutter_map_tile_caching.dart index 342bd60f..5b490223 100644 --- a/lib/flutter_map_tile_caching.dart +++ b/lib/flutter_map_tile_caching.dart @@ -33,6 +33,7 @@ import 'package:path_provider/path_provider.dart'; import 'package:stream_transform/stream_transform.dart'; import 'package:watcher/watcher.dart'; +import 'src/backend/export_std.dart'; import 'src/bulk_download/instance.dart'; import 'src/bulk_download/rate_limited_stream.dart'; import 'src/bulk_download/tile_loops/shared.dart'; @@ -51,6 +52,7 @@ import 'src/misc/obscure_query_params.dart'; import 'src/misc/typedefs.dart'; import 'src/providers/image_provider.dart'; +export 'src/backend/export_std.dart'; export 'src/errors/browsing.dart'; export 'src/errors/damaged_store.dart'; export 'src/errors/initialisation.dart'; diff --git a/lib/fmtc_module_api.dart b/lib/fmtc_module_api.dart index ff8a07a3..dfbf0ae6 100644 --- a/lib/fmtc_module_api.dart +++ b/lib/fmtc_module_api.dart @@ -27,6 +27,7 @@ /// **Do not use in normal applications. I may be unable to offer support.** library fmtc_module_api; +export 'src/backend/export_plus.dart'; export 'src/db/defs/metadata.dart'; export 'src/db/defs/store_descriptor.dart'; export 'src/db/defs/tile.dart'; diff --git a/lib/src/backend/export_plus.dart b/lib/src/backend/export_plus.dart new file mode 100644 index 00000000..56d57862 --- /dev/null +++ b/lib/src/backend/export_plus.dart @@ -0,0 +1,2 @@ +export 'export_std.dart'; +export 'interfaces/models.dart'; diff --git a/lib/src/backend/export_std.dart b/lib/src/backend/export_std.dart new file mode 100644 index 00000000..5f95a06f --- /dev/null +++ b/lib/src/backend/export_std.dart @@ -0,0 +1,3 @@ +export 'errors.dart'; +export 'impls/objectbox/backend.dart'; +export 'interfaces/backend.dart'; diff --git a/lib/src/backend/impls/objectbox/backend.dart b/lib/src/backend/impls/objectbox/backend.dart index 045ca8e3..29976d35 100644 --- a/lib/src/backend/impls/objectbox/backend.dart +++ b/lib/src/backend/impls/objectbox/backend.dart @@ -1,8 +1,8 @@ import 'dart:async'; import 'dart:io'; +import 'dart:typed_data'; import 'package:collection/collection.dart'; -import 'package:meta/meta.dart'; import 'package:path_provider/path_provider.dart'; import '../../../misc/exts.dart'; @@ -12,35 +12,31 @@ import 'models/generated/objectbox.g.dart'; import 'models/models.dart'; /// Implementation of [FMTCBackend] that uses ObjectBox as the storage database -/// -/// Only the factory constructor ([ObjectBoxBackend.new]), and -/// [friendlyIdentifier], should be used in end-applications. Other methods are -/// for internal use only. -class ObjectBoxBackend implements FMTCBackend { +abstract interface class ObjectBoxBackend implements FMTCBackend { + /// Implementation of [FMTCBackend] that uses ObjectBox as the storage + /// database factory ObjectBoxBackend() => _instance; - static final ObjectBoxBackend _instance = ObjectBoxBackend._(); - ObjectBoxBackend._(); + static final _instance = _ObjectBoxBackendImpl._(); +} + +class _ObjectBoxBackendImpl implements ObjectBoxBackend { + _ObjectBoxBackendImpl._(); - Store? _root; // Must not be closed if not `null` - Store get _expectRoot => _root ?? (throw RootUnavailable()); + Store? root; // Must not be closed if not `null` + Store get expectRoot => root ?? (throw RootUnavailable()); - Future _getStore(String name) async => - (await _expectRoot + Future getStore(String name) async => + (await expectRoot .box() .query(ObjectBoxStore_.name.equals(name)) .build() - .findFirstAsync()) ?? + .findUniqueAsync()) ?? (throw StoreUnavailable(storeName: name)); @override String get friendlyIdentifier => 'ObjectBox'; @override - @internal - bool get supportsSharing => false; - - @override - @internal Future initialise({ String? rootDirectory, }) async { @@ -49,50 +45,49 @@ class ObjectBoxBackend implements FMTCBackend { : Directory(rootDirectory)) >> 'fmtc') .create(recursive: true); - _root = await openStore(directory: dir.absolute.path); + root = await openStore(directory: dir.absolute.path); } @override - @internal Future destroy({ bool deleteRoot = false, }) async { - _expectRoot; + expectRoot; - await Directory((_root!..close()).directoryPath).delete(recursive: true); - _root = null; + await Directory((root!..close()).directoryPath).delete(recursive: true); + root = null; } @override - @internal Future createStore({ required String storeName, }) async { - await _expectRoot + await expectRoot .box() .putAsync(ObjectBoxStore(name: storeName), mode: PutMode.insert); } @override - @internal Future resetStore({ required String storeName, }) async { - _expectRoot; + expectRoot; - await _root!.runInTransactionAsync( + await root!.runInTransactionAsync( TxMode.write, (store, storeName) { - final tiles = _root!.box(); + final tiles = root!.box(); final removeIds = []; - final tilesBelongingToStore = (tiles.query() - ..linkMany(ObjectBoxTile_.stores, - ObjectBoxStore_.name.equals(storeName))) + final query = (tiles.query() + ..linkMany( + ObjectBoxTile_.stores, + ObjectBoxStore_.name.equals(storeName), + )) .build(); tiles.putMany( - tilesBelongingToStore + query .find() .map((tile) { tile.stores.removeWhere((store) => store.name == storeName); @@ -104,7 +99,7 @@ class ObjectBoxBackend implements FMTCBackend { .toList(), mode: PutMode.update, ); - tilesBelongingToStore.close(); + query.close(); tiles.query(ObjectBoxTile_.id.oneOf(removeIds)).build() ..remove() @@ -115,54 +110,99 @@ class ObjectBoxBackend implements FMTCBackend { } @override - @internal Future renameStore({ required String currentStoreName, required String newStoreName, }) async => - _expectRoot + expectRoot .box() - .putAsync((await _getStore(currentStoreName))..name = newStoreName); + .putAsync((await getStore(currentStoreName))..name = newStoreName); @override - @internal Future deleteStore({ required String storeName, }) async { - //await resetStore(storeName: storeName); - await _expectRoot + // await resetStore(storeName: storeName); + // might need to reset relations? + + final query = expectRoot .box() .query(ObjectBoxStore_.name.equals(storeName)) - .build() - .removeAsync(); + .build(); + await query.removeAsync(); + query.close(); } @override - @internal - Future> readTile({required String url}) async { - final query = _expectRoot + Future readTile({ + required String url, + }) async { + final query = expectRoot .box() .query(ObjectBoxTile_.url.equals(url)) .build(); - final tiles = await query.findAsync(); + final tile = await query.findUniqueAsync(); query.close(); - return tiles; + return tile; } @override - @internal - FutureOr createTile() {} - @override - @internal - FutureOr updateTile() {} - @override - @internal - FutureOr deleteTile() {} + Future createTile({ + required String url, + required Uint8List bytes, + required String storeName, + }) async { + expectRoot; + + await root!.runInTransactionAsync( + TxMode.write, + (store, args) { + final tiles = root!.box(); + + final query = tiles.query(ObjectBoxTile_.url.equals(args.url)).build(); + + tiles.put( + (query.findUnique() ?? + ObjectBoxTile( + url: args.url, + lastModified: DateTime.now(), + bytes: args.bytes, + )) + ..stores.add(ObjectBoxStore(name: args.storeName)), + ); + + query.close(); + }, + (url: url, bytes: bytes, storeName: storeName), + ); + } @override - @internal - FutureOr readLatestTile() {} - @override - @internal - FutureOr pruneTilesOlderThan({required DateTime expiry}) {} + Future deleteTile({ + required String url, + required String storeName, + }) async { + final tiles = expectRoot.box(); + + final query = (tiles.query(ObjectBoxTile_.url.equals(url)) + ..linkMany( + ObjectBoxTile_.stores, + ObjectBoxStore_.name.equals(storeName), + )) + .build(); + final tile = query.findUnique(); + if (tile == null) return null; + + tile.stores.removeWhere((store) => store.name == storeName); + + if (tile.stores.isEmpty) { + await query.removeAsync(); + query.close(); + return true; + } + + await tiles.putAsync(tile, mode: PutMode.update); + query.close(); + return false; + } } diff --git a/lib/src/backend/impls/objectbox/models/generated/objectbox-model.json b/lib/src/backend/impls/objectbox/models/generated/objectbox-model.json index 9df75546..8a8d009b 100644 --- a/lib/src/backend/impls/objectbox/models/generated/objectbox-model.json +++ b/lib/src/backend/impls/objectbox/models/generated/objectbox-model.json @@ -18,7 +18,7 @@ "id": "2:5839091915824466165", "name": "name", "type": 9, - "flags": 2048, + "flags": 2080, "indexId": "4:4070553972642232057" } ], diff --git a/lib/src/backend/impls/objectbox/models/generated/objectbox.g.dart b/lib/src/backend/impls/objectbox/models/generated/objectbox.g.dart index e9818d90..5d8d4c13 100644 --- a/lib/src/backend/impls/objectbox/models/generated/objectbox.g.dart +++ b/lib/src/backend/impls/objectbox/models/generated/objectbox.g.dart @@ -34,7 +34,7 @@ final _entities = [ id: const IdUid(2, 5839091915824466165), name: 'name', type: 9, - flags: 2048, + flags: 2080, indexId: const IdUid(4, 4070553972642232057)) ], relations: [], diff --git a/lib/src/backend/impls/objectbox/models/models.dart b/lib/src/backend/impls/objectbox/models/models.dart index dfdafd65..845d8966 100644 --- a/lib/src/backend/impls/objectbox/models/models.dart +++ b/lib/src/backend/impls/objectbox/models/models.dart @@ -5,12 +5,13 @@ import 'package:objectbox/objectbox.dart'; import '../../../interfaces/models.dart'; @Entity() -class ObjectBoxStore implements BackendStore { +base class ObjectBoxStore extends BackendStore { @Id() int id = 0; @override @Index() + @Unique() String name; @Index() @@ -21,7 +22,7 @@ class ObjectBoxStore implements BackendStore { } @Entity() -class ObjectBoxTile implements BackendTile { +base class ObjectBoxTile extends BackendTile { @Id() int id = 0; diff --git a/lib/src/backend/interfaces/backend.dart b/lib/src/backend/interfaces/backend.dart index e19fa624..03850cd2 100644 --- a/lib/src/backend/interfaces/backend.dart +++ b/lib/src/backend/interfaces/backend.dart @@ -1,6 +1,5 @@ import 'dart:async'; - -import 'package:meta/meta.dart'; +import 'dart:typed_data'; import '../../../flutter_map_tile_caching.dart'; import 'models.dart'; @@ -9,60 +8,48 @@ import 'models.dart'; /// 'backend' (usually one root) /// /// To implementers: +/// * Use a public 'cover-up' and separate private implementation /// * Use singletons (with a factory) to ensure consistent state management /// * Prefer throwing included implementation-generic errors/exceptions -/// * Mark all attributes/methods `@internal`, except the factory /// /// To end-users: /// * Use [FMTCSettings.backend] to set a custom backend -/// * Only the constructor ([FMTCBackend.new]) of an implementation should be -/// used in end applications abstract interface class FMTCBackend { const FMTCBackend(); abstract final String friendlyIdentifier; - @internal - abstract final bool supportsSharing; - - @internal FutureOr initialise({ String? rootDirectory, }); - @internal FutureOr destroy({ bool deleteRoot = false, }); - @internal FutureOr createStore({ required String storeName, }); - @internal FutureOr resetStore({ required String storeName, }); - @internal FutureOr renameStore({ required String currentStoreName, required String newStoreName, }); - @internal FutureOr deleteStore({ required String storeName, }); - @internal - Future> readTile({required String url}); - @internal - FutureOr createTile(); - @internal - FutureOr updateTile(); - @internal - FutureOr deleteTile(); - - @internal - FutureOr readLatestTile(); - @internal - FutureOr pruneTilesOlderThan({required DateTime expiry}); + FutureOr readTile({ + required String url, + }); + FutureOr createTile({ + required String url, + required Uint8List bytes, + required String storeName, + }); + FutureOr deleteTile({ + required String url, + required String storeName, + }); } diff --git a/lib/src/backend/interfaces/models.dart b/lib/src/backend/interfaces/models.dart index ff6dc95e..ff7dfa6d 100644 --- a/lib/src/backend/interfaces/models.dart +++ b/lib/src/backend/interfaces/models.dart @@ -1,11 +1,29 @@ import 'dart:typed_data'; -abstract interface class BackendStore { +abstract base class BackendStore { abstract String name; + + /// Uses [name] for equality comparisons only (unless the two objects are + /// [identical]) + @override + bool operator ==(Object? other) => + identical(this, other) || (other is BackendStore && name == other.name); + + @override + int get hashCode => name.hashCode; } -abstract interface class BackendTile { +abstract base class BackendTile { abstract String url; abstract DateTime lastModified; abstract Uint8List bytes; + + /// Uses [url] for equality comparisons only (unless the two objects are + /// [identical]) + @override + bool operator ==(Object? other) => + identical(this, other) || (other is BackendTile && url == other.url); + + @override + int get hashCode => url.hashCode; } From 5aa4ef878aeb36a6c968e1753f13d5f47ab8ce8d Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sat, 4 Nov 2023 19:04:49 +0000 Subject: [PATCH 077/168] Updated .gitignore Former-commit-id: 8b340281bf1ae04f72be7030608a329ecf8fba44 [formerly 3b7d8d6db9ab73fecfda1055c13fd5633fdb6659] Former-commit-id: b188bc312a5ea38e9d7d4e14188b40685071d0de --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 25dd6b1e..b61e4d04 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ # Custom local/ -.fvm/ +*.g.dart # Miscellaneous *.class From fb5839bacddeb4b5cf6f82a5a60abc0c07cf026f Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sat, 4 Nov 2023 19:05:23 +0000 Subject: [PATCH 078/168] Applied new .gitignore Former-commit-id: a70cbd68e18ff469491b32f28316cdae9c4bad75 [formerly d3abcfe211542f40bce8679ef647fecf37f5ecea] Former-commit-id: 7d2e58c1eed30f6383082a13e2ccb22225d0491d --- .../models/generated/objectbox.g.dart | 230 ------------------ 1 file changed, 230 deletions(-) delete mode 100644 lib/src/backend/impls/objectbox/models/generated/objectbox.g.dart diff --git a/lib/src/backend/impls/objectbox/models/generated/objectbox.g.dart b/lib/src/backend/impls/objectbox/models/generated/objectbox.g.dart deleted file mode 100644 index 5d8d4c13..00000000 --- a/lib/src/backend/impls/objectbox/models/generated/objectbox.g.dart +++ /dev/null @@ -1,230 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND -// This code was generated by ObjectBox. To update it run the generator again: -// With a Flutter package, run `flutter pub run build_runner build`. -// With a Dart package, run `dart run build_runner build`. -// See also https://docs.objectbox.io/getting-started#generate-objectbox-code - -// ignore_for_file: camel_case_types, depend_on_referenced_packages -// coverage:ignore-file - -import 'dart:typed_data'; - -import 'package:flat_buffers/flat_buffers.dart' as fb; -import 'package:objectbox/internal.dart'; // generated code can access "internal" functionality -import 'package:objectbox/objectbox.dart'; -import 'package:objectbox_flutter_libs/objectbox_flutter_libs.dart'; - -import '../../../../../../src/backend/impls/objectbox/models/models.dart'; - -export 'package:objectbox/objectbox.dart'; // so that callers only have to import this file - -final _entities = [ - ModelEntity( - id: const IdUid(3, 3564285316728157354), - name: 'ObjectBoxStore', - lastPropertyId: const IdUid(2, 5839091915824466165), - flags: 0, - properties: [ - ModelProperty( - id: const IdUid(1, 5129628855357846983), - name: 'id', - type: 6, - flags: 1), - ModelProperty( - id: const IdUid(2, 5839091915824466165), - name: 'name', - type: 9, - flags: 2080, - indexId: const IdUid(4, 4070553972642232057)) - ], - relations: [], - backlinks: [ - ModelBacklink(name: 'tiles', srcEntity: 'ObjectBoxTile', srcField: '') - ]), - ModelEntity( - id: const IdUid(4, 1221776040269555016), - name: 'ObjectBoxTile', - lastPropertyId: const IdUid(4, 325740197791906264), - flags: 0, - properties: [ - ModelProperty( - id: const IdUid(1, 6708383205929458485), - name: 'id', - type: 6, - flags: 1), - ModelProperty( - id: const IdUid(2, 7632311904162794182), - name: 'url', - type: 9, - flags: 34848, - indexId: const IdUid(5, 6190682863606461840)), - ModelProperty( - id: const IdUid(3, 4021600588433010488), - name: 'lastModified', - type: 10, - flags: 8, - indexId: const IdUid(6, 7105980852148339018)), - ModelProperty( - id: const IdUid(4, 325740197791906264), - name: 'bytes', - type: 23, - flags: 0) - ], - relations: [ - ModelRelation( - id: const IdUid(2, 7253865609688140673), - name: 'stores', - targetId: const IdUid(3, 3564285316728157354)) - ], - backlinks: []) -]; - -/// Shortcut for [Store.new] that passes [getObjectBoxModel] and for Flutter -/// apps by default a [directory] using `defaultStoreDirectory()` from the -/// ObjectBox Flutter library. -/// -/// Note: for desktop apps it is recommended to specify a unique [directory]. -/// -/// See [Store.new] for an explanation of all parameters. -Future openStore( - {String? directory, - int? maxDBSizeInKB, - int? fileMode, - int? maxReaders, - bool queriesCaseSensitiveDefault = true, - String? macosApplicationGroup}) async => - Store(getObjectBoxModel(), - directory: directory ?? (await defaultStoreDirectory()).path, - maxDBSizeInKB: maxDBSizeInKB, - fileMode: fileMode, - maxReaders: maxReaders, - queriesCaseSensitiveDefault: queriesCaseSensitiveDefault, - macosApplicationGroup: macosApplicationGroup); - -/// Returns the ObjectBox model definition for this project for use with -/// [Store.new]. -ModelDefinition getObjectBoxModel() { - final model = ModelInfo( - entities: _entities, - lastEntityId: const IdUid(4, 1221776040269555016), - lastIndexId: const IdUid(6, 7105980852148339018), - lastRelationId: const IdUid(2, 7253865609688140673), - lastSequenceId: const IdUid(0, 0), - retiredEntityUids: const [6351682704282101940, 3030016702019813306], - retiredIndexUids: const [], - retiredPropertyUids: const [ - 4615710800892160360, - 7634309479420249012, - 1360422513191232937, - 2679707907762759593, - 4242260097954056817, - 5886094580441791807 - ], - retiredRelationUids: const [], - modelVersion: 5, - modelVersionParserMinimum: 5, - version: 1); - - final bindings = { - ObjectBoxStore: EntityDefinition( - model: _entities[0], - toOneRelations: (ObjectBoxStore object) => [], - toManyRelations: (ObjectBoxStore object) => - {RelInfo.toManyBacklink(2, object.id): object.tiles}, - getId: (ObjectBoxStore object) => object.id, - setId: (ObjectBoxStore object, int id) { - object.id = id; - }, - objectToFB: (ObjectBoxStore object, fb.Builder fbb) { - final nameOffset = fbb.writeString(object.name); - fbb.startTable(3); - fbb.addInt64(0, object.id); - fbb.addOffset(1, nameOffset); - fbb.finish(fbb.endTable()); - return object.id; - }, - objectFromFB: (Store store, ByteData fbData) { - final buffer = fb.BufferContext(fbData); - final rootOffset = buffer.derefObject(0); - final nameParam = const fb.StringReader(asciiOptimization: true) - .vTableGet(buffer, rootOffset, 6, ''); - final object = ObjectBoxStore(name: nameParam) - ..id = const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0); - InternalToManyAccess.setRelInfo(object.tiles, store, - RelInfo.toManyBacklink(2, object.id)); - return object; - }), - ObjectBoxTile: EntityDefinition( - model: _entities[1], - toOneRelations: (ObjectBoxTile object) => [], - toManyRelations: (ObjectBoxTile object) => - {RelInfo.toMany(2, object.id): object.stores}, - getId: (ObjectBoxTile object) => object.id, - setId: (ObjectBoxTile object, int id) { - object.id = id; - }, - objectToFB: (ObjectBoxTile object, fb.Builder fbb) { - final urlOffset = fbb.writeString(object.url); - final bytesOffset = fbb.writeListInt8(object.bytes); - fbb.startTable(5); - fbb.addInt64(0, object.id); - fbb.addOffset(1, urlOffset); - fbb.addInt64(2, object.lastModified.millisecondsSinceEpoch); - fbb.addOffset(3, bytesOffset); - fbb.finish(fbb.endTable()); - return object.id; - }, - objectFromFB: (Store store, ByteData fbData) { - final buffer = fb.BufferContext(fbData); - final rootOffset = buffer.derefObject(0); - final urlParam = const fb.StringReader(asciiOptimization: true) - .vTableGet(buffer, rootOffset, 6, ''); - final lastModifiedParam = DateTime.fromMillisecondsSinceEpoch( - const fb.Int64Reader().vTableGet(buffer, rootOffset, 8, 0)); - final bytesParam = const fb.Uint8ListReader(lazy: false) - .vTableGet(buffer, rootOffset, 10, Uint8List(0)) as Uint8List; - final object = ObjectBoxTile( - url: urlParam, lastModified: lastModifiedParam, bytes: bytesParam) - ..id = const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0); - InternalToManyAccess.setRelInfo(object.stores, store, - RelInfo.toMany(2, object.id)); - return object; - }) - }; - - return ModelDefinition(model, bindings); -} - -/// [ObjectBoxStore] entity fields to define ObjectBox queries. -class ObjectBoxStore_ { - /// see [ObjectBoxStore.id] - static final id = - QueryIntegerProperty(_entities[0].properties[0]); - - /// see [ObjectBoxStore.name] - static final name = - QueryStringProperty(_entities[0].properties[1]); -} - -/// [ObjectBoxTile] entity fields to define ObjectBox queries. -class ObjectBoxTile_ { - /// see [ObjectBoxTile.id] - static final id = - QueryIntegerProperty(_entities[1].properties[0]); - - /// see [ObjectBoxTile.url] - static final url = - QueryStringProperty(_entities[1].properties[1]); - - /// see [ObjectBoxTile.lastModified] - static final lastModified = - QueryIntegerProperty(_entities[1].properties[2]); - - /// see [ObjectBoxTile.bytes] - static final bytes = - QueryByteVectorProperty(_entities[1].properties[3]); - - /// see [ObjectBoxTile.stores] - static final stores = QueryRelationToMany( - _entities[1].relations[0]); -} From d7808ec7d58674738f349abb6684ae98c8df7a3f Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sun, 5 Nov 2023 14:20:02 +0000 Subject: [PATCH 079/168] Handle store statistics in `ObjectBoxBackend` Added scope for implementation specific arguments in `FMTCBackend.initialise` Added documentation to backend operations Former-commit-id: fcca756c2b32e15f5669d0212c90a3581e5c5e28 [formerly cad382ef00148f4265206b46986aac3aface165f] Former-commit-id: 9428f9eeb2ace572fa35f2d89899d207b462bcee --- .vscode/tasks.json | 3 +- lib/src/backend/errors.dart | 20 ++- lib/src/backend/impls/objectbox/backend.dart | 163 +++++++++++++----- .../models/generated/objectbox-model.json | 60 +++---- .../impls/objectbox/models/models.dart | 10 +- lib/src/backend/interfaces/backend.dart | 43 ++++- 6 files changed, 220 insertions(+), 79 deletions(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 8f2b877b..dc562dec 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -8,8 +8,7 @@ "pub", "run", "build_runner", - "build", - "--delete-conflicting-outputs" + "build" ], "problemMatcher": [ "$dart-build_runner" diff --git a/lib/src/backend/errors.dart b/lib/src/backend/errors.dart index e409d6bb..67dbd1fd 100644 --- a/lib/src/backend/errors.dart +++ b/lib/src/backend/errors.dart @@ -5,7 +5,7 @@ /// * it was invalid/corrupt /// * ... or it was otherwise unavailable /// -/// To be thrown by backend implementations. +/// To be thrown by backend implementations. For resolution by end-user. class RootUnavailable extends Error { @override String toString() => @@ -15,7 +15,7 @@ class RootUnavailable extends Error { /// Indicates that the specified store structure was not available for use in /// operations, likely because it didn't exist /// -/// To be thrown by backend implementations. +/// To be thrown by backend implementations. For resolution by end-user. class StoreUnavailable extends Error { final String storeName; @@ -25,3 +25,19 @@ class StoreUnavailable extends Error { String toString() => 'StoreUnavailable: The requested store "$storeName" was unavailable'; } + +/// Indicates that the specified tile could not be updated because it did not +/// already exist +/// +/// To be thrown by backend implementations. For resolution by FMTC. +/// +/// If you have this error in your application, please file a bug report. +class TileCannotUpdate extends Error { + final String url; + + TileCannotUpdate({required this.url}); + + @override + String toString() => + 'TileCannotUpdate: The requested tile ("$url") did not exist, and so cannot be updated'; +} diff --git a/lib/src/backend/impls/objectbox/backend.dart b/lib/src/backend/impls/objectbox/backend.dart index 29976d35..0643f261 100644 --- a/lib/src/backend/impls/objectbox/backend.dart +++ b/lib/src/backend/impls/objectbox/backend.dart @@ -25,27 +25,36 @@ class _ObjectBoxBackendImpl implements ObjectBoxBackend { Store? root; // Must not be closed if not `null` Store get expectRoot => root ?? (throw RootUnavailable()); - Future getStore(String name) async => - (await expectRoot - .box() - .query(ObjectBoxStore_.name.equals(name)) - .build() - .findUniqueAsync()) ?? - (throw StoreUnavailable(storeName: name)); - @override String get friendlyIdentifier => 'ObjectBox'; + /// {@macro fmtc_backend_initialise} + /// + /// This implementation additionally accepts the following [implSpecificArgs]: + /// + /// * 'macosApplicationGroup' (`String`): when creating a sandboxed macOS app, + /// use to specify the application group (of less than 20 chars). See + /// [the ObjectBox docs](https://docs.objectbox.io/getting-started) for + /// details. + /// * 'maxReaders' (`int`): for debugging purposes only @override Future initialise({ String? rootDirectory, + int? maxDatabaseSize, + Map implSpecificArgs = const {}, }) async { final dir = await ((rootDirectory == null ? await getApplicationDocumentsDirectory() : Directory(rootDirectory)) >> 'fmtc') .create(recursive: true); - root = await openStore(directory: dir.absolute.path); + root = await openStore( + directory: dir.absolute.path, + maxDBSizeInKB: maxDatabaseSize, + macosApplicationGroup: + implSpecificArgs['macosApplicationGroup'] as String?, + maxReaders: implSpecificArgs['maxReaders'] as int?, + ); } @override @@ -54,7 +63,11 @@ class _ObjectBoxBackendImpl implements ObjectBoxBackend { }) async { expectRoot; - await Directory((root!..close()).directoryPath).delete(recursive: true); + if (deleteRoot) { + await Directory((root!..close()).directoryPath).delete(recursive: true); + } else { + root!.close(); + } root = null; } @@ -62,9 +75,14 @@ class _ObjectBoxBackendImpl implements ObjectBoxBackend { Future createStore({ required String storeName, }) async { - await expectRoot - .box() - .putAsync(ObjectBoxStore(name: storeName), mode: PutMode.insert); + await expectRoot.box().putAsync( + ObjectBoxStore( + name: storeName, + numberOfTiles: 0, + numberOfBytes: 0, + ), + mode: PutMode.insert, + ); } @override @@ -80,14 +98,14 @@ class _ObjectBoxBackendImpl implements ObjectBoxBackend { final removeIds = []; - final query = (tiles.query() + final tilesQuery = (tiles.query() ..linkMany( ObjectBoxTile_.stores, ObjectBoxStore_.name.equals(storeName), )) .build(); tiles.putMany( - query + tilesQuery .find() .map((tile) { tile.stores.removeWhere((store) => store.name == storeName); @@ -99,7 +117,7 @@ class _ObjectBoxBackendImpl implements ObjectBoxBackend { .toList(), mode: PutMode.update, ); - query.close(); + tilesQuery.close(); tiles.query(ObjectBoxTile_.id.oneOf(removeIds)).build() ..remove() @@ -113,10 +131,18 @@ class _ObjectBoxBackendImpl implements ObjectBoxBackend { Future renameStore({ required String currentStoreName, required String newStoreName, - }) async => - expectRoot - .box() - .putAsync((await getStore(currentStoreName))..name = newStoreName); + }) async { + final storeQuery = expectRoot + .box() + .query(ObjectBoxStore_.name.equals(currentStoreName)) + .build(); + + await root!.box().putAsync( + (await storeQuery.findUniqueAsync() ?? + (throw StoreUnavailable(storeName: currentStoreName))) + ..name = newStoreName, + ); + } @override Future deleteStore({ @@ -125,12 +151,12 @@ class _ObjectBoxBackendImpl implements ObjectBoxBackend { // await resetStore(storeName: storeName); // might need to reset relations? - final query = expectRoot + final storeQuery = expectRoot .box() .query(ObjectBoxStore_.name.equals(storeName)) .build(); - await query.removeAsync(); - query.close(); + await storeQuery.removeAsync(); + storeQuery.close(); } @override @@ -149,31 +175,77 @@ class _ObjectBoxBackendImpl implements ObjectBoxBackend { @override Future createTile({ required String url, - required Uint8List bytes, + required Uint8List? bytes, required String storeName, }) async { expectRoot; + final tiles = root!.box(); + final stores = root!.box(); + + final tilesQuery = tiles.query(ObjectBoxTile_.url.equals(url)).build(); + final existingTile = tilesQuery.findUnique(); + tilesQuery.close(); + + final storeQuery = + stores.query(ObjectBoxStore_.name.equals(storeName)).build(); + final store = storeQuery.findUnique() ?? + (throw StoreUnavailable(storeName: storeName)); + storeQuery.close(); + await root!.runInTransactionAsync( TxMode.write, (store, args) { final tiles = root!.box(); + final stores = root!.box(); - final query = tiles.query(ObjectBoxTile_.url.equals(args.url)).build(); - - tiles.put( - (query.findUnique() ?? + switch ((args.existingTile == null, args.bytes == null)) { + case (true, false): // No existing tile + tiles.put( ObjectBoxTile( url: args.url, lastModified: DateTime.now(), - bytes: args.bytes, - )) - ..stores.add(ObjectBoxStore(name: args.storeName)), - ); - - query.close(); + bytes: args.bytes!, + )..stores.add(args.store), + ); + stores.put( + args.store + ..numberOfTiles += 1 + ..numberOfBytes += args.bytes!.lengthInBytes, + ); + break; + case (false, true): // Existing tile, no update + // Only take action if it's not already belonging to the store + if (!args.existingTile!.stores.contains(args.store)) { + tiles.put(args.existingTile!..stores.add(args.store)); + stores.put( + args.store + ..numberOfTiles += 1 + ..numberOfBytes += args.existingTile!.bytes.lengthInBytes, + ); + } + break; + case (false, false): // Existing tile, update required + tiles.put( + args.existingTile! + ..lastModified = DateTime.now() + ..bytes = args.bytes!, + ); + stores.putMany( + args.existingTile!.stores + .map( + (store) => store + ..numberOfBytes += (args.bytes!.lengthInBytes - + args.existingTile!.bytes.lengthInBytes), + ) + .toList(), + ); + break; + case (true, true): // FMTC internal error + throw TileCannotUpdate(url: args.url); + } }, - (url: url, bytes: bytes, storeName: storeName), + (url: url, bytes: bytes, existingTile: existingTile, store: store), ); } @@ -184,25 +256,32 @@ class _ObjectBoxBackendImpl implements ObjectBoxBackend { }) async { final tiles = expectRoot.box(); - final query = (tiles.query(ObjectBoxTile_.url.equals(url)) - ..linkMany( - ObjectBoxTile_.stores, - ObjectBoxStore_.name.equals(storeName), - )) - .build(); + // Find the tile by URL + final query = tiles.query(ObjectBoxTile_.url.equals(url)).build(); final tile = query.findUnique(); if (tile == null) return null; + // For the correct store, adjust the statistics + for (final store in tile.stores) { + if (store.name != storeName) continue; + store + ..numberOfTiles -= 1 + ..numberOfBytes -= tile.bytes.lengthInBytes; + } + + // Remove the store relation from the tile tile.stores.removeWhere((store) => store.name == storeName); + // Delete the tile if it belongs to no stores if (tile.stores.isEmpty) { await query.removeAsync(); query.close(); return true; } - await tiles.putAsync(tile, mode: PutMode.update); + // Otherwise just update the tile query.close(); + await tiles.putAsync(tile, mode: PutMode.update); return false; } } diff --git a/lib/src/backend/impls/objectbox/models/generated/objectbox-model.json b/lib/src/backend/impls/objectbox/models/generated/objectbox-model.json index 8a8d009b..b75bfe65 100644 --- a/lib/src/backend/impls/objectbox/models/generated/objectbox-model.json +++ b/lib/src/backend/impls/objectbox/models/generated/objectbox-model.json @@ -4,85 +4,85 @@ "_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.", "entities": [ { - "id": "3:3564285316728157354", - "lastPropertyId": "2:5839091915824466165", + "id": "1:7419244569066266196", + "lastPropertyId": "4:3677248801338209880", "name": "ObjectBoxStore", "properties": [ { - "id": "1:5129628855357846983", + "id": "1:1858780610237179333", "name": "id", "type": 6, "flags": 1 }, { - "id": "2:5839091915824466165", + "id": "2:2256416726389092989", "name": "name", "type": 9, "flags": 2080, - "indexId": "4:4070553972642232057" + "indexId": "1:3645033738238218113" + }, + { + "id": "3:8181895676109246629", + "name": "numberOfTiles", + "type": 6 + }, + { + "id": "4:3677248801338209880", + "name": "numberOfBytes", + "type": 8 } ], "relations": [] }, { - "id": "4:1221776040269555016", - "lastPropertyId": "4:325740197791906264", + "id": "2:9006700906555106229", + "lastPropertyId": "4:7354783822834437324", "name": "ObjectBoxTile", "properties": [ { - "id": "1:6708383205929458485", + "id": "1:1583610199049416297", "name": "id", "type": 6, "flags": 1 }, { - "id": "2:7632311904162794182", + "id": "2:6188685410059426284", "name": "url", "type": 9, "flags": 34848, - "indexId": "5:6190682863606461840" + "indexId": "2:3427244788486794902" }, { - "id": "3:4021600588433010488", + "id": "3:1617954085447053640", "name": "lastModified", "type": 10, "flags": 8, - "indexId": "6:7105980852148339018" + "indexId": "3:2057043365010558859" }, { - "id": "4:325740197791906264", + "id": "4:7354783822834437324", "name": "bytes", "type": 23 } ], "relations": [ { - "id": "2:7253865609688140673", + "id": "1:3843657930463993464", "name": "stores", - "targetId": "3:3564285316728157354" + "targetId": "1:7419244569066266196" } ] } ], - "lastEntityId": "4:1221776040269555016", - "lastIndexId": "6:7105980852148339018", - "lastRelationId": "2:7253865609688140673", + "lastEntityId": "2:9006700906555106229", + "lastIndexId": "3:2057043365010558859", + "lastRelationId": "1:3843657930463993464", "lastSequenceId": "0:0", "modelVersion": 5, "modelVersionParserMinimum": 5, - "retiredEntityUids": [ - 6351682704282101940, - 3030016702019813306 - ], + "retiredEntityUids": [], "retiredIndexUids": [], - "retiredPropertyUids": [ - 4615710800892160360, - 7634309479420249012, - 1360422513191232937, - 2679707907762759593, - 4242260097954056817, - 5886094580441791807 - ], + "retiredPropertyUids": [], "retiredRelationUids": [], "version": 1 } \ No newline at end of file diff --git a/lib/src/backend/impls/objectbox/models/models.dart b/lib/src/backend/impls/objectbox/models/models.dart index 845d8966..eeb080f1 100644 --- a/lib/src/backend/impls/objectbox/models/models.dart +++ b/lib/src/backend/impls/objectbox/models/models.dart @@ -14,11 +14,19 @@ base class ObjectBoxStore extends BackendStore { @Unique() String name; + int numberOfTiles; + + double numberOfBytes; + @Index() @Backlink() final tiles = ToMany(); - ObjectBoxStore({required this.name}); + ObjectBoxStore({ + required this.name, + required this.numberOfTiles, + required this.numberOfBytes, + }); } @Entity() diff --git a/lib/src/backend/interfaces/backend.dart b/lib/src/backend/interfaces/backend.dart index 03850cd2..e38fb3a2 100644 --- a/lib/src/backend/interfaces/backend.dart +++ b/lib/src/backend/interfaces/backend.dart @@ -1,6 +1,8 @@ import 'dart:async'; import 'dart:typed_data'; +import 'package:path_provider/path_provider.dart'; + import '../../../flutter_map_tile_caching.dart'; import 'models.dart'; @@ -13,14 +15,34 @@ import 'models.dart'; /// * Prefer throwing included implementation-generic errors/exceptions /// /// To end-users: -/// * Use [FMTCSettings.backend] to set a custom backend +/// * Use [FMTCSettings.backend] to set a custom backend abstract interface class FMTCBackend { const FMTCBackend(); abstract final String friendlyIdentifier; + /// {@template fmtc_backend_initialise} + /// Initialise this backend & create the root + /// + /// [rootDirectory] defaults to '[getApplicationDocumentsDirectory]/fmtc'. + /// + /// [maxDatabaseSize] defaults to 1 GB shared across all stores. Specify the + /// amount in KB. + /// {@endtemplate} + /// + /// Some implementations may accept/require additional arguments that may + /// be set through [implSpecificArgs]. See their documentation for more + /// information. + /// + /// --- + /// + /// Note to implementers: if you accept implementation specific arguments, + /// override the documentation on this method, and use the + /// 'fmtc_backend_initialise' macro at the top to retain the standard docs. FutureOr initialise({ String? rootDirectory, + int? maxDatabaseSize, + Map implSpecificArgs = const {}, }); FutureOr destroy({ bool deleteRoot = false, @@ -40,14 +62,31 @@ abstract interface class FMTCBackend { required String storeName, }); + /// Get a raw tile by URL FutureOr readTile({ required String url, }); + + /// Create or update a tile + /// + /// If the tile already existed, it will be added to the specified store. + /// Otherwise, [bytes] must be specified, and the tile will be created and + /// added. + /// + /// If [bytes] is provided and the tile already existed, it will be updated for + /// all stores. FutureOr createTile({ required String url, - required Uint8List bytes, + required Uint8List? bytes, required String storeName, }); + + /// Remove the tile from the store, deleting it if orphaned + /// + /// Returns: + /// * `null` : if there was no existing tile + /// * `true` : if the tile itself could be deleted (it was orphaned) + /// * `false`: if the tile still belonged to at least store FutureOr deleteTile({ required String url, required String storeName, From 15d483c66a9cb764c5a66d9bd9489c0c4061b1b4 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Fri, 24 Nov 2023 11:32:14 +0000 Subject: [PATCH 080/168] Progress with migration to ObjectBox Former-commit-id: d0ed21907f7cf95a6acb563d25b3d1cd75535072 [formerly ef3d56f20085b102ab65ba1427d8d11b629c7621] Former-commit-id: 6574d542dbbc93ff0b63a3f510506f2042d1bf43 --- lib/flutter_map_tile_caching.dart | 2 +- lib/src/backend/export_plus.dart | 2 + lib/src/backend/export_std.dart | 1 - lib/src/backend/{ => impl_tools}/errors.dart | 28 ++ lib/src/backend/impl_tools/no_sync.dart | 135 +++++++ lib/src/backend/impls/objectbox/backend.dart | 334 +++++++----------- lib/src/backend/impls/objectbox/worker.dart | 319 +++++++++++++++++ lib/src/backend/interfaces/backend.dart | 190 +++++++++- lib/src/bulk_download/manager.dart | 26 +- lib/src/bulk_download/thread.dart | 8 +- .../bulk_download/tile_loops/generate.dart | 24 +- lib/src/misc/store_db_impl.dart | 11 - lib/src/misc/with_backend_access.dart | 12 + lib/src/providers/image_provider.dart | 27 +- lib/src/store/download.dart | 10 +- lib/src/store/metadata.dart | 2 +- lib/src/store/statistics.dart | 6 +- test/fmtc_test.dart | 20 +- tile_server/bin/tile_server.dart | 8 +- 19 files changed, 873 insertions(+), 292 deletions(-) rename lib/src/backend/{ => impl_tools}/errors.dart (58%) create mode 100644 lib/src/backend/impl_tools/no_sync.dart create mode 100644 lib/src/backend/impls/objectbox/worker.dart delete mode 100644 lib/src/misc/store_db_impl.dart create mode 100644 lib/src/misc/with_backend_access.dart diff --git a/lib/flutter_map_tile_caching.dart b/lib/flutter_map_tile_caching.dart index 5b490223..0f32c267 100644 --- a/lib/flutter_map_tile_caching.dart +++ b/lib/flutter_map_tile_caching.dart @@ -64,7 +64,7 @@ part 'src/bulk_download/manager.dart'; part 'src/bulk_download/thread.dart'; part 'src/bulk_download/tile_event.dart'; part 'src/fmtc.dart'; -part 'src/misc/store_db_impl.dart'; +part 'src/misc/with_backend_access.dart'; part 'src/providers/tile_provider.dart'; part 'src/regions/base_region.dart'; part 'src/regions/circle.dart'; diff --git a/lib/src/backend/export_plus.dart b/lib/src/backend/export_plus.dart index 56d57862..07a1d7f8 100644 --- a/lib/src/backend/export_plus.dart +++ b/lib/src/backend/export_plus.dart @@ -1,2 +1,4 @@ export 'export_std.dart'; +export 'impl_tools/errors.dart'; +export 'impl_tools/no_sync.dart'; export 'interfaces/models.dart'; diff --git a/lib/src/backend/export_std.dart b/lib/src/backend/export_std.dart index 5f95a06f..22aca207 100644 --- a/lib/src/backend/export_std.dart +++ b/lib/src/backend/export_std.dart @@ -1,3 +1,2 @@ -export 'errors.dart'; export 'impls/objectbox/backend.dart'; export 'interfaces/backend.dart'; diff --git a/lib/src/backend/errors.dart b/lib/src/backend/impl_tools/errors.dart similarity index 58% rename from lib/src/backend/errors.dart rename to lib/src/backend/impl_tools/errors.dart index 67dbd1fd..f64872b3 100644 --- a/lib/src/backend/errors.dart +++ b/lib/src/backend/impl_tools/errors.dart @@ -12,6 +12,18 @@ class RootUnavailable extends Error { 'RootUnavailable: The requested backend/root was unavailable'; } +/// Indicates that the backend/root structure could not be initialised, because +/// it was already initialised. +/// +/// Try destroying it first. +/// +/// To be thrown by backend implementations. For resolution by end-user. +class RootAlreadyInitialised extends Error { + @override + String toString() => + 'RootUnavailable: The requested backend/root could not be initialised because it was already initialised'; +} + /// Indicates that the specified store structure was not available for use in /// operations, likely because it didn't exist /// @@ -41,3 +53,19 @@ class TileCannotUpdate extends Error { String toString() => 'TileCannotUpdate: The requested tile ("$url") did not exist, and so cannot be updated'; } + +/// Indicates that the backend implementation does not support the invoked +/// synchronous operation. +/// +/// Use the asynchronous version instead. +/// +/// Note that there is no equivalent error for async operations: if there is no +/// specific async version of an operation, it should redirect to the sync +/// version. +/// +/// To be thrown by backend implementations. For resolution by end-user. +class SyncOperationUnsupported extends Error { + @override + String toString() => + 'SyncOperationUnsupported: The backend implementation does not support the invoked synchronous operation.'; +} diff --git a/lib/src/backend/impl_tools/no_sync.dart b/lib/src/backend/impl_tools/no_sync.dart new file mode 100644 index 00000000..fdf38a7c --- /dev/null +++ b/lib/src/backend/impl_tools/no_sync.dart @@ -0,0 +1,135 @@ +import 'dart:typed_data'; + +import '../interfaces/backend.dart'; +import 'errors.dart'; + +/// A shortcut to declare that an [FMTCBackend] does not support any synchronous +/// versions of methods +mixin FMTCBackendNoSync implements FMTCBackend { + /// This synchronous method is unsupported by this implementation - use + /// [initialise] instead + @override + Never initialiseSync({ + String? rootDirectory, + int? maxDatabaseSize, + Map implSpecificArgs = const {}, + }) => + throw SyncOperationUnsupported(); + + @override + final supportsSyncInitialise = false; + + /// This synchronous method is unsupported by this implementation - use + /// [destroy] instead + @override + Never destroySync({ + bool deleteRoot = false, + }) => + throw SyncOperationUnsupported(); + + @override + final supportsSyncDestroy = false; + + /// This synchronous method is unsupported by this implementation - use + /// [createStore] instead + @override + Never createStoreSync({ + required String storeName, + }) => + throw SyncOperationUnsupported(); + + @override + final supportsSyncCreateStore = false; + + /// This synchronous method is unsupported by this implementation - use + /// [resetStore] instead + @override + Never resetStoreSync({ + required String storeName, + }) => + throw SyncOperationUnsupported(); + + @override + final supportsSyncResetStore = false; + + /// This synchronous method is unsupported by this implementation - use + /// [renameStore] instead + @override + Never renameStoreSync({ + required String currentStoreName, + required String newStoreName, + }) => + throw SyncOperationUnsupported(); + + @override + final supportsSyncRenameStore = false; + + /// This synchronous method is unsupported by this implementation - use + /// [deleteStore] instead + @override + Never deleteStoreSync({ + required String storeName, + }) => + throw SyncOperationUnsupported(); + + @override + final supportsSyncDeleteStore = false; + + /// This synchronous method is unsupported by this implementation - use + /// [getStoreSize] instead + @override + Never getStoreSizeSync({ + required String storeName, + }) => + throw SyncOperationUnsupported(); + + @override + final supportsSyncGetStoreSize = false; + + /// This synchronous method is unsupported by this implementation - use + /// [getStoreLength] instead + @override + Never getStoreLengthSync({ + required String storeName, + }) => + throw SyncOperationUnsupported(); + + @override + final supportsSyncGetStoreLength = false; + + /// This synchronous method is unsupported by this implementation - use + /// [readTile] instead + @override + Never readTileSync({ + required String url, + }) => + throw SyncOperationUnsupported(); + + @override + final supportsSyncReadTile = false; + + /// This synchronous method is unsupported by this implementation - use + /// [writeTile] instead + @override + Never writeTileSync({ + required String storeName, + required String url, + required Uint8List? bytes, + }) => + throw SyncOperationUnsupported(); + + @override + final supportsSyncWriteTile = false; + + /// This synchronous method is unsupported by this implementation - use + /// [deleteTile] instead + @override + Never deleteTileSync({ + required String storeName, + required String url, + }) => + throw SyncOperationUnsupported(); + + @override + final supportsSyncDeleteTile = false; +} diff --git a/lib/src/backend/impls/objectbox/backend.dart b/lib/src/backend/impls/objectbox/backend.dart index 0643f261..ae1b3d27 100644 --- a/lib/src/backend/impls/objectbox/backend.dart +++ b/lib/src/backend/impls/objectbox/backend.dart @@ -1,16 +1,21 @@ import 'dart:async'; import 'dart:io'; +import 'dart:isolate'; import 'dart:typed_data'; import 'package:collection/collection.dart'; import 'package:path_provider/path_provider.dart'; import '../../../misc/exts.dart'; -import '../../errors.dart'; +import '../../impl_tools/errors.dart'; +import '../../impl_tools/no_sync.dart'; import '../../interfaces/backend.dart'; +import '../../interfaces/models.dart'; import 'models/generated/objectbox.g.dart'; import 'models/models.dart'; +part 'worker.dart'; + /// Implementation of [FMTCBackend] that uses ObjectBox as the storage database abstract interface class ObjectBoxBackend implements FMTCBackend { /// Implementation of [FMTCBackend] that uses ObjectBox as the storage @@ -19,11 +24,41 @@ abstract interface class ObjectBoxBackend implements FMTCBackend { static final _instance = _ObjectBoxBackendImpl._(); } -class _ObjectBoxBackendImpl implements ObjectBoxBackend { +class _ObjectBoxBackendImpl with FMTCBackendNoSync implements ObjectBoxBackend { _ObjectBoxBackendImpl._(); - Store? root; // Must not be closed if not `null` - Store get expectRoot => root ?? (throw RootUnavailable()); + void get expectInitialised => _sendPort ?? (throw RootUnavailable()); + SendPort? _sendPort; + Completer? _workerCompleter; + Completer<_WorkerRes>? _workerCmdRes; + + //final _cmdQueue = <_WorkerKey, Completer<_WorkerRes>?>{ + // for (final v in _WorkerKey.values) v: null, + //}; + + Future<_WorkerRes> sendCmd(_WorkerCmd cmd) async { + expectInitialised; + + // If a command is already pending response, wait for it to complete + await _workerCmdRes?.future; + + _workerCmdRes = Completer(); + _sendPort!.send(cmd); + final res = await _workerCmdRes!.future; + _workerCmdRes = null; + return res; + } + + Future workerListener(ReceivePort receivePort) async { + await for (final _WorkerRes evt in receivePort) { + if (evt.key == _WorkerKey.initialise_) { + _sendPort = evt.data!['sendPort']! as SendPort; + } else { + _workerCmdRes!.complete(evt); + } + } + _workerCompleter!.complete(); + } @override String get friendlyIdentifier => 'ObjectBox'; @@ -37,251 +72,142 @@ class _ObjectBoxBackendImpl implements ObjectBoxBackend { /// [the ObjectBox docs](https://docs.objectbox.io/getting-started) for /// details. /// * 'maxReaders' (`int`): for debugging purposes only + /// + /// These arguments are optional. However, failure to provide them in the + /// specified type will result in an uncaught type casting error. @override Future initialise({ String? rootDirectory, int? maxDatabaseSize, Map implSpecificArgs = const {}, }) async { - final dir = await ((rootDirectory == null - ? await getApplicationDocumentsDirectory() - : Directory(rootDirectory)) >> - 'fmtc') - .create(recursive: true); - root = await openStore( - directory: dir.absolute.path, - maxDBSizeInKB: maxDatabaseSize, - macosApplicationGroup: - implSpecificArgs['macosApplicationGroup'] as String?, - maxReaders: implSpecificArgs['maxReaders'] as int?, + if (_sendPort != null) throw RootAlreadyInitialised(); + + // Setup worker isolate + final receivePort = ReceivePort(); + await Isolate.spawn( + _worker, + ( + sendPort: receivePort.sendPort, + rootDirectory: rootDirectory, + maxDatabaseSize: maxDatabaseSize, + macosApplicationGroup: + implSpecificArgs['macosApplicationGroup']! as String, + maxReaders: implSpecificArgs['maxReaders']! as int, + ), + onExit: receivePort.sendPort, + debugName: '[FMTC] ObjectBox Backend Worker', ); + + _workerCompleter = Completer(); + return; } @override Future destroy({ bool deleteRoot = false, }) async { - expectRoot; + expectInitialised; - if (deleteRoot) { - await Directory((root!..close()).directoryPath).delete(recursive: true); - } else { - root!.close(); - } - root = null; + unawaited( + sendCmd( + (key: _WorkerKey.destroy_, args: {'deleteRoot': deleteRoot}), + ), + ); + await _workerCompleter!.future; + _sendPort = null; } @override Future createStore({ required String storeName, - }) async { - await expectRoot.box().putAsync( - ObjectBoxStore( - name: storeName, - numberOfTiles: 0, - numberOfBytes: 0, - ), - mode: PutMode.insert, - ); - } + }) => + sendCmd((key: _WorkerKey.createStore, args: {'storeName': storeName})); @override Future resetStore({ required String storeName, - }) async { - expectRoot; - - await root!.runInTransactionAsync( - TxMode.write, - (store, storeName) { - final tiles = root!.box(); - - final removeIds = []; - - final tilesQuery = (tiles.query() - ..linkMany( - ObjectBoxTile_.stores, - ObjectBoxStore_.name.equals(storeName), - )) - .build(); - tiles.putMany( - tilesQuery - .find() - .map((tile) { - tile.stores.removeWhere((store) => store.name == storeName); - if (tile.stores.isNotEmpty) return tile; - removeIds.add(tile.id); - return null; - }) - .whereNotNull() - .toList(), - mode: PutMode.update, - ); - tilesQuery.close(); - - tiles.query(ObjectBoxTile_.id.oneOf(removeIds)).build() - ..remove() - ..close(); - }, - storeName, - ); - } + }) => + sendCmd((key: _WorkerKey.resetStore, args: {'storeName': storeName})); @override Future renameStore({ required String currentStoreName, required String newStoreName, - }) async { - final storeQuery = expectRoot - .box() - .query(ObjectBoxStore_.name.equals(currentStoreName)) - .build(); - - await root!.box().putAsync( - (await storeQuery.findUniqueAsync() ?? - (throw StoreUnavailable(storeName: currentStoreName))) - ..name = newStoreName, - ); - } + }) => + sendCmd( + ( + key: _WorkerKey.renameStore, + args: { + 'currentStoreName': currentStoreName, + 'newStoreName': newStoreName, + } + ), + ); @override Future deleteStore({ required String storeName, - }) async { - // await resetStore(storeName: storeName); - // might need to reset relations? + }) => + sendCmd((key: _WorkerKey.deleteStore, args: {'storeName': storeName})); - final storeQuery = expectRoot - .box() - .query(ObjectBoxStore_.name.equals(storeName)) - .build(); - await storeQuery.removeAsync(); - storeQuery.close(); - } + @override + Future getStoreSize({ + required String storeName, + }) async => + (await sendCmd( + (key: _WorkerKey.getStoreSize, args: {'storeName': storeName}), + )) + .data!['size']! as double; + + @override + Future getStoreLength({ + required String storeName, + }) async => + (await sendCmd( + (key: _WorkerKey.getStoreLength, args: {'storeName': storeName}), + )) + .data!['length']! as int; @override Future readTile({ required String url, - }) async { - final query = expectRoot - .box() - .query(ObjectBoxTile_.url.equals(url)) - .build(); - final tile = await query.findUniqueAsync(); - query.close(); - return tile; - } + }) async => + (await sendCmd((key: _WorkerKey.readTile, args: {'url': url}))) + .data!['tile'] as ObjectBoxTile?; @override - Future createTile({ + Future writeTile({ + required String storeName, required String url, required Uint8List? bytes, - required String storeName, - }) async { - expectRoot; - - final tiles = root!.box(); - final stores = root!.box(); - - final tilesQuery = tiles.query(ObjectBoxTile_.url.equals(url)).build(); - final existingTile = tilesQuery.findUnique(); - tilesQuery.close(); - - final storeQuery = - stores.query(ObjectBoxStore_.name.equals(storeName)).build(); - final store = storeQuery.findUnique() ?? - (throw StoreUnavailable(storeName: storeName)); - storeQuery.close(); - - await root!.runInTransactionAsync( - TxMode.write, - (store, args) { - final tiles = root!.box(); - final stores = root!.box(); - - switch ((args.existingTile == null, args.bytes == null)) { - case (true, false): // No existing tile - tiles.put( - ObjectBoxTile( - url: args.url, - lastModified: DateTime.now(), - bytes: args.bytes!, - )..stores.add(args.store), - ); - stores.put( - args.store - ..numberOfTiles += 1 - ..numberOfBytes += args.bytes!.lengthInBytes, - ); - break; - case (false, true): // Existing tile, no update - // Only take action if it's not already belonging to the store - if (!args.existingTile!.stores.contains(args.store)) { - tiles.put(args.existingTile!..stores.add(args.store)); - stores.put( - args.store - ..numberOfTiles += 1 - ..numberOfBytes += args.existingTile!.bytes.lengthInBytes, - ); - } - break; - case (false, false): // Existing tile, update required - tiles.put( - args.existingTile! - ..lastModified = DateTime.now() - ..bytes = args.bytes!, - ); - stores.putMany( - args.existingTile!.stores - .map( - (store) => store - ..numberOfBytes += (args.bytes!.lengthInBytes - - args.existingTile!.bytes.lengthInBytes), - ) - .toList(), - ); - break; - case (true, true): // FMTC internal error - throw TileCannotUpdate(url: args.url); - } - }, - (url: url, bytes: bytes, existingTile: existingTile, store: store), - ); - } + }) => + sendCmd( + ( + key: _WorkerKey.writeTile, + args: {'storeName': storeName, 'url': url, 'bytes': bytes} + ), + ); @override Future deleteTile({ required String url, required String storeName, - }) async { - final tiles = expectRoot.box(); - - // Find the tile by URL - final query = tiles.query(ObjectBoxTile_.url.equals(url)).build(); - final tile = query.findUnique(); - if (tile == null) return null; - - // For the correct store, adjust the statistics - for (final store in tile.stores) { - if (store.name != storeName) continue; - store - ..numberOfTiles -= 1 - ..numberOfBytes -= tile.bytes.lengthInBytes; - } + }) async => + (await sendCmd( + ( + key: _WorkerKey.deleteStore, + args: {'storeName': storeName, 'url': url} + ), + )) + .data!['wasOrphaned'] as bool?; - // Remove the store relation from the tile - tile.stores.removeWhere((store) => store.name == storeName); - - // Delete the tile if it belongs to no stores - if (tile.stores.isEmpty) { - await query.removeAsync(); - query.close(); - return true; - } - - // Otherwise just update the tile - query.close(); - await tiles.putAsync(tile, mode: PutMode.update); - return false; - } + @override + Future removeOldestTile({ + required String storeName, + }) async => + (await sendCmd( + (key: _WorkerKey.removeOldestTile, args: {'storeName': storeName}), + )) + .data!['tileDeleted']! as bool; } diff --git a/lib/src/backend/impls/objectbox/worker.dart b/lib/src/backend/impls/objectbox/worker.dart new file mode 100644 index 00000000..251ca598 --- /dev/null +++ b/lib/src/backend/impls/objectbox/worker.dart @@ -0,0 +1,319 @@ +part of 'backend.dart'; + +typedef _WorkerCmd = ({_WorkerKey key, Map args}); +typedef _WorkerRes = ({_WorkerKey key, Map? data}); + +enum _WorkerKey { + initialise_, // Only valid as a response + destroy_, // Only valid as a request + createStore, + resetStore, + renameStore, + deleteStore, + getStoreSize, + getStoreLength, + readTile, + writeTile, + deleteTile, + removeOldestTile, +} + +Future _worker( + ({ + SendPort sendPort, + String? rootDirectory, + int? maxDatabaseSize, + String? macosApplicationGroup, + int? maxReaders, + }) input, +) async { + // Setup comms + final receivePort = ReceivePort(); + void sendRes(_WorkerRes m) => input.sendPort.send(m); + + // Initialise database + final rootDirectory = await (input.rootDirectory == null + ? await getApplicationDocumentsDirectory() + : Directory(input.rootDirectory!) >> 'fmtc') + .create(recursive: true); + final root = await openStore( + directory: rootDirectory.absolute.path, + maxDBSizeInKB: input.maxDatabaseSize, + macosApplicationGroup: input.macosApplicationGroup, + maxReaders: input.maxReaders, + ); + + // Respond with comms channel for future cmds + sendRes( + ( + key: _WorkerKey.initialise_, + data: {'sendPort': receivePort.sendPort}, + ), + ); + + // Await cmds, perform work, and respond + await for (final _WorkerCmd cmd in receivePort) { + switch (cmd.key) { + case _WorkerKey.initialise_: + throw UnsupportedError('Invalid operation'); + case _WorkerKey.destroy_: + root.close(); + if (cmd.args['deleteRoot'] == true) { + rootDirectory.deleteSync(recursive: true); + } + Isolate.exit(); + case _WorkerKey.createStore: + root.box().put( + ObjectBoxStore( + name: cmd.args['storeName']! as String, + numberOfTiles: 0, + numberOfBytes: 0, + ), + mode: PutMode.insert, + ); + sendRes((key: cmd.key, data: null)); + break; + case _WorkerKey.resetStore: + final storeName = cmd.args['storeName']! as String; + final removeIds = []; + + final tiles = root.box(); + + final tilesQuery = (tiles.query() + ..linkMany( + ObjectBoxTile_.stores, + ObjectBoxStore_.name.equals(storeName), + )) + .build(); + + root.runInTransaction( + TxMode.write, + () { + tiles.putMany( + tilesQuery + .find() + .map((tile) { + tile.stores.removeWhere((store) => store.name == storeName); + if (tile.stores.isNotEmpty) return tile; + removeIds.add(tile.id); + return null; + }) + .whereNotNull() + .toList(), + mode: PutMode.update, + ); + tilesQuery.close(); + + tiles.query(ObjectBoxTile_.id.oneOf(removeIds)).build() + ..remove() + ..close(); + }, + ); + sendRes((key: cmd.key, data: null)); + break; + case _WorkerKey.renameStore: + final currentStoreName = cmd.args['currentStoreName']! as String; + final newStoreName = cmd.args['newStoreName']! as String; + + root.box().put( + root + .box() + .query(ObjectBoxStore_.name.equals(currentStoreName)) + .build() + .findUnique() ?? + (throw StoreUnavailable(storeName: currentStoreName)) + ..name = newStoreName, + ); + + sendRes((key: cmd.key, data: null)); + break; + case _WorkerKey.deleteStore: + root + .box() + .query( + ObjectBoxStore_.name.equals(cmd.args['storeName']! as String), + ) + .build() + ..remove() + ..close(); + + sendRes((key: cmd.key, data: null)); + break; + case _WorkerKey.getStoreSize: + final storeName = cmd.args['storeName']! as String; + + final query = root + .box() + .query(ObjectBoxStore_.name.equals(storeName)) + .build(); + final kib = (query.findUnique() ?? + (throw StoreUnavailable(storeName: storeName))) + .numberOfBytes / + 1024; + query.close(); + + sendRes((key: cmd.key, data: {'size': kib})); + break; + case _WorkerKey.getStoreLength: + final storeName = cmd.args['storeName']! as String; + + final query = root + .box() + .query(ObjectBoxStore_.name.equals(storeName)) + .build(); + final length = (query.findUnique() ?? + (throw StoreUnavailable(storeName: storeName))) + .numberOfTiles; + query.close(); + + sendRes((key: cmd.key, data: {'length': length})); + break; + case _WorkerKey.readTile: + final query = root + .box() + .query(ObjectBoxTile_.url.equals(cmd.args['url']! as String)) + .build(); + final tile = query.findUnique(); + query.close(); + + sendRes((key: cmd.key, data: {'tile': tile})); + break; + case _WorkerKey.writeTile: + final storeName = cmd.args['storeName']! as String; + final url = cmd.args['url']! as String; + final bytes = cmd.args['bytes'] as Uint8List?; + + final tiles = root.box(); + final stores = root.box(); + + final tilesQuery = tiles.query(ObjectBoxTile_.url.equals(url)).build(); + final existingTile = tilesQuery.findUnique(); + tilesQuery.close(); + + final storeQuery = + stores.query(ObjectBoxStore_.name.equals(storeName)).build(); + final store = storeQuery.findUnique() ?? + (throw StoreUnavailable(storeName: storeName)); + storeQuery.close(); + + root.runInTransaction( + TxMode.write, + () { + switch ((existingTile == null, bytes == null)) { + case (true, false): // No existing tile + tiles.put( + ObjectBoxTile( + url: url, + lastModified: DateTime.now(), + bytes: bytes!, + )..stores.add(store), + ); + stores.put( + store + ..numberOfTiles += 1 + ..numberOfBytes += bytes.lengthInBytes, + ); + break; + case (false, true): // Existing tile, no update + // Only take action if it's not already belonging to the store + if (!existingTile!.stores.contains(store)) { + tiles.put(existingTile..stores.add(store)); + stores.put( + store + ..numberOfTiles += 1 + ..numberOfBytes += existingTile.bytes.lengthInBytes, + ); + } + break; + case (false, false): // Existing tile, update required + tiles.put( + existingTile! + ..lastModified = DateTime.now() + ..bytes = bytes!, + ); + stores.putMany( + existingTile.stores + .map( + (store) => store + ..numberOfBytes += (bytes.lengthInBytes - + existingTile.bytes.lengthInBytes), + ) + .toList(), + ); + break; + case (true, true): // FMTC internal error + throw TileCannotUpdate(url: url); + } + }, + ); + + sendRes((key: cmd.key, data: null)); + break; + case _WorkerKey.deleteTile: + final storeName = cmd.args['storeName']! as String; + final url = cmd.args['url']! as String; + + final tiles = root.box(); + + // Find the tile by URL + final query = tiles.query(ObjectBoxTile_.url.equals(url)).build(); + final tile = query.findUnique(); + if (tile == null) { + sendRes((key: cmd.key, data: {'wasOrphaned': null})); + break; + } + + // For the correct store, adjust the statistics + for (final store in tile.stores) { + if (store.name != storeName) continue; + root.box().put( + store + ..numberOfTiles -= 1 + ..numberOfBytes -= tile.bytes.lengthInBytes, + mode: PutMode.update, + ); + break; + } + + // Remove the store relation from the tile + tile.stores.removeWhere((store) => store.name == storeName); + + // Delete the tile if it belongs to no stores + if (tile.stores.isEmpty) { + query + ..remove() + ..close(); + sendRes((key: cmd.key, data: {'wasOrphaned': true})); + break; + } + + // Otherwise just update the tile + query.close(); + tiles.put(tile, mode: PutMode.update); + sendRes((key: cmd.key, data: {'wasOrphaned': false})); + break; + case _WorkerKey.removeOldestTile: + final storeName = cmd.args['storeName']! as String; + + final tiles = root.box(); + + final query = (tiles.query().order(ObjectBoxTile_.lastModified) + ..linkMany( + ObjectBoxTile_.stores, + ObjectBoxStore_.name.equals(storeName), + )) + .build(); + + int? deleteId; + await for (final tile in query.stream()) { + if (tile.stores.length == 1) { + deleteId = tile.id; + break; + } + } + query.close(); + + break; + } + } +} diff --git a/lib/src/backend/interfaces/backend.dart b/lib/src/backend/interfaces/backend.dart index e38fb3a2..e0f87390 100644 --- a/lib/src/backend/interfaces/backend.dart +++ b/lib/src/backend/interfaces/backend.dart @@ -1,9 +1,11 @@ import 'dart:async'; import 'dart:typed_data'; +import 'package:meta/meta.dart'; import 'package:path_provider/path_provider.dart'; import '../../../flutter_map_tile_caching.dart'; +import '../impl_tools/errors.dart'; import 'models.dart'; /// An abstract interface that FMTC will use to communicate with a storage @@ -16,6 +18,7 @@ import 'models.dart'; /// /// To end-users: /// * Use [FMTCSettings.backend] to set a custom backend +/// * Not all sync versions of methods are guaranteed to have implementations abstract interface class FMTCBackend { const FMTCBackend(); @@ -39,34 +42,164 @@ abstract interface class FMTCBackend { /// Note to implementers: if you accept implementation specific arguments, /// override the documentation on this method, and use the /// 'fmtc_backend_initialise' macro at the top to retain the standard docs. - FutureOr initialise({ + Future initialise({ String? rootDirectory, int? maxDatabaseSize, Map implSpecificArgs = const {}, }); - FutureOr destroy({ + + /// {@macro fmtc_backend_initialise} + /// + /// --- + /// + /// Note to implementers: if you accept implementation specific arguments, + /// override the documentation on this method, and use the + /// 'fmtc_backend_initialise' macro at the top to retain the standard docs. + void initialiseSync({ + String? rootDirectory, + int? maxDatabaseSize, + Map implSpecificArgs = const {}, + }); + + /// Whether [initialiseSync] is implemented + /// + /// If `false`, calling will throw an [SyncOperationUnsupported] error. + abstract final bool supportsSyncInitialise; + + /// Uninitialise this backend, and release whatever resources it is consuming + /// + /// If [deleteRoot] is `true`, then the storage medium will be permanently + /// deleted. + Future destroy({ + bool deleteRoot = false, + }); + + /// Uninitialise this backend, and release whatever resources it is consuming + /// + /// If [deleteRoot] is `true`, then the storage medium will be permanently + /// deleted. + void destroySync({ bool deleteRoot = false, }); - FutureOr createStore({ + /// Whether [destroySync] is implemented + /// + /// If `false`, calling will throw an [SyncOperationUnsupported] error. + abstract final bool supportsSyncDestroy; + + /// Create a new store with the specified name + Future createStore({ + required String storeName, + }); + + /// Create a new store with the specified name + void createStoreSync({ + required String storeName, + }); + + /// Whether [createStoreSync] is implemented + /// + /// If `false`, calling will throw an [SyncOperationUnsupported] error. + abstract final bool supportsSyncCreateStore; + + /// Remove all the tiles from within the specified store + Future resetStore({ required String storeName, }); - FutureOr resetStore({ + + /// Remove all the tiles from within the specified store + void resetStoreSync({ required String storeName, }); - FutureOr renameStore({ + + /// Whether [resetStoreSync] is implemented + /// + /// If `false`, calling will throw an [SyncOperationUnsupported] error. + abstract final bool supportsSyncResetStore; + + /// Change the name of the store named [currentStoreName] to [newStoreName] + Future renameStore({ + required String currentStoreName, + required String newStoreName, + }); + + /// Change the name of the store named [currentStoreName] to [newStoreName] + void renameStoreSync({ required String currentStoreName, required String newStoreName, }); - FutureOr deleteStore({ + + /// Whether [renameStoreSync] is implemented + /// + /// If `false`, calling will throw an [SyncOperationUnsupported] error. + abstract final bool supportsSyncRenameStore; + + /// Delete the specified store + Future deleteStore({ + required String storeName, + }); + + /// Delete the specified store + void deleteStoreSync({ + required String storeName, + }); + + /// Whether [deleteStoreSync] is implemented + /// + /// If `false`, calling will throw an [SyncOperationUnsupported] error. + abstract final bool supportsSyncDeleteStore; + + /// Retrieve the total size (in kibibytes KiB) of the image bytes of all the + /// tiles that belong to the specified store + /// + /// This does not return any other data that adds to the 'real' store size + Future getStoreSize({ required String storeName, }); + /// Retrieve the total size (in kibibytes KiB) of the image bytes of all the + /// tiles that belong to the specified store + /// + /// This does not return any other data that adds to the 'real' store size + double getStoreSizeSync({ + required String storeName, + }); + + /// Whether [getStoreSizeSync] is implemented + /// + /// If `false`, calling will throw an [SyncOperationUnsupported] error. + abstract final bool supportsSyncGetStoreSize; + + /// Retrieve the number of tiles that belong to the specified store + Future getStoreLength({ + required String storeName, + }); + + /// Retrieve the number of tiles that belong to the specified store + int getStoreLengthSync({ + required String storeName, + }); + + /// Whether [getStoreLengthSync] is implemented + /// + /// If `false`, calling will throw an [SyncOperationUnsupported] error. + abstract final bool supportsSyncGetStoreLength; + /// Get a raw tile by URL - FutureOr readTile({ + Future readTile({ required String url, }); + /// Get a raw tile by URL + BackendTile? readTileSync({ + required String url, + }); + + /// Whether [readTileSync] is implemented + /// + /// If `false`, calling will throw an [SyncOperationUnsupported] error. + abstract final bool supportsSyncReadTile; + /// Create or update a tile /// /// If the tile already existed, it will be added to the specified store. @@ -75,10 +208,40 @@ abstract interface class FMTCBackend { /// /// If [bytes] is provided and the tile already existed, it will be updated for /// all stores. - FutureOr createTile({ + Future writeTile({ + required String storeName, required String url, required Uint8List? bytes, + }); + + /// Create or update a tile + /// + /// If the tile already existed, it will be added to the specified store. + /// Otherwise, [bytes] must be specified, and the tile will be created and + /// added. + /// + /// If [bytes] is provided and the tile already existed, it will be updated for + /// all stores. + void writeTileSync({ + required String storeName, + required String url, + required Uint8List? bytes, + }); + + /// Whether [writeTileSync] is implemented + /// + /// If `false`, calling will throw an [SyncOperationUnsupported] error. + abstract final bool supportsSyncWriteTile; + + /// Remove the tile from the store, deleting it if orphaned + /// + /// Returns: + /// * `null` : if there was no existing tile + /// * `true` : if the tile itself could be deleted (it was orphaned) + /// * `false`: if the tile still belonged to at least store + Future deleteTile({ required String storeName, + required String url, }); /// Remove the tile from the store, deleting it if orphaned @@ -87,8 +250,17 @@ abstract interface class FMTCBackend { /// * `null` : if there was no existing tile /// * `true` : if the tile itself could be deleted (it was orphaned) /// * `false`: if the tile still belonged to at least store - FutureOr deleteTile({ + bool? deleteTileSync({ + required String storeName, required String url, + }); + + /// Whether [deleteTileSync] is implemented + /// + /// If `false`, calling will throw an [SyncOperationUnsupported] error. + abstract final bool supportsSyncDeleteTile; + + Future removeOldestTile({ required String storeName, }); } diff --git a/lib/src/bulk_download/manager.dart b/lib/src/bulk_download/manager.dart index 5bc5d87e..77c8efc1 100644 --- a/lib/src/bulk_download/manager.dart +++ b/lib/src/bulk_download/manager.dart @@ -67,7 +67,7 @@ Future _downloadManager( } // Setup tile generator isolate - final tileRecievePort = ReceivePort(); + final tilereceivePort = ReceivePort(); final tileIsolate = await Isolate.spawn( input.region.when( rectangle: (_) => TilesGenerator.rectangleTiles, @@ -75,11 +75,11 @@ Future _downloadManager( line: (_) => TilesGenerator.lineTiles, customPolygon: (_) => TilesGenerator.customPolygonTiles, ), - (sendPort: tileRecievePort.sendPort, region: input.region), - onExit: tileRecievePort.sendPort, + (sendPort: tilereceivePort.sendPort, region: input.region), + onExit: tilereceivePort.sendPort, debugName: '[FMTC] Tile Coords Generator Thread', ); - final rawTileStream = tileRecievePort.skip(input.region.start).take( + final rawTileStream = tilereceivePort.skip(input.region.start).take( input.region.end == null ? largestInt : (input.region.end! - input.region.start), @@ -119,7 +119,7 @@ Future _downloadManager( } // Setup two-way communications with root - final rootRecievePort = ReceivePort(); + final rootreceivePort = ReceivePort(); void send(Object? m) => input.sendPort.send(m); // Setup cancel, pause, and resume handling @@ -131,7 +131,7 @@ Future _downloadManager( final threadPausedStates = generateThreadPausedStates(); final cancelSignal = Completer(); var pauseResumeSignal = Completer()..complete(); - rootRecievePort.listen( + rootreceivePort.listen( (e) async { if (e == null) { try { @@ -172,7 +172,7 @@ Future _downloadManager( ); // Now it's safe, start accepting communications from the root - send(rootRecievePort.sendPort); + send(rootreceivePort.sendPort); // Start download threads & wait for download to complete/cancelled downloadDuration.start(); @@ -183,11 +183,11 @@ Future _downloadManager( if (cancelSignal.isCompleted) return; // Start thread worker isolate & setup two-way communications - final downloadThreadRecievePort = ReceivePort(); + final downloadThreadreceivePort = ReceivePort(); await Isolate.spawn( _singleDownloadThread, ( - sendPort: downloadThreadRecievePort.sendPort, + sendPort: downloadThreadreceivePort.sendPort, storeId: storeId, rootDirectory: input.rootDirectory, options: input.region.options, @@ -197,7 +197,7 @@ Future _downloadManager( obscuredQueryParams: input.obscuredQueryParams, headers: headers, ), - onExit: downloadThreadRecievePort.sendPort, + onExit: downloadThreadreceivePort.sendPort, debugName: '[FMTC] Bulk Download Thread #$threadNo', ); late final SendPort sendPort; @@ -214,7 +214,7 @@ Future _downloadManager( .then((sp) => sp.send(null)), ); - downloadThreadRecievePort.listen( + downloadThreadreceivePort.listen( (evt) async { // Thread is sending tile data if (evt is TileEvent) { @@ -288,7 +288,7 @@ Future _downloadManager( } // Thread ended, goto `onDone` - if (evt == null) return downloadThreadRecievePort.close(); + if (evt == null) return downloadThreadreceivePort.close(); }, onDone: () { try { @@ -323,7 +323,7 @@ Future _downloadManager( ); // Cleanup resources and shutdown - rootRecievePort.close(); + rootreceivePort.close(); tileIsolate.kill(priority: Isolate.immediate); await tileQueue.cancel(immediate: true); Isolate.exit(); diff --git a/lib/src/bulk_download/thread.dart b/lib/src/bulk_download/thread.dart index ed42d00b..5b9a9d81 100644 --- a/lib/src/bulk_download/thread.dart +++ b/lib/src/bulk_download/thread.dart @@ -17,12 +17,12 @@ Future _singleDownloadThread( }) input, ) async { // Setup two-way communications - final recievePort = ReceivePort(); + final receivePort = ReceivePort(); void send(Object m) => input.sendPort.send(m); - send(recievePort.sendPort); + send(receivePort.sendPort); // Setup tile queue - final tileQueue = StreamQueue(recievePort); + final tileQueue = StreamQueue(receivePort); // Open a reference to the Isar DB for the current store final db = Isar.openSync( @@ -45,7 +45,7 @@ Future _singleDownloadThread( // Cleanup resources and shutdown if no more coords available if (rawCoords == null) { - recievePort.close(); + receivePort.close(); await tileQueue.cancel(immediate: true); httpClient.close(); diff --git a/lib/src/bulk_download/tile_loops/generate.dart b/lib/src/bulk_download/tile_loops/generate.dart index a56c1b63..dcff6816 100644 --- a/lib/src/bulk_download/tile_loops/generate.dart +++ b/lib/src/bulk_download/tile_loops/generate.dart @@ -11,9 +11,9 @@ class TilesGenerator { final northWest = region.originalRegion.bounds.northWest; final southEast = region.originalRegion.bounds.southEast; - final recievePort = ReceivePort(); - input.sendPort.send(recievePort.sendPort); - final requestQueue = StreamQueue(recievePort); + final receivePort = ReceivePort(); + input.sendPort.send(receivePort.sendPort); + final requestQueue = StreamQueue(receivePort); for (double zoomLvl = region.minZoom.toDouble(); zoomLvl <= region.maxZoom; @@ -50,9 +50,9 @@ class TilesGenerator { final region = input.region as DownloadableRegion; final circleOutline = region.originalRegion.toOutline(); - final recievePort = ReceivePort(); - input.sendPort.send(recievePort.sendPort); - final requestQueue = StreamQueue(recievePort); + final receivePort = ReceivePort(); + input.sendPort.send(receivePort.sendPort); + final requestQueue = StreamQueue(receivePort); // Format: Map>> final Map>> outlineTileNums = {}; @@ -137,9 +137,9 @@ class TilesGenerator { final region = input.region as DownloadableRegion; final lineOutline = region.originalRegion.toOutlines(1); - final recievePort = ReceivePort(); - input.sendPort.send(recievePort.sendPort); - final requestQueue = StreamQueue(recievePort); + final receivePort = ReceivePort(); + input.sendPort.send(receivePort.sendPort); + final requestQueue = StreamQueue(receivePort); for (double zoomLvl = region.minZoom.toDouble(); zoomLvl <= region.maxZoom; @@ -244,9 +244,9 @@ class TilesGenerator { final region = input.region as DownloadableRegion; final customPolygonOutline = region.originalRegion.outline; - final recievePort = ReceivePort(); - input.sendPort.send(recievePort.sendPort); - final requestQueue = StreamQueue(recievePort); + final receivePort = ReceivePort(); + input.sendPort.send(receivePort.sendPort); + final requestQueue = StreamQueue(receivePort); for (double zoomLvl = region.minZoom.toDouble(); zoomLvl <= region.maxZoom; diff --git a/lib/src/misc/store_db_impl.dart b/lib/src/misc/store_db_impl.dart deleted file mode 100644 index 747d710a..00000000 --- a/lib/src/misc/store_db_impl.dart +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright © Luka S (JaffaKetchup) under GPL-v3 -// A full license can be found at .\LICENSE - -part of flutter_map_tile_caching; - -abstract class _StoreDb { - const _StoreDb(this._store); - - final StoreDirectory _store; - Isar get _db => FMTCRegistry.instance(_store.storeName); -} diff --git a/lib/src/misc/with_backend_access.dart b/lib/src/misc/with_backend_access.dart new file mode 100644 index 00000000..0f6efa6c --- /dev/null +++ b/lib/src/misc/with_backend_access.dart @@ -0,0 +1,12 @@ +// Copyright © Luka S (JaffaKetchup) under GPL-v3 +// A full license can be found at .\LICENSE + +part of flutter_map_tile_caching; + +abstract base class _WithBackendAccess { + const _WithBackendAccess(this._store); + + final StoreDirectory _store; + FMTCBackend get _backend => FMTC.instance.settings.backend; + String get _storeName => _store.storeName; +} diff --git a/lib/src/providers/image_provider.dart b/lib/src/providers/image_provider.dart index 0a43f81c..757f1192 100644 --- a/lib/src/providers/image_provider.dart +++ b/lib/src/providers/image_provider.dart @@ -32,23 +32,21 @@ class FMTCImageProvider extends ImageProvider { /// The coordinates of the tile to be fetched final TileCoordinates coords; - /// Configured root directory - final String directory; + FMTCBackend get _backend => FMTC.instance.settings.backend; - /// The database to write tiles to - final Isar db; + /// Configured root directory + // final String directory; - static final _removeOldestQueue = Queue(timeout: const Duration(seconds: 1)); - static final _cacheHitsQueue = Queue(); - static final _cacheMissesQueue = Queue(); + //static final _removeOldestQueue = Queue(timeout: const Duration(seconds: 1)); + //static final _cacheHitsQueue = Queue(); + //static final _cacheMissesQueue = Queue(); /// Create a specialised [ImageProvider] dedicated to 'flutter_map_tile_caching' FMTCImageProvider({ required this.provider, required this.options, required this.coords, - required this.directory, - }) : db = FMTCRegistry.instance(provider.storeDirectory.storeName); + }); @override ImageStreamCompleter loadImage( @@ -62,9 +60,8 @@ class FMTCImageProvider extends ImageProvider { scale: 1, debugLabel: coords.toString(), informationCollector: () => [ - DiagnosticsProperty('Tile coordinates', coords), - DiagnosticsProperty('Root directory', directory), DiagnosticsProperty('Store name', provider.storeDirectory.storeName), + DiagnosticsProperty('Tile coordinates', coords), DiagnosticsProperty('Current provider', key), ], ); @@ -102,7 +99,7 @@ class FMTCImageProvider extends ImageProvider { obscuredQueryParams: provider.settings.obscuredQueryParams, ); - final existingTile = await db.tiles.get(DatabaseTools.hash(matcherUrl)); + final existingTile = await _backend.readTile(url: matcherUrl); final needsCreating = existingTile == null; final needsUpdating = !needsCreating && @@ -224,8 +221,10 @@ class FMTCImageProvider extends ImageProvider { // Cache the tile retrieved from the network response unawaited( - db.writeTxn( - () => db.tiles.put(DbTile(url: matcherUrl, bytes: responseBytes)), + _backend.writeTile( + storeName: provider.storeDirectory.storeName, + url: matcherUrl, + bytes: responseBytes, ), ); diff --git a/lib/src/store/download.dart b/lib/src/store/download.dart index 13c496c1..78aa9e5b 100644 --- a/lib/src/store/download.dart +++ b/lib/src/store/download.dart @@ -165,11 +165,11 @@ class DownloadManagement { } // Start download thread - final recievePort = ReceivePort(); + final receivePort = ReceivePort(); await Isolate.spawn( _downloadManager, ( - sendPort: recievePort.sendPort, + sendPort: receivePort.sendPort, rootDirectory: FMTC.instance.rootDirectory.directory.absolute.path, region: region, storeName: _storeDirectory.storeName, @@ -184,7 +184,7 @@ class DownloadManagement { FMTC.instance.settings.defaultTileProviderSettings .obscuredQueryParams, ), - onExit: recievePort.sendPort, + onExit: receivePort.sendPort, debugName: '[FMTC] Master Bulk Download Thread', ); @@ -192,7 +192,7 @@ class DownloadManagement { final cancelCompleter = Completer(); Completer? pauseCompleter; - await for (final evt in recievePort) { + await for (final evt in receivePort) { // Handle new progress message if (evt is DownloadProgress) { yield evt; @@ -229,7 +229,7 @@ class DownloadManagement { } // Handle shutdown (both normal and cancellation) - recievePort.close(); + receivePort.close(); await FMTC.instance.rootDirectory.recovery.cancel(recoveryId); DownloadInstance.unregister(instanceId); cancelCompleter.complete(); diff --git a/lib/src/store/metadata.dart b/lib/src/store/metadata.dart index af32c903..d7953cc4 100644 --- a/lib/src/store/metadata.dart +++ b/lib/src/store/metadata.dart @@ -8,7 +8,7 @@ part of flutter_map_tile_caching; /// Uses a key-value format where both key and value must be [String]. More /// advanced requirements should use another package, as this is a basic /// implementation. -class StoreMetadata extends _StoreDb { +class StoreMetadata extends _WithBackendAccess { const StoreMetadata._(super._store); /// Add a new key-value pair to the store asynchronously diff --git a/lib/src/store/statistics.dart b/lib/src/store/statistics.dart index 5344c919..a6172b42 100644 --- a/lib/src/store/statistics.dart +++ b/lib/src/store/statistics.dart @@ -4,18 +4,18 @@ part of flutter_map_tile_caching; /// Provides statistics about a [StoreDirectory] -class StoreStats extends _StoreDb { +final class StoreStats extends _WithBackendAccess { const StoreStats._(super._store); /// Retrieve the total size of the stored tiles and metadata in kibibytes (KiB) /// /// Prefer [storeSizeAsync] to avoid blocking the UI thread. Otherwise, this /// has slightly better performance. - double get storeSize => _db.getSizeSync(includeIndexes: true) / 1024; + double get storeSize => _backend.getStoreSizeSync(storeName: _storeName); /// Retrieve the total size of the stored tiles and metadata in kibibytes (KiB) Future get storeSizeAsync async => - await _db.getSize(includeIndexes: true) / 1024; + _backend.getStoreSize(storeName: _storeName); /// Retrieve the number of stored tiles synchronously /// diff --git a/test/fmtc_test.dart b/test/fmtc_test.dart index cd054cf4..72b5702f 100644 --- a/test/fmtc_test.dart +++ b/test/fmtc_test.dart @@ -14,7 +14,7 @@ import 'package:test/test.dart'; void main() { Future countByGenerator(DownloadableRegion region) async { - final tileRecievePort = ReceivePort(); + final tilereceivePort = ReceivePort(); final tileIsolate = await Isolate.spawn( region.when( rectangle: (_) => TilesGenerator.rectangleTiles, @@ -22,15 +22,15 @@ void main() { line: (_) => TilesGenerator.lineTiles, customPolygon: (_) => TilesGenerator.customPolygonTiles, ), - (sendPort: tileRecievePort.sendPort, region: region), - onExit: tileRecievePort.sendPort, + (sendPort: tilereceivePort.sendPort, region: region), + onExit: tilereceivePort.sendPort, debugName: '[FMTC] Tile Coords Generator Thread', ); late final SendPort requestTilePort; int evts = -1; - await for (final evt in tileRecievePort) { + await for (final evt in tilereceivePort) { if (evt == null) break; if (evt is SendPort) requestTilePort = evt; requestTilePort.send(null); @@ -38,7 +38,7 @@ void main() { } tileIsolate.kill(priority: Isolate.immediate); - tileRecievePort.close(); + tilereceivePort.close(); return evts; } @@ -261,7 +261,7 @@ void main() { Future> listGenerator( DownloadableRegion region, ) async { - final tileRecievePort = ReceivePort(); + final tilereceivePort = ReceivePort(); final tileIsolate = await Isolate.spawn( region.when( rectangle: (_) => TilesGenerator.rectangleTiles, @@ -269,15 +269,15 @@ void main() { line: (_) => TilesGenerator.lineTiles, customPolygon: (_) => TilesGenerator.customPolygonTiles, ), - (sendPort: tileRecievePort.sendPort, region: region), - onExit: tileRecievePort.sendPort, + (sendPort: tilereceivePort.sendPort, region: region), + onExit: tilereceivePort.sendPort, debugName: '[FMTC] Tile Coords Generator Thread', ); late final SendPort requestTilePort; final Set<(int, int, int)> evts = {}; - await for (final evt in tileRecievePort) { + await for (final evt in tilereceivePort) { if (evt == null) break; if (evt is SendPort) { requestTilePort = evt..send(null); @@ -288,7 +288,7 @@ void main() { } tileIsolate.kill(priority: Isolate.immediate); - tileRecievePort.close(); + tilereceivePort.close(); return evts; } diff --git a/tile_server/bin/tile_server.dart b/tile_server/bin/tile_server.dart index 6ee36d9a..aa1e3076 100644 --- a/tile_server/bin/tile_server.dart +++ b/tile_server/bin/tile_server.dart @@ -53,7 +53,7 @@ Future main(List _) async { ); // Handle keyboard events - final keyboardHandlerRecievePort = ReceivePort(); + final keyboardHandlerreceivePort = ReceivePort(); await Isolate.spawn( (sendPort) { while (true) { @@ -65,10 +65,10 @@ Future main(List _) async { if (key.controlChar == ControlCharacter.arrowDown) sendPort.send(-1); } }, - keyboardHandlerRecievePort.sendPort, - onExit: keyboardHandlerRecievePort.sendPort, + keyboardHandlerreceivePort.sendPort, + onExit: keyboardHandlerreceivePort.sendPort, ); - keyboardHandlerRecievePort.listen( + keyboardHandlerreceivePort.listen( (message) => // Control artificial delay currentArtificialDelay += artificialDelayChangeAmount * message, From 816e742b4f5cef1797101a456975cdf7ca7cab25 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Fri, 24 Nov 2023 13:22:21 +0000 Subject: [PATCH 081/168] Complete(?) implementation of `removeOldestTile` Moved backend tools behind internal barrier, to prevent unintended usage Updated dependencies Former-commit-id: 34eec0303412b7f97103ffa0f46f8fe51b90d034 [formerly 389e40ac592405c147db88a788f1cd238e48a3e5] Former-commit-id: 5bf23fdcf1df2baadf0890e487842d4493aae660 --- .vscode/tasks.json | 7 +- lib/src/backend/impl_tools/no_sync.dart | 36 ++++++--- lib/src/backend/impls/objectbox/backend.dart | 84 +++++++++++++++++--- lib/src/backend/impls/objectbox/worker.dart | 42 ++++++++-- lib/src/backend/interfaces/backend.dart | 32 +++++++- lib/src/misc/with_backend_access.dart | 4 +- pubspec.yaml | 23 +++--- 7 files changed, 176 insertions(+), 52 deletions(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index dc562dec..94d950d1 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -2,10 +2,9 @@ "version": "2.0.0", "tasks": [ { - "type": "flutter", - "command": "flutter", + "type": "dart", + "command": "dart", "args": [ - "pub", "run", "build_runner", "build" @@ -15,7 +14,7 @@ ], "group": "build", "label": "Run Code Generator", - "detail": "flutter pub run build_runner build" + "detail": "dart run build_runner build" } ] } \ No newline at end of file diff --git a/lib/src/backend/impl_tools/no_sync.dart b/lib/src/backend/impl_tools/no_sync.dart index fdf38a7c..b537cf60 100644 --- a/lib/src/backend/impl_tools/no_sync.dart +++ b/lib/src/backend/impl_tools/no_sync.dart @@ -1,15 +1,16 @@ import 'dart:typed_data'; import '../interfaces/backend.dart'; +import '../interfaces/models.dart'; import 'errors.dart'; /// A shortcut to declare that an [FMTCBackend] does not support any synchronous /// versions of methods -mixin FMTCBackendNoSync implements FMTCBackend { +mixin FMTCBackendNoSync implements FMTCBackendInternal { /// This synchronous method is unsupported by this implementation - use /// [initialise] instead @override - Never initialiseSync({ + void initialiseSync({ String? rootDirectory, int? maxDatabaseSize, Map implSpecificArgs = const {}, @@ -22,7 +23,7 @@ mixin FMTCBackendNoSync implements FMTCBackend { /// This synchronous method is unsupported by this implementation - use /// [destroy] instead @override - Never destroySync({ + void destroySync({ bool deleteRoot = false, }) => throw SyncOperationUnsupported(); @@ -33,7 +34,7 @@ mixin FMTCBackendNoSync implements FMTCBackend { /// This synchronous method is unsupported by this implementation - use /// [createStore] instead @override - Never createStoreSync({ + void createStoreSync({ required String storeName, }) => throw SyncOperationUnsupported(); @@ -44,7 +45,7 @@ mixin FMTCBackendNoSync implements FMTCBackend { /// This synchronous method is unsupported by this implementation - use /// [resetStore] instead @override - Never resetStoreSync({ + void resetStoreSync({ required String storeName, }) => throw SyncOperationUnsupported(); @@ -55,7 +56,7 @@ mixin FMTCBackendNoSync implements FMTCBackend { /// This synchronous method is unsupported by this implementation - use /// [renameStore] instead @override - Never renameStoreSync({ + void renameStoreSync({ required String currentStoreName, required String newStoreName, }) => @@ -67,7 +68,7 @@ mixin FMTCBackendNoSync implements FMTCBackend { /// This synchronous method is unsupported by this implementation - use /// [deleteStore] instead @override - Never deleteStoreSync({ + void deleteStoreSync({ required String storeName, }) => throw SyncOperationUnsupported(); @@ -78,7 +79,7 @@ mixin FMTCBackendNoSync implements FMTCBackend { /// This synchronous method is unsupported by this implementation - use /// [getStoreSize] instead @override - Never getStoreSizeSync({ + double getStoreSizeSync({ required String storeName, }) => throw SyncOperationUnsupported(); @@ -89,7 +90,7 @@ mixin FMTCBackendNoSync implements FMTCBackend { /// This synchronous method is unsupported by this implementation - use /// [getStoreLength] instead @override - Never getStoreLengthSync({ + int getStoreLengthSync({ required String storeName, }) => throw SyncOperationUnsupported(); @@ -100,7 +101,7 @@ mixin FMTCBackendNoSync implements FMTCBackend { /// This synchronous method is unsupported by this implementation - use /// [readTile] instead @override - Never readTileSync({ + BackendTile? readTileSync({ required String url, }) => throw SyncOperationUnsupported(); @@ -111,7 +112,7 @@ mixin FMTCBackendNoSync implements FMTCBackend { /// This synchronous method is unsupported by this implementation - use /// [writeTile] instead @override - Never writeTileSync({ + void writeTileSync({ required String storeName, required String url, required Uint8List? bytes, @@ -124,7 +125,7 @@ mixin FMTCBackendNoSync implements FMTCBackend { /// This synchronous method is unsupported by this implementation - use /// [deleteTile] instead @override - Never deleteTileSync({ + bool? deleteTileSync({ required String storeName, required String url, }) => @@ -132,4 +133,15 @@ mixin FMTCBackendNoSync implements FMTCBackend { @override final supportsSyncDeleteTile = false; + + /// This synchronous method is unsupported by this implementation - use + /// [removeOldestTile] instead + @override + void removeOldestTileSync({ + required String storeName, + }) => + throw SyncOperationUnsupported(); + + @override + final supportsSyncRemoveOldestTile = false; } diff --git a/lib/src/backend/impls/objectbox/backend.dart b/lib/src/backend/impls/objectbox/backend.dart index ae1b3d27..095d6e24 100644 --- a/lib/src/backend/impls/objectbox/backend.dart +++ b/lib/src/backend/impls/objectbox/backend.dart @@ -3,39 +3,57 @@ import 'dart:io'; import 'dart:isolate'; import 'dart:typed_data'; +import 'package:async/async.dart'; import 'package:collection/collection.dart'; +import 'package:meta/meta.dart' as meta; import 'package:path_provider/path_provider.dart'; import '../../../misc/exts.dart'; import '../../impl_tools/errors.dart'; import '../../impl_tools/no_sync.dart'; import '../../interfaces/backend.dart'; -import '../../interfaces/models.dart'; import 'models/generated/objectbox.g.dart'; import 'models/models.dart'; part 'worker.dart'; +final class ObjectBoxBackend implements FMTCBackend { + @override + @meta.internal + FMTCBackendInternal get internal => ObjectBoxBackendInternal._instance; +} + /// Implementation of [FMTCBackend] that uses ObjectBox as the storage database -abstract interface class ObjectBoxBackend implements FMTCBackend { - /// Implementation of [FMTCBackend] that uses ObjectBox as the storage - /// database - factory ObjectBoxBackend() => _instance; +/// +/// Only to be accessed by FMTC via [ObjectBoxBackend.internal] +abstract interface class ObjectBoxBackendInternal + implements FMTCBackendInternal { static final _instance = _ObjectBoxBackendImpl._(); } -class _ObjectBoxBackendImpl with FMTCBackendNoSync implements ObjectBoxBackend { +class _ObjectBoxBackendImpl + with FMTCBackendNoSync + implements ObjectBoxBackendInternal { _ObjectBoxBackendImpl._(); void get expectInitialised => _sendPort ?? (throw RootUnavailable()); + + // Worker Comms SendPort? _sendPort; Completer? _workerCompleter; Completer<_WorkerRes>? _workerCmdRes; - //final _cmdQueue = <_WorkerKey, Completer<_WorkerRes>?>{ // for (final v in _WorkerKey.values) v: null, //}; + // TODO: Make cmds that need to be idempotent for performance, like + // delete oldest tile + + // `deleteOldestTile` + int _numberOfOldestTilesToDelete = 0; + Timer? _deleteOldestTileTimer; + String? _storeNameOfCurrentDeleteOldestTimer; + Future<_WorkerRes> sendCmd(_WorkerCmd cmd) async { expectInitialised; @@ -202,12 +220,54 @@ class _ObjectBoxBackendImpl with FMTCBackendNoSync implements ObjectBoxBackend { )) .data!['wasOrphaned'] as bool?; + void _sendRemoveOldestTileCmd(String storeName) { + sendCmd( + ( + key: _WorkerKey.removeOldestTile, + args: { + 'storeName': storeName, + 'number': _numberOfOldestTilesToDelete, + } + ), + ); + _numberOfOldestTilesToDelete = 0; + } + + @override + void removeOldestTileSync({ + required String storeName, + }) { + // Attempts to avoid flooding worker with requests to delete oldest tile, + // and 'batches' them instead + + if (_storeNameOfCurrentDeleteOldestTimer != storeName) { + // If the store has changed, failing to reset the batch/queue will mean + // tiles are removed from the wrong store + _storeNameOfCurrentDeleteOldestTimer = storeName; + if (_deleteOldestTileTimer != null && _deleteOldestTileTimer!.isActive) { + _deleteOldestTileTimer!.cancel(); + _sendRemoveOldestTileCmd(storeName); + } + } + + if (_deleteOldestTileTimer != null && _deleteOldestTileTimer!.isActive) { + _deleteOldestTileTimer!.cancel(); + _deleteOldestTileTimer = Timer( + const Duration(milliseconds: 500), + () => _sendRemoveOldestTileCmd(storeName), + ); + return; + } + + _deleteOldestTileTimer = Timer( + const Duration(seconds: 1), + () => _sendRemoveOldestTileCmd(storeName), + ); + } + @override - Future removeOldestTile({ + Future removeOldestTile({ required String storeName, }) async => - (await sendCmd( - (key: _WorkerKey.removeOldestTile, args: {'storeName': storeName}), - )) - .data!['tileDeleted']! as bool; + removeOldestTileSync(storeName: storeName); } diff --git a/lib/src/backend/impls/objectbox/worker.dart b/lib/src/backend/impls/objectbox/worker.dart index 251ca598..5abcb03c 100644 --- a/lib/src/backend/impls/objectbox/worker.dart +++ b/lib/src/backend/impls/objectbox/worker.dart @@ -294,25 +294,51 @@ Future _worker( break; case _WorkerKey.removeOldestTile: final storeName = cmd.args['storeName']! as String; + final numTilesToRemove = cmd.args['number']! as int; final tiles = root.box(); - final query = (tiles.query().order(ObjectBoxTile_.lastModified) + final tilesQuery = (tiles.query().order(ObjectBoxTile_.lastModified) ..linkMany( ObjectBoxTile_.stores, ObjectBoxStore_.name.equals(storeName), )) .build(); + final deleteTiles = await tilesQuery + .stream() + .where((tile) => tile.stores.length == 1) + .take(numTilesToRemove) + .toList(); + tilesQuery.close(); - int? deleteId; - await for (final tile in query.stream()) { - if (tile.stores.length == 1) { - deleteId = tile.id; - break; - } + if (deleteTiles.isEmpty) { + sendRes((key: cmd.key, data: null)); + break; } - query.close(); + final storeQuery = root + .box() + .query(ObjectBoxStore_.name.equals(storeName)) + .build(); + final store = storeQuery.findUnique() ?? + (throw StoreUnavailable(storeName: storeName)); + storeQuery.close(); + + root.runInTransaction( + TxMode.write, + () { + root.box().put( + store + ..numberOfTiles -= numTilesToRemove + ..numberOfBytes -= + deleteTiles.map((e) => e.bytes.lengthInBytes).sum, + mode: PutMode.update, + ); + tiles.removeMany(deleteTiles.map((e) => e.id).toList()); + }, + ); + + sendRes((key: cmd.key, data: null)); break; } } diff --git a/lib/src/backend/interfaces/backend.dart b/lib/src/backend/interfaces/backend.dart index e0f87390..61d7202f 100644 --- a/lib/src/backend/interfaces/backend.dart +++ b/lib/src/backend/interfaces/backend.dart @@ -11,17 +11,34 @@ import 'models.dart'; /// An abstract interface that FMTC will use to communicate with a storage /// 'backend' (usually one root) /// +/// See also [FMTCBackendInternal], which has the actual methods. This is +/// provided as a means to warn users to avoid using the backend directly. +/// /// To implementers: -/// * Use a public 'cover-up' and separate private implementation -/// * Use singletons (with a factory) to ensure consistent state management +/// * Provide a seperate [FMTCBackend] & [FMTCBackendInternal] implementation +/// (both public scope), and a private scope `FMTCBackendImpl` +/// * Annotate your [FMTCBackend.internal] method with '@internal' +/// * Always make [FMTCBackendInternal] a singleton 'cover-up' for +/// `FMTCBackendImpl`, without a constructor, as the `FMTCBackendImpl` will +/// be accessed via [FMTCBackend.internal] /// * Prefer throwing included implementation-generic errors/exceptions /// /// To end-users: /// * Use [FMTCSettings.backend] to set a custom backend /// * Not all sync versions of methods are guaranteed to have implementations +/// * Never access the [internal] method of a backend abstract interface class FMTCBackend { const FMTCBackend(); + @protected + FMTCBackendInternal get internal; +} + +/// An abstract interface that FMTC will use to communicate with a storage +/// 'backend' (usually one root) +/// +/// See [FMTCBackend] for more information. +abstract interface class FMTCBackendInternal { abstract final String friendlyIdentifier; /// {@template fmtc_backend_initialise} @@ -260,7 +277,16 @@ abstract interface class FMTCBackend { /// If `false`, calling will throw an [SyncOperationUnsupported] error. abstract final bool supportsSyncDeleteTile; - Future removeOldestTile({ + Future removeOldestTile({ required String storeName, }); + + void removeOldestTileSync({ + required String storeName, + }); + + /// Whether [removeOldestTileSync] is implemented + /// + /// If `false`, calling will throw an [SyncOperationUnsupported] error. + abstract final bool supportsSyncRemoveOldestTile; } diff --git a/lib/src/misc/with_backend_access.dart b/lib/src/misc/with_backend_access.dart index 0f6efa6c..f6483a36 100644 --- a/lib/src/misc/with_backend_access.dart +++ b/lib/src/misc/with_backend_access.dart @@ -1,12 +1,14 @@ // Copyright © Luka S (JaffaKetchup) under GPL-v3 // A full license can be found at .\LICENSE +// ignore_for_file: invalid_use_of_protected_member + part of flutter_map_tile_caching; abstract base class _WithBackendAccess { const _WithBackendAccess(this._store); final StoreDirectory _store; - FMTCBackend get _backend => FMTC.instance.settings.backend; + FMTCBackendInternal get _backend => FMTC.instance.settings.backend.internal; String get _storeName => _store.storeName; } diff --git a/pubspec.yaml b/pubspec.yaml index 2de9d4a9..79768e34 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -27,30 +27,29 @@ environment: flutter: ">=3.10.0" dependencies: - async: ^2.9.0 - collection: ^1.16.0 + async: ^2.11.0 + collection: ^1.18.0 dart_earcut: ^1.0.1 flutter: sdk: flutter - flutter_map: ^6.0.0 + flutter_map: ^6.0.1 http: ^1.1.0 isar: ^3.1.0+1 isar_flutter_libs: ^3.1.0+1 latlong2: ^0.9.0 - meta: ^1.7.0 + meta: ^1.11.0 objectbox: ^2.3.1 objectbox_flutter_libs: any - path: ^1.8.2 - path_provider: ^2.0.7 - queue: ^3.1.0+1 - stream_transform: ^2.0.0 - watcher: ^1.0.2 + path: ^1.8.3 + path_provider: ^2.1.1 + queue: ^3.1.0+2 + stream_transform: ^2.1.0 + watcher: ^1.1.0 dev_dependencies: build_runner: ^2.4.6 - #isar_generator: ^3.1.0+1 - objectbox_generator: any - test: ^1.24.4 + objectbox_generator: ^2.3.1 + test: ^1.24.9 flutter: null From c843a60d9c560e4c01087055d128159920f0be79 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Tue, 26 Dec 2023 17:40:10 +0000 Subject: [PATCH 082/168] Added cache hits & misses retrieval Started connection of backend to frontend Former-commit-id: 111aeabcccc6ff4fb7d0e9b14ea83bf8f80a5d12 [formerly f9f4009c6dcfd7888b893422c6de653c3ae193cd] Former-commit-id: 14708c700318746911df694b12be74a2a3b88f6e --- example/pubspec.yaml | 22 ++++---- lib/src/backend/impl_tools/no_sync.dart | 22 ++++++++ lib/src/backend/impls/objectbox/backend.dart | 50 ++++++++++------- .../models/generated/objectbox-model.json | 12 ++++- .../impls/objectbox/models/models.dart | 10 ++++ lib/src/backend/impls/objectbox/worker.dart | 53 +++++++++++++++++++ lib/src/backend/interfaces/backend.dart | 40 ++++++++++++-- lib/src/backend/interfaces/models.dart | 16 ++++++ lib/src/misc/with_backend_access.dart | 3 +- lib/src/store/manage.dart | 12 ++--- lib/src/store/statistics.dart | 27 ++++------ pubspec.yaml | 14 ++--- 12 files changed, 214 insertions(+), 67 deletions(-) diff --git a/example/pubspec.yaml b/example/pubspec.yaml index a6c8648f..8d9ec7c6 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -11,28 +11,28 @@ environment: dependencies: auto_size_text: ^3.0.0 - badges: ^3.0.2 + badges: ^3.1.2 better_open_file: ^3.6.4 - collection: ^1.17.1 + collection: ^1.18.0 dart_earcut: ^1.0.1 file_picker: ^5.2.10 flutter: sdk: flutter - flutter_foreground_task: ^6.0.0+1 - flutter_map: ^6.0.0 + flutter_foreground_task: ^6.1.2 + flutter_map: ^6.1.0 flutter_map_animations: ^0.5.3 - flutter_map_tile_caching: + flutter_map_tile_caching: ^9.0.0-dev.5 flutter_speed_dial: ^7.0.0 fmtc_plus_sharing: ^8.0.0 - google_fonts: ^5.1.0 + google_fonts: ^6.1.0 gpx: ^2.2.1 - http: ^1.0.0 - intl: ^0.18.0 + http: ^1.1.2 + intl: ^0.19.0 latlong2: ^0.9.0 osm_nominatim: ^3.0.0 - path: ^1.8.3 - provider: ^6.0.3 - stream_transform: ^2.0.0 + path: ^1.9.0 + provider: ^6.1.1 + stream_transform: ^2.1.0 validators: ^3.0.0 version: ^3.0.2 diff --git a/lib/src/backend/impl_tools/no_sync.dart b/lib/src/backend/impl_tools/no_sync.dart index b537cf60..5c101af9 100644 --- a/lib/src/backend/impl_tools/no_sync.dart +++ b/lib/src/backend/impl_tools/no_sync.dart @@ -98,6 +98,28 @@ mixin FMTCBackendNoSync implements FMTCBackendInternal { @override final supportsSyncGetStoreLength = false; + /// This synchronous method is unsupported by this implementation - use + /// [getStoreHits] instead + @override + int getStoreHitsSync({ + required String storeName, + }) => + throw SyncOperationUnsupported(); + + @override + final supportsSyncGetStoreHits = false; + + /// This synchronous method is unsupported by this implementation - use + /// [getStoreMisses] instead + @override + int getStoreMissesSync({ + required String storeName, + }) => + throw SyncOperationUnsupported(); + + @override + final supportsSyncGetStoreMisses = false; + /// This synchronous method is unsupported by this implementation - use /// [readTile] instead @override diff --git a/lib/src/backend/impls/objectbox/backend.dart b/lib/src/backend/impls/objectbox/backend.dart index 095d6e24..5333d56d 100644 --- a/lib/src/backend/impls/objectbox/backend.dart +++ b/lib/src/backend/impls/objectbox/backend.dart @@ -3,7 +3,6 @@ import 'dart:io'; import 'dart:isolate'; import 'dart:typed_data'; -import 'package:async/async.dart'; import 'package:collection/collection.dart'; import 'package:meta/meta.dart' as meta; import 'package:path_provider/path_provider.dart'; @@ -46,13 +45,10 @@ class _ObjectBoxBackendImpl // for (final v in _WorkerKey.values) v: null, //}; - // TODO: Make cmds that need to be idempotent for performance, like - // delete oldest tile - - // `deleteOldestTile` - int _numberOfOldestTilesToDelete = 0; - Timer? _deleteOldestTileTimer; - String? _storeNameOfCurrentDeleteOldestTimer; + // `deleteOldestTile` tracking & debouncing + int _dotLength = 0; + Timer? _dotDebouncer; + String? _dotStore; Future<_WorkerRes> sendCmd(_WorkerCmd cmd) async { expectInitialised; @@ -187,6 +183,24 @@ class _ObjectBoxBackendImpl )) .data!['length']! as int; + @override + Future getStoreHits({ + required String storeName, + }) async => + (await sendCmd( + (key: _WorkerKey.getStoreHits, args: {'storeName': storeName}), + )) + .data!['hits']! as int; + + @override + Future getStoreMisses({ + required String storeName, + }) async => + (await sendCmd( + (key: _WorkerKey.getStoreMisses, args: {'storeName': storeName}), + )) + .data!['misses']! as int; + @override Future readTile({ required String url, @@ -226,11 +240,11 @@ class _ObjectBoxBackendImpl key: _WorkerKey.removeOldestTile, args: { 'storeName': storeName, - 'number': _numberOfOldestTilesToDelete, + 'number': _dotLength, } ), ); - _numberOfOldestTilesToDelete = 0; + _dotLength = 0; } @override @@ -240,26 +254,26 @@ class _ObjectBoxBackendImpl // Attempts to avoid flooding worker with requests to delete oldest tile, // and 'batches' them instead - if (_storeNameOfCurrentDeleteOldestTimer != storeName) { + if (_dotStore != storeName) { // If the store has changed, failing to reset the batch/queue will mean // tiles are removed from the wrong store - _storeNameOfCurrentDeleteOldestTimer = storeName; - if (_deleteOldestTileTimer != null && _deleteOldestTileTimer!.isActive) { - _deleteOldestTileTimer!.cancel(); + _dotStore = storeName; + if (_dotDebouncer != null && _dotDebouncer!.isActive) { + _dotDebouncer!.cancel(); _sendRemoveOldestTileCmd(storeName); } } - if (_deleteOldestTileTimer != null && _deleteOldestTileTimer!.isActive) { - _deleteOldestTileTimer!.cancel(); - _deleteOldestTileTimer = Timer( + if (_dotDebouncer != null && _dotDebouncer!.isActive) { + _dotDebouncer!.cancel(); + _dotDebouncer = Timer( const Duration(milliseconds: 500), () => _sendRemoveOldestTileCmd(storeName), ); return; } - _deleteOldestTileTimer = Timer( + _dotDebouncer = Timer( const Duration(seconds: 1), () => _sendRemoveOldestTileCmd(storeName), ); diff --git a/lib/src/backend/impls/objectbox/models/generated/objectbox-model.json b/lib/src/backend/impls/objectbox/models/generated/objectbox-model.json index b75bfe65..edb674e3 100644 --- a/lib/src/backend/impls/objectbox/models/generated/objectbox-model.json +++ b/lib/src/backend/impls/objectbox/models/generated/objectbox-model.json @@ -5,7 +5,7 @@ "entities": [ { "id": "1:7419244569066266196", - "lastPropertyId": "4:3677248801338209880", + "lastPropertyId": "6:3446172587861422549", "name": "ObjectBoxStore", "properties": [ { @@ -30,6 +30,16 @@ "id": "4:3677248801338209880", "name": "numberOfBytes", "type": 8 + }, + { + "id": "5:1702586868505261124", + "name": "hits", + "type": 6 + }, + { + "id": "6:3446172587861422549", + "name": "misses", + "type": 6 } ], "relations": [] diff --git a/lib/src/backend/impls/objectbox/models/models.dart b/lib/src/backend/impls/objectbox/models/models.dart index eeb080f1..5db648e1 100644 --- a/lib/src/backend/impls/objectbox/models/models.dart +++ b/lib/src/backend/impls/objectbox/models/models.dart @@ -14,10 +14,18 @@ base class ObjectBoxStore extends BackendStore { @Unique() String name; + @override int numberOfTiles; + @override double numberOfBytes; + @override + int hits; + + @override + int misses; + @Index() @Backlink() final tiles = ToMany(); @@ -26,6 +34,8 @@ base class ObjectBoxStore extends BackendStore { required this.name, required this.numberOfTiles, required this.numberOfBytes, + required this.hits, + required this.misses, }); } diff --git a/lib/src/backend/impls/objectbox/worker.dart b/lib/src/backend/impls/objectbox/worker.dart index 5abcb03c..fc3a4578 100644 --- a/lib/src/backend/impls/objectbox/worker.dart +++ b/lib/src/backend/impls/objectbox/worker.dart @@ -12,6 +12,8 @@ enum _WorkerKey { deleteStore, getStoreSize, getStoreLength, + getStoreHits, + getStoreMisses, readTile, writeTile, deleteTile, @@ -68,6 +70,8 @@ Future _worker( name: cmd.args['storeName']! as String, numberOfTiles: 0, numberOfBytes: 0, + hits: 0, + misses: 0, ), mode: PutMode.insert, ); @@ -78,6 +82,7 @@ Future _worker( final removeIds = []; final tiles = root.box(); + final stores = root.box(); final tilesQuery = (tiles.query() ..linkMany( @@ -86,6 +91,9 @@ Future _worker( )) .build(); + final storeQuery = + stores.query(ObjectBoxStore_.name.equals(storeName)).build(); + root.runInTransaction( TxMode.write, () { @@ -107,6 +115,21 @@ Future _worker( tiles.query(ObjectBoxTile_.id.oneOf(removeIds)).build() ..remove() ..close(); + + final store = storeQuery.findUnique() ?? + (throw StoreUnavailable(storeName: storeName)); + storeQuery.close(); + + assert(store.tiles.isEmpty); + + stores.put( + store + ..tiles.clear() + ..numberOfTiles = 0 + ..numberOfBytes = 0 + ..hits = 0 + ..misses = 0, + ); }, ); sendRes((key: cmd.key, data: null)); @@ -168,6 +191,34 @@ Future _worker( sendRes((key: cmd.key, data: {'length': length})); break; + case _WorkerKey.getStoreHits: + final storeName = cmd.args['storeName']! as String; + + final query = root + .box() + .query(ObjectBoxStore_.name.equals(storeName)) + .build(); + final hits = (query.findUnique() ?? + (throw StoreUnavailable(storeName: storeName))) + .hits; + query.close(); + + sendRes((key: cmd.key, data: {'hits': hits})); + break; + case _WorkerKey.getStoreMisses: + final storeName = cmd.args['storeName']! as String; + + final query = root + .box() + .query(ObjectBoxStore_.name.equals(storeName)) + .build(); + final misses = (query.findUnique() ?? + (throw StoreUnavailable(storeName: storeName))) + .misses; + query.close(); + + sendRes((key: cmd.key, data: {'misses': misses})); + break; case _WorkerKey.readTile: final query = root .box() @@ -176,6 +227,8 @@ Future _worker( final tile = query.findUnique(); query.close(); + // TODO: Hits & misses + sendRes((key: cmd.key, data: {'tile': tile})); break; case _WorkerKey.writeTile: diff --git a/lib/src/backend/interfaces/backend.dart b/lib/src/backend/interfaces/backend.dart index 61d7202f..cd802ea6 100644 --- a/lib/src/backend/interfaces/backend.dart +++ b/lib/src/backend/interfaces/backend.dart @@ -26,12 +26,12 @@ import 'models.dart'; /// To end-users: /// * Use [FMTCSettings.backend] to set a custom backend /// * Not all sync versions of methods are guaranteed to have implementations -/// * Never access the [internal] method of a backend -abstract interface class FMTCBackend { +/// * Avoid calling the [internal] method of a backend +abstract interface class FMTCBackend { const FMTCBackend(); @protected - FMTCBackendInternal get internal; + Internal get internal; } /// An abstract interface that FMTC will use to communicate with a storage @@ -202,6 +202,40 @@ abstract interface class FMTCBackendInternal { /// If `false`, calling will throw an [SyncOperationUnsupported] error. abstract final bool supportsSyncGetStoreLength; + /// Retrieve the number of times that a tile was successfully retrieved from + /// the specified store when browsing + Future getStoreHits({ + required String storeName, + }); + + /// Retrieve the number of times that a tile was successfully retrieved from + /// the specified store when browsing + int getStoreHitsSync({ + required String storeName, + }); + + /// Whether [getStoreHitsSync] is implemented + /// + /// If `false`, calling will throw an [SyncOperationUnsupported] error. + abstract final bool supportsSyncGetStoreHits; + + /// Retrieve the number of times that a tile was attempted to be retrieved from + /// the specified store when browsing, but was not present + Future getStoreMisses({ + required String storeName, + }); + + /// Retrieve the number of times that a tile was attempted to be retrieved from + /// the specified store when browsing, but was not present + int getStoreMissesSync({ + required String storeName, + }); + + /// Whether [getStoreMissesSync] is implemented + /// + /// If `false`, calling will throw an [SyncOperationUnsupported] error. + abstract final bool supportsSyncGetStoreMisses; + /// Get a raw tile by URL Future readTile({ required String url, diff --git a/lib/src/backend/interfaces/models.dart b/lib/src/backend/interfaces/models.dart index ff7dfa6d..bbc67d33 100644 --- a/lib/src/backend/interfaces/models.dart +++ b/lib/src/backend/interfaces/models.dart @@ -1,15 +1,26 @@ import 'dart:typed_data'; +import 'package:meta/meta.dart'; + abstract base class BackendStore { abstract String name; + abstract int numberOfTiles; + abstract double numberOfBytes; + abstract int hits; + abstract int misses; /// Uses [name] for equality comparisons only (unless the two objects are /// [identical]) + /// + /// Overriding this in an implementation may cause FMTC logic to break, and is + /// therefore not recommended. @override + @nonVirtual bool operator ==(Object? other) => identical(this, other) || (other is BackendStore && name == other.name); @override + @nonVirtual int get hashCode => name.hashCode; } @@ -20,10 +31,15 @@ abstract base class BackendTile { /// Uses [url] for equality comparisons only (unless the two objects are /// [identical]) + /// + /// Overriding this in an implementation may cause FMTC logic to break, and is + /// therefore not recommended. @override + @nonVirtual bool operator ==(Object? other) => identical(this, other) || (other is BackendTile && url == other.url); @override + @nonVirtual int get hashCode => url.hashCode; } diff --git a/lib/src/misc/with_backend_access.dart b/lib/src/misc/with_backend_access.dart index f6483a36..e95d512e 100644 --- a/lib/src/misc/with_backend_access.dart +++ b/lib/src/misc/with_backend_access.dart @@ -1,14 +1,13 @@ // Copyright © Luka S (JaffaKetchup) under GPL-v3 // A full license can be found at .\LICENSE -// ignore_for_file: invalid_use_of_protected_member - part of flutter_map_tile_caching; abstract base class _WithBackendAccess { const _WithBackendAccess(this._store); final StoreDirectory _store; + // ignore: invalid_use_of_protected_member FMTCBackendInternal get _backend => FMTC.instance.settings.backend.internal; String get _storeName => _store.storeName; } diff --git a/lib/src/store/manage.dart b/lib/src/store/manage.dart index b83f6259..83adfc16 100644 --- a/lib/src/store/manage.dart +++ b/lib/src/store/manage.dart @@ -5,16 +5,10 @@ part of flutter_map_tile_caching; /// Manages a [StoreDirectory]'s representation on the filesystem, such as /// creation and deletion -class StoreManagement { - StoreManagement._(StoreDirectory storeDirectory) - : _name = storeDirectory.storeName, - _id = DatabaseTools.hash(storeDirectory.storeName), - _registry = FMTCRegistry.instance, - _rootDirectory = FMTC.instance.rootDirectory.directory; +final class StoreManagement extends _WithBackendAccess { + StoreManagement._(super.store) + : _rootDirectory = FMTC.instance.rootDirectory.directory; - final String _name; - final int _id; - final FMTCRegistry _registry; final Directory _rootDirectory; /// Check whether this store is ready for use diff --git a/lib/src/store/statistics.dart b/lib/src/store/statistics.dart index a6172b42..a8e9eaaa 100644 --- a/lib/src/store/statistics.dart +++ b/lib/src/store/statistics.dart @@ -14,39 +14,42 @@ final class StoreStats extends _WithBackendAccess { double get storeSize => _backend.getStoreSizeSync(storeName: _storeName); /// Retrieve the total size of the stored tiles and metadata in kibibytes (KiB) - Future get storeSizeAsync async => + Future get storeSizeAsync => _backend.getStoreSize(storeName: _storeName); /// Retrieve the number of stored tiles synchronously /// /// Prefer [storeLengthAsync] to avoid blocking the UI thread. Otherwise, this /// has slightly better performance. - int get storeLength => _db.tiles.countSync(); + int get storeLength => _backend.getStoreLengthSync(storeName: _storeName); /// Retrieve the number of stored tiles asynchronously - Future get storeLengthAsync => _db.tiles.count(); + Future get storeLengthAsync => + _backend.getStoreLength(storeName: _storeName); /// Retrieve the number of tiles that were successfully retrieved from the /// store during browsing synchronously /// /// Prefer [cacheHitsAsync] to avoid blocking the UI thread. Otherwise, this /// has slightly better performance. - int get cacheHits => _db.descriptorSync.hits; + int get cacheHits => _backend.getStoreHitsSync(storeName: _storeName); /// Retrieve the number of tiles that were successfully retrieved from the /// store during browsing asynchronously - Future get cacheHitsAsync async => (await _db.descriptor).hits; + Future get cacheHitsAsync async => + _backend.getStoreHits(storeName: _storeName); /// Retrieve the number of tiles that were unsuccessfully retrieved from the /// store during browsing synchronously /// /// Prefer [cacheMissesAsync] to avoid blocking the UI thread. Otherwise, this /// has slightly better performance. - int get cacheMisses => _db.descriptorSync.misses; + int get cacheMisses => _backend.getStoreMissesSync(storeName: _storeName); /// Retrieve the number of tiles that were unsuccessfully retrieved from the /// store during browsing asynchronously - Future get cacheMissesAsync async => (await _db.descriptor).misses; + Future get cacheMissesAsync async => + _backend.getStoreMisses(storeName: _storeName); /// Watch for changes in the current store /// @@ -74,15 +77,7 @@ final class StoreStats extends _WithBackendAccess { StoreParts.stats, ], }) => - StreamGroup.merge([ - if (storeParts.contains(StoreParts.metadata)) - _db.metadata.watchLazy(fireImmediately: fireImmediately), - if (storeParts.contains(StoreParts.tiles)) - _db.tiles.watchLazy(fireImmediately: fireImmediately), - if (storeParts.contains(StoreParts.stats)) - _db.storeDescriptor - .watchObjectLazy(0, fireImmediately: fireImmediately), - ]).debounce(debounce ?? Duration.zero); + throw UnimplementedError(); } /// Parts of a store which can be watched diff --git a/pubspec.yaml b/pubspec.yaml index 79768e34..7219eead 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -32,24 +32,24 @@ dependencies: dart_earcut: ^1.0.1 flutter: sdk: flutter - flutter_map: ^6.0.1 - http: ^1.1.0 + flutter_map: ^6.1.0 + http: ^1.1.2 isar: ^3.1.0+1 isar_flutter_libs: ^3.1.0+1 latlong2: ^0.9.0 meta: ^1.11.0 - objectbox: ^2.3.1 + objectbox: ^2.4.0 objectbox_flutter_libs: any - path: ^1.8.3 + path: ^1.9.0 path_provider: ^2.1.1 queue: ^3.1.0+2 stream_transform: ^2.1.0 watcher: ^1.1.0 dev_dependencies: - build_runner: ^2.4.6 - objectbox_generator: ^2.3.1 - test: ^1.24.9 + build_runner: ^2.4.7 + objectbox_generator: ^2.4.0 + test: ^1.25.0 flutter: null From 446f5bd74214ff8f153a3a0964c87be2026a9372 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Fri, 29 Dec 2023 18:28:15 +0100 Subject: [PATCH 083/168] Improved backend worker lifecycle & communications stability Improved error handling Added `storeExists` method Former-commit-id: c9c6bbf783b38c33a656f824836d9b70e55fe9e7 [formerly 2fe0e7af09000b663835def041b67a514293a34c] Former-commit-id: 65551e3d8755b9316a1df4d36ecb81390208c8f1 --- lib/src/backend/impl_tools/errors.dart | 54 +- lib/src/backend/impl_tools/no_sync.dart | 12 + lib/src/backend/impls/objectbox/backend.dart | 191 ++--- lib/src/backend/impls/objectbox/worker.dart | 690 ++++++++++--------- lib/src/backend/interfaces/backend.dart | 22 + lib/src/store/manage.dart | 19 +- 6 files changed, 540 insertions(+), 448 deletions(-) diff --git a/lib/src/backend/impl_tools/errors.dart b/lib/src/backend/impl_tools/errors.dart index f64872b3..e9be79f2 100644 --- a/lib/src/backend/impl_tools/errors.dart +++ b/lib/src/backend/impl_tools/errors.dart @@ -1,50 +1,58 @@ +/// An error to be thrown by backend implementations in known events only +/// +/// A backend can create custom errors of this type, which is useful to show +/// that the backend is throwing a known expected error, rather than an +/// unexpected one. +base class FMTCBackendError extends Error {} + /// Indicates that the backend/root structure (ie. database and/or directory) was /// not available for use in operations, because either: /// * it was already closed /// * it was never created -/// * it was invalid/corrupt -/// * ... or it was otherwise unavailable -/// -/// To be thrown by backend implementations. For resolution by end-user. -class RootUnavailable extends Error { +/// * it was closed immediately whilst this operation was in progress +final class RootUnavailable extends FMTCBackendError { @override String toString() => 'RootUnavailable: The requested backend/root was unavailable'; } /// Indicates that the backend/root structure could not be initialised, because -/// it was already initialised. -/// -/// Try destroying it first. -/// -/// To be thrown by backend implementations. For resolution by end-user. -class RootAlreadyInitialised extends Error { +/// it was already initialised +final class RootAlreadyInitialised extends FMTCBackendError { @override String toString() => - 'RootUnavailable: The requested backend/root could not be initialised because it was already initialised'; + 'RootAlreadyInitialised: The requested backend/root could not be initialised because it was already initialised'; } /// Indicates that the specified store structure was not available for use in /// operations, likely because it didn't exist -/// -/// To be thrown by backend implementations. For resolution by end-user. -class StoreUnavailable extends Error { +final class StoreNotExists extends FMTCBackendError { final String storeName; - StoreUnavailable({required this.storeName}); + StoreNotExists({required this.storeName}); @override String toString() => - 'StoreUnavailable: The requested store "$storeName" was unavailable'; + 'StoreNotExists: The requested store "$storeName" did not exist'; +} + +/// Indicates that the specified store structure could not be created because it +/// already existed +final class StoreAlreadyExists extends FMTCBackendError { + final String storeName; + + StoreAlreadyExists({required this.storeName}); + + @override + String toString() => + 'StoreAlreadyExists: The requested store "$storeName" already existed'; } /// Indicates that the specified tile could not be updated because it did not /// already exist /// -/// To be thrown by backend implementations. For resolution by FMTC. -/// /// If you have this error in your application, please file a bug report. -class TileCannotUpdate extends Error { +final class TileCannotUpdate extends FMTCBackendError { final String url; TileCannotUpdate({required this.url}); @@ -55,16 +63,14 @@ class TileCannotUpdate extends Error { } /// Indicates that the backend implementation does not support the invoked -/// synchronous operation. +/// synchronous operation /// /// Use the asynchronous version instead. /// /// Note that there is no equivalent error for async operations: if there is no /// specific async version of an operation, it should redirect to the sync /// version. -/// -/// To be thrown by backend implementations. For resolution by end-user. -class SyncOperationUnsupported extends Error { +final class SyncOperationUnsupported extends FMTCBackendError { @override String toString() => 'SyncOperationUnsupported: The backend implementation does not support the invoked synchronous operation.'; diff --git a/lib/src/backend/impl_tools/no_sync.dart b/lib/src/backend/impl_tools/no_sync.dart index 5c101af9..a730054a 100644 --- a/lib/src/backend/impl_tools/no_sync.dart +++ b/lib/src/backend/impl_tools/no_sync.dart @@ -25,12 +25,24 @@ mixin FMTCBackendNoSync implements FMTCBackendInternal { @override void destroySync({ bool deleteRoot = false, + bool immediate = false, }) => throw SyncOperationUnsupported(); @override final supportsSyncDestroy = false; + /// This synchronous method is unsupported by this implementation - use + /// [storeExists] instead + @override + bool storeExistsSync({ + required String storeName, + }) => + throw SyncOperationUnsupported(); + + @override + final supportsSyncStoreExists = false; + /// This synchronous method is unsupported by this implementation - use /// [createStore] instead @override diff --git a/lib/src/backend/impls/objectbox/backend.dart b/lib/src/backend/impls/objectbox/backend.dart index 5333d56d..1941868c 100644 --- a/lib/src/backend/impls/objectbox/backend.dart +++ b/lib/src/backend/impls/objectbox/backend.dart @@ -1,9 +1,9 @@ import 'dart:async'; import 'dart:io'; import 'dart:isolate'; -import 'dart:typed_data'; import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; import 'package:meta/meta.dart' as meta; import 'package:path_provider/path_provider.dart'; @@ -37,41 +37,36 @@ class _ObjectBoxBackendImpl void get expectInitialised => _sendPort ?? (throw RootUnavailable()); - // Worker Comms + // Worker communication SendPort? _sendPort; - Completer? _workerCompleter; - Completer<_WorkerRes>? _workerCmdRes; - //final _cmdQueue = <_WorkerKey, Completer<_WorkerRes>?>{ - // for (final v in _WorkerKey.values) v: null, - //}; + final Map?>> _workerRes = {}; + late int _workerId; + late Completer _workerComplete; // `deleteOldestTile` tracking & debouncing int _dotLength = 0; Timer? _dotDebouncer; String? _dotStore; - Future<_WorkerRes> sendCmd(_WorkerCmd cmd) async { + Future?> _sendCmd({ + required _WorkerCmdType type, + required Map args, + }) async { expectInitialised; - // If a command is already pending response, wait for it to complete - await _workerCmdRes?.future; + final id = ++_workerId; + _workerRes[id] = Completer(); + _sendPort!.send((id: id, type: type, args: args)); + final res = await _workerRes[id]!.future; + _workerRes.remove(id); - _workerCmdRes = Completer(); - _sendPort!.send(cmd); - final res = await _workerCmdRes!.future; - _workerCmdRes = null; - return res; - } + final err = res?['error']; + if (err == null) return res; - Future workerListener(ReceivePort receivePort) async { - await for (final _WorkerRes evt in receivePort) { - if (evt.key == _WorkerKey.initialise_) { - _sendPort = evt.data!['sendPort']! as SendPort; - } else { - _workerCmdRes!.complete(evt); - } - } - _workerCompleter!.complete(); + if (err is FMTCBackendError) throw err; + debugPrint('An unexpected error in the FMTC backend occurred:'); + // ignore: only_throw_errors + throw err; } @override @@ -99,6 +94,23 @@ class _ObjectBoxBackendImpl // Setup worker isolate final receivePort = ReceivePort(); + _workerId = 0; + _workerRes.clear(); + _workerRes[0] = Completer(); + unawaited( + _workerRes[0]!.future.then((res) { + _sendPort = res!['sendPort']; + _workerRes.remove(0); + }), + ); + _workerComplete = Completer(); + receivePort.listen( + (evt) { + evt as ({int id, Map? data}); + _workerRes[evt.id]!.complete(evt.data); + }, + onDone: () => _workerComplete.complete(), + ); await Isolate.spawn( _worker, ( @@ -112,101 +124,127 @@ class _ObjectBoxBackendImpl onExit: receivePort.sendPort, debugName: '[FMTC] ObjectBox Backend Worker', ); - - _workerCompleter = Completer(); - return; } @override Future destroy({ bool deleteRoot = false, + bool immediate = false, }) async { expectInitialised; + if (!immediate) { + await Future.wait(_workerRes.values.map((e) => e.future)); + } + unawaited( - sendCmd( - (key: _WorkerKey.destroy_, args: {'deleteRoot': deleteRoot}), + _sendCmd( + type: _WorkerCmdType.destroy_, + args: {'deleteRoot': deleteRoot}, ), ); - await _workerCompleter!.future; + await _workerComplete.future; + _sendPort = null; + + for (final completer in _workerRes.values) { + completer.complete({'error': RootUnavailable()}); + } } + @override + Future storeExists({ + required String storeName, + }) async => + (await _sendCmd( + type: _WorkerCmdType.storeExists, + args: {'storeName': storeName}, + ))!['exists']; + @override Future createStore({ required String storeName, }) => - sendCmd((key: _WorkerKey.createStore, args: {'storeName': storeName})); + _sendCmd( + type: _WorkerCmdType.createStore, + args: {'storeName': storeName}, + ); @override Future resetStore({ required String storeName, }) => - sendCmd((key: _WorkerKey.resetStore, args: {'storeName': storeName})); + _sendCmd( + type: _WorkerCmdType.resetStore, + args: {'storeName': storeName}, + ); @override Future renameStore({ required String currentStoreName, required String newStoreName, }) => - sendCmd( - ( - key: _WorkerKey.renameStore, - args: { - 'currentStoreName': currentStoreName, - 'newStoreName': newStoreName, - } - ), + _sendCmd( + type: _WorkerCmdType.renameStore, + args: { + 'currentStoreName': currentStoreName, + 'newStoreName': newStoreName, + }, ); @override Future deleteStore({ required String storeName, }) => - sendCmd((key: _WorkerKey.deleteStore, args: {'storeName': storeName})); + _sendCmd( + type: _WorkerCmdType.deleteStore, + args: {'storeName': storeName}, + ); @override Future getStoreSize({ required String storeName, }) async => - (await sendCmd( - (key: _WorkerKey.getStoreSize, args: {'storeName': storeName}), - )) - .data!['size']! as double; + (await _sendCmd( + type: _WorkerCmdType.getStoreSize, + args: {'storeName': storeName}, + ))!['size']; @override Future getStoreLength({ required String storeName, }) async => - (await sendCmd( - (key: _WorkerKey.getStoreLength, args: {'storeName': storeName}), - )) - .data!['length']! as int; + (await _sendCmd( + type: _WorkerCmdType.getStoreLength, + args: {'storeName': storeName}, + ))!['length']; @override Future getStoreHits({ required String storeName, }) async => - (await sendCmd( - (key: _WorkerKey.getStoreHits, args: {'storeName': storeName}), - )) - .data!['hits']! as int; + (await _sendCmd( + type: _WorkerCmdType.getStoreHits, + args: {'storeName': storeName}, + ))!['hits']; @override Future getStoreMisses({ required String storeName, }) async => - (await sendCmd( - (key: _WorkerKey.getStoreMisses, args: {'storeName': storeName}), - )) - .data!['misses']! as int; + (await _sendCmd( + type: _WorkerCmdType.getStoreMisses, + args: {'storeName': storeName}, + ))!['misses']! as int; @override Future readTile({ required String url, }) async => - (await sendCmd((key: _WorkerKey.readTile, args: {'url': url}))) - .data!['tile'] as ObjectBoxTile?; + (await _sendCmd( + type: _WorkerCmdType.readTile, + args: {'url': url}, + ))!['tile']; @override Future writeTile({ @@ -214,11 +252,9 @@ class _ObjectBoxBackendImpl required String url, required Uint8List? bytes, }) => - sendCmd( - ( - key: _WorkerKey.writeTile, - args: {'storeName': storeName, 'url': url, 'bytes': bytes} - ), + _sendCmd( + type: _WorkerCmdType.writeTile, + args: {'storeName': storeName, 'url': url, 'bytes': bytes}, ); @override @@ -226,23 +262,18 @@ class _ObjectBoxBackendImpl required String url, required String storeName, }) async => - (await sendCmd( - ( - key: _WorkerKey.deleteStore, - args: {'storeName': storeName, 'url': url} - ), - )) - .data!['wasOrphaned'] as bool?; + (await _sendCmd( + type: _WorkerCmdType.deleteStore, + args: {'storeName': storeName, 'url': url}, + ))!['wasOrphaned']; void _sendRemoveOldestTileCmd(String storeName) { - sendCmd( - ( - key: _WorkerKey.removeOldestTile, - args: { - 'storeName': storeName, - 'number': _dotLength, - } - ), + _sendCmd( + type: _WorkerCmdType.removeOldestTile, + args: { + 'storeName': storeName, + 'number': _dotLength, + }, ); _dotLength = 0; } diff --git a/lib/src/backend/impls/objectbox/worker.dart b/lib/src/backend/impls/objectbox/worker.dart index fc3a4578..7296dd25 100644 --- a/lib/src/backend/impls/objectbox/worker.dart +++ b/lib/src/backend/impls/objectbox/worker.dart @@ -1,11 +1,9 @@ part of 'backend.dart'; -typedef _WorkerCmd = ({_WorkerKey key, Map args}); -typedef _WorkerRes = ({_WorkerKey key, Map? data}); - -enum _WorkerKey { +enum _WorkerCmdType { initialise_, // Only valid as a response destroy_, // Only valid as a request + storeExists, createStore, resetStore, renameStore, @@ -31,7 +29,8 @@ Future _worker( ) async { // Setup comms final receivePort = ReceivePort(); - void sendRes(_WorkerRes m) => input.sendPort.send(m); + void sendRes(({int id, Map? data}) m) => + input.sendPort.send(m); // Initialise database final rootDirectory = await (input.rootDirectory == null @@ -48,351 +47,388 @@ Future _worker( // Respond with comms channel for future cmds sendRes( ( - key: _WorkerKey.initialise_, + id: 0, data: {'sendPort': receivePort.sendPort}, ), ); + // Setup util functions + ObjectBoxStore? getStore(String storeName) { + final query = root + .box() + .query(ObjectBoxStore_.name.equals(storeName)) + .build(); + final store = query.findUnique(); + query.close(); + return store; + } + // Await cmds, perform work, and respond - await for (final _WorkerCmd cmd in receivePort) { - switch (cmd.key) { - case _WorkerKey.initialise_: - throw UnsupportedError('Invalid operation'); - case _WorkerKey.destroy_: - root.close(); - if (cmd.args['deleteRoot'] == true) { - rootDirectory.deleteSync(recursive: true); - } - Isolate.exit(); - case _WorkerKey.createStore: - root.box().put( - ObjectBoxStore( - name: cmd.args['storeName']! as String, - numberOfTiles: 0, - numberOfBytes: 0, - hits: 0, - misses: 0, - ), - mode: PutMode.insert, - ); - sendRes((key: cmd.key, data: null)); - break; - case _WorkerKey.resetStore: - final storeName = cmd.args['storeName']! as String; - final removeIds = []; - - final tiles = root.box(); - final stores = root.box(); - - final tilesQuery = (tiles.query() - ..linkMany( - ObjectBoxTile_.stores, - ObjectBoxStore_.name.equals(storeName), - )) - .build(); - - final storeQuery = - stores.query(ObjectBoxStore_.name.equals(storeName)).build(); - - root.runInTransaction( - TxMode.write, - () { - tiles.putMany( - tilesQuery - .find() - .map((tile) { - tile.stores.removeWhere((store) => store.name == storeName); - if (tile.stores.isNotEmpty) return tile; - removeIds.add(tile.id); - return null; - }) - .whereNotNull() - .toList(), - mode: PutMode.update, - ); - tilesQuery.close(); - - tiles.query(ObjectBoxTile_.id.oneOf(removeIds)).build() - ..remove() - ..close(); + await for (final ({ + int id, + _WorkerCmdType type, + Map args, + }) cmd in receivePort) { + try { + switch (cmd.type) { + case _WorkerCmdType.initialise_: + throw UnsupportedError('Invalid operation'); + case _WorkerCmdType.destroy_: + root.close(); + if (cmd.args['deleteRoot'] == true) { + rootDirectory.deleteSync(recursive: true); + } + // TODO: Consider final message + Isolate.exit(); + case _WorkerCmdType.storeExists: + sendRes( + ( + id: cmd.id, + data: { + 'exists': getStore(cmd.args['storeName']! as String) != null, + }, + ), + ); + break; + case _WorkerCmdType.createStore: + final storeName = cmd.args['storeName']! as String; - final store = storeQuery.findUnique() ?? - (throw StoreUnavailable(storeName: storeName)); - storeQuery.close(); - - assert(store.tiles.isEmpty); - - stores.put( - store - ..tiles.clear() - ..numberOfTiles = 0 - ..numberOfBytes = 0 - ..hits = 0 - ..misses = 0, - ); - }, - ); - sendRes((key: cmd.key, data: null)); - break; - case _WorkerKey.renameStore: - final currentStoreName = cmd.args['currentStoreName']! as String; - final newStoreName = cmd.args['newStoreName']! as String; - - root.box().put( - root - .box() - .query(ObjectBoxStore_.name.equals(currentStoreName)) - .build() - .findUnique() ?? - (throw StoreUnavailable(storeName: currentStoreName)) - ..name = newStoreName, - ); - - sendRes((key: cmd.key, data: null)); - break; - case _WorkerKey.deleteStore: - root - .box() - .query( - ObjectBoxStore_.name.equals(cmd.args['storeName']! as String), - ) - .build() - ..remove() - ..close(); - - sendRes((key: cmd.key, data: null)); - break; - case _WorkerKey.getStoreSize: - final storeName = cmd.args['storeName']! as String; - - final query = root - .box() - .query(ObjectBoxStore_.name.equals(storeName)) - .build(); - final kib = (query.findUnique() ?? - (throw StoreUnavailable(storeName: storeName))) - .numberOfBytes / - 1024; - query.close(); - - sendRes((key: cmd.key, data: {'size': kib})); - break; - case _WorkerKey.getStoreLength: - final storeName = cmd.args['storeName']! as String; - - final query = root - .box() - .query(ObjectBoxStore_.name.equals(storeName)) - .build(); - final length = (query.findUnique() ?? - (throw StoreUnavailable(storeName: storeName))) - .numberOfTiles; - query.close(); - - sendRes((key: cmd.key, data: {'length': length})); - break; - case _WorkerKey.getStoreHits: - final storeName = cmd.args['storeName']! as String; - - final query = root - .box() - .query(ObjectBoxStore_.name.equals(storeName)) - .build(); - final hits = (query.findUnique() ?? - (throw StoreUnavailable(storeName: storeName))) - .hits; - query.close(); - - sendRes((key: cmd.key, data: {'hits': hits})); - break; - case _WorkerKey.getStoreMisses: - final storeName = cmd.args['storeName']! as String; - - final query = root - .box() - .query(ObjectBoxStore_.name.equals(storeName)) - .build(); - final misses = (query.findUnique() ?? - (throw StoreUnavailable(storeName: storeName))) - .misses; - query.close(); - - sendRes((key: cmd.key, data: {'misses': misses})); - break; - case _WorkerKey.readTile: - final query = root - .box() - .query(ObjectBoxTile_.url.equals(cmd.args['url']! as String)) - .build(); - final tile = query.findUnique(); - query.close(); - - // TODO: Hits & misses - - sendRes((key: cmd.key, data: {'tile': tile})); - break; - case _WorkerKey.writeTile: - final storeName = cmd.args['storeName']! as String; - final url = cmd.args['url']! as String; - final bytes = cmd.args['bytes'] as Uint8List?; - - final tiles = root.box(); - final stores = root.box(); - - final tilesQuery = tiles.query(ObjectBoxTile_.url.equals(url)).build(); - final existingTile = tilesQuery.findUnique(); - tilesQuery.close(); - - final storeQuery = - stores.query(ObjectBoxStore_.name.equals(storeName)).build(); - final store = storeQuery.findUnique() ?? - (throw StoreUnavailable(storeName: storeName)); - storeQuery.close(); - - root.runInTransaction( - TxMode.write, - () { - switch ((existingTile == null, bytes == null)) { - case (true, false): // No existing tile - tiles.put( - ObjectBoxTile( - url: url, - lastModified: DateTime.now(), - bytes: bytes!, - )..stores.add(store), - ); - stores.put( - store - ..numberOfTiles += 1 - ..numberOfBytes += bytes.lengthInBytes, - ); - break; - case (false, true): // Existing tile, no update - // Only take action if it's not already belonging to the store - if (!existingTile!.stores.contains(store)) { - tiles.put(existingTile..stores.add(store)); - stores.put( - store - ..numberOfTiles += 1 - ..numberOfBytes += existingTile.bytes.lengthInBytes, - ); - } - break; - case (false, false): // Existing tile, update required - tiles.put( - existingTile! - ..lastModified = DateTime.now() - ..bytes = bytes!, - ); - stores.putMany( - existingTile.stores - .map( - (store) => store - ..numberOfBytes += (bytes.lengthInBytes - - existingTile.bytes.lengthInBytes), - ) - .toList(), + try { + root.box().put( + ObjectBoxStore( + name: storeName, + numberOfTiles: 0, + numberOfBytes: 0, + hits: 0, + misses: 0, + ), + mode: PutMode.insert, ); - break; - case (true, true): // FMTC internal error - throw TileCannotUpdate(url: url); - } - }, - ); - - sendRes((key: cmd.key, data: null)); - break; - case _WorkerKey.deleteTile: - final storeName = cmd.args['storeName']! as String; - final url = cmd.args['url']! as String; - - final tiles = root.box(); - - // Find the tile by URL - final query = tiles.query(ObjectBoxTile_.url.equals(url)).build(); - final tile = query.findUnique(); - if (tile == null) { - sendRes((key: cmd.key, data: {'wasOrphaned': null})); + } catch (e) { + throw StoreAlreadyExists(storeName: storeName); + } + sendRes((id: cmd.id, data: null)); break; - } + case _WorkerCmdType.resetStore: + final storeName = cmd.args['storeName']! as String; + final removeIds = []; + + final tiles = root.box(); + final stores = root.box(); + + final tilesQuery = (tiles.query() + ..linkMany( + ObjectBoxTile_.stores, + ObjectBoxStore_.name.equals(storeName), + )) + .build(); + + final storeQuery = + stores.query(ObjectBoxStore_.name.equals(storeName)).build(); + + root.runInTransaction( + TxMode.write, + () { + tiles.putMany( + tilesQuery + .find() + .map((tile) { + tile.stores + .removeWhere((store) => store.name == storeName); + if (tile.stores.isNotEmpty) return tile; + removeIds.add(tile.id); + return null; + }) + .whereNotNull() + .toList(), + mode: PutMode.update, + ); + tilesQuery.close(); - // For the correct store, adjust the statistics - for (final store in tile.stores) { - if (store.name != storeName) continue; - root.box().put( + tiles.query(ObjectBoxTile_.id.oneOf(removeIds)).build() + ..remove() + ..close(); + + final store = storeQuery.findUnique() ?? + (throw StoreNotExists(storeName: storeName)); + storeQuery.close(); + + assert(store.tiles.isEmpty); + + stores.put( store - ..numberOfTiles -= 1 - ..numberOfBytes -= tile.bytes.lengthInBytes, - mode: PutMode.update, + ..tiles.clear() + ..numberOfTiles = 0 + ..numberOfBytes = 0 + ..hits = 0 + ..misses = 0, ); + }, + ); + sendRes((id: cmd.id, data: null)); break; - } + case _WorkerCmdType.renameStore: + final currentStoreName = cmd.args['currentStoreName']! as String; + final newStoreName = cmd.args['newStoreName']! as String; - // Remove the store relation from the tile - tile.stores.removeWhere((store) => store.name == storeName); + root.box().put( + getStore(currentStoreName) ?? + (throw StoreNotExists(storeName: currentStoreName)) + ..name = newStoreName, + ); - // Delete the tile if it belongs to no stores - if (tile.stores.isEmpty) { - query + sendRes((id: cmd.id, data: null)); + break; + case _WorkerCmdType.deleteStore: + root + .box() + .query( + ObjectBoxStore_.name.equals(cmd.args['storeName']! as String), + ) + .build() ..remove() ..close(); - sendRes((key: cmd.key, data: {'wasOrphaned': true})); + + sendRes((id: cmd.id, data: null)); + break; + case _WorkerCmdType.getStoreSize: + final storeName = cmd.args['storeName']! as String; + + sendRes( + ( + id: cmd.id, + data: { + 'size': (getStore(storeName) ?? + (throw StoreNotExists(storeName: storeName))) + .numberOfBytes / + 1024, + } + ), + ); + break; + case _WorkerCmdType.getStoreLength: + final storeName = cmd.args['storeName']! as String; + + sendRes( + ( + id: cmd.id, + data: { + 'length': (getStore(storeName) ?? + (throw StoreNotExists(storeName: storeName))) + .numberOfTiles, + } + ), + ); + break; + case _WorkerCmdType.getStoreHits: + final storeName = cmd.args['storeName']! as String; + + sendRes( + ( + id: cmd.id, + data: { + 'hits': (getStore(storeName) ?? + (throw StoreNotExists(storeName: storeName))) + .hits, + } + ), + ); + break; + case _WorkerCmdType.getStoreMisses: + final storeName = cmd.args['storeName']! as String; + + sendRes( + ( + id: cmd.id, + data: { + 'misses': (getStore(storeName) ?? + (throw StoreNotExists(storeName: storeName))) + .misses, + } + ), + ); break; - } - - // Otherwise just update the tile - query.close(); - tiles.put(tile, mode: PutMode.update); - sendRes((key: cmd.key, data: {'wasOrphaned': false})); - break; - case _WorkerKey.removeOldestTile: - final storeName = cmd.args['storeName']! as String; - final numTilesToRemove = cmd.args['number']! as int; - - final tiles = root.box(); - - final tilesQuery = (tiles.query().order(ObjectBoxTile_.lastModified) - ..linkMany( - ObjectBoxTile_.stores, - ObjectBoxStore_.name.equals(storeName), - )) - .build(); - final deleteTiles = await tilesQuery - .stream() - .where((tile) => tile.stores.length == 1) - .take(numTilesToRemove) - .toList(); - tilesQuery.close(); - - if (deleteTiles.isEmpty) { - sendRes((key: cmd.key, data: null)); + case _WorkerCmdType.readTile: + final query = root + .box() + .query(ObjectBoxTile_.url.equals(cmd.args['url']! as String)) + .build(); + final tile = query.findUnique(); + query.close(); + + // TODO: Hits & misses + + sendRes((id: cmd.id, data: {'tile': tile})); + break; + case _WorkerCmdType.writeTile: + final storeName = cmd.args['storeName']! as String; + final url = cmd.args['url']! as String; + final bytes = cmd.args['bytes'] as Uint8List?; + + final tiles = root.box(); + final stores = root.box(); + + final tilesQuery = + tiles.query(ObjectBoxTile_.url.equals(url)).build(); + + final storeQuery = + stores.query(ObjectBoxStore_.name.equals(storeName)).build(); + + root.runInTransaction( + TxMode.write, + () { + final existingTile = tilesQuery.findUnique(); + tilesQuery.close(); + + final store = storeQuery.findUnique() ?? + (throw StoreNotExists(storeName: storeName)); + storeQuery.close(); + + switch ((existingTile == null, bytes == null)) { + case (true, false): // No existing tile + tiles.put( + ObjectBoxTile( + url: url, + lastModified: DateTime.now(), + bytes: bytes!, + )..stores.add(store), + ); + stores.put( + store + ..numberOfTiles += 1 + ..numberOfBytes += bytes.lengthInBytes, + ); + break; + case (false, true): // Existing tile, no update + // Only take action if it's not already belonging to the store + if (!existingTile!.stores.contains(store)) { + tiles.put(existingTile..stores.add(store)); + stores.put( + store + ..numberOfTiles += 1 + ..numberOfBytes += existingTile.bytes.lengthInBytes, + ); + } + break; + case (false, false): // Existing tile, update required + tiles.put( + existingTile! + ..lastModified = DateTime.now() + ..bytes = bytes!, + ); + stores.putMany( + existingTile.stores + .map( + (store) => store + ..numberOfBytes += (bytes.lengthInBytes - + existingTile.bytes.lengthInBytes), + ) + .toList(), + ); + break; + case (true, true): // FMTC internal error + throw TileCannotUpdate(url: url); + } + }, + ); + + sendRes((id: cmd.id, data: null)); break; - } - - final storeQuery = root - .box() - .query(ObjectBoxStore_.name.equals(storeName)) - .build(); - final store = storeQuery.findUnique() ?? - (throw StoreUnavailable(storeName: storeName)); - storeQuery.close(); - - root.runInTransaction( - TxMode.write, - () { + case _WorkerCmdType.deleteTile: + final storeName = cmd.args['storeName']! as String; + final url = cmd.args['url']! as String; + + final tiles = root.box(); + + // Find the tile by URL + final query = tiles.query(ObjectBoxTile_.url.equals(url)).build(); + final tile = query.findUnique(); + if (tile == null) { + sendRes((id: cmd.id, data: {'wasOrphaned': null})); + break; + } + + // For the correct store, adjust the statistics + for (final store in tile.stores) { + if (store.name != storeName) continue; root.box().put( store - ..numberOfTiles -= numTilesToRemove - ..numberOfBytes -= - deleteTiles.map((e) => e.bytes.lengthInBytes).sum, + ..numberOfTiles -= 1 + ..numberOfBytes -= tile.bytes.lengthInBytes, mode: PutMode.update, ); - tiles.removeMany(deleteTiles.map((e) => e.id).toList()); - }, - ); + break; + } + + // Remove the store relation from the tile + tile.stores.removeWhere((store) => store.name == storeName); + + // Delete the tile if it belongs to no stores + if (tile.stores.isEmpty) { + query + ..remove() + ..close(); + sendRes((id: cmd.id, data: {'wasOrphaned': true})); + break; + } + + // Otherwise just update the tile + query.close(); + tiles.put(tile, mode: PutMode.update); + sendRes((id: cmd.id, data: {'wasOrphaned': false})); + break; + case _WorkerCmdType.removeOldestTile: + final storeName = cmd.args['storeName']! as String; + final numTilesToRemove = cmd.args['number']! as int; + + final tiles = root.box(); + + final tilesQuery = (tiles.query().order(ObjectBoxTile_.lastModified) + ..linkMany( + ObjectBoxTile_.stores, + ObjectBoxStore_.name.equals(storeName), + )) + .build(); + final deleteTiles = await tilesQuery + .stream() + .where((tile) => tile.stores.length == 1) + .take(numTilesToRemove) + .toList(); + tilesQuery.close(); + + if (deleteTiles.isEmpty) { + sendRes((id: cmd.id, data: null)); + break; + } + + final storeQuery = root + .box() + .query(ObjectBoxStore_.name.equals(storeName)) + .build(); + + root.runInTransaction( + TxMode.write, + () { + final store = storeQuery.findUnique() ?? + (throw StoreNotExists(storeName: storeName)); + storeQuery.close(); + + root.box().put( + store + ..numberOfTiles -= numTilesToRemove + ..numberOfBytes -= + deleteTiles.map((e) => e.bytes.lengthInBytes).sum, + mode: PutMode.update, + ); + tiles.removeMany(deleteTiles.map((e) => e.id).toList()); + }, + ); - sendRes((key: cmd.key, data: null)); - break; + sendRes((id: cmd.id, data: null)); + break; + } + } catch (e) { + sendRes((id: cmd.id, data: {'error': e})); } } } diff --git a/lib/src/backend/interfaces/backend.dart b/lib/src/backend/interfaces/backend.dart index cd802ea6..1e8e0c1c 100644 --- a/lib/src/backend/interfaces/backend.dart +++ b/lib/src/backend/interfaces/backend.dart @@ -95,8 +95,15 @@ abstract interface class FMTCBackendInternal { /// /// If [deleteRoot] is `true`, then the storage medium will be permanently /// deleted. + /// + /// If [immediate] is `true`, any operations currently underway will be lost. + /// If `false`, all operations currently underway will be allowed to complete, + /// but any operations started after this method call will be lost. A lost + /// operation may throw [RootUnavailable]. This parameter may not have a + /// noticable/any effect in some implementations. void destroySync({ bool deleteRoot = false, + bool immediate = false, }); /// Whether [destroySync] is implemented @@ -104,6 +111,21 @@ abstract interface class FMTCBackendInternal { /// If `false`, calling will throw an [SyncOperationUnsupported] error. abstract final bool supportsSyncDestroy; + /// Whether the store currently exists + Future storeExists({ + required String storeName, + }); + + /// Whether the store currently exists + bool storeExistsSync({ + required String storeName, + }); + + /// Whether [storeExistsSync] is implemented + /// + /// If `false`, calling will throw an [SyncOperationUnsupported] error. + abstract final bool supportsSyncStoreExists; + /// Create a new store with the specified name Future createStore({ required String storeName, diff --git a/lib/src/store/manage.dart b/lib/src/store/manage.dart index 83adfc16..a231151a 100644 --- a/lib/src/store/manage.dart +++ b/lib/src/store/manage.dart @@ -11,23 +11,8 @@ final class StoreManagement extends _WithBackendAccess { final Directory _rootDirectory; - /// Check whether this store is ready for use - /// - /// It must be registered, and its underlying database must be open, for this - /// method to return `true`. - /// - /// This is a safe method, and will not throw the [FMTCStoreNotReady] error, - /// except in exceptional circumstances. - bool get ready { - try { - _registry(_name); - return true; - // ignore: avoid_catching_errors - } on FMTCStoreNotReady catch (e) { - if (e.registered) rethrow; - return false; - } - } + /// Whether this store exists + Future get ready => _backend.storeExists(storeName: _storeName); /// Create this store asynchronously /// From 9ebba074768d4c112e58e3d2224f45df9510c2bf Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sat, 30 Dec 2023 22:41:24 +0100 Subject: [PATCH 084/168] General backend-worker comms improvements Change imports/exports Former-commit-id: e203fc96ae36cd482720ef6d0badb09f22792926 [formerly 56a2721db24172b676f308c465db47caa10c722d] Former-commit-id: 692bf51e98076534f98b907e59dd92a672b9fdf9 --- lib/flutter_map_tile_caching.dart | 6 +- lib/fmtc_module_api.dart | 36 ----- lib/src/backend/export_plus.dart | 4 - lib/src/backend/export_std.dart | 2 - lib/src/backend/exports.dart | 5 + lib/src/backend/impls/objectbox/backend.dart | 51 ++++-- .../impls/objectbox/models/models.dart | 2 - lib/src/backend/impls/objectbox/worker.dart | 1 + lib/src/backend/interfaces/backend.dart | 2 - lib/src/backend/interfaces/models.dart | 2 - .../{impl_tools => interfaces}/no_sync.dart | 6 +- .../backend/{impl_tools => utils}/errors.dart | 0 lib/src/errors/store_not_ready.dart | 39 ----- lib/src/store/manage.dart | 149 ++++-------------- 14 files changed, 81 insertions(+), 224 deletions(-) delete mode 100644 lib/fmtc_module_api.dart delete mode 100644 lib/src/backend/export_plus.dart delete mode 100644 lib/src/backend/export_std.dart create mode 100644 lib/src/backend/exports.dart rename lib/src/backend/{impl_tools => interfaces}/no_sync.dart (97%) rename lib/src/backend/{impl_tools => utils}/errors.dart (100%) delete mode 100644 lib/src/errors/store_not_ready.dart diff --git a/lib/flutter_map_tile_caching.dart b/lib/flutter_map_tile_caching.dart index 0f32c267..94d7fe2b 100644 --- a/lib/flutter_map_tile_caching.dart +++ b/lib/flutter_map_tile_caching.dart @@ -33,7 +33,7 @@ import 'package:path_provider/path_provider.dart'; import 'package:stream_transform/stream_transform.dart'; import 'package:watcher/watcher.dart'; -import 'src/backend/export_std.dart'; +import 'src/backend/exports.dart'; import 'src/bulk_download/instance.dart'; import 'src/bulk_download/rate_limited_stream.dart'; import 'src/bulk_download/tile_loops/shared.dart'; @@ -45,18 +45,16 @@ import 'src/db/registry.dart'; import 'src/db/tools.dart'; import 'src/errors/browsing.dart'; import 'src/errors/initialisation.dart'; -import 'src/errors/store_not_ready.dart'; import 'src/misc/exts.dart'; import 'src/misc/int_extremes.dart'; import 'src/misc/obscure_query_params.dart'; import 'src/misc/typedefs.dart'; import 'src/providers/image_provider.dart'; -export 'src/backend/export_std.dart'; +export 'src/backend/exports.dart'; export 'src/errors/browsing.dart'; export 'src/errors/damaged_store.dart'; export 'src/errors/initialisation.dart'; -export 'src/errors/store_not_ready.dart'; export 'src/misc/typedefs.dart'; part 'src/bulk_download/download_progress.dart'; diff --git a/lib/fmtc_module_api.dart b/lib/fmtc_module_api.dart deleted file mode 100644 index dfbf0ae6..00000000 --- a/lib/fmtc_module_api.dart +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright © Luka S (JaffaKetchup) under GPL-v3 -// A full license can be found at .\LICENSE - -// ignore_for_file: invalid_export_of_internal_element - -/// Restricted API which exports internal functionality, necessary for the FMTC -/// modules to work correctly -/// -/// When importing this library, also import 'flutter_map_tile_caching.dart' for -/// the full functionality set. -/// -/// --- -/// -/// "With great power comes great responsibility" - Someone -/// -/// This library forms part of a layer of abstraction between you, FMTC -/// internals, and underlying databases. Importing this library removes that -/// abstraction, making it easy to disrupt FMTC's normal operations with -/// incorrect usage. For example, it is possible to force close an open Isar -/// database, leading to an erroneous & invalid state. -/// -/// If you are using this to create a custom module, go ahead! Please do get in -/// touch, I'm always interested to hear what the community is making, and I may -/// be able to offer some insight into the darker corners and workings of FMTC. -/// Note that not necessarily all internal APIs are exposed through this library. -/// -/// **Do not use in normal applications. I may be unable to offer support.** -library fmtc_module_api; - -export 'src/backend/export_plus.dart'; -export 'src/db/defs/metadata.dart'; -export 'src/db/defs/store_descriptor.dart'; -export 'src/db/defs/tile.dart'; -export 'src/db/registry.dart'; -export 'src/db/tools.dart'; -export 'src/misc/exts.dart'; diff --git a/lib/src/backend/export_plus.dart b/lib/src/backend/export_plus.dart deleted file mode 100644 index 07a1d7f8..00000000 --- a/lib/src/backend/export_plus.dart +++ /dev/null @@ -1,4 +0,0 @@ -export 'export_std.dart'; -export 'impl_tools/errors.dart'; -export 'impl_tools/no_sync.dart'; -export 'interfaces/models.dart'; diff --git a/lib/src/backend/export_std.dart b/lib/src/backend/export_std.dart deleted file mode 100644 index 22aca207..00000000 --- a/lib/src/backend/export_std.dart +++ /dev/null @@ -1,2 +0,0 @@ -export 'impls/objectbox/backend.dart'; -export 'interfaces/backend.dart'; diff --git a/lib/src/backend/exports.dart b/lib/src/backend/exports.dart new file mode 100644 index 00000000..f5f38c1a --- /dev/null +++ b/lib/src/backend/exports.dart @@ -0,0 +1,5 @@ +export 'impls/objectbox/backend.dart'; +export 'interfaces/backend.dart'; +export 'interfaces/models.dart'; +export 'interfaces/no_sync.dart'; +export 'utils/errors.dart'; diff --git a/lib/src/backend/impls/objectbox/backend.dart b/lib/src/backend/impls/objectbox/backend.dart index 1941868c..f68500e4 100644 --- a/lib/src/backend/impls/objectbox/backend.dart +++ b/lib/src/backend/impls/objectbox/backend.dart @@ -8,9 +8,9 @@ import 'package:meta/meta.dart' as meta; import 'package:path_provider/path_provider.dart'; import '../../../misc/exts.dart'; -import '../../impl_tools/errors.dart'; -import '../../impl_tools/no_sync.dart'; import '../../interfaces/backend.dart'; +import '../../interfaces/no_sync.dart'; +import '../../utils/errors.dart'; import 'models/generated/objectbox.g.dart'; import 'models/models.dart'; @@ -42,11 +42,12 @@ class _ObjectBoxBackendImpl final Map?>> _workerRes = {}; late int _workerId; late Completer _workerComplete; + late StreamSubscription _workerHandler; // `deleteOldestTile` tracking & debouncing - int _dotLength = 0; - Timer? _dotDebouncer; - String? _dotStore; + late int _dotLength; + late Timer _dotDebouncer; + late String? _dotStore; Future?> _sendCmd({ required _WorkerCmdType type, @@ -92,10 +93,13 @@ class _ObjectBoxBackendImpl }) async { if (_sendPort != null) throw RootAlreadyInitialised(); - // Setup worker isolate - final receivePort = ReceivePort(); + // Reset non-comms-related non-resource-intensive state _workerId = 0; _workerRes.clear(); + _dotStore = null; + _dotLength = 0; + + // Prepare to recieve `SendPort` from worker _workerRes[0] = Completer(); unawaited( _workerRes[0]!.future.then((res) { @@ -103,14 +107,19 @@ class _ObjectBoxBackendImpl _workerRes.remove(0); }), ); + + // Setup worker comms/response handler + final receivePort = ReceivePort(); _workerComplete = Completer(); - receivePort.listen( + _workerHandler = receivePort.listen( (evt) { evt as ({int id, Map? data}); _workerRes[evt.id]!.complete(evt.data); }, onDone: () => _workerComplete.complete(), ); + + // Spawn worker isolate await Isolate.spawn( _worker, ( @@ -133,20 +142,30 @@ class _ObjectBoxBackendImpl }) async { expectInitialised; - if (!immediate) { - await Future.wait(_workerRes.values.map((e) => e.future)); - } + // Wait for all currently underway operations to complete before destroying + // the isolate (if not `immediate`) + if (!immediate) await Future.wait(_workerRes.values.map((e) => e.future)); + // Send self-destruct cmd to worker, and don't wait for any response unawaited( _sendCmd( type: _WorkerCmdType.destroy_, args: {'deleteRoot': deleteRoot}, ), ); + + // Wait for worker to exit (worker handler will exit and signal) await _workerComplete.future; - _sendPort = null; + // Resource-intensive state cleanup only (other cleanup done during init) + _sendPort = null; // Indicate ready for re-init + await _workerHandler.cancel(); + _dotDebouncer.cancel(); + + print('passed _workerHandler cancel'); + // Kill any remaining operations with an error (they'll never recieve a + // response from the worker) for (final completer in _workerRes.values) { completer.complete({'error': RootUnavailable()}); } @@ -289,14 +308,14 @@ class _ObjectBoxBackendImpl // If the store has changed, failing to reset the batch/queue will mean // tiles are removed from the wrong store _dotStore = storeName; - if (_dotDebouncer != null && _dotDebouncer!.isActive) { - _dotDebouncer!.cancel(); + if (_dotDebouncer.isActive) { + _dotDebouncer.cancel(); _sendRemoveOldestTileCmd(storeName); } } - if (_dotDebouncer != null && _dotDebouncer!.isActive) { - _dotDebouncer!.cancel(); + if (_dotDebouncer.isActive) { + _dotDebouncer.cancel(); _dotDebouncer = Timer( const Duration(milliseconds: 500), () => _sendRemoveOldestTileCmd(storeName), diff --git a/lib/src/backend/impls/objectbox/models/models.dart b/lib/src/backend/impls/objectbox/models/models.dart index 5db648e1..6614096b 100644 --- a/lib/src/backend/impls/objectbox/models/models.dart +++ b/lib/src/backend/impls/objectbox/models/models.dart @@ -14,10 +14,8 @@ base class ObjectBoxStore extends BackendStore { @Unique() String name; - @override int numberOfTiles; - @override double numberOfBytes; @override diff --git a/lib/src/backend/impls/objectbox/worker.dart b/lib/src/backend/impls/objectbox/worker.dart index 7296dd25..ff928bed 100644 --- a/lib/src/backend/impls/objectbox/worker.dart +++ b/lib/src/backend/impls/objectbox/worker.dart @@ -154,6 +154,7 @@ Future _worker( storeQuery.close(); assert(store.tiles.isEmpty); + // TODO: Hits & misses stores.put( store diff --git a/lib/src/backend/interfaces/backend.dart b/lib/src/backend/interfaces/backend.dart index 1e8e0c1c..c23d0798 100644 --- a/lib/src/backend/interfaces/backend.dart +++ b/lib/src/backend/interfaces/backend.dart @@ -5,8 +5,6 @@ import 'package:meta/meta.dart'; import 'package:path_provider/path_provider.dart'; import '../../../flutter_map_tile_caching.dart'; -import '../impl_tools/errors.dart'; -import 'models.dart'; /// An abstract interface that FMTC will use to communicate with a storage /// 'backend' (usually one root) diff --git a/lib/src/backend/interfaces/models.dart b/lib/src/backend/interfaces/models.dart index bbc67d33..9697f221 100644 --- a/lib/src/backend/interfaces/models.dart +++ b/lib/src/backend/interfaces/models.dart @@ -4,8 +4,6 @@ import 'package:meta/meta.dart'; abstract base class BackendStore { abstract String name; - abstract int numberOfTiles; - abstract double numberOfBytes; abstract int hits; abstract int misses; diff --git a/lib/src/backend/impl_tools/no_sync.dart b/lib/src/backend/interfaces/no_sync.dart similarity index 97% rename from lib/src/backend/impl_tools/no_sync.dart rename to lib/src/backend/interfaces/no_sync.dart index a730054a..ab9a6a42 100644 --- a/lib/src/backend/impl_tools/no_sync.dart +++ b/lib/src/backend/interfaces/no_sync.dart @@ -1,8 +1,8 @@ import 'dart:typed_data'; -import '../interfaces/backend.dart'; -import '../interfaces/models.dart'; -import 'errors.dart'; +import '../utils/errors.dart'; +import 'backend.dart'; +import 'models.dart'; /// A shortcut to declare that an [FMTCBackend] does not support any synchronous /// versions of methods diff --git a/lib/src/backend/impl_tools/errors.dart b/lib/src/backend/utils/errors.dart similarity index 100% rename from lib/src/backend/impl_tools/errors.dart rename to lib/src/backend/utils/errors.dart diff --git a/lib/src/errors/store_not_ready.dart b/lib/src/errors/store_not_ready.dart deleted file mode 100644 index 2d374d38..00000000 --- a/lib/src/errors/store_not_ready.dart +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright © Luka S (JaffaKetchup) under GPL-v3 -// A full license can be found at .\LICENSE - -import 'package:meta/meta.dart'; - -/// An [Error] indicating that a store did not exist when it was expected to -/// -/// Commonly thrown by statistic operations, but can be thrown from multiple -/// other places. -class FMTCStoreNotReady extends Error { - /// The store name that the method tried to access - final String storeName; - - /// A human readable description of the error, and steps that may be taken to - /// avoid this error being thrown again - final String message; - - /// Whether this store was registered internally. - /// - /// Represents a serious internal FMTC error if `true`, as represented by - /// [message]. - final bool registered; - - /// An [Error] indicating that a store did not exist when it was expected to - /// - /// Commonly thrown by statistic operations, but can be thrown from multiple - /// other places. - @internal - FMTCStoreNotReady({ - required this.storeName, - required this.registered, - }) : message = registered - ? "The store ('$storeName') was registered, but the underlying database was not open, at this time. This is an erroneous state in FMTC: if this error appears in your application, please open an issue on GitHub immediately." - : "The store ('$storeName') does not exist at this time, and is not ready. Ensure that your application does not use the method that triggered this error unless it is sure that the store will exist at this point."; - - /// Similar to [message], but suitable for console output in an unknown context - @override - String toString() => 'FMTCStoreNotReady: $message'; -} diff --git a/lib/src/store/manage.dart b/lib/src/store/manage.dart index a231151a..86c53914 100644 --- a/lib/src/store/manage.dart +++ b/lib/src/store/manage.dart @@ -5,111 +5,51 @@ part of flutter_map_tile_caching; /// Manages a [StoreDirectory]'s representation on the filesystem, such as /// creation and deletion +/// +/// If the store is not in the expected state (of existence) when invoking an +/// operation, then an error will be thrown (likely [StoreNotExists] or +/// [StoreAlreadyExists]). It is recommended to check [ready] or [readySync] when +/// necessary. final class StoreManagement extends _WithBackendAccess { - StoreManagement._(super.store) - : _rootDirectory = FMTC.instance.rootDirectory.directory; - - final Directory _rootDirectory; + StoreManagement._(super.store); /// Whether this store exists Future get ready => _backend.storeExists(storeName: _storeName); - /// Create this store asynchronously - /// - /// Does nothing if the store already exists. - Future createAsync() async { - if (ready) return; - - final db = await Isar.open( - [DbStoreDescriptorSchema, DbTileSchema, DbMetadataSchema], - name: _id.toString(), - directory: _rootDirectory.path, - maxSizeMiB: FMTC.instance.settings.databaseMaxSize, - compactOnLaunch: FMTC.instance.settings.databaseCompactCondition, - inspector: false, - ); - await db.writeTxn( - () => db.storeDescriptor.put(DbStoreDescriptor(name: _name)), - ); - _registry.register(_id, db); - } + /// Whether this store exists + bool get readySync => _backend.storeExistsSync(storeName: _storeName); - /// Create this store synchronously - /// - /// Prefer [createAsync] to avoid blocking the UI thread. Otherwise, this has - /// slightly better performance. - /// - /// Does nothing if the store already exists. - void create() { - if (ready) return; + /// Create this store + Future create() => _backend.createStore(storeName: _storeName); - final db = Isar.openSync( - [DbStoreDescriptorSchema, DbTileSchema, DbMetadataSchema], - name: _id.toString(), - directory: _rootDirectory.path, - maxSizeMiB: FMTC.instance.settings.databaseMaxSize, - compactOnLaunch: FMTC.instance.settings.databaseCompactCondition, - inspector: false, - ); - db.writeTxnSync( - () => db.storeDescriptor.putSync(DbStoreDescriptor(name: _name)), - ); - _registry.register(_id, db); - } + /// Create this store + void createSync() => _backend.createStoreSync(storeName: _storeName); /// Delete this store /// - /// This will remove all traces of this store from the user's device. Use with - /// caution! - /// - /// Does nothing if the store does not already exist. - Future delete() async { - if (!ready) return; - - final store = _registry.unregister(_id); - if (store?.isOpen ?? false) await store!.close(deleteFromDisk: true); - } + /// This operation cannot be undone! Ensure you confirm with the user that + /// this action is expected. + Future delete() => _backend.deleteStore(storeName: _storeName); - /// Removes all tiles from this store synchronously - /// - /// Also resets the cache hits & misses statistic. + /// Delete this store /// - /// This method requires the store to be [ready], else an [FMTCStoreNotReady] - /// error will be raised. - Future resetAsync() async { - final db = _registry(_name); - await db.writeTxn(() async { - await db.tiles.clear(); - await db.storeDescriptor.put( - (await db.descriptor) - ..hits = 0 - ..misses = 0, - ); - }); - } + /// This operation cannot be undone! Ensure you confirm with the user that + /// this action is expected. + void deleteSync() => _backend.deleteStoreSync(storeName: _storeName); - /// Removes all tiles from this store asynchronously - /// - /// Also resets the cache hits & misses statistic. + /// Removes all tiles from this store /// - /// Prefer [resetAsync] to avoid blocking the UI thread. Otherwise, this has - /// slightly better performance. + /// This operation cannot be undone! Ensure you confirm with the user that + /// this action is expected. + Future reset() => _backend.resetStore(storeName: _storeName); + + /// Removes all tiles from this store /// - /// This method requires the store to be [ready], else an [FMTCStoreNotReady] - /// error will be raised. - void reset() { - final db = _registry(_name); - db.writeTxnSync(() { - db.tiles.clearSync(); - db.storeDescriptor.putSync( - db.descriptorSync - ..hits = 0 - ..misses = 0, - ); - }); - } + /// This operation cannot be undone! Ensure you confirm with the user that + /// this action is expected. + void resetSync() => _backend.resetStoreSync(storeName: _storeName); - /// Rename the store directory asynchronously + /// Rename the store directory /// /// The old [StoreDirectory] will still retain it's link to the old store, so /// always use the new returned value instead: returns a new [StoreDirectory] @@ -118,33 +58,14 @@ final class StoreManagement extends _WithBackendAccess { /// This method requires the store to be [ready], else an [FMTCStoreNotReady] /// error will be raised. Future rename(String newStoreName) async { - // Unregister and close old database without deleting it - final store = _registry.unregister(_id); - if (store == null) { - _registry(_name); - throw StateError( - 'This error represents a serious internal error in FMTC. Please raise a bug report if seen in any application', - ); - } - await store.close(); - - // Manually change the database's filename - await (_rootDirectory >>> '$_id.isar').rename( - (_rootDirectory >>> '${DatabaseTools.hash(newStoreName)}.isar').path, - ); - - // Register the new database (it will be re-opened) - final newStore = StoreDirectory._(newStoreName, autoCreate: false); - await newStore.manage.createAsync(); - - // Update the name stored inside the database - await _registry(newStoreName).writeTxn( - () => _registry(newStoreName) - .storeDescriptor - .put(DbStoreDescriptor(name: newStoreName)), + await _backend.renameStore( + currentStoreName: _storeName, + newStoreName: newStoreName, ); - return newStore; + // TODO: `autoCreate` and entire shortcut will now be broken by default + // consider whether this bi-synchronousable approach is sustainable + return StoreDirectory._(newStoreName, autoCreate: false); } /// Delete all tiles older that were last modified before [expiry] From cb61e89721273ecde72652479f3a984ab955f953 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sun, 31 Dec 2023 22:12:15 +0100 Subject: [PATCH 085/168] Removed synchronous operations to improve simplicity and DX Attach most of backend management to frontend Fixed bug in `removeOldestTile` logic Removed `TileCannotUpdate` error as it is a library error not a user error Improved documentation and make use of templates and macros to de-duplicate doc-strings Minor internal improvements elsewhere Former-commit-id: 964867442faa94d9963df63cdb332b1f6781f06a [formerly c2e2a66550e2aef290b4c356fb52ea9d60ff7864] Former-commit-id: a78e9c03a02d9dded49db72dcdf28a2ffc040a20 --- lib/flutter_map_tile_caching.dart | 6 - lib/src/backend/exports.dart | 1 - lib/src/backend/impls/objectbox/backend.dart | 48 ++-- lib/src/backend/impls/objectbox/worker.dart | 4 +- lib/src/backend/interfaces/backend.dart | 232 ++++--------------- lib/src/backend/interfaces/no_sync.dart | 181 --------------- lib/src/backend/utils/errors.dart | 28 --- lib/src/bulk_download/instance.dart | 4 +- lib/src/bulk_download/manager.dart | 18 +- lib/src/db/defs/metadata.dart | 23 -- lib/src/db/defs/recovery.dart | 60 ----- lib/src/db/defs/store_descriptor.dart | 19 -- lib/src/db/defs/tile.dart | 26 --- lib/src/db/registry.dart | 207 ----------------- lib/src/db/tools.dart | 54 ----- lib/src/fmtc.dart | 2 + lib/src/store/directory.dart | 18 +- lib/src/store/manage.dart | 49 +--- lib/src/store/statistics.dart | 47 +--- 19 files changed, 93 insertions(+), 934 deletions(-) delete mode 100644 lib/src/backend/interfaces/no_sync.dart delete mode 100644 lib/src/db/defs/metadata.dart delete mode 100644 lib/src/db/defs/recovery.dart delete mode 100644 lib/src/db/defs/store_descriptor.dart delete mode 100644 lib/src/db/defs/tile.dart delete mode 100644 lib/src/db/registry.dart delete mode 100644 lib/src/db/tools.dart diff --git a/lib/flutter_map_tile_caching.dart b/lib/flutter_map_tile_caching.dart index 94d7fe2b..f676e3fe 100644 --- a/lib/flutter_map_tile_caching.dart +++ b/lib/flutter_map_tile_caching.dart @@ -37,12 +37,6 @@ import 'src/backend/exports.dart'; import 'src/bulk_download/instance.dart'; import 'src/bulk_download/rate_limited_stream.dart'; import 'src/bulk_download/tile_loops/shared.dart'; -import 'src/db/defs/metadata.dart'; -import 'src/db/defs/recovery.dart'; -import 'src/db/defs/store_descriptor.dart'; -import 'src/db/defs/tile.dart'; -import 'src/db/registry.dart'; -import 'src/db/tools.dart'; import 'src/errors/browsing.dart'; import 'src/errors/initialisation.dart'; import 'src/misc/exts.dart'; diff --git a/lib/src/backend/exports.dart b/lib/src/backend/exports.dart index f5f38c1a..26cfaf49 100644 --- a/lib/src/backend/exports.dart +++ b/lib/src/backend/exports.dart @@ -1,5 +1,4 @@ export 'impls/objectbox/backend.dart'; export 'interfaces/backend.dart'; export 'interfaces/models.dart'; -export 'interfaces/no_sync.dart'; export 'utils/errors.dart'; diff --git a/lib/src/backend/impls/objectbox/backend.dart b/lib/src/backend/impls/objectbox/backend.dart index f68500e4..4619592b 100644 --- a/lib/src/backend/impls/objectbox/backend.dart +++ b/lib/src/backend/impls/objectbox/backend.dart @@ -9,7 +9,6 @@ import 'package:path_provider/path_provider.dart'; import '../../../misc/exts.dart'; import '../../interfaces/backend.dart'; -import '../../interfaces/no_sync.dart'; import '../../utils/errors.dart'; import 'models/generated/objectbox.g.dart'; import 'models/models.dart'; @@ -30,9 +29,7 @@ abstract interface class ObjectBoxBackendInternal static final _instance = _ObjectBoxBackendImpl._(); } -class _ObjectBoxBackendImpl - with FMTCBackendNoSync - implements ObjectBoxBackendInternal { +class _ObjectBoxBackendImpl implements ObjectBoxBackendInternal { _ObjectBoxBackendImpl._(); void get expectInitialised => _sendPort ?? (throw RootUnavailable()); @@ -73,7 +70,7 @@ class _ObjectBoxBackendImpl @override String get friendlyIdentifier => 'ObjectBox'; - /// {@macro fmtc_backend_initialise} + /// {@macro fmtc.backend.initialise} /// /// This implementation additionally accepts the following [implSpecificArgs]: /// @@ -286,21 +283,11 @@ class _ObjectBoxBackendImpl args: {'storeName': storeName, 'url': url}, ))!['wasOrphaned']; - void _sendRemoveOldestTileCmd(String storeName) { - _sendCmd( - type: _WorkerCmdType.removeOldestTile, - args: { - 'storeName': storeName, - 'number': _dotLength, - }, - ); - _dotLength = 0; - } - @override - void removeOldestTileSync({ + Future removeOldestTile({ required String storeName, - }) { + required int numToRemove, + }) async { // Attempts to avoid flooding worker with requests to delete oldest tile, // and 'batches' them instead @@ -310,7 +297,8 @@ class _ObjectBoxBackendImpl _dotStore = storeName; if (_dotDebouncer.isActive) { _dotDebouncer.cancel(); - _sendRemoveOldestTileCmd(storeName); + _sendROTCmd(storeName); + _dotLength += numToRemove; } } @@ -318,20 +306,22 @@ class _ObjectBoxBackendImpl _dotDebouncer.cancel(); _dotDebouncer = Timer( const Duration(milliseconds: 500), - () => _sendRemoveOldestTileCmd(storeName), + () => _sendROTCmd(storeName), ); + _dotLength += numToRemove; return; } - _dotDebouncer = Timer( - const Duration(seconds: 1), - () => _sendRemoveOldestTileCmd(storeName), - ); + _dotDebouncer = + Timer(const Duration(seconds: 1), () => _sendROTCmd(storeName)); + _dotLength += numToRemove; } - @override - Future removeOldestTile({ - required String storeName, - }) async => - removeOldestTileSync(storeName: storeName); + void _sendROTCmd(String storeName) { + _sendCmd( + type: _WorkerCmdType.removeOldestTile, + args: {'storeName': storeName, 'number': _dotLength}, + ); + _dotLength = 0; + } } diff --git a/lib/src/backend/impls/objectbox/worker.dart b/lib/src/backend/impls/objectbox/worker.dart index ff928bed..5aee80c9 100644 --- a/lib/src/backend/impls/objectbox/worker.dart +++ b/lib/src/backend/impls/objectbox/worker.dart @@ -328,7 +328,9 @@ Future _worker( ); break; case (true, true): // FMTC internal error - throw TileCannotUpdate(url: url); + throw StateError( + 'FMTC ObjectBox backend internal state error: $url', + ); } }, ); diff --git a/lib/src/backend/interfaces/backend.dart b/lib/src/backend/interfaces/backend.dart index c23d0798..94ca7107 100644 --- a/lib/src/backend/interfaces/backend.dart +++ b/lib/src/backend/interfaces/backend.dart @@ -23,7 +23,6 @@ import '../../../flutter_map_tile_caching.dart'; /// /// To end-users: /// * Use [FMTCSettings.backend] to set a custom backend -/// * Not all sync versions of methods are guaranteed to have implementations /// * Avoid calling the [internal] method of a backend abstract interface class FMTCBackend { const FMTCBackend(); @@ -39,56 +38,30 @@ abstract interface class FMTCBackend { abstract interface class FMTCBackendInternal { abstract final String friendlyIdentifier; - /// {@template fmtc_backend_initialise} + /// {@template fmtc.backend.initialise} /// Initialise this backend & create the root /// /// [rootDirectory] defaults to '[getApplicationDocumentsDirectory]/fmtc'. /// /// [maxDatabaseSize] defaults to 1 GB shared across all stores. Specify the /// amount in KB. - /// {@endtemplate} /// /// Some implementations may accept/require additional arguments that may /// be set through [implSpecificArgs]. See their documentation for more /// information. + /// {@endtemplate} /// /// --- /// /// Note to implementers: if you accept implementation specific arguments, - /// override the documentation on this method, and use the - /// 'fmtc_backend_initialise' macro at the top to retain the standard docs. + /// ensure you properly document these. Future initialise({ String? rootDirectory, int? maxDatabaseSize, Map implSpecificArgs = const {}, }); - /// {@macro fmtc_backend_initialise} - /// - /// --- - /// - /// Note to implementers: if you accept implementation specific arguments, - /// override the documentation on this method, and use the - /// 'fmtc_backend_initialise' macro at the top to retain the standard docs. - void initialiseSync({ - String? rootDirectory, - int? maxDatabaseSize, - Map implSpecificArgs = const {}, - }); - - /// Whether [initialiseSync] is implemented - /// - /// If `false`, calling will throw an [SyncOperationUnsupported] error. - abstract final bool supportsSyncInitialise; - - /// Uninitialise this backend, and release whatever resources it is consuming - /// - /// If [deleteRoot] is `true`, then the storage medium will be permanently - /// deleted. - Future destroy({ - bool deleteRoot = false, - }); - + /// {@template fmtc.backend.destroy} /// Uninitialise this backend, and release whatever resources it is consuming /// /// If [deleteRoot] is `true`, then the storage medium will be permanently @@ -99,178 +72,90 @@ abstract interface class FMTCBackendInternal { /// but any operations started after this method call will be lost. A lost /// operation may throw [RootUnavailable]. This parameter may not have a /// noticable/any effect in some implementations. - void destroySync({ - bool deleteRoot = false, - bool immediate = false, + /// {@endtemplate} + Future destroy({ + required bool deleteRoot, + required bool immediate, }); - /// Whether [destroySync] is implemented - /// - /// If `false`, calling will throw an [SyncOperationUnsupported] error. - abstract final bool supportsSyncDestroy; - - /// Whether the store currently exists + /// {@template fmtc.backend.storeExists} + /// Check whether the specified store currently exists + /// {@endtemplate} Future storeExists({ required String storeName, }); - /// Whether the store currently exists - bool storeExistsSync({ - required String storeName, - }); - - /// Whether [storeExistsSync] is implemented - /// - /// If `false`, calling will throw an [SyncOperationUnsupported] error. - abstract final bool supportsSyncStoreExists; - + /// {@template fmtc.backend.createStore} /// Create a new store with the specified name + /// {@endtemplate} Future createStore({ required String storeName, }); - /// Create a new store with the specified name - void createStoreSync({ - required String storeName, - }); - - /// Whether [createStoreSync] is implemented + /// {@template fmtc.backend.deleteStore} + /// Delete the specified store /// - /// If `false`, calling will throw an [SyncOperationUnsupported] error. - abstract final bool supportsSyncCreateStore; - - /// Remove all the tiles from within the specified store - Future resetStore({ + /// This operation cannot be undone! Ensure you confirm with the user that + /// this action is expected. + /// {@endtemplate} + Future deleteStore({ required String storeName, }); + /// {@template fmtc.backend.resetStore} /// Remove all the tiles from within the specified store - void resetStoreSync({ + /// + /// This operation cannot be undone! Ensure you confirm with the user that + /// this action is expected. + /// {@endtemplate} + Future resetStore({ required String storeName, }); - /// Whether [resetStoreSync] is implemented - /// - /// If `false`, calling will throw an [SyncOperationUnsupported] error. - abstract final bool supportsSyncResetStore; - /// Change the name of the store named [currentStoreName] to [newStoreName] Future renameStore({ required String currentStoreName, required String newStoreName, }); - /// Change the name of the store named [currentStoreName] to [newStoreName] - void renameStoreSync({ - required String currentStoreName, - required String newStoreName, - }); - - /// Whether [renameStoreSync] is implemented - /// - /// If `false`, calling will throw an [SyncOperationUnsupported] error. - abstract final bool supportsSyncRenameStore; - - /// Delete the specified store - Future deleteStore({ - required String storeName, - }); - - /// Delete the specified store - void deleteStoreSync({ - required String storeName, - }); - - /// Whether [deleteStoreSync] is implemented - /// - /// If `false`, calling will throw an [SyncOperationUnsupported] error. - abstract final bool supportsSyncDeleteStore; - + /// {@template fmtc.backend.getStoreSize} /// Retrieve the total size (in kibibytes KiB) of the image bytes of all the /// tiles that belong to the specified store /// - /// This does not return any other data that adds to the 'real' store size + /// This does not return any other data that adds to the 'real' store size. + /// {@endtemplate} Future getStoreSize({ required String storeName, }); - /// Retrieve the total size (in kibibytes KiB) of the image bytes of all the - /// tiles that belong to the specified store - /// - /// This does not return any other data that adds to the 'real' store size - double getStoreSizeSync({ - required String storeName, - }); - - /// Whether [getStoreSizeSync] is implemented - /// - /// If `false`, calling will throw an [SyncOperationUnsupported] error. - abstract final bool supportsSyncGetStoreSize; - + /// {@template fmtc.backend.getStoreLength} /// Retrieve the number of tiles that belong to the specified store + /// {@endtemplate} Future getStoreLength({ required String storeName, }); - /// Retrieve the number of tiles that belong to the specified store - int getStoreLengthSync({ - required String storeName, - }); - - /// Whether [getStoreLengthSync] is implemented - /// - /// If `false`, calling will throw an [SyncOperationUnsupported] error. - abstract final bool supportsSyncGetStoreLength; - + /// {@template fmtc.backend.getStoreHits} /// Retrieve the number of times that a tile was successfully retrieved from /// the specified store when browsing + /// {@endtemplate} Future getStoreHits({ required String storeName, }); - /// Retrieve the number of times that a tile was successfully retrieved from - /// the specified store when browsing - int getStoreHitsSync({ - required String storeName, - }); - - /// Whether [getStoreHitsSync] is implemented - /// - /// If `false`, calling will throw an [SyncOperationUnsupported] error. - abstract final bool supportsSyncGetStoreHits; - + /// {@template fmtc.backend.getStoreMisses} /// Retrieve the number of times that a tile was attempted to be retrieved from /// the specified store when browsing, but was not present + /// {@endtemplate} Future getStoreMisses({ required String storeName, }); - /// Retrieve the number of times that a tile was attempted to be retrieved from - /// the specified store when browsing, but was not present - int getStoreMissesSync({ - required String storeName, - }); - - /// Whether [getStoreMissesSync] is implemented - /// - /// If `false`, calling will throw an [SyncOperationUnsupported] error. - abstract final bool supportsSyncGetStoreMisses; - /// Get a raw tile by URL Future readTile({ required String url, }); - /// Get a raw tile by URL - BackendTile? readTileSync({ - required String url, - }); - - /// Whether [readTileSync] is implemented - /// - /// If `false`, calling will throw an [SyncOperationUnsupported] error. - abstract final bool supportsSyncReadTile; - /// Create or update a tile /// /// If the tile already existed, it will be added to the specified store. @@ -285,25 +170,6 @@ abstract interface class FMTCBackendInternal { required Uint8List? bytes, }); - /// Create or update a tile - /// - /// If the tile already existed, it will be added to the specified store. - /// Otherwise, [bytes] must be specified, and the tile will be created and - /// added. - /// - /// If [bytes] is provided and the tile already existed, it will be updated for - /// all stores. - void writeTileSync({ - required String storeName, - required String url, - required Uint8List? bytes, - }); - - /// Whether [writeTileSync] is implemented - /// - /// If `false`, calling will throw an [SyncOperationUnsupported] error. - abstract final bool supportsSyncWriteTile; - /// Remove the tile from the store, deleting it if orphaned /// /// Returns: @@ -315,32 +181,12 @@ abstract interface class FMTCBackendInternal { required String url, }); - /// Remove the tile from the store, deleting it if orphaned - /// - /// Returns: - /// * `null` : if there was no existing tile - /// * `true` : if the tile itself could be deleted (it was orphaned) - /// * `false`: if the tile still belonged to at least store - bool? deleteTileSync({ - required String storeName, - required String url, - }); - - /// Whether [deleteTileSync] is implemented - /// - /// If `false`, calling will throw an [SyncOperationUnsupported] error. - abstract final bool supportsSyncDeleteTile; - + /// {@template fmtc.backend.removeOldestTile} + /// Remove the specified number of tiles from the specified store, in the order + /// of oldest first, and where each tile does not belong to any other store + /// {@endtemplate} Future removeOldestTile({ required String storeName, + required int numToRemove, }); - - void removeOldestTileSync({ - required String storeName, - }); - - /// Whether [removeOldestTileSync] is implemented - /// - /// If `false`, calling will throw an [SyncOperationUnsupported] error. - abstract final bool supportsSyncRemoveOldestTile; } diff --git a/lib/src/backend/interfaces/no_sync.dart b/lib/src/backend/interfaces/no_sync.dart deleted file mode 100644 index ab9a6a42..00000000 --- a/lib/src/backend/interfaces/no_sync.dart +++ /dev/null @@ -1,181 +0,0 @@ -import 'dart:typed_data'; - -import '../utils/errors.dart'; -import 'backend.dart'; -import 'models.dart'; - -/// A shortcut to declare that an [FMTCBackend] does not support any synchronous -/// versions of methods -mixin FMTCBackendNoSync implements FMTCBackendInternal { - /// This synchronous method is unsupported by this implementation - use - /// [initialise] instead - @override - void initialiseSync({ - String? rootDirectory, - int? maxDatabaseSize, - Map implSpecificArgs = const {}, - }) => - throw SyncOperationUnsupported(); - - @override - final supportsSyncInitialise = false; - - /// This synchronous method is unsupported by this implementation - use - /// [destroy] instead - @override - void destroySync({ - bool deleteRoot = false, - bool immediate = false, - }) => - throw SyncOperationUnsupported(); - - @override - final supportsSyncDestroy = false; - - /// This synchronous method is unsupported by this implementation - use - /// [storeExists] instead - @override - bool storeExistsSync({ - required String storeName, - }) => - throw SyncOperationUnsupported(); - - @override - final supportsSyncStoreExists = false; - - /// This synchronous method is unsupported by this implementation - use - /// [createStore] instead - @override - void createStoreSync({ - required String storeName, - }) => - throw SyncOperationUnsupported(); - - @override - final supportsSyncCreateStore = false; - - /// This synchronous method is unsupported by this implementation - use - /// [resetStore] instead - @override - void resetStoreSync({ - required String storeName, - }) => - throw SyncOperationUnsupported(); - - @override - final supportsSyncResetStore = false; - - /// This synchronous method is unsupported by this implementation - use - /// [renameStore] instead - @override - void renameStoreSync({ - required String currentStoreName, - required String newStoreName, - }) => - throw SyncOperationUnsupported(); - - @override - final supportsSyncRenameStore = false; - - /// This synchronous method is unsupported by this implementation - use - /// [deleteStore] instead - @override - void deleteStoreSync({ - required String storeName, - }) => - throw SyncOperationUnsupported(); - - @override - final supportsSyncDeleteStore = false; - - /// This synchronous method is unsupported by this implementation - use - /// [getStoreSize] instead - @override - double getStoreSizeSync({ - required String storeName, - }) => - throw SyncOperationUnsupported(); - - @override - final supportsSyncGetStoreSize = false; - - /// This synchronous method is unsupported by this implementation - use - /// [getStoreLength] instead - @override - int getStoreLengthSync({ - required String storeName, - }) => - throw SyncOperationUnsupported(); - - @override - final supportsSyncGetStoreLength = false; - - /// This synchronous method is unsupported by this implementation - use - /// [getStoreHits] instead - @override - int getStoreHitsSync({ - required String storeName, - }) => - throw SyncOperationUnsupported(); - - @override - final supportsSyncGetStoreHits = false; - - /// This synchronous method is unsupported by this implementation - use - /// [getStoreMisses] instead - @override - int getStoreMissesSync({ - required String storeName, - }) => - throw SyncOperationUnsupported(); - - @override - final supportsSyncGetStoreMisses = false; - - /// This synchronous method is unsupported by this implementation - use - /// [readTile] instead - @override - BackendTile? readTileSync({ - required String url, - }) => - throw SyncOperationUnsupported(); - - @override - final supportsSyncReadTile = false; - - /// This synchronous method is unsupported by this implementation - use - /// [writeTile] instead - @override - void writeTileSync({ - required String storeName, - required String url, - required Uint8List? bytes, - }) => - throw SyncOperationUnsupported(); - - @override - final supportsSyncWriteTile = false; - - /// This synchronous method is unsupported by this implementation - use - /// [deleteTile] instead - @override - bool? deleteTileSync({ - required String storeName, - required String url, - }) => - throw SyncOperationUnsupported(); - - @override - final supportsSyncDeleteTile = false; - - /// This synchronous method is unsupported by this implementation - use - /// [removeOldestTile] instead - @override - void removeOldestTileSync({ - required String storeName, - }) => - throw SyncOperationUnsupported(); - - @override - final supportsSyncRemoveOldestTile = false; -} diff --git a/lib/src/backend/utils/errors.dart b/lib/src/backend/utils/errors.dart index e9be79f2..620620b9 100644 --- a/lib/src/backend/utils/errors.dart +++ b/lib/src/backend/utils/errors.dart @@ -47,31 +47,3 @@ final class StoreAlreadyExists extends FMTCBackendError { String toString() => 'StoreAlreadyExists: The requested store "$storeName" already existed'; } - -/// Indicates that the specified tile could not be updated because it did not -/// already exist -/// -/// If you have this error in your application, please file a bug report. -final class TileCannotUpdate extends FMTCBackendError { - final String url; - - TileCannotUpdate({required this.url}); - - @override - String toString() => - 'TileCannotUpdate: The requested tile ("$url") did not exist, and so cannot be updated'; -} - -/// Indicates that the backend implementation does not support the invoked -/// synchronous operation -/// -/// Use the asynchronous version instead. -/// -/// Note that there is no equivalent error for async operations: if there is no -/// specific async version of an operation, it should redirect to the sync -/// version. -final class SyncOperationUnsupported extends FMTCBackendError { - @override - String toString() => - 'SyncOperationUnsupported: The backend implementation does not support the invoked synchronous operation.'; -} diff --git a/lib/src/bulk_download/instance.dart b/lib/src/bulk_download/instance.dart index b97f9877..ea8111af 100644 --- a/lib/src/bulk_download/instance.dart +++ b/lib/src/bulk_download/instance.dart @@ -9,9 +9,7 @@ class DownloadInstance { static final _instances = {}; static DownloadInstance? registerIfAvailable(Object id) => - _instances.containsKey(id) - ? null - : _instances[id] ??= DownloadInstance._(id); + _instances[id] != null ? null : _instances[id] = DownloadInstance._(id); static bool unregister(Object id) => _instances.remove(id) != null; static DownloadInstance? get(Object id) => _instances[id]; diff --git a/lib/src/bulk_download/manager.dart b/lib/src/bulk_download/manager.dart index 77c8efc1..dde188c8 100644 --- a/lib/src/bulk_download/manager.dart +++ b/lib/src/bulk_download/manager.dart @@ -119,7 +119,7 @@ Future _downloadManager( } // Setup two-way communications with root - final rootreceivePort = ReceivePort(); + final rootReceivePort = ReceivePort(); void send(Object? m) => input.sendPort.send(m); // Setup cancel, pause, and resume handling @@ -131,7 +131,7 @@ Future _downloadManager( final threadPausedStates = generateThreadPausedStates(); final cancelSignal = Completer(); var pauseResumeSignal = Completer()..complete(); - rootreceivePort.listen( + rootReceivePort.listen( (e) async { if (e == null) { try { @@ -172,7 +172,7 @@ Future _downloadManager( ); // Now it's safe, start accepting communications from the root - send(rootreceivePort.sendPort); + send(rootReceivePort.sendPort); // Start download threads & wait for download to complete/cancelled downloadDuration.start(); @@ -183,11 +183,11 @@ Future _downloadManager( if (cancelSignal.isCompleted) return; // Start thread worker isolate & setup two-way communications - final downloadThreadreceivePort = ReceivePort(); + final downloadThreadReceivePort = ReceivePort(); await Isolate.spawn( _singleDownloadThread, ( - sendPort: downloadThreadreceivePort.sendPort, + sendPort: downloadThreadReceivePort.sendPort, storeId: storeId, rootDirectory: input.rootDirectory, options: input.region.options, @@ -197,7 +197,7 @@ Future _downloadManager( obscuredQueryParams: input.obscuredQueryParams, headers: headers, ), - onExit: downloadThreadreceivePort.sendPort, + onExit: downloadThreadReceivePort.sendPort, debugName: '[FMTC] Bulk Download Thread #$threadNo', ); late final SendPort sendPort; @@ -214,7 +214,7 @@ Future _downloadManager( .then((sp) => sp.send(null)), ); - downloadThreadreceivePort.listen( + downloadThreadReceivePort.listen( (evt) async { // Thread is sending tile data if (evt is TileEvent) { @@ -288,7 +288,7 @@ Future _downloadManager( } // Thread ended, goto `onDone` - if (evt == null) return downloadThreadreceivePort.close(); + if (evt == null) return downloadThreadReceivePort.close(); }, onDone: () { try { @@ -323,7 +323,7 @@ Future _downloadManager( ); // Cleanup resources and shutdown - rootreceivePort.close(); + rootReceivePort.close(); tileIsolate.kill(priority: Isolate.immediate); await tileQueue.cancel(immediate: true); Isolate.exit(); diff --git a/lib/src/db/defs/metadata.dart b/lib/src/db/defs/metadata.dart deleted file mode 100644 index 60e46656..00000000 --- a/lib/src/db/defs/metadata.dart +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright © Luka S (JaffaKetchup) under GPL-v3 -// A full license can be found at .\LICENSE - -import 'package:isar/isar.dart'; -import 'package:meta/meta.dart'; - -import '../tools.dart'; - -part 'metadata.g.dart'; - -@internal -@Collection(accessor: 'metadata') -class DbMetadata { - Id get id => DatabaseTools.hash(name); - - final String name; - final String data; - - DbMetadata({ - required this.name, - required this.data, - }); -} diff --git a/lib/src/db/defs/recovery.dart b/lib/src/db/defs/recovery.dart deleted file mode 100644 index 708730c1..00000000 --- a/lib/src/db/defs/recovery.dart +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright © Luka S (JaffaKetchup) under GPL-v3 -// A full license can be found at .\LICENSE - -import 'package:isar/isar.dart'; -import 'package:meta/meta.dart'; - -part 'recovery.g.dart'; - -@internal -enum RegionType { rectangle, circle, line, customPolygon } - -@internal -@Collection(accessor: 'recovery') -class DbRecoverableRegion { - final Id id; - final String storeName; - final DateTime time; - @enumerated - final RegionType type; - - final byte minZoom; - final byte maxZoom; - - final short start; - final short? end; - - final float? nwLat; - final float? nwLng; - final float? seLat; - final float? seLng; - - final float? centerLat; - final float? centerLng; - final float? circleRadius; - - final List? linePointsLat; - final List? linePointsLng; - final float? lineRadius; - - DbRecoverableRegion({ - required this.id, - required this.storeName, - required this.time, - required this.type, - required this.minZoom, - required this.maxZoom, - required this.start, - this.end, - this.nwLat, - this.nwLng, - this.seLat, - this.seLng, - this.centerLat, - this.centerLng, - this.circleRadius, - this.linePointsLat, - this.linePointsLng, - this.lineRadius, - }); -} diff --git a/lib/src/db/defs/store_descriptor.dart b/lib/src/db/defs/store_descriptor.dart deleted file mode 100644 index 5977b398..00000000 --- a/lib/src/db/defs/store_descriptor.dart +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright © Luka S (JaffaKetchup) under GPL-v3 -// A full license can be found at .\LICENSE - -import 'package:isar/isar.dart'; -import 'package:meta/meta.dart'; - -part 'store_descriptor.g.dart'; - -@internal -@Collection(accessor: 'storeDescriptor') -class DbStoreDescriptor { - final Id id = 0; - final String name; - - int hits = 0; - int misses = 0; - - DbStoreDescriptor({required this.name}); -} diff --git a/lib/src/db/defs/tile.dart b/lib/src/db/defs/tile.dart deleted file mode 100644 index 79c7ffe9..00000000 --- a/lib/src/db/defs/tile.dart +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright © Luka S (JaffaKetchup) under GPL-v3 -// A full license can be found at .\LICENSE - -import 'package:isar/isar.dart'; -import 'package:meta/meta.dart'; - -import '../tools.dart'; - -part 'tile.g.dart'; - -@internal -@Collection(accessor: 'tiles') -class DbTile { - Id get id => DatabaseTools.hash(url); - - final String url; - final List bytes; - - @Index() - final DateTime lastModified; - - DbTile({ - required this.url, - required this.bytes, - }) : lastModified = DateTime.now(); -} diff --git a/lib/src/db/registry.dart b/lib/src/db/registry.dart deleted file mode 100644 index 707631d2..00000000 --- a/lib/src/db/registry.dart +++ /dev/null @@ -1,207 +0,0 @@ -// Copyright © Luka S (JaffaKetchup) under GPL-v3 -// A full license can be found at .\LICENSE - -import 'dart:io'; - -import 'package:isar/isar.dart'; -import 'package:meta/meta.dart'; -import 'package:path/path.dart' as p; -import 'package:stream_transform/stream_transform.dart'; - -import '../../flutter_map_tile_caching.dart'; -import '../misc/exts.dart'; -import 'defs/metadata.dart'; -import 'defs/recovery.dart'; -import 'defs/store_descriptor.dart'; -import 'defs/tile.dart'; -import 'tools.dart'; - -/// Manages the stores available -/// -/// It is very important for the [_storeDatabases] state to remain in sync with -/// the actual state of the [directory], otherwise unexpected behaviour may -/// occur. -@internal -class FMTCRegistry { - const FMTCRegistry._({ - required this.directory, - required this.recoveryDatabase, - required Map storeDatabases, - }) : _storeDatabases = storeDatabases; - - static late FMTCRegistry instance; - - final Directory directory; - final Isar recoveryDatabase; - final Map _storeDatabases; - - static Future initialise({ - required Directory directory, - required int databaseMaxSize, - required CompactCondition? databaseCompactCondition, - required void Function(FMTCInitialisationException error)? errorHandler, - required IOSink? initialisationSafetyWriteSink, - required List? safeModeSuccessfulIDs, - required bool debugMode, - }) async { - // Set up initialisation safety features - bool hasLocatedCorruption = false; - - Future deleteDatabaseAndRelatedFiles(String base) async { - try { - await Future.wait( - await directory - .list() - .where((e) => e is File && p.basename(e.path).startsWith(base)) - .map((e) async { - if (await e.exists()) return e.delete(); - }).toList(), - ); - // ignore: empty_catches - } catch (e) {} - } - - Future registerSafeDatabase(String base) async { - initialisationSafetyWriteSink?.writeln(base); - await initialisationSafetyWriteSink?.flush(); - } - - // Prepare open database method - Future?> openIsar(String id, File file) async { - try { - return MapEntry( - int.parse(id), - await Isar.open( - [DbStoreDescriptorSchema, DbTileSchema, DbMetadataSchema], - name: id, - directory: directory.absolute.path, - maxSizeMiB: databaseMaxSize, - compactOnLaunch: databaseCompactCondition, - inspector: debugMode, - ), - ); - } catch (err) { - await deleteDatabaseAndRelatedFiles(p.basename(file.path)); - errorHandler?.call( - FMTCInitialisationException( - 'Failed to initialise a store because Isar failed to open the database.', - FMTCInitialisationExceptionType.isarFailure, - originalError: err, - ), - ); - return null; - } - } - - // Open recovery database - if (!(safeModeSuccessfulIDs?.contains('.recovery') ?? true) && - await (directory >>> '.recovery.isar').exists()) { - await deleteDatabaseAndRelatedFiles('.recovery'); - hasLocatedCorruption = true; - errorHandler?.call( - FMTCInitialisationException( - 'Failed to open a database because it was not listed as safe/stable on last initialisation.', - FMTCInitialisationExceptionType.corruptedDatabase, - storeName: '.recovery', - ), - ); - } - final recoveryDatabase = await Isar.open( - [ - DbRecoverableRegionSchema, - if (debugMode) ...[ - DbStoreDescriptorSchema, - DbTileSchema, - DbMetadataSchema, - ], - ], - name: '.recovery', - directory: directory.absolute.path, - maxSizeMiB: databaseMaxSize, - compactOnLaunch: databaseCompactCondition, - inspector: debugMode, - ); - await registerSafeDatabase('.recovery'); - - // Open store databases - return instance = FMTCRegistry._( - directory: directory, - recoveryDatabase: recoveryDatabase, - storeDatabases: Map.fromEntries( - await directory - .list() - .where( - (e) => - e is File && - !p.basename(e.path).startsWith('.') && - p.extension(e.path) == '.isar', - ) - .asyncMap((file) async { - final id = p.basenameWithoutExtension(file.path); - final path = p.basename(file.path); - - // Check whether the database is safe - if (!hasLocatedCorruption && - safeModeSuccessfulIDs != null && - !safeModeSuccessfulIDs.contains(id)) { - await deleteDatabaseAndRelatedFiles(path); - hasLocatedCorruption = true; - errorHandler?.call( - FMTCInitialisationException( - 'Failed to open a database because it was not listed as safe/stable on last initialisation.', - FMTCInitialisationExceptionType.corruptedDatabase, - ), - ); - return null; - } - - // Open the database - MapEntry? entry = await openIsar(id, file as File); - if (entry == null) return null; - - // Correct the database ID (filename) if the store name doesn't - // match - final storeName = (await entry.value.descriptor).name; - final realId = DatabaseTools.hash(storeName); - if (realId != int.parse(id)) { - await entry.value.close(); - file = await file - .rename(Directory(p.dirname(file.path)) > '$realId.isar'); - entry = await openIsar(realId.toString(), file); - await deleteDatabaseAndRelatedFiles(path); - } - - // Register the database as safe and add it to the registry - await registerSafeDatabase(id); - return entry; - }) - .whereNotNull() - .toList(), - ), - ); - } - - Future uninitialise({bool delete = false}) async { - await Future.wait([ - ...FMTC.instance.rootDirectory.stats.storesAvailable - .map((s) => s.manage.delete()), - recoveryDatabase.close(deleteFromDisk: delete), - ]); - } - - Isar call(String storeName) { - final id = DatabaseTools.hash(storeName); - final isRegistered = _storeDatabases.containsKey(id); - if (!(isRegistered && _storeDatabases[id]!.isOpen)) { - throw FMTCStoreNotReady( - storeName: storeName, - registered: isRegistered, - ); - } - return _storeDatabases[id]!; - } - - Isar register(int id, Isar db) => _storeDatabases[id] = db; - Isar? unregister(int id) => _storeDatabases.remove(id); - Map get storeDatabases => _storeDatabases; -} diff --git a/lib/src/db/tools.dart b/lib/src/db/tools.dart deleted file mode 100644 index f378a041..00000000 --- a/lib/src/db/tools.dart +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright © Luka S (JaffaKetchup) under GPL-v3 -// A full license can be found at .\LICENSE - -import 'package:isar/isar.dart'; -import 'package:meta/meta.dart'; - -import '../../flutter_map_tile_caching.dart'; -import 'defs/store_descriptor.dart'; - -@internal -class DatabaseTools { - static int hash(String string) { - final str = string.trim(); - - // ignore: avoid_js_rounded_ints - int hash = 0xcbf29ce484222325; - int i = 0; - - while (i < str.length) { - final codeUnit = str.codeUnitAt(i++); - hash ^= codeUnit >> 8; - hash *= 0x100000001b3; - hash ^= codeUnit & 0xFF; - hash *= 0x100000001b3; - } - - return hash; - } -} - -@internal -extension IsarExts on Isar { - Future get descriptor async { - final descriptor = await storeDescriptor.get(0); - if (descriptor == null) { - throw FMTCDamagedStoreException( - 'Failed to perform an operation on a store due to the core descriptor being missing.', - FMTCDamagedStoreExceptionType.missingStoreDescriptor, - ); - } - return descriptor; - } - - DbStoreDescriptor get descriptorSync { - final descriptor = storeDescriptor.getSync(0); - if (descriptor == null) { - throw FMTCDamagedStoreException( - 'Failed to perform an operation on a store due to the core descriptor being missing.', - FMTCDamagedStoreExceptionType.missingStoreDescriptor, - ); - } - return descriptor; - } -} diff --git a/lib/src/fmtc.dart b/lib/src/fmtc.dart index 8eda10aa..a283c0f1 100644 --- a/lib/src/fmtc.dart +++ b/lib/src/fmtc.dart @@ -39,6 +39,8 @@ class FlutterMapTileCaching { required bool debugMode, }) : _debugMode = debugMode; + /// {@macro fmtc.backend.initialise} + /// /// Initialise and prepare FMTC, by creating all necessary directories/files /// and configuring the [FlutterMapTileCaching] singleton /// diff --git a/lib/src/store/directory.dart b/lib/src/store/directory.dart index a3b73d05..ced18d77 100644 --- a/lib/src/store/directory.dart +++ b/lib/src/store/directory.dart @@ -3,22 +3,10 @@ part of flutter_map_tile_caching; -/// Represents a store of tiles -/// -/// The tile store itself is a database containing a descriptor, tiles and -/// metadata. -/// -/// The name originates from previous versions of this library, where it -/// represented a real directory instead of a database. -/// -/// Reach through [FlutterMapTileCaching.call]. +/// Container for a [storeName] which includes methods and getters to access +/// functionality based on the specified store class StoreDirectory { - StoreDirectory._( - this.storeName, { - required bool autoCreate, - }) { - if (autoCreate) manage.create(); - } + const StoreDirectory._(this.storeName); /// The user-friendly name of the store directory final String storeName; diff --git a/lib/src/store/manage.dart b/lib/src/store/manage.dart index 86c53914..b16c03fd 100644 --- a/lib/src/store/manage.dart +++ b/lib/src/store/manage.dart @@ -11,70 +11,39 @@ part of flutter_map_tile_caching; /// [StoreAlreadyExists]). It is recommended to check [ready] or [readySync] when /// necessary. final class StoreManagement extends _WithBackendAccess { - StoreManagement._(super.store); + const StoreManagement._(super.store); - /// Whether this store exists + /// {@macro fmtc.backend.storeExists} Future get ready => _backend.storeExists(storeName: _storeName); - /// Whether this store exists - bool get readySync => _backend.storeExistsSync(storeName: _storeName); - - /// Create this store + /// {@macro fmtc.backend.createStore} Future create() => _backend.createStore(storeName: _storeName); - /// Create this store - void createSync() => _backend.createStoreSync(storeName: _storeName); - - /// Delete this store - /// - /// This operation cannot be undone! Ensure you confirm with the user that - /// this action is expected. + /// {@macro fmtc.backend.deleteStore} Future delete() => _backend.deleteStore(storeName: _storeName); - /// Delete this store - /// - /// This operation cannot be undone! Ensure you confirm with the user that - /// this action is expected. - void deleteSync() => _backend.deleteStoreSync(storeName: _storeName); - - /// Removes all tiles from this store - /// - /// This operation cannot be undone! Ensure you confirm with the user that - /// this action is expected. + /// {@macro fmtc.backend.resetStore} Future reset() => _backend.resetStore(storeName: _storeName); - /// Removes all tiles from this store - /// - /// This operation cannot be undone! Ensure you confirm with the user that - /// this action is expected. - void resetSync() => _backend.resetStoreSync(storeName: _storeName); - - /// Rename the store directory + /// Rename the store to [newStoreName] /// /// The old [StoreDirectory] will still retain it's link to the old store, so /// always use the new returned value instead: returns a new [StoreDirectory] /// after a successful renaming operation. - /// - /// This method requires the store to be [ready], else an [FMTCStoreNotReady] - /// error will be raised. Future rename(String newStoreName) async { await _backend.renameStore( currentStoreName: _storeName, newStoreName: newStoreName, ); - // TODO: `autoCreate` and entire shortcut will now be broken by default - // consider whether this bi-synchronousable approach is sustainable - return StoreDirectory._(newStoreName, autoCreate: false); + return StoreDirectory._(newStoreName); } /// Delete all tiles older that were last modified before [expiry] /// /// Ignores [FMTCTileProviderSettings.cachedValidDuration]. - Future pruneTilesOlderThan({required DateTime expiry}) => compute( - _pruneTilesOlderThanWorker, - [_name, _rootDirectory.absolute.path, expiry], - ); + Future pruneTilesOlderThan({required DateTime expiry}) => + _backend.pruneTilesOlderThan(expiry: expiry); /// Retrieves the most recently modified tile from the store, extracts it's /// bytes, and renders them to an [Image] diff --git a/lib/src/store/statistics.dart b/lib/src/store/statistics.dart index a8e9eaaa..a83f70f0 100644 --- a/lib/src/store/statistics.dart +++ b/lib/src/store/statistics.dart @@ -7,49 +7,18 @@ part of flutter_map_tile_caching; final class StoreStats extends _WithBackendAccess { const StoreStats._(super._store); - /// Retrieve the total size of the stored tiles and metadata in kibibytes (KiB) - /// - /// Prefer [storeSizeAsync] to avoid blocking the UI thread. Otherwise, this - /// has slightly better performance. - double get storeSize => _backend.getStoreSizeSync(storeName: _storeName); - - /// Retrieve the total size of the stored tiles and metadata in kibibytes (KiB) - Future get storeSizeAsync => - _backend.getStoreSize(storeName: _storeName); - - /// Retrieve the number of stored tiles synchronously - /// - /// Prefer [storeLengthAsync] to avoid blocking the UI thread. Otherwise, this - /// has slightly better performance. - int get storeLength => _backend.getStoreLengthSync(storeName: _storeName); + /// {@macro fmtc.backend.getStoreSize} + Future get size => _backend.getStoreSize(storeName: _storeName); - /// Retrieve the number of stored tiles asynchronously - Future get storeLengthAsync => - _backend.getStoreLength(storeName: _storeName); + /// {@macro fmtc.backend.getStoreLength} + Future get length => _backend.getStoreLength(storeName: _storeName); - /// Retrieve the number of tiles that were successfully retrieved from the - /// store during browsing synchronously - /// - /// Prefer [cacheHitsAsync] to avoid blocking the UI thread. Otherwise, this - /// has slightly better performance. - int get cacheHits => _backend.getStoreHitsSync(storeName: _storeName); - - /// Retrieve the number of tiles that were successfully retrieved from the - /// store during browsing asynchronously - Future get cacheHitsAsync async => + /// {@macro fmtc.backend.getStoreHits} + Future get cacheHits async => _backend.getStoreHits(storeName: _storeName); - /// Retrieve the number of tiles that were unsuccessfully retrieved from the - /// store during browsing synchronously - /// - /// Prefer [cacheMissesAsync] to avoid blocking the UI thread. Otherwise, this - /// has slightly better performance. - int get cacheMisses => _backend.getStoreMissesSync(storeName: _storeName); - - /// Retrieve the number of tiles that were unsuccessfully retrieved from the - /// store during browsing asynchronously - Future get cacheMissesAsync async => - _backend.getStoreMisses(storeName: _storeName); + /// {@macro fmtc.backend.getStoreMisses} + Future get cacheMisses => _backend.getStoreMisses(storeName: _storeName); /// Watch for changes in the current store /// From fc26ec127794da2a3358a828495ae90aff78a05a Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Mon, 1 Jan 2024 21:49:47 +0100 Subject: [PATCH 086/168] Added `removeOldestTilesAboveLimit` & `removeTilesOlderThan` to backend Removed apparently useless `removeOldestTile` from backend (with debouncing mechanism) Switched to `DateTime.timestamp` from `now` when backend tiles are concerned to avoid timezone bugs Refactored part of `deleteTiles` backend method into independent method Improved documentation Former-commit-id: c2d14c07b28b29b3cd69fc38b1e0c1be3e541f15 [formerly 5d5ce6240e65606f2ff681733ce01cb16c450fa2] Former-commit-id: f6024ccfab00b5e448dbb8c0a5f5273dfef388da --- lib/src/backend/impls/objectbox/backend.dart | 56 ++-- lib/src/backend/impls/objectbox/worker.dart | 312 +++++++++++-------- lib/src/backend/interfaces/backend.dart | 47 ++- lib/src/providers/image_provider.dart | 2 +- lib/src/store/manage.dart | 32 +- 5 files changed, 267 insertions(+), 182 deletions(-) diff --git a/lib/src/backend/impls/objectbox/backend.dart b/lib/src/backend/impls/objectbox/backend.dart index 4619592b..2751c531 100644 --- a/lib/src/backend/impls/objectbox/backend.dart +++ b/lib/src/backend/impls/objectbox/backend.dart @@ -41,10 +41,11 @@ class _ObjectBoxBackendImpl implements ObjectBoxBackendInternal { late Completer _workerComplete; late StreamSubscription _workerHandler; - // `deleteOldestTile` tracking & debouncing - late int _dotLength; - late Timer _dotDebouncer; - late String? _dotStore; + // TODO: Verify if necessary and remove if not + //`removeOldestTilesAboveLimit` tracking & debouncing + //late int _rotalLength; + //late Timer _rotalDebouncer; + //late String? _rotalStore; Future?> _sendCmd({ required _WorkerCmdType type, @@ -93,8 +94,8 @@ class _ObjectBoxBackendImpl implements ObjectBoxBackendInternal { // Reset non-comms-related non-resource-intensive state _workerId = 0; _workerRes.clear(); - _dotStore = null; - _dotLength = 0; + //_rotalStore = null; + //_rotalLength = 0; // Prepare to recieve `SendPort` from worker _workerRes[0] = Completer(); @@ -157,7 +158,7 @@ class _ObjectBoxBackendImpl implements ObjectBoxBackendInternal { // Resource-intensive state cleanup only (other cleanup done during init) _sendPort = null; // Indicate ready for re-init await _workerHandler.cancel(); - _dotDebouncer.cancel(); + //_rotalDebouncer.cancel(); print('passed _workerHandler cancel'); @@ -281,13 +282,19 @@ class _ObjectBoxBackendImpl implements ObjectBoxBackendInternal { (await _sendCmd( type: _WorkerCmdType.deleteStore, args: {'storeName': storeName, 'url': url}, - ))!['wasOrphaned']; + ))!['wasOrphan']; @override - Future removeOldestTile({ + Future removeOldestTilesAboveLimit({ required String storeName, - required int numToRemove, - }) async { + required int tilesLimit, + }) async => + (await _sendCmd( + type: _WorkerCmdType.removeOldestTilesAboveLimit, + args: {'storeName': storeName, 'tilesLimit': tilesLimit}, + ))!['numOrphans']; + + /* FOR ABOVE METHOD // Attempts to avoid flooding worker with requests to delete oldest tile, // and 'batches' them instead @@ -315,13 +322,24 @@ class _ObjectBoxBackendImpl implements ObjectBoxBackendInternal { _dotDebouncer = Timer(const Duration(seconds: 1), () => _sendROTCmd(storeName)); _dotLength += numToRemove; - } - void _sendROTCmd(String storeName) { - _sendCmd( - type: _WorkerCmdType.removeOldestTile, - args: {'storeName': storeName, 'number': _dotLength}, - ); - _dotLength = 0; - } + // may need to be moved out + void _sendROTCmd(String storeName) { + _sendCmd( + type: _WorkerCmdType.removeOldestTile, + args: {'storeName': storeName, 'number': _dotLength}, + ); + _dotLength = 0; + } + */ + + @override + Future removeTilesOlderThan({ + required String storeName, + required DateTime expiry, + }) async => + (await _sendCmd( + type: _WorkerCmdType.removeTilesOlderThan, + args: {'storeName': storeName, 'expiry': expiry}, + ))!['numOrphans']; } diff --git a/lib/src/backend/impls/objectbox/worker.dart b/lib/src/backend/impls/objectbox/worker.dart index 5aee80c9..dd05f020 100644 --- a/lib/src/backend/impls/objectbox/worker.dart +++ b/lib/src/backend/impls/objectbox/worker.dart @@ -15,7 +15,8 @@ enum _WorkerCmdType { readTile, writeTile, deleteTile, - removeOldestTile, + removeOldestTilesAboveLimit, + removeTilesOlderThan, } Future _worker( @@ -27,10 +28,15 @@ Future _worker( int? maxReaders, }) input, ) async { + //! SETUP !// + // Setup comms final receivePort = ReceivePort(); - void sendRes(({int id, Map? data}) m) => - input.sendPort.send(m); + void sendRes({ + required int id, + Map? data, + }) => + input.sendPort.send((id: id, data: data)); // Initialise database final rootDirectory = await (input.rootDirectory == null @@ -46,13 +52,13 @@ Future _worker( // Respond with comms channel for future cmds sendRes( - ( - id: 0, - data: {'sendPort': receivePort.sendPort}, - ), + id: 0, + data: {'sendPort': receivePort.sendPort}, ); - // Setup util functions + //! UTIL FUNCTIONS !// + + /// Convert store name to database store object ObjectBoxStore? getStore(String storeName) { final query = root .box() @@ -63,7 +69,47 @@ Future _worker( return store; } - // Await cmds, perform work, and respond + /// Delete the specified tiles from the specified store + /// + /// Note that [tilesQuery] is not closed internally. Ensure it is closed after + /// usage. + /// + /// Returns whether each tile was actually deleted (whether it was an orphan), + /// in iteration order of [tilesQuery.find]. + Iterable deleteTiles({ + required String storeName, + required Query tilesQuery, + }) { + final stores = root.box(); + final tiles = root.box(); + + return tilesQuery.find().map((tile) { + // For the correct store, adjust the statistics + for (final store in tile.stores) { + if (store.name != storeName) continue; + stores.put( + store + ..numberOfTiles -= 1 + ..numberOfBytes -= tile.bytes.lengthInBytes, + mode: PutMode.update, + ); + break; + } + + // Remove the store relation from the tile + tile.stores.removeWhere((store) => store.name == storeName); + + // Delete the tile if it belongs to no stores + if (tile.stores.isEmpty) return tiles.remove(tile.id); + + // Otherwise just update the tile + tiles.put(tile, mode: PutMode.update); + return false; + }); + } + + //! MAIN LOOP !// + await for (final ({ int id, _WorkerCmdType type, @@ -78,17 +124,17 @@ Future _worker( if (cmd.args['deleteRoot'] == true) { rootDirectory.deleteSync(recursive: true); } + // TODO: Consider final message Isolate.exit(); case _WorkerCmdType.storeExists: sendRes( - ( - id: cmd.id, - data: { - 'exists': getStore(cmd.args['storeName']! as String) != null, - }, - ), + id: cmd.id, + data: { + 'exists': getStore(cmd.args['storeName']! as String) != null, + }, ); + break; case _WorkerCmdType.createStore: final storeName = cmd.args['storeName']! as String; @@ -107,7 +153,9 @@ Future _worker( } catch (e) { throw StoreAlreadyExists(storeName: storeName); } - sendRes((id: cmd.id, data: null)); + + sendRes(id: cmd.id); + break; case _WorkerCmdType.resetStore: final storeName = cmd.args['storeName']! as String; @@ -166,7 +214,9 @@ Future _worker( ); }, ); - sendRes((id: cmd.id, data: null)); + + sendRes(id: cmd.id); + break; case _WorkerCmdType.renameStore: final currentStoreName = cmd.args['currentStoreName']! as String; @@ -178,7 +228,8 @@ Future _worker( ..name = newStoreName, ); - sendRes((id: cmd.id, data: null)); + sendRes(id: cmd.id); + break; case _WorkerCmdType.deleteStore: root @@ -190,76 +241,74 @@ Future _worker( ..remove() ..close(); - sendRes((id: cmd.id, data: null)); + sendRes(id: cmd.id); + break; case _WorkerCmdType.getStoreSize: final storeName = cmd.args['storeName']! as String; sendRes( - ( - id: cmd.id, - data: { - 'size': (getStore(storeName) ?? - (throw StoreNotExists(storeName: storeName))) - .numberOfBytes / - 1024, - } - ), + id: cmd.id, + data: { + 'size': (getStore(storeName) ?? + (throw StoreNotExists(storeName: storeName))) + .numberOfBytes / + 1024, + }, ); + break; case _WorkerCmdType.getStoreLength: final storeName = cmd.args['storeName']! as String; sendRes( - ( - id: cmd.id, - data: { - 'length': (getStore(storeName) ?? - (throw StoreNotExists(storeName: storeName))) - .numberOfTiles, - } - ), + id: cmd.id, + data: { + 'length': (getStore(storeName) ?? + (throw StoreNotExists(storeName: storeName))) + .numberOfTiles, + }, ); + break; case _WorkerCmdType.getStoreHits: final storeName = cmd.args['storeName']! as String; sendRes( - ( - id: cmd.id, - data: { - 'hits': (getStore(storeName) ?? - (throw StoreNotExists(storeName: storeName))) - .hits, - } - ), + id: cmd.id, + data: { + 'hits': (getStore(storeName) ?? + (throw StoreNotExists(storeName: storeName))) + .hits, + }, ); + break; case _WorkerCmdType.getStoreMisses: final storeName = cmd.args['storeName']! as String; sendRes( - ( - id: cmd.id, - data: { - 'misses': (getStore(storeName) ?? - (throw StoreNotExists(storeName: storeName))) - .misses, - } - ), + id: cmd.id, + data: { + 'misses': (getStore(storeName) ?? + (throw StoreNotExists(storeName: storeName))) + .misses, + }, ); + break; case _WorkerCmdType.readTile: final query = root .box() .query(ObjectBoxTile_.url.equals(cmd.args['url']! as String)) .build(); - final tile = query.findUnique(); - query.close(); // TODO: Hits & misses - sendRes((id: cmd.id, data: {'tile': tile})); + sendRes(id: cmd.id, data: {'tile': query.findUnique()}); + + query.close(); + break; case _WorkerCmdType.writeTile: final storeName = cmd.args['storeName']! as String; @@ -290,7 +339,7 @@ Future _worker( tiles.put( ObjectBoxTile( url: url, - lastModified: DateTime.now(), + lastModified: DateTime.timestamp(), bytes: bytes!, )..stores.add(store), ); @@ -314,7 +363,7 @@ Future _worker( case (false, false): // Existing tile, update required tiles.put( existingTile! - ..lastModified = DateTime.now() + ..lastModified = DateTime.timestamp() ..bytes = bytes!, ); stores.putMany( @@ -335,103 +384,116 @@ Future _worker( }, ); - sendRes((id: cmd.id, data: null)); + sendRes(id: cmd.id); + break; case _WorkerCmdType.deleteTile: final storeName = cmd.args['storeName']! as String; final url = cmd.args['url']! as String; - final tiles = root.box(); - - // Find the tile by URL - final query = tiles.query(ObjectBoxTile_.url.equals(url)).build(); - final tile = query.findUnique(); - if (tile == null) { - sendRes((id: cmd.id, data: {'wasOrphaned': null})); - break; - } - - // For the correct store, adjust the statistics - for (final store in tile.stores) { - if (store.name != storeName) continue; - root.box().put( - store - ..numberOfTiles -= 1 - ..numberOfBytes -= tile.bytes.lengthInBytes, - mode: PutMode.update, - ); - break; - } - - // Remove the store relation from the tile - tile.stores.removeWhere((store) => store.name == storeName); + final query = root + .box() + .query(ObjectBoxTile_.url.equals(url)) + .build(); - // Delete the tile if it belongs to no stores - if (tile.stores.isEmpty) { - query - ..remove() - ..close(); - sendRes((id: cmd.id, data: {'wasOrphaned': true})); - break; - } + sendRes( + id: cmd.id, + data: { + 'wasOrphan': root + .runInTransaction( + TxMode.write, + () => deleteTiles(storeName: storeName, tilesQuery: query), + ) + .singleOrNull, + }, + ); - // Otherwise just update the tile query.close(); - tiles.put(tile, mode: PutMode.update); - sendRes((id: cmd.id, data: {'wasOrphaned': false})); + break; - case _WorkerCmdType.removeOldestTile: + case _WorkerCmdType.removeOldestTilesAboveLimit: final storeName = cmd.args['storeName']! as String; - final numTilesToRemove = cmd.args['number']! as int; - - final tiles = root.box(); + final tilesLimit = cmd.args['tilesLimit']! as int; - final tilesQuery = (tiles.query().order(ObjectBoxTile_.lastModified) + final tilesQuery = (root + .box() + .query() + .order(ObjectBoxTile_.lastModified) ..linkMany( ObjectBoxTile_.stores, ObjectBoxStore_.name.equals(storeName), )) .build(); - final deleteTiles = await tilesQuery - .stream() - .where((tile) => tile.stores.length == 1) - .take(numTilesToRemove) - .toList(); - tilesQuery.close(); - - if (deleteTiles.isEmpty) { - sendRes((id: cmd.id, data: null)); - break; - } final storeQuery = root .box() .query(ObjectBoxStore_.name.equals(storeName)) .build(); - root.runInTransaction( - TxMode.write, - () { - final store = storeQuery.findUnique() ?? - (throw StoreNotExists(storeName: storeName)); - storeQuery.close(); + sendRes( + id: cmd.id, + data: { + 'numOrphans': root + .runInTransaction( + TxMode.write, + () { + final store = storeQuery.findUnique() ?? + (throw StoreNotExists(storeName: storeName)); + + final numToRemove = store.numberOfTiles - tilesLimit; + + return numToRemove <= 0 + ? const Iterable.empty() + : deleteTiles( + storeName: storeName, + tilesQuery: tilesQuery..limit = numToRemove, + ); + }, + ) + .where((e) => e) + .length, + }, + ); - root.box().put( - store - ..numberOfTiles -= numTilesToRemove - ..numberOfBytes -= - deleteTiles.map((e) => e.bytes.lengthInBytes).sum, - mode: PutMode.update, - ); - tiles.removeMany(deleteTiles.map((e) => e.id).toList()); + storeQuery.close(); + tilesQuery.close(); + + break; + case _WorkerCmdType.removeTilesOlderThan: + final storeName = cmd.args['storeName']! as String; + final expiry = cmd.args['expiry']! as DateTime; + + final tilesQuery = (root.box().query( + ObjectBoxTile_.lastModified + .greaterThan(expiry.millisecondsSinceEpoch), + )..linkMany( + ObjectBoxTile_.stores, + ObjectBoxStore_.name.equals(storeName), + )) + .build(); + + sendRes( + id: cmd.id, + data: { + 'numOrphans': root + .runInTransaction( + TxMode.write, + () => deleteTiles( + storeName: storeName, + tilesQuery: tilesQuery, + ), + ) + .where((e) => e) + .length, }, ); - sendRes((id: cmd.id, data: null)); + tilesQuery.close(); + break; } } catch (e) { - sendRes((id: cmd.id, data: {'error': e})); + sendRes(id: cmd.id, data: {'error': e}); } } } diff --git a/lib/src/backend/interfaces/backend.dart b/lib/src/backend/interfaces/backend.dart index 94ca7107..6b8039f2 100644 --- a/lib/src/backend/interfaces/backend.dart +++ b/lib/src/backend/interfaces/backend.dart @@ -34,8 +34,12 @@ abstract interface class FMTCBackend { /// An abstract interface that FMTC will use to communicate with a storage /// 'backend' (usually one root) /// +/// Methods with a doc template in the doc string are for 'direct' public +/// invocation. +/// /// See [FMTCBackend] for more information. abstract interface class FMTCBackendInternal { + /// Generic description/name of this backend abstract final String friendlyIdentifier; /// {@template fmtc.backend.initialise} @@ -112,7 +116,9 @@ abstract interface class FMTCBackendInternal { required String storeName, }); - /// Change the name of the store named [currentStoreName] to [newStoreName] + /// {@template fmtc.backend.renameStore} + /// Change the name of the specified store to the specified new store name + /// {@endtemplate} Future renameStore({ required String currentStoreName, required String newStoreName, @@ -156,7 +162,8 @@ abstract interface class FMTCBackendInternal { required String url, }); - /// Create or update a tile + /// Create or update a tile (given a [url] and its [bytes]) in the specified + /// store /// /// If the tile already existed, it will be added to the specified store. /// Otherwise, [bytes] must be specified, and the tile will be created and @@ -170,23 +177,45 @@ abstract interface class FMTCBackendInternal { required Uint8List? bytes, }); - /// Remove the tile from the store, deleting it if orphaned + /// Remove the tile from the specified store, deleting it if was orphaned + /// + /// As tiles can belong to multiple stores, a tile cannot be safely 'truly' + /// deleted unless it does not belong to any other stores (it was an orphan). + /// A tile that is not an orphan will just be 'removed' from the specified + /// store. /// /// Returns: /// * `null` : if there was no existing tile /// * `true` : if the tile itself could be deleted (it was orphaned) - /// * `false`: if the tile still belonged to at least store + /// * `false`: if the tile still belonged to at least one other store Future deleteTile({ required String storeName, required String url, }); - /// {@template fmtc.backend.removeOldestTile} - /// Remove the specified number of tiles from the specified store, in the order - /// of oldest first, and where each tile does not belong to any other store + // TODO: Verify below and add to belower doc string + // + // It is recommended to invoke this operation as few times as possible, for + // example by debouncing, as this operation may be expensive. + + /// Remove tiles in excess of the specified limit from the specified store, + /// oldest first + /// + /// Returns the number of tiles that were actually deleted (they were + /// orphaned). See [deleteTile] for more information about orphan tiles. + Future removeOldestTilesAboveLimit({ + required String storeName, + required int tilesLimit, + }); + + /// {@template fmtc.backend.removeTilesOlderThan} + /// Remove tiles that were last modified after expiry from the specified store + /// + /// Returns the number of tiles that were actually deleted (they were + /// orphaned). See [deleteTile] for more information about orphan tiles. /// {@endtemplate} - Future removeOldestTile({ + Future removeTilesOlderThan({ required String storeName, - required int numToRemove, + required DateTime expiry, }); } diff --git a/lib/src/providers/image_provider.dart b/lib/src/providers/image_provider.dart index 757f1192..15746d04 100644 --- a/lib/src/providers/image_provider.dart +++ b/lib/src/providers/image_provider.dart @@ -105,7 +105,7 @@ class FMTCImageProvider extends ImageProvider { final needsUpdating = !needsCreating && (provider.settings.behavior == CacheBehavior.onlineFirst || (provider.settings.cachedValidDuration != Duration.zero && - DateTime.now().millisecondsSinceEpoch - + DateTime.timestamp().millisecondsSinceEpoch - existingTile.lastModified.millisecondsSinceEpoch > provider.settings.cachedValidDuration.inMilliseconds)); diff --git a/lib/src/store/manage.dart b/lib/src/store/manage.dart index b16c03fd..2109d7af 100644 --- a/lib/src/store/manage.dart +++ b/lib/src/store/manage.dart @@ -25,7 +25,7 @@ final class StoreManagement extends _WithBackendAccess { /// {@macro fmtc.backend.resetStore} Future reset() => _backend.resetStore(storeName: _storeName); - /// Rename the store to [newStoreName] + /// {@macro fmtc.backend.renameStore} /// /// The old [StoreDirectory] will still retain it's link to the old store, so /// always use the new returned value instead: returns a new [StoreDirectory] @@ -39,11 +39,9 @@ final class StoreManagement extends _WithBackendAccess { return StoreDirectory._(newStoreName); } - /// Delete all tiles older that were last modified before [expiry] - /// - /// Ignores [FMTCTileProviderSettings.cachedValidDuration]. - Future pruneTilesOlderThan({required DateTime expiry}) => - _backend.pruneTilesOlderThan(expiry: expiry); + /// {@macro fmtc.backend.removeTilesOlderThan} + Future removeTilesOlderThan({required DateTime expiry}) => + _backend.removeTilesOlderThan(storeName: _storeName, expiry: expiry); /// Retrieves the most recently modified tile from the store, extracts it's /// bytes, and renders them to an [Image] @@ -177,25 +175,3 @@ final class StoreManagement extends _WithBackendAccess { ); } } - -Future _pruneTilesOlderThanWorker(List args) async { - final db = Isar.openSync( - [DbStoreDescriptorSchema, DbTileSchema, DbMetadataSchema], - name: DatabaseTools.hash(args[0]).toString(), - directory: args[1], - inspector: false, - ); - - db.writeTxnSync( - () => db.tiles.deleteAllSync( - db.tiles - .where() - .lastModifiedLessThan(args[2]) - .findAllSync() - .map((t) => t.id) - .toList(), - ), - ); - - await db.close(); -} From 8d9627e3ecb51019f920949baaecf62f35f38fad Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 4 Jan 2024 13:12:22 +0000 Subject: [PATCH 087/168] Added `tileExistsInStore` method to backend Added requirement for tiles & stores relations to abstract database models, with generic typing Connected `removeOldestTilesAboveLimit` method to image provider Removed 'queue' package Former-commit-id: c8a3bca700b0228b1e93a00fc9b5dd343bc3f496 [formerly 6fc0864270ec7f19a57a804ad54d56192a53930c] Former-commit-id: 07affe47fcb7901705fa208a8a3f7a39769d8ce5 --- lib/src/backend/impls/objectbox/backend.dart | 10 +++ .../impls/objectbox/models/models.dart | 6 +- lib/src/backend/impls/objectbox/worker.dart | 27 +++++++- lib/src/backend/interfaces/backend.dart | 6 ++ lib/src/backend/interfaces/models.dart | 6 +- lib/src/misc/obscure_query_params.dart | 3 + lib/src/providers/image_provider.dart | 63 +++---------------- lib/src/providers/tile_provider.dart | 40 ++++-------- pubspec.yaml | 1 - 9 files changed, 71 insertions(+), 91 deletions(-) diff --git a/lib/src/backend/impls/objectbox/backend.dart b/lib/src/backend/impls/objectbox/backend.dart index 2751c531..5fa86e08 100644 --- a/lib/src/backend/impls/objectbox/backend.dart +++ b/lib/src/backend/impls/objectbox/backend.dart @@ -254,6 +254,16 @@ class _ObjectBoxBackendImpl implements ObjectBoxBackendInternal { args: {'storeName': storeName}, ))!['misses']! as int; + @override + Future tileExistsInStore({ + required String storeName, + required String url, + }) async => + (await _sendCmd( + type: _WorkerCmdType.tileExistsInStore, + args: {'storeName': storeName, 'url': url}, + ))!['exists']; + @override Future readTile({ required String url, diff --git a/lib/src/backend/impls/objectbox/models/models.dart b/lib/src/backend/impls/objectbox/models/models.dart index 6614096b..573e9d54 100644 --- a/lib/src/backend/impls/objectbox/models/models.dart +++ b/lib/src/backend/impls/objectbox/models/models.dart @@ -5,7 +5,7 @@ import 'package:objectbox/objectbox.dart'; import '../../../interfaces/models.dart'; @Entity() -base class ObjectBoxStore extends BackendStore { +base class ObjectBoxStore extends BackendStore> { @Id() int id = 0; @@ -24,6 +24,7 @@ base class ObjectBoxStore extends BackendStore { @override int misses; + @override @Index() @Backlink() final tiles = ToMany(); @@ -38,7 +39,7 @@ base class ObjectBoxStore extends BackendStore { } @Entity() -base class ObjectBoxTile extends BackendTile { +base class ObjectBoxTile extends BackendTile> { @Id() int id = 0; @@ -55,6 +56,7 @@ base class ObjectBoxTile extends BackendTile { @override Uint8List bytes; + @override @Index() final stores = ToMany(); diff --git a/lib/src/backend/impls/objectbox/worker.dart b/lib/src/backend/impls/objectbox/worker.dart index dd05f020..7d1be4ac 100644 --- a/lib/src/backend/impls/objectbox/worker.dart +++ b/lib/src/backend/impls/objectbox/worker.dart @@ -12,6 +12,7 @@ enum _WorkerCmdType { getStoreLength, getStoreHits, getStoreMisses, + tileExistsInStore, readTile, writeTile, deleteTile, @@ -59,6 +60,9 @@ Future _worker( //! UTIL FUNCTIONS !// /// Convert store name to database store object + /// + /// Returns `null` if store not found. Throw the [StoreNotExists] error if it + /// was required. ObjectBoxStore? getStore(String storeName) { final query = root .box() @@ -296,15 +300,32 @@ Future _worker( }, ); + break; + case _WorkerCmdType.tileExistsInStore: + final storeName = cmd.args['storeName']! as String; + final url = cmd.args['url']! as String; + + final query = + (root.box().query(ObjectBoxTile_.url.equals(url)) + ..linkMany( + ObjectBoxTile_.stores, + ObjectBoxStore_.name.equals(storeName), + )) + .build(); + + sendRes(id: cmd.id, data: {'exists': query.count() == 1}); + + query.close(); + break; case _WorkerCmdType.readTile: + final url = cmd.args['url']! as String; + final query = root .box() - .query(ObjectBoxTile_.url.equals(cmd.args['url']! as String)) + .query(ObjectBoxTile_.url.equals(url)) .build(); - // TODO: Hits & misses - sendRes(id: cmd.id, data: {'tile': query.findUnique()}); query.close(); diff --git a/lib/src/backend/interfaces/backend.dart b/lib/src/backend/interfaces/backend.dart index 6b8039f2..0318002b 100644 --- a/lib/src/backend/interfaces/backend.dart +++ b/lib/src/backend/interfaces/backend.dart @@ -157,6 +157,12 @@ abstract interface class FMTCBackendInternal { required String storeName, }); + /// Check whether the specified tile exists in the specified store + Future tileExistsInStore({ + required String storeName, + required String url, + }); + /// Get a raw tile by URL Future readTile({ required String url, diff --git a/lib/src/backend/interfaces/models.dart b/lib/src/backend/interfaces/models.dart index 9697f221..bea5b33e 100644 --- a/lib/src/backend/interfaces/models.dart +++ b/lib/src/backend/interfaces/models.dart @@ -2,10 +2,11 @@ import 'dart:typed_data'; import 'package:meta/meta.dart'; -abstract base class BackendStore { +abstract base class BackendStore>> { abstract String name; abstract int hits; abstract int misses; + T get tiles; /// Uses [name] for equality comparisons only (unless the two objects are /// [identical]) @@ -22,10 +23,11 @@ abstract base class BackendStore { int get hashCode => name.hashCode; } -abstract base class BackendTile { +abstract base class BackendTile>> { abstract String url; abstract DateTime lastModified; abstract Uint8List bytes; + S get stores; /// Uses [url] for equality comparisons only (unless the two objects are /// [identical]) diff --git a/lib/src/misc/obscure_query_params.dart b/lib/src/misc/obscure_query_params.dart index cd35232e..ba8eb5f5 100644 --- a/lib/src/misc/obscure_query_params.dart +++ b/lib/src/misc/obscure_query_params.dart @@ -1,6 +1,9 @@ // Copyright © Luka S (JaffaKetchup) under GPL-v3 // A full license can be found at .\LICENSE +import 'package:meta/meta.dart'; + +@internal String obscureQueryParams({ required String url, required Iterable obscuredQueryParams, diff --git a/lib/src/providers/image_provider.dart b/lib/src/providers/image_provider.dart index 15746d04..513a22f3 100644 --- a/lib/src/providers/image_provider.dart +++ b/lib/src/providers/image_provider.dart @@ -10,15 +10,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:http/http.dart'; -import 'package:isar/isar.dart'; -import 'package:queue/queue.dart'; import '../../flutter_map_tile_caching.dart'; -import '../db/defs/metadata.dart'; -import '../db/defs/store_descriptor.dart'; -import '../db/defs/tile.dart'; -import '../db/registry.dart'; -import '../db/tools.dart'; import '../misc/obscure_query_params.dart'; /// A specialised [ImageProvider] dedicated to 'flutter_map_tile_caching' @@ -32,15 +25,6 @@ class FMTCImageProvider extends ImageProvider { /// The coordinates of the tile to be fetched final TileCoordinates coords; - FMTCBackend get _backend => FMTC.instance.settings.backend; - - /// Configured root directory - // final String directory; - - //static final _removeOldestQueue = Queue(timeout: const Duration(seconds: 1)); - //static final _cacheHitsQueue = Queue(); - //static final _cacheMissesQueue = Queue(); - /// Create a specialised [ImageProvider] dedicated to 'flutter_map_tile_caching' FMTCImageProvider({ required this.provider, @@ -48,6 +32,9 @@ class FMTCImageProvider extends ImageProvider { required this.coords, }); + // ignore: invalid_use_of_protected_member + FMTCBackendInternal get _backend => FMTC.instance.settings.backend.internal; + @override ImageStreamCompleter loadImage( FMTCImageProvider key, @@ -112,7 +99,7 @@ class FMTCImageProvider extends ImageProvider { // Prepare a list of image bytes and prefill if there's already a cached // tile available Uint8List? bytes; - if (!needsCreating) bytes = Uint8List.fromList(existingTile.bytes); + if (!needsCreating) bytes = existingTile.bytes; // If there is a cached tile that's in date available, use it if (!needsCreating && !needsUpdating) { @@ -132,10 +119,6 @@ class FMTCImageProvider extends ImageProvider { ); } - // From this point, a tile must exist (but it may be outdated). However, an - // outdated tile is better than no tile at all, so in the event of an error, - // always return the existing tile's bytes - // Setup a network request for the tile & handle network exceptions final request = Request('GET', Uri.parse(networkUrl)) ..headers.addAll(provider.headers); @@ -230,16 +213,11 @@ class FMTCImageProvider extends ImageProvider { // Clear out old tiles if the maximum store length has been exceeded if (needsCreating && provider.settings.maxStoreLength != 0) { + // TODO: Check if performance is acceptable without checking limit excess first unawaited( - _removeOldestQueue.add( - () => compute( - _removeOldestTile, - [ - provider.storeDirectory.storeName, - directory, - provider.settings.maxStoreLength, - ], - ), + _backend.removeOldestTilesAboveLimit( + storeName: provider.storeDirectory.storeName, + tilesLimit: provider.settings.maxStoreLength, ), ); } @@ -279,28 +257,3 @@ class FMTCImageProvider extends ImageProvider { @override int get hashCode => Object.hash(coords, provider, options); } - -Future _removeOldestTile(List args) async { - final db = Isar.openSync( - [DbStoreDescriptorSchema, DbTileSchema, DbMetadataSchema], - name: DatabaseTools.hash(args[0]).toString(), - directory: args[1], - inspector: false, - ); - - db.writeTxnSync( - () => db.tiles.deleteAllSync( - db.tiles - .where() - .anyLastModified() - .limit( - (db.tiles.countSync() - args[2]).clamp(0, double.maxFinite).toInt(), - ) - .findAllSync() - .map((t) => t.id) - .toList(), - ), - ); - - await db.close(); -} diff --git a/lib/src/providers/tile_provider.dart b/lib/src/providers/tile_provider.dart index a165bed5..e37c3db9 100644 --- a/lib/src/providers/tile_provider.dart +++ b/lib/src/providers/tile_provider.dart @@ -38,6 +38,9 @@ class FMTCTileProvider extends TileProvider { }, ); + // ignore: invalid_use_of_protected_member + FMTCBackendInternal get _backend => FMTC.instance.settings.backend.internal; + /// Closes the open [httpClient] - this will make the provider unable to /// perform network requests @override @@ -54,39 +57,20 @@ class FMTCTileProvider extends TileProvider { provider: this, options: options, coords: coords, - directory: FMTC.instance.rootDirectory.directory.absolute.path, ); - /// Check whether a specified tile is cached in the current store synchronously - bool checkTileCached({ - required TileCoordinates coords, - required TileLayer options, - }) => - FMTCRegistry.instance(storeDirectory.storeName).tiles.getSync( - DatabaseTools.hash( - obscureQueryParams( - url: getTileUrl(coords, options), - obscuredQueryParams: settings.obscuredQueryParams, - ), - ), - ) != - null; - /// Check whether a specified tile is cached in the current store - /// asynchronously - Future checkTileCachedAsync({ + Future checkTileCached({ required TileCoordinates coords, required TileLayer options, - }) async => - await FMTCRegistry.instance(storeDirectory.storeName).tiles.get( - DatabaseTools.hash( - obscureQueryParams( - url: getTileUrl(coords, options), - obscuredQueryParams: settings.obscuredQueryParams, - ), - ), - ) != - null; + }) => + _backend.tileExistsInStore( + storeName: storeDirectory.storeName, + url: obscureQueryParams( + url: getTileUrl(coords, options), + obscuredQueryParams: settings.obscuredQueryParams, + ), + ); @override bool operator ==(Object other) => diff --git a/pubspec.yaml b/pubspec.yaml index 7219eead..29ddb2a4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -42,7 +42,6 @@ dependencies: objectbox_flutter_libs: any path: ^1.9.0 path_provider: ^2.1.1 - queue: ^3.1.0+2 stream_transform: ^2.1.0 watcher: ^1.1.0 From 6c86b74b0251dcda21ce813429c488810590cafa Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 4 Jan 2024 13:31:05 +0000 Subject: [PATCH 088/168] Added `readLatestTile` method to backend Completed frontend store stats & management methods connection to backend Former-commit-id: 8d8e767ba6dde270a253262ff73d8fc788978db3 [formerly 6b45a39c006a797e27d218a6663801ac242b7d3b] Former-commit-id: 394f6bb0d8177e453f57bb3de2f63fbc5c0662d8 --- lib/src/backend/impls/objectbox/backend.dart | 9 ++ lib/src/backend/impls/objectbox/worker.dart | 19 +++++ lib/src/backend/interfaces/backend.dart | 8 ++ lib/src/store/manage.dart | 88 +------------------- 4 files changed, 40 insertions(+), 84 deletions(-) diff --git a/lib/src/backend/impls/objectbox/backend.dart b/lib/src/backend/impls/objectbox/backend.dart index 5fa86e08..970c9390 100644 --- a/lib/src/backend/impls/objectbox/backend.dart +++ b/lib/src/backend/impls/objectbox/backend.dart @@ -273,6 +273,15 @@ class _ObjectBoxBackendImpl implements ObjectBoxBackendInternal { args: {'url': url}, ))!['tile']; + @override + Future readLatestTile({ + required String storeName, + }) async => + (await _sendCmd( + type: _WorkerCmdType.readLatestTile, + args: {'storeName': storeName}, + ))!['tile']; + @override Future writeTile({ required String storeName, diff --git a/lib/src/backend/impls/objectbox/worker.dart b/lib/src/backend/impls/objectbox/worker.dart index 7d1be4ac..dd76296f 100644 --- a/lib/src/backend/impls/objectbox/worker.dart +++ b/lib/src/backend/impls/objectbox/worker.dart @@ -14,6 +14,7 @@ enum _WorkerCmdType { getStoreMisses, tileExistsInStore, readTile, + readLatestTile, writeTile, deleteTile, removeOldestTilesAboveLimit, @@ -330,6 +331,24 @@ Future _worker( query.close(); + break; + case _WorkerCmdType.readLatestTile: + final storeName = cmd.args['storeName']! as String; + + final query = (root + .box() + .query() + .order(ObjectBoxTile_.lastModified, flags: Order.descending) + ..linkMany( + ObjectBoxTile_.stores, + ObjectBoxStore_.name.equals(storeName), + )) + .build(); + + sendRes(id: cmd.id, data: {'tile': query.findFirst()}); + + query.close(); + break; case _WorkerCmdType.writeTile: final storeName = cmd.args['storeName']! as String; diff --git a/lib/src/backend/interfaces/backend.dart b/lib/src/backend/interfaces/backend.dart index 0318002b..0c1df580 100644 --- a/lib/src/backend/interfaces/backend.dart +++ b/lib/src/backend/interfaces/backend.dart @@ -168,6 +168,14 @@ abstract interface class FMTCBackendInternal { required String url, }); + /// {@template fmtc.backend.readLatestTile} + /// Retrieve the tile most recently modified in the specified store, if any + /// tiles exist + /// {@endtemplate} + Future readLatestTile({ + required String storeName, + }); + /// Create or update a tile (given a [url] and its [bytes]) in the specified /// store /// diff --git a/lib/src/store/manage.dart b/lib/src/store/manage.dart index 2109d7af..bb3fa3d4 100644 --- a/lib/src/store/manage.dart +++ b/lib/src/store/manage.dart @@ -8,8 +8,7 @@ part of flutter_map_tile_caching; /// /// If the store is not in the expected state (of existence) when invoking an /// operation, then an error will be thrown (likely [StoreNotExists] or -/// [StoreAlreadyExists]). It is recommended to check [ready] or [readySync] when -/// necessary. +/// [StoreAlreadyExists]). It is recommended to check [ready] when necessary. final class StoreManagement extends _WithBackendAccess { const StoreManagement._(super.store); @@ -43,82 +42,8 @@ final class StoreManagement extends _WithBackendAccess { Future removeTilesOlderThan({required DateTime expiry}) => _backend.removeTilesOlderThan(storeName: _storeName, expiry: expiry); - /// Retrieves the most recently modified tile from the store, extracts it's - /// bytes, and renders them to an [Image] - /// - /// Prefer [tileImageAsync] to avoid blocking the UI thread. Otherwise, this - /// has slightly better performance. - /// - /// Eventually returns `null` if there are no cached tiles in this store, - /// otherwise an [Image] with [size] height and width. - /// - /// This method requires the store to be [ready], else an [FMTCStoreNotReady] - /// error will be raised. - Image? tileImage({ - double? size, - Key? key, - double scale = 1.0, - ImageFrameBuilder? frameBuilder, - ImageErrorWidgetBuilder? errorBuilder, - String? semanticLabel, - bool excludeFromSemantics = false, - Color? color, - Animation? opacity, - BlendMode? colorBlendMode, - BoxFit? fit, - AlignmentGeometry alignment = Alignment.center, - ImageRepeat repeat = ImageRepeat.noRepeat, - Rect? centerSlice, - bool matchTextDirection = false, - bool gaplessPlayback = false, - bool isAntiAlias = false, - FilterQuality filterQuality = FilterQuality.low, - int? cacheWidth, - int? cacheHeight, - }) { - final latestTile = _registry(_name) - .tiles - .where(sort: Sort.desc) - .anyLastModified() - .limit(1) - .findFirstSync(); - if (latestTile == null) return null; - - return Image.memory( - Uint8List.fromList(latestTile.bytes), - key: key, - scale: scale, - frameBuilder: frameBuilder, - errorBuilder: errorBuilder, - semanticLabel: semanticLabel, - excludeFromSemantics: excludeFromSemantics, - width: size, - height: size, - color: color, - opacity: opacity, - colorBlendMode: colorBlendMode, - fit: fit, - alignment: alignment, - repeat: repeat, - centerSlice: centerSlice, - matchTextDirection: matchTextDirection, - gaplessPlayback: gaplessPlayback, - isAntiAlias: isAntiAlias, - filterQuality: filterQuality, - cacheWidth: cacheWidth, - cacheHeight: cacheHeight, - ); - } - - /// Retrieves the most recently modified tile from the store, extracts it's - /// bytes, and renders them to an [Image] - /// - /// Eventually returns `null` if there are no cached tiles in this store, - /// otherwise an [Image] with [size] height and width. - /// - /// This method requires the store to be [ready], else an [FMTCStoreNotReady] - /// error will be raised. - /// + /// {@macro fmtc.backend.readLatestTile} + /// , then render the bytes to an [Image] Future tileImageAsync({ double? size, Key? key, @@ -141,12 +66,7 @@ final class StoreManagement extends _WithBackendAccess { int? cacheWidth, int? cacheHeight, }) async { - final latestTile = await _registry(_name) - .tiles - .where(sort: Sort.desc) - .anyLastModified() - .limit(1) - .findFirst(); + final latestTile = await _backend.readLatestTile(storeName: _storeName); if (latestTile == null) return null; return Image.memory( From 07c988f22ad27a201b7e5f340e6388b0189e3282 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 4 Jan 2024 13:59:29 +0000 Subject: [PATCH 089/168] Renamed `StoreDirectory` to `FMTCStore` with deprecation typedef Removed `FlutterMapTileCaching[]` operator De-privatised `FMTCStore`'s constructor Added documentation Other minor improvements Former-commit-id: 073c369a1adaea41194ca6eafb75fd5624460599 [formerly 477ad773c9cbe71ae1e6a2f16069c5fd855b313e] Former-commit-id: 9c877d6b04299cd10f3227d2bc76977f69f6e09f --- .../components/start_download_button.dart | 2 +- .../components/store_selector.dart | 6 +- .../components/download_layout.dart | 2 +- .../components/main_statistics.dart | 2 +- .../main/pages/downloading/downloading.dart | 6 +- .../state/region_selection_provider.dart | 6 +- .../lib/screens/main/pages/stores/stores.dart | 4 +- .../store_editor/components/header.dart | 9 ++- lib/src/backend/interfaces/backend.dart | 5 +- lib/src/errors/browsing.dart | 2 +- lib/src/errors/initialisation.dart | 66 ------------------- lib/src/fmtc.dart | 44 ++++--------- lib/src/misc/with_backend_access.dart | 2 +- lib/src/providers/tile_provider.dart | 4 +- lib/src/root/directory.dart | 2 +- lib/src/root/statistics.dart | 16 ++--- lib/src/settings/fmtc_settings.dart | 7 ++ lib/src/store/directory.dart | 38 +++++++++-- lib/src/store/download.dart | 4 +- lib/src/store/export.dart | 2 +- lib/src/store/manage.dart | 10 +-- lib/src/store/metadata.dart | 2 +- lib/src/store/statistics.dart | 2 +- 23 files changed, 99 insertions(+), 144 deletions(-) delete mode 100644 lib/src/errors/initialisation.dart diff --git a/example/lib/screens/configure_download/components/start_download_button.dart b/example/lib/screens/configure_download/components/start_download_button.dart index 782aa00c..b018176b 100644 --- a/example/lib/screens/configure_download/components/start_download_button.dart +++ b/example/lib/screens/configure_download/components/start_download_button.dart @@ -24,7 +24,7 @@ class StartDownloadButton extends StatelessWidget { Selector( selector: (context, provider) => provider.isReady, builder: (context, isReady, _) => - Selector( + Selector( selector: (context, provider) => provider.selectedStore, builder: (context, selectedStore, child) => IgnorePointer( ignoring: selectedStore == null, diff --git a/example/lib/screens/configure_download/components/store_selector.dart b/example/lib/screens/configure_download/components/store_selector.dart index 50c78560..4d56ad1c 100644 --- a/example/lib/screens/configure_download/components/store_selector.dart +++ b/example/lib/screens/configure_download/components/store_selector.dart @@ -21,12 +21,12 @@ class _StoreSelectorState extends State { IntrinsicWidth( child: Consumer2( builder: (context, downloadProvider, generalProvider, _) => - FutureBuilder>( + FutureBuilder>( future: FMTC.instance.rootDirectory.stats.storesAvailableAsync, - builder: (context, snapshot) => DropdownButton( + builder: (context, snapshot) => DropdownButton( items: snapshot.data ?.map( - (e) => DropdownMenuItem( + (e) => DropdownMenuItem( value: e, child: Text(e.storeName), ), diff --git a/example/lib/screens/main/pages/downloading/components/download_layout.dart b/example/lib/screens/main/pages/downloading/components/download_layout.dart index 54de03f4..356e5f91 100644 --- a/example/lib/screens/main/pages/downloading/components/download_layout.dart +++ b/example/lib/screens/main/pages/downloading/components/download_layout.dart @@ -17,7 +17,7 @@ class DownloadLayout extends StatelessWidget { required this.moveToMapPage, }); - final StoreDirectory storeDirectory; + final FMTCStore storeDirectory; final DownloadProgress download; final void Function() moveToMapPage; diff --git a/example/lib/screens/main/pages/downloading/components/main_statistics.dart b/example/lib/screens/main/pages/downloading/components/main_statistics.dart index 54b2dfa5..1e8c8bf6 100644 --- a/example/lib/screens/main/pages/downloading/components/main_statistics.dart +++ b/example/lib/screens/main/pages/downloading/components/main_statistics.dart @@ -19,7 +19,7 @@ class MainStatistics extends StatefulWidget { }); final DownloadProgress download; - final StoreDirectory storeDirectory; + final FMTCStore storeDirectory; final void Function() moveToMapPage; @override diff --git a/example/lib/screens/main/pages/downloading/downloading.dart b/example/lib/screens/main/pages/downloading/downloading.dart index 176daad0..cf73acc6 100644 --- a/example/lib/screens/main/pages/downloading/downloading.dart +++ b/example/lib/screens/main/pages/downloading/downloading.dart @@ -59,7 +59,7 @@ class _DownloadingPageState extends State child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Selector( + Selector( selector: (context, provider) => provider.selectedStore, builder: (context, selectedStore, _) => Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -109,8 +109,8 @@ class _DownloadingPageState extends State } return DownloadLayout( - storeDirectory: context - .select( + storeDirectory: + context.select( (provider) => provider.selectedStore, )!, download: snapshot.data!, diff --git a/example/lib/screens/main/pages/region_selection/state/region_selection_provider.dart b/example/lib/screens/main/pages/region_selection/state/region_selection_provider.dart index ff28fbbd..f78deeb6 100644 --- a/example/lib/screens/main/pages/region_selection/state/region_selection_provider.dart +++ b/example/lib/screens/main/pages/region_selection/state/region_selection_provider.dart @@ -107,9 +107,9 @@ class RegionSelectionProvider extends ChangeNotifier { notifyListeners(); } - StoreDirectory? _selectedStore; - StoreDirectory? get selectedStore => _selectedStore; - void setSelectedStore(StoreDirectory? newStore, {bool notify = true}) { + FMTCStore? _selectedStore; + FMTCStore? get selectedStore => _selectedStore; + void setSelectedStore(FMTCStore? newStore, {bool notify = true}) { _selectedStore = newStore; if (notify) notifyListeners(); } diff --git a/example/lib/screens/main/pages/stores/stores.dart b/example/lib/screens/main/pages/stores/stores.dart index 2c7b1fbf..940b86fc 100644 --- a/example/lib/screens/main/pages/stores/stores.dart +++ b/example/lib/screens/main/pages/stores/stores.dart @@ -17,7 +17,7 @@ class StoresPage extends StatefulWidget { } class _StoresPageState extends State { - late Future> _stores; + late Future> _stores; @override void initState() { @@ -46,7 +46,7 @@ class _StoresPageState extends State { const Header(), const SizedBox(height: 12), Expanded( - child: FutureBuilder>( + child: FutureBuilder>( future: _stores, builder: (context, snapshot) => snapshot.hasError ? throw snapshot.error! as FMTCDamagedStoreException diff --git a/example/lib/screens/store_editor/components/header.dart b/example/lib/screens/store_editor/components/header.dart index 0326f3af..a7ef125b 100644 --- a/example/lib/screens/store_editor/components/header.dart +++ b/example/lib/screens/store_editor/components/header.dart @@ -41,11 +41,10 @@ AppBar buildHeader({ if (formKey.currentState!.validate()) { formKey.currentState!.save(); - final StoreDirectory? existingStore = - widget.existingStoreName == null - ? null - : FMTC.instance(widget.existingStoreName!); - final StoreDirectory newStore = existingStore == null + final FMTCStore? existingStore = widget.existingStoreName == null + ? null + : FMTC.instance(widget.existingStoreName!); + final FMTCStore newStore = existingStore == null ? FMTC.instance(newValues['storeName']!) : await existingStore.manage.rename(newValues['storeName']!); if (!mounted) return; diff --git a/lib/src/backend/interfaces/backend.dart b/lib/src/backend/interfaces/backend.dart index 0c1df580..bae78c50 100644 --- a/lib/src/backend/interfaces/backend.dart +++ b/lib/src/backend/interfaces/backend.dart @@ -9,8 +9,8 @@ import '../../../flutter_map_tile_caching.dart'; /// An abstract interface that FMTC will use to communicate with a storage /// 'backend' (usually one root) /// -/// See also [FMTCBackendInternal], which has the actual methods. This is -/// provided as a means to warn users to avoid using the backend directly. +/// See also [FMTCBackendInternal], which has the actual method signatures. This +/// is provided as a means to warn users to avoid using the backend directly. /// /// To implementers: /// * Provide a seperate [FMTCBackend] & [FMTCBackendInternal] implementation @@ -20,6 +20,7 @@ import '../../../flutter_map_tile_caching.dart'; /// `FMTCBackendImpl`, without a constructor, as the `FMTCBackendImpl` will /// be accessed via [FMTCBackend.internal] /// * Prefer throwing included implementation-generic errors/exceptions +/// * See the default [ObjectBoxBackend] implementation for an example /// /// To end-users: /// * Use [FMTCSettings.backend] to set a custom backend diff --git a/lib/src/errors/browsing.dart b/lib/src/errors/browsing.dart index 832e9691..cb3fffb9 100644 --- a/lib/src/errors/browsing.dart +++ b/lib/src/errors/browsing.dart @@ -113,7 +113,7 @@ enum FMTCBrowsingErrorType { /// server /// /// Try specifying a normal HTTP/1.1 [IOClient] when using - /// [StoreDirectory.getTileProvider]. Check that the [TileLayer.urlTemplate] is + /// [FMTCStore.getTileProvider]. Check that the [TileLayer.urlTemplate] is /// correct, that any necessary authorization data is correctly included, and /// that the server serves the viewed region. unknownFetchException( diff --git a/lib/src/errors/initialisation.dart b/lib/src/errors/initialisation.dart deleted file mode 100644 index 572ed2d5..00000000 --- a/lib/src/errors/initialisation.dart +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright © Luka S (JaffaKetchup) under GPL-v3 -// A full license can be found at .\LICENSE - -import 'package:meta/meta.dart'; - -/// An [Exception] raised when FMTC failed to initialise a store -/// -/// Can be thrown for multiple reasons. See [type] and -/// [FMTCInitialisationExceptionType] for more information. -/// -/// A failed store initialisation will always result in the store being deleted -/// ASAP, regardless of its contents. -class FMTCInitialisationException implements Exception { - /// Friendly message - final String message; - - /// Programmatic error descriptor - final FMTCInitialisationExceptionType type; - - /// Name of the store that could not be initialised, if known - final String? storeName; - - /// Original error object (error is not directly thrown by FMTC), if applicable - final Object? originalError; - - /// An [Exception] raised when FMTC failed to initialise a store - /// - /// Can be thrown for multiple reasons. See [type] and - /// [FMTCInitialisationExceptionType] for more information. - /// - /// A failed store initialisation will always result in the store being deleted - /// ASAP, regardless of its contents. - @internal - FMTCInitialisationException( - this.message, - this.type, { - this.storeName, - this.originalError, - }); - - @override - String toString() => 'FMTCInitialisationException: $message'; -} - -/// Pragmatic error descriptor for a [FMTCInitialisationException.message] -/// -/// See documentation on that object for more information. -enum FMTCInitialisationExceptionType { - /// Paired with friendly message: - /// "Failed to initialise a store because it was not listed as safe/stable on last - /// initialisation." - /// - /// This signifies that the application has previously fatally crashed during - /// initialisation, but that the initialisation safety system has now removed - /// the store database. - corruptedDatabase, - - /// Paired with friendly message: - /// "Failed to initialise a store because Isar failed to open the database." - /// - /// This usually means the store database was valid, but Isar was not - /// configured correctly or encountered another issue. Unlike - /// [corruptedDatabase], this does not cause an app crash. Consult the - /// [FMTCInitialisationException.originalError] for more information. - isarFailure, -} diff --git a/lib/src/fmtc.dart b/lib/src/fmtc.dart index a283c0f1..765d3739 100644 --- a/lib/src/fmtc.dart +++ b/lib/src/fmtc.dart @@ -133,39 +133,23 @@ class FlutterMapTileCaching { ); } - /// The singleton instance of [FlutterMapTileCaching] at call time - /// - /// Must not be read or written directly, except in - /// [FlutterMapTileCaching.instance] and [FlutterMapTileCaching.initialise] - /// respectively. - static FlutterMapTileCaching? _instance; - /// Get the configured instance of [FlutterMapTileCaching], after /// [FlutterMapTileCaching.initialise] has been called, for further actions - static FlutterMapTileCaching get instance { - if (_instance == null) { - throw StateError( - 'Use `FlutterMapTileCaching.initialise()` before getting `FlutterMapTileCaching.instance`.', - ); - } - return _instance!; - } + static FlutterMapTileCaching get instance => + _instance ?? + (throw StateError( + ''' +Use `FlutterMapTileCaching.initialise()` before getting +`FlutterMapTileCaching.instance` (or a method which requires an instance). + ''', + )); + static FlutterMapTileCaching? _instance; - /// Get a [StoreDirectory] by name, without creating it automatically + /// Construct a [FMTCStore] by name /// - /// Use `.manage.create()` to create it asynchronously. Alternatively, use - /// `[]` to get a store by name and automatically create it synchronously. - StoreDirectory call(String storeName) => StoreDirectory._( - storeName, - autoCreate: false, - ); - - /// Get a [StoreDirectory] by name, and create it synchronously automatically + /// {@macro fmtc.fmtcstore.sub.noautocreate} /// - /// Prefer [call]/`()` wherever possible, as this method blocks the thread. - /// Note that that method does not automatically create the store. - StoreDirectory operator [](String storeName) => StoreDirectory._( - storeName, - autoCreate: true, - ); + /// Equivalent to constructing the [FMTCStore] directly. This method is + /// provided for backwards-compatibility. + FMTCStore call(String storeName) => FMTCStore(storeName); } diff --git a/lib/src/misc/with_backend_access.dart b/lib/src/misc/with_backend_access.dart index e95d512e..adc1f5af 100644 --- a/lib/src/misc/with_backend_access.dart +++ b/lib/src/misc/with_backend_access.dart @@ -6,7 +6,7 @@ part of flutter_map_tile_caching; abstract base class _WithBackendAccess { const _WithBackendAccess(this._store); - final StoreDirectory _store; + final FMTCStore _store; // ignore: invalid_use_of_protected_member FMTCBackendInternal get _backend => FMTC.instance.settings.backend.internal; String get _storeName => _store.storeName; diff --git a/lib/src/providers/tile_provider.dart b/lib/src/providers/tile_provider.dart index e37c3db9..3d2486fb 100644 --- a/lib/src/providers/tile_provider.dart +++ b/lib/src/providers/tile_provider.dart @@ -5,10 +5,10 @@ part of flutter_map_tile_caching; /// FMTC's custom [TileProvider] for use in a [TileLayer] /// -/// Create from the store directory chain, eg. [StoreDirectory.getTileProvider]. +/// Create from the store directory chain, eg. [FMTCStore.getTileProvider]. class FMTCTileProvider extends TileProvider { /// The store directory attached to this provider - final StoreDirectory storeDirectory; + final FMTCStore storeDirectory; /// The tile provider settings to use /// diff --git a/lib/src/root/directory.dart b/lib/src/root/directory.dart index f610c63b..dde2afe3 100644 --- a/lib/src/root/directory.dart +++ b/lib/src/root/directory.dart @@ -5,7 +5,7 @@ part of flutter_map_tile_caching; /// Represents the root directory and root databases /// -/// Note that this does not provide direct access to any [StoreDirectory]s. +/// Note that this does not provide direct access to any [FMTCStore]s. /// /// The name originates from previous versions of this library, where it /// represented a real directory instead of a database. diff --git a/lib/src/root/statistics.dart b/lib/src/root/statistics.dart index ee5e4dc4..100f1cca 100644 --- a/lib/src/root/statistics.dart +++ b/lib/src/root/statistics.dart @@ -9,19 +9,19 @@ class RootStats { FMTCRegistry get _registry => FMTCRegistry.instance; - /// List all the available [StoreDirectory]s synchronously + /// List all the available [FMTCStore]s synchronously /// /// Prefer [storesAvailableAsync] to avoid blocking the UI thread. Otherwise, /// this has slightly better performance. - List get storesAvailable => _registry.storeDatabases.values - .map((e) => StoreDirectory._(e.descriptorSync.name, autoCreate: false)) + List get storesAvailable => _registry.storeDatabases.values + .map((e) => FMTCStore._(e.descriptorSync.name, autoCreate: false)) .toList(); - /// List all the available [StoreDirectory]s asynchronously - Future> get storesAvailableAsync => Future.wait( + /// List all the available [FMTCStore]s asynchronously + Future> get storesAvailableAsync => Future.wait( _registry.storeDatabases.values.map( (e) async => - StoreDirectory._((await e.descriptor).name, autoCreate: false), + FMTCStore._((await e.descriptor).name, autoCreate: false), ), ); @@ -69,7 +69,7 @@ class RootStats { /// changed. /// /// Recursively watch specific stores (using [StoreStats.watchChanges]) by - /// providing them as a list of [StoreDirectory]s to [recursive]. To watch all + /// providing them as a list of [FMTCStore]s to [recursive]. To watch all /// stores, use the [storesAvailable]/[storesAvailableAsync] getter as the /// argument. By default, no sub-stores are watched (empty list), meaning only /// events that affect the actual store database (eg. store creations) will be @@ -87,7 +87,7 @@ class RootStats { Stream watchChanges({ Duration? debounce = const Duration(milliseconds: 200), bool fireImmediately = false, - List recursive = const [], + List recursive = const [], bool watchRecovery = false, List storeParts = const [ StoreParts.metadata, diff --git a/lib/src/settings/fmtc_settings.dart b/lib/src/settings/fmtc_settings.dart index 3d10dc59..c051b310 100644 --- a/lib/src/settings/fmtc_settings.dart +++ b/lib/src/settings/fmtc_settings.dart @@ -5,6 +5,13 @@ part of flutter_map_tile_caching; /// Global FMTC settings class FMTCSettings { + /// The database or other storage mechanism that FMTC will use as a cache + /// 'backend' + /// + /// Defaults to [ObjectBoxBackend], which uses the ObjectBox library and + /// database. + /// + /// See [FMTCBackend] for more information. final FMTCBackend backend; /// Default settings used when creating an [FMTCTileProvider] diff --git a/lib/src/store/directory.dart b/lib/src/store/directory.dart index ced18d77..e1f40b11 100644 --- a/lib/src/store/directory.dart +++ b/lib/src/store/directory.dart @@ -3,10 +3,40 @@ part of flutter_map_tile_caching; +/// Equivalent to [FMTCStore], provided to ease migration only +/// +/// The name refers to earlier versions of this library where the filesystem +/// was used for storage, instead of a database. +/// +/// This deprecation typedef will be removed in a future release: migrate to +/// [FMTCStore]. +@Deprecated( + ''' +Migrate to `FMTCStore`. This deprecation typedef is provided to ease migration +only. It will be removed in a future version. +''', +) +typedef StoreDirectory = FMTCStore; + /// Container for a [storeName] which includes methods and getters to access -/// functionality based on the specified store -class StoreDirectory { - const StoreDirectory._(this.storeName); +/// functionality based on the specified store using resources from the ambient +/// initialised [FlutterMapTileCaching] +/// +/// {@template fmtc.fmtcstore.sub.noautocreate} +/// Note that constructing an instance of this class will not automatically +/// create it, as this is an asynchronous operation. To create this store, use +/// [manage] > [StoreManagement.create]. +/// {@endtemplate} +/// +/// May be constructed via [FlutterMapTileCaching.call], or directly. +class FMTCStore { + /// Container for a [storeName] which includes methods and getters to access + /// functionality based on the specified store + /// + /// {@macro fmtc.fmtcstore.sub.noautocreate} + /// + /// May be constructed via [FlutterMapTileCaching.call], or directly. + const FMTCStore(this.storeName); /// The user-friendly name of the store directory final String storeName; @@ -57,7 +87,7 @@ class StoreDirectory { @override bool operator ==(Object other) => identical(this, other) || - (other is StoreDirectory && other.storeName == storeName); + (other is FMTCStore && other.storeName == storeName); @override int get hashCode => storeName.hashCode; diff --git a/lib/src/store/download.dart b/lib/src/store/download.dart index 78aa9e5b..5a801151 100644 --- a/lib/src/store/download.dart +++ b/lib/src/store/download.dart @@ -3,7 +3,7 @@ part of flutter_map_tile_caching; -/// Provides tools to manage bulk downloading to a specific [StoreDirectory] +/// Provides tools to manage bulk downloading to a specific [FMTCStore] /// /// The 'fmtc_plus_background_downloading' module must be installed to add the /// background downloading functionality. @@ -28,7 +28,7 @@ part of flutter_map_tile_caching; /// [DownloadInstance]. class DownloadManagement { const DownloadManagement._(this._storeDirectory); - final StoreDirectory _storeDirectory; + final FMTCStore _storeDirectory; /// Download a specified [DownloadableRegion] in the foreground, with a /// recovery session by default diff --git a/lib/src/store/export.dart b/lib/src/store/export.dart index cea2337b..888e63e9 100644 --- a/lib/src/store/export.dart +++ b/lib/src/store/export.dart @@ -15,5 +15,5 @@ class StoreExport { /// Do not use in normal applications. @internal @protected - final StoreDirectory storeDirectory; + final FMTCStore storeDirectory; } diff --git a/lib/src/store/manage.dart b/lib/src/store/manage.dart index bb3fa3d4..21a22f14 100644 --- a/lib/src/store/manage.dart +++ b/lib/src/store/manage.dart @@ -3,7 +3,7 @@ part of flutter_map_tile_caching; -/// Manages a [StoreDirectory]'s representation on the filesystem, such as +/// Manages a [FMTCStore]'s representation on the filesystem, such as /// creation and deletion /// /// If the store is not in the expected state (of existence) when invoking an @@ -26,16 +26,16 @@ final class StoreManagement extends _WithBackendAccess { /// {@macro fmtc.backend.renameStore} /// - /// The old [StoreDirectory] will still retain it's link to the old store, so - /// always use the new returned value instead: returns a new [StoreDirectory] + /// The old [FMTCStore] will still retain it's link to the old store, so + /// always use the new returned value instead: returns a new [FMTCStore] /// after a successful renaming operation. - Future rename(String newStoreName) async { + Future rename(String newStoreName) async { await _backend.renameStore( currentStoreName: _storeName, newStoreName: newStoreName, ); - return StoreDirectory._(newStoreName); + return FMTCStore(newStoreName); } /// {@macro fmtc.backend.removeTilesOlderThan} diff --git a/lib/src/store/metadata.dart b/lib/src/store/metadata.dart index d7953cc4..1a5242c8 100644 --- a/lib/src/store/metadata.dart +++ b/lib/src/store/metadata.dart @@ -3,7 +3,7 @@ part of flutter_map_tile_caching; -/// Manage custom miscellaneous information tied to a [StoreDirectory] +/// Manage custom miscellaneous information tied to a [FMTCStore] /// /// Uses a key-value format where both key and value must be [String]. More /// advanced requirements should use another package, as this is a basic diff --git a/lib/src/store/statistics.dart b/lib/src/store/statistics.dart index a83f70f0..dea986e4 100644 --- a/lib/src/store/statistics.dart +++ b/lib/src/store/statistics.dart @@ -3,7 +3,7 @@ part of flutter_map_tile_caching; -/// Provides statistics about a [StoreDirectory] +/// Provides statistics about a [FMTCStore] final class StoreStats extends _WithBackendAccess { const StoreStats._(super._store); From 4a00ccf52f9a0a356a011a9b43ec86fca5b0d3c9 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 4 Jan 2024 14:01:44 +0000 Subject: [PATCH 090/168] Added mini copyright header to new files Removed bad import/export from main file Former-commit-id: bfc7fffb3164025a5383ebfffd8714f7fc264c42 [formerly 452e91f7751642bc3e6c2b9e0c40c018ef6f2231] Former-commit-id: eaf91e2f02627f4c5d4fd7d88115a6d515cff2bc --- lib/flutter_map_tile_caching.dart | 2 -- lib/src/backend/exports.dart | 3 +++ lib/src/backend/impls/objectbox/backend.dart | 3 +++ lib/src/backend/impls/objectbox/models/models.dart | 3 +++ lib/src/backend/impls/objectbox/worker.dart | 3 +++ lib/src/backend/interfaces/backend.dart | 3 +++ lib/src/backend/interfaces/models.dart | 3 +++ lib/src/backend/utils/errors.dart | 3 +++ 8 files changed, 21 insertions(+), 2 deletions(-) diff --git a/lib/flutter_map_tile_caching.dart b/lib/flutter_map_tile_caching.dart index f676e3fe..41253a59 100644 --- a/lib/flutter_map_tile_caching.dart +++ b/lib/flutter_map_tile_caching.dart @@ -38,7 +38,6 @@ import 'src/bulk_download/instance.dart'; import 'src/bulk_download/rate_limited_stream.dart'; import 'src/bulk_download/tile_loops/shared.dart'; import 'src/errors/browsing.dart'; -import 'src/errors/initialisation.dart'; import 'src/misc/exts.dart'; import 'src/misc/int_extremes.dart'; import 'src/misc/obscure_query_params.dart'; @@ -48,7 +47,6 @@ import 'src/providers/image_provider.dart'; export 'src/backend/exports.dart'; export 'src/errors/browsing.dart'; export 'src/errors/damaged_store.dart'; -export 'src/errors/initialisation.dart'; export 'src/misc/typedefs.dart'; part 'src/bulk_download/download_progress.dart'; diff --git a/lib/src/backend/exports.dart b/lib/src/backend/exports.dart index 26cfaf49..c92184e7 100644 --- a/lib/src/backend/exports.dart +++ b/lib/src/backend/exports.dart @@ -1,3 +1,6 @@ +// Copyright © Luka S (JaffaKetchup) under GPL-v3 +// A full license can be found at .\LICENSE + export 'impls/objectbox/backend.dart'; export 'interfaces/backend.dart'; export 'interfaces/models.dart'; diff --git a/lib/src/backend/impls/objectbox/backend.dart b/lib/src/backend/impls/objectbox/backend.dart index 970c9390..e745a6c2 100644 --- a/lib/src/backend/impls/objectbox/backend.dart +++ b/lib/src/backend/impls/objectbox/backend.dart @@ -1,3 +1,6 @@ +// Copyright © Luka S (JaffaKetchup) under GPL-v3 +// A full license can be found at .\LICENSE + import 'dart:async'; import 'dart:io'; import 'dart:isolate'; diff --git a/lib/src/backend/impls/objectbox/models/models.dart b/lib/src/backend/impls/objectbox/models/models.dart index 573e9d54..d7350f1b 100644 --- a/lib/src/backend/impls/objectbox/models/models.dart +++ b/lib/src/backend/impls/objectbox/models/models.dart @@ -1,3 +1,6 @@ +// Copyright © Luka S (JaffaKetchup) under GPL-v3 +// A full license can be found at .\LICENSE + import 'dart:typed_data'; import 'package:objectbox/objectbox.dart'; diff --git a/lib/src/backend/impls/objectbox/worker.dart b/lib/src/backend/impls/objectbox/worker.dart index dd76296f..b1143869 100644 --- a/lib/src/backend/impls/objectbox/worker.dart +++ b/lib/src/backend/impls/objectbox/worker.dart @@ -1,3 +1,6 @@ +// Copyright © Luka S (JaffaKetchup) under GPL-v3 +// A full license can be found at .\LICENSE + part of 'backend.dart'; enum _WorkerCmdType { diff --git a/lib/src/backend/interfaces/backend.dart b/lib/src/backend/interfaces/backend.dart index bae78c50..9d7fb130 100644 --- a/lib/src/backend/interfaces/backend.dart +++ b/lib/src/backend/interfaces/backend.dart @@ -1,3 +1,6 @@ +// Copyright © Luka S (JaffaKetchup) under GPL-v3 +// A full license can be found at .\LICENSE + import 'dart:async'; import 'dart:typed_data'; diff --git a/lib/src/backend/interfaces/models.dart b/lib/src/backend/interfaces/models.dart index bea5b33e..5ef55a5c 100644 --- a/lib/src/backend/interfaces/models.dart +++ b/lib/src/backend/interfaces/models.dart @@ -1,3 +1,6 @@ +// Copyright © Luka S (JaffaKetchup) under GPL-v3 +// A full license can be found at .\LICENSE + import 'dart:typed_data'; import 'package:meta/meta.dart'; diff --git a/lib/src/backend/utils/errors.dart b/lib/src/backend/utils/errors.dart index 620620b9..82c05b45 100644 --- a/lib/src/backend/utils/errors.dart +++ b/lib/src/backend/utils/errors.dart @@ -1,3 +1,6 @@ +// Copyright © Luka S (JaffaKetchup) under GPL-v3 +// A full license can be found at .\LICENSE + /// An error to be thrown by backend implementations in known events only /// /// A backend can create custom errors of this type, which is useful to show From 07583bca8895ec3d1bbe361c2523f866e4abccdf Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 4 Jan 2024 18:38:44 +0000 Subject: [PATCH 091/168] Added metadata support Added deprecations for store stats & metadata Replaced 4 seperate statistic get methods in backend with a single unified one (and expose more efficient method to frontend) Removed `FMTCSettings` Integrated `FMTC.initialise` Adjusted scope/visibility of ObjectBox backend sub-library Loosened requirements for implementers of backend structures Improved documentation Other minor improvements throughout Started syntax migration of example application Former-commit-id: dd18c516fe9da10b4c8cac117c15c880703e250c [formerly 87f7bc23835bc5274c8321e7e73b69c9ae890d96] Former-commit-id: 4518b09c0774180b394bb3dbc0374d6c0f1a6892 --- example/lib/main.dart | 6 +- .../components/start_download_button.dart | 2 +- .../main/pages/map/components/map_view.dart | 2 +- .../region_selection/region_selection.dart | 4 +- .../pages/stores/components/store_tile.dart | 27 +- .../store_editor/components/header.dart | 10 +- .../screens/store_editor/store_editor.dart | 10 +- lib/flutter_map_tile_caching.dart | 10 +- .../{exports.dart => export_external.dart} | 1 - lib/src/backend/export_internal.dart | 6 + lib/src/backend/impls/objectbox/backend.dart | 113 ++++--- .../models/generated/objectbox-model.json | 66 ++-- .../impls/objectbox/models/models.dart | 28 +- lib/src/backend/impls/objectbox/worker.dart | 290 ++++++++++++------ lib/src/backend/interfaces/backend.dart | 119 ++++--- lib/src/backend/interfaces/models.dart | 41 ++- lib/src/fmtc.dart | 145 ++++----- lib/src/misc/deprecations.dart | 82 +++++ lib/src/misc/int_extremes.dart | 4 + lib/src/misc/typedefs.dart | 10 - lib/src/misc/with_backend_access.dart | 2 +- lib/src/providers/image_provider.dart | 12 +- lib/src/providers/tile_provider.dart | 33 +- .../tile_provider_settings.dart | 0 lib/src/settings/fmtc_settings.dart | 61 ---- lib/src/store/directory.dart | 6 +- lib/src/store/download.dart | 5 +- lib/src/store/manage.dart | 6 +- lib/src/store/metadata.dart | 91 ++---- lib/src/store/statistics.dart | 43 ++- 30 files changed, 696 insertions(+), 539 deletions(-) rename lib/src/backend/{exports.dart => export_external.dart} (85%) create mode 100644 lib/src/backend/export_internal.dart create mode 100644 lib/src/misc/deprecations.dart delete mode 100644 lib/src/misc/typedefs.dart rename lib/src/{settings => providers}/tile_provider_settings.dart (100%) delete mode 100644 lib/src/settings/fmtc_settings.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index 1d518719..f4dd2271 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -24,11 +24,7 @@ void main() async { ); String? damagedDatabaseDeleted; - await FlutterMapTileCaching.initialise( - errorHandler: (error) => damagedDatabaseDeleted = error.message, - settings: FMTCSettings(databaseMaxSize: 51200), - debugMode: true, - ); + await FlutterMapTileCaching.initialise(); await FMTC.instance.rootDirectory.migrator.fromV6(urlTemplates: []); diff --git a/example/lib/screens/configure_download/components/start_download_button.dart b/example/lib/screens/configure_download/components/start_download_button.dart index b018176b..e2b0ff82 100644 --- a/example/lib/screens/configure_download/components/start_download_button.dart +++ b/example/lib/screens/configure_download/components/start_download_button.dart @@ -90,7 +90,7 @@ class StartDownloadButton extends StatelessWidget { final navigator = Navigator.of(context); final metadata = await regionSelectionProvider - .selectedStore!.metadata.readAsync; + .selectedStore!.metadata.read; downloadingProvider.setDownloadProgress( regionSelectionProvider.selectedStore!.download diff --git a/example/lib/screens/main/pages/map/components/map_view.dart b/example/lib/screens/main/pages/map/components/map_view.dart index af7cac5b..1c6bf499 100644 --- a/example/lib/screens/main/pages/map/components/map_view.dart +++ b/example/lib/screens/main/pages/map/components/map_view.dart @@ -24,7 +24,7 @@ class MapView extends StatelessWidget { FutureBuilder?>( future: currentStore == null ? Future.sync(() => {}) - : FMTC.instance(currentStore).metadata.readAsync, + : FMTC.instance(currentStore).metadata.read, builder: (context, metadata) { if (!metadata.hasData || metadata.data == null || diff --git a/example/lib/screens/main/pages/region_selection/region_selection.dart b/example/lib/screens/main/pages/region_selection/region_selection.dart index d961ae18..9484da26 100644 --- a/example/lib/screens/main/pages/region_selection/region_selection.dart +++ b/example/lib/screens/main/pages/region_selection/region_selection.dart @@ -225,7 +225,7 @@ class _RegionSelectionPageState extends State { FutureBuilder?>( future: currentStore == null ? Future.value() - : FMTC.instance(currentStore).metadata.readAsync, + : FMTC.instance(currentStore).metadata.read, builder: (context, metadata) { if (currentStore != null && metadata.data == null) { return const LoadingIndicator('Preparing Map'); @@ -262,7 +262,7 @@ class _RegionSelectionPageState extends State { : FMTC .instance(currentStore) .getTileProvider() - .checkTileCachedAsync( + .checkTileCached( coords: tile.coordinates, options: TileLayer(urlTemplate: urlTemplate), diff --git a/example/lib/screens/main/pages/stores/components/store_tile.dart b/example/lib/screens/main/pages/stores/components/store_tile.dart index 30a729bf..d33f3e68 100644 --- a/example/lib/screens/main/pages/stores/components/store_tile.dart +++ b/example/lib/screens/main/pages/stores/components/store_tile.dart @@ -23,10 +23,10 @@ class StoreTile extends StatefulWidget { } class _StoreTileState extends State { - Future? _tiles; + Future? _length; Future? _size; - Future? _cacheHits; - Future? _cacheMisses; + Future? _hits; + Future? _misses; Future? _image; bool _deletingProgress = false; @@ -36,18 +36,21 @@ class _StoreTileState extends State { late final _store = FMTC.instance(widget.storeName); void _loadStatistics() { - _tiles = _store.stats.storeLengthAsync.then((l) => l.toString()); - _size = _store.stats.storeSizeAsync.then((s) => (s * 1024).asReadableSize); - _cacheHits = _store.stats.cacheHitsAsync.then((h) => h.toString()); - _cacheMisses = _store.stats.cacheMissesAsync.then((m) => m.toString()); - _image = _store.manage.tileImageAsync(size: 125); + final stats = _store.stats.all; + + _length = stats.then((s) => s.length.toString()); + _size = stats.then((s) => (s.size * 1024).asReadableSize); + _hits = stats.then((s) => s.hits.toString()); + _misses = stats.then((s) => s.misses.toString()); + + _image = _store.manage.tileImage(size: 125); setState(() {}); } List> get stats => [ FutureBuilder( - future: _tiles, + future: _length, builder: (context, snapshot) => StatDisplay( statistic: snapshot.connectionState != ConnectionState.done ? null @@ -65,7 +68,7 @@ class _StoreTileState extends State { ), ), FutureBuilder( - future: _cacheHits, + future: _hits, builder: (context, snapshot) => StatDisplay( statistic: snapshot.connectionState != ConnectionState.done ? null @@ -74,7 +77,7 @@ class _StoreTileState extends State { ), ), FutureBuilder( - future: _cacheMisses, + future: _misses, builder: (context, snapshot) => StatDisplay( statistic: snapshot.connectionState != ConnectionState.done ? null @@ -177,7 +180,7 @@ class _StoreTileState extends State { setState( () => _emptyingProgress = true, ); - await _store.manage.resetAsync(); + await _store.manage.reset(); setState( () => _emptyingProgress = false, ); diff --git a/example/lib/screens/store_editor/components/header.dart b/example/lib/screens/store_editor/components/header.dart index a7ef125b..46422687 100644 --- a/example/lib/screens/store_editor/components/header.dart +++ b/example/lib/screens/store_editor/components/header.dart @@ -56,23 +56,23 @@ AppBar buildHeader({ downloadProvider.setSelectedStore(newStore); }*/ - await newStore.manage.createAsync(); + await newStore.manage.create(); - await newStore.metadata.addAsync( + await newStore.metadata.set( key: 'sourceURL', value: newValues['sourceURL']!, ); - await newStore.metadata.addAsync( + await newStore.metadata.set( key: 'validDuration', value: newValues['validDuration']!, ); - await newStore.metadata.addAsync( + await newStore.metadata.set( key: 'maxLength', value: newValues['maxLength']!, ); if (widget.existingStoreName == null || useNewCacheModeValue) { - await newStore.metadata.addAsync( + await newStore.metadata.set( key: 'behaviour', value: cacheModeValue ?? 'cacheFirst', ); diff --git a/example/lib/screens/store_editor/store_editor.dart b/example/lib/screens/store_editor/store_editor.dart index bb003326..c36a7190 100644 --- a/example/lib/screens/store_editor/store_editor.dart +++ b/example/lib/screens/store_editor/store_editor.dart @@ -45,12 +45,11 @@ class _StoreEditorPopupState extends State { @override Widget build(BuildContext context) => Consumer( - builder: (context, downloadProvider, _) => WillPopScope( - onWillPop: () async { + builder: (context, downloadProvider, _) => PopScope( + onPopInvoked: (_) { scaffoldMessenger.showSnackBar( const SnackBar(content: Text('Changes not saved')), ); - return true; }, child: Scaffold( appBar: buildHeader( @@ -68,10 +67,7 @@ class _StoreEditorPopupState extends State { child: FutureBuilder?>( future: widget.existingStoreName == null ? Future.sync(() => {}) - : FMTC - .instance(widget.existingStoreName!) - .metadata - .readAsync, + : FMTC.instance(widget.existingStoreName!).metadata.read, builder: (context, metadata) { if (!metadata.hasData || metadata.data == null) { return const LoadingIndicator('Retrieving Settings'); diff --git a/lib/flutter_map_tile_caching.dart b/lib/flutter_map_tile_caching.dart index 41253a59..0161d451 100644 --- a/lib/flutter_map_tile_caching.dart +++ b/lib/flutter_map_tile_caching.dart @@ -33,7 +33,7 @@ import 'package:path_provider/path_provider.dart'; import 'package:stream_transform/stream_transform.dart'; import 'package:watcher/watcher.dart'; -import 'src/backend/exports.dart'; +import 'src/backend/export_internal.dart'; import 'src/bulk_download/instance.dart'; import 'src/bulk_download/rate_limited_stream.dart'; import 'src/bulk_download/tile_loops/shared.dart'; @@ -41,19 +41,18 @@ import 'src/errors/browsing.dart'; import 'src/misc/exts.dart'; import 'src/misc/int_extremes.dart'; import 'src/misc/obscure_query_params.dart'; -import 'src/misc/typedefs.dart'; import 'src/providers/image_provider.dart'; -export 'src/backend/exports.dart'; +export 'src/backend/export_external.dart'; export 'src/errors/browsing.dart'; export 'src/errors/damaged_store.dart'; -export 'src/misc/typedefs.dart'; part 'src/bulk_download/download_progress.dart'; part 'src/bulk_download/manager.dart'; part 'src/bulk_download/thread.dart'; part 'src/bulk_download/tile_event.dart'; part 'src/fmtc.dart'; +part 'src/misc/deprecations.dart'; part 'src/misc/with_backend_access.dart'; part 'src/providers/tile_provider.dart'; part 'src/regions/base_region.dart'; @@ -69,8 +68,7 @@ part 'src/root/manage.dart'; part 'src/root/migrator.dart'; part 'src/root/recovery.dart'; part 'src/root/statistics.dart'; -part 'src/settings/fmtc_settings.dart'; -part 'src/settings/tile_provider_settings.dart'; +part 'src/providers/tile_provider_settings.dart'; part 'src/store/directory.dart'; part 'src/store/download.dart'; part 'src/store/export.dart'; diff --git a/lib/src/backend/exports.dart b/lib/src/backend/export_external.dart similarity index 85% rename from lib/src/backend/exports.dart rename to lib/src/backend/export_external.dart index c92184e7..66db6d46 100644 --- a/lib/src/backend/exports.dart +++ b/lib/src/backend/export_external.dart @@ -3,5 +3,4 @@ export 'impls/objectbox/backend.dart'; export 'interfaces/backend.dart'; -export 'interfaces/models.dart'; export 'utils/errors.dart'; diff --git a/lib/src/backend/export_internal.dart b/lib/src/backend/export_internal.dart new file mode 100644 index 00000000..055972c9 --- /dev/null +++ b/lib/src/backend/export_internal.dart @@ -0,0 +1,6 @@ +// Copyright © Luka S (JaffaKetchup) under GPL-v3 +// A full license can be found at .\LICENSE + +export 'export_external.dart'; + +export 'interfaces/models.dart'; diff --git a/lib/src/backend/impls/objectbox/backend.dart b/lib/src/backend/impls/objectbox/backend.dart index e745a6c2..b706e67c 100644 --- a/lib/src/backend/impls/objectbox/backend.dart +++ b/lib/src/backend/impls/objectbox/backend.dart @@ -2,31 +2,30 @@ // A full license can be found at .\LICENSE import 'dart:async'; +import 'dart:convert'; import 'dart:io'; import 'dart:isolate'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:meta/meta.dart' as meta; -import 'package:path_provider/path_provider.dart'; -import '../../../misc/exts.dart'; -import '../../interfaces/backend.dart'; -import '../../utils/errors.dart'; +import '../../../../flutter_map_tile_caching.dart'; import 'models/generated/objectbox.g.dart'; import 'models/models.dart'; part 'worker.dart'; +/// Implementation of [FMTCBackend] that uses ObjectBox as the storage database final class ObjectBoxBackend implements FMTCBackend { + /// It is not recommended to access this method externally @override @meta.internal FMTCBackendInternal get internal => ObjectBoxBackendInternal._instance; } -/// Implementation of [FMTCBackend] that uses ObjectBox as the storage database -/// -/// Only to be accessed by FMTC via [ObjectBoxBackend.internal] +/// Internal implementation of [FMTCBackend] that uses ObjectBox as the storage +/// database abstract interface class ObjectBoxBackendInternal implements FMTCBackendInternal { static final _instance = _ObjectBoxBackendImpl._(); @@ -74,7 +73,10 @@ class _ObjectBoxBackendImpl implements ObjectBoxBackendInternal { @override String get friendlyIdentifier => 'ObjectBox'; - /// {@macro fmtc.backend.initialise} + /// See [FMTCBackendInternal.initialise] & [FlutterMapTileCaching.initialise] + /// for more info. + /// + /// --- /// /// This implementation additionally accepts the following [implSpecificArgs]: /// @@ -82,15 +84,15 @@ class _ObjectBoxBackendImpl implements ObjectBoxBackendInternal { /// use to specify the application group (of less than 20 chars). See /// [the ObjectBox docs](https://docs.objectbox.io/getting-started) for /// details. - /// * 'maxReaders' (`int`): for debugging purposes only + /// * 'maxDatabaseSize' (`int`): the maximum size the database file can grow + /// to. Exceeding it throws [DbFullException]. Defaults to 10 GB. /// /// These arguments are optional. However, failure to provide them in the /// specified type will result in an uncaught type casting error. @override Future initialise({ - String? rootDirectory, - int? maxDatabaseSize, - Map implSpecificArgs = const {}, + required Directory rootDirectory, + required Map implSpecificArgs, }) async { if (_sendPort != null) throw RootAlreadyInitialised(); @@ -126,10 +128,9 @@ class _ObjectBoxBackendImpl implements ObjectBoxBackendInternal { ( sendPort: receivePort.sendPort, rootDirectory: rootDirectory, - maxDatabaseSize: maxDatabaseSize, + maxDatabaseSize: implSpecificArgs['maxDatabaseSize'] as int?, macosApplicationGroup: - implSpecificArgs['macosApplicationGroup']! as String, - maxReaders: implSpecificArgs['maxReaders']! as int, + implSpecificArgs['macosApplicationGroup'] as String?, ), onExit: receivePort.sendPort, debugName: '[FMTC] ObjectBox Backend Worker', @@ -222,40 +223,13 @@ class _ObjectBoxBackendImpl implements ObjectBoxBackendInternal { ); @override - Future getStoreSize({ - required String storeName, - }) async => - (await _sendCmd( - type: _WorkerCmdType.getStoreSize, - args: {'storeName': storeName}, - ))!['size']; - - @override - Future getStoreLength({ - required String storeName, - }) async => - (await _sendCmd( - type: _WorkerCmdType.getStoreLength, - args: {'storeName': storeName}, - ))!['length']; - - @override - Future getStoreHits({ + Future<({double size, int length, int hits, int misses})> getStoreStats({ required String storeName, }) async => (await _sendCmd( - type: _WorkerCmdType.getStoreHits, + type: _WorkerCmdType.getStoreStats, args: {'storeName': storeName}, - ))!['hits']; - - @override - Future getStoreMisses({ - required String storeName, - }) async => - (await _sendCmd( - type: _WorkerCmdType.getStoreMisses, - args: {'storeName': storeName}, - ))!['misses']! as int; + ))!['store']; @override Future tileExistsInStore({ @@ -364,4 +338,53 @@ class _ObjectBoxBackendImpl implements ObjectBoxBackendInternal { type: _WorkerCmdType.removeTilesOlderThan, args: {'storeName': storeName, 'expiry': expiry}, ))!['numOrphans']; + + @override + Future> readMetadata({ + required String storeName, + }) async => + (await _sendCmd( + type: _WorkerCmdType.readMetadata, + args: {'storeName': storeName}, + ))!['metadata']; + + @override + Future setMetadata({ + required String storeName, + required String key, + required String value, + }) => + _sendCmd( + type: _WorkerCmdType.setMetadata, + args: {'storeName': storeName, 'key': key, 'value': value}, + ); + + @override + Future setBulkMetadata({ + required String storeName, + required Map kvs, + }) => + _sendCmd( + type: _WorkerCmdType.setBulkMetadata, + args: {'storeName': storeName, 'kvs': kvs}, + ); + + @override + Future removeMetadata({ + required String storeName, + required String key, + }) async => + (await _sendCmd( + type: _WorkerCmdType.removeMetadata, + args: {'storeName': storeName, 'key': key}, + ))!['removedValue']; + + @override + Future resetMetadata({ + required String storeName, + }) => + _sendCmd( + type: _WorkerCmdType.resetMetadata, + args: {'storeName': storeName}, + ); } diff --git a/lib/src/backend/impls/objectbox/models/generated/objectbox-model.json b/lib/src/backend/impls/objectbox/models/generated/objectbox-model.json index edb674e3..59dabaac 100644 --- a/lib/src/backend/impls/objectbox/models/generated/objectbox-model.json +++ b/lib/src/backend/impls/objectbox/models/generated/objectbox-model.json @@ -4,95 +4,103 @@ "_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.", "entities": [ { - "id": "1:7419244569066266196", - "lastPropertyId": "6:3446172587861422549", + "id": "1:4818990512469112793", + "lastPropertyId": "9:7001905095427330194", "name": "ObjectBoxStore", "properties": [ { - "id": "1:1858780610237179333", + "id": "1:3336028670326288161", "name": "id", "type": 6, "flags": 1 }, { - "id": "2:2256416726389092989", + "id": "2:4687139407104104001", "name": "name", "type": 9, "flags": 2080, - "indexId": "1:3645033738238218113" + "indexId": "1:7472642561499264209" }, { - "id": "3:8181895676109246629", - "name": "numberOfTiles", + "id": "5:6058775585602184944", + "name": "hits", "type": 6 }, { - "id": "4:3677248801338209880", - "name": "numberOfBytes", - "type": 8 + "id": "6:6933472703948745593", + "name": "misses", + "type": 6 }, { - "id": "5:1702586868505261124", - "name": "hits", - "type": 6 + "id": "7:3575235946528079942", + "name": "metadataJson", + "type": 9 }, { - "id": "6:3446172587861422549", - "name": "misses", + "id": "8:9173249510215692378", + "name": "length", "type": 6 + }, + { + "id": "9:7001905095427330194", + "name": "size", + "type": 8 } ], "relations": [] }, { - "id": "2:9006700906555106229", - "lastPropertyId": "4:7354783822834437324", + "id": "2:5729851195378529265", + "lastPropertyId": "4:4688190119175680445", "name": "ObjectBoxTile", "properties": [ { - "id": "1:1583610199049416297", + "id": "1:145404380988255117", "name": "id", "type": 6, "flags": 1 }, { - "id": "2:6188685410059426284", + "id": "2:5879597573334804833", "name": "url", "type": 9, "flags": 34848, - "indexId": "2:3427244788486794902" + "indexId": "2:916751376918221285" }, { - "id": "3:1617954085447053640", + "id": "3:7079942909661621360", "name": "lastModified", "type": 10, "flags": 8, - "indexId": "3:2057043365010558859" + "indexId": "3:1775764954149910715" }, { - "id": "4:7354783822834437324", + "id": "4:4688190119175680445", "name": "bytes", "type": 23 } ], "relations": [ { - "id": "1:3843657930463993464", + "id": "1:8266116100918510486", "name": "stores", - "targetId": "1:7419244569066266196" + "targetId": "1:4818990512469112793" } ] } ], - "lastEntityId": "2:9006700906555106229", - "lastIndexId": "3:2057043365010558859", - "lastRelationId": "1:3843657930463993464", + "lastEntityId": "2:5729851195378529265", + "lastIndexId": "3:1775764954149910715", + "lastRelationId": "1:8266116100918510486", "lastSequenceId": "0:0", "modelVersion": 5, "modelVersionParserMinimum": 5, "retiredEntityUids": [], "retiredIndexUids": [], - "retiredPropertyUids": [], + "retiredPropertyUids": [ + 4684432242170874534, + 2520378516311313562 + ], "retiredRelationUids": [], "version": 1 } \ No newline at end of file diff --git a/lib/src/backend/impls/objectbox/models/models.dart b/lib/src/backend/impls/objectbox/models/models.dart index d7350f1b..9e2935d3 100644 --- a/lib/src/backend/impls/objectbox/models/models.dart +++ b/lib/src/backend/impls/objectbox/models/models.dart @@ -8,7 +8,7 @@ import 'package:objectbox/objectbox.dart'; import '../../../interfaces/models.dart'; @Entity() -base class ObjectBoxStore extends BackendStore> { +base class ObjectBoxStore extends BackendStore { @Id() int id = 0; @@ -17,32 +17,27 @@ base class ObjectBoxStore extends BackendStore> { @Unique() String name; - int numberOfTiles; - - double numberOfBytes; - - @override - int hits; - - @override - int misses; - - @override @Index() @Backlink() final tiles = ToMany(); + int length; + double size; + int hits; + int misses; + String metadataJson; + ObjectBoxStore({ required this.name, - required this.numberOfTiles, - required this.numberOfBytes, + required this.length, + required this.size, required this.hits, required this.misses, - }); + }) : metadataJson = ''; } @Entity() -base class ObjectBoxTile extends BackendTile> { +base class ObjectBoxTile extends BackendTile { @Id() int id = 0; @@ -59,7 +54,6 @@ base class ObjectBoxTile extends BackendTile> { @override Uint8List bytes; - @override @Index() final stores = ToMany(); diff --git a/lib/src/backend/impls/objectbox/worker.dart b/lib/src/backend/impls/objectbox/worker.dart index b1143869..b7a6da90 100644 --- a/lib/src/backend/impls/objectbox/worker.dart +++ b/lib/src/backend/impls/objectbox/worker.dart @@ -11,10 +11,7 @@ enum _WorkerCmdType { resetStore, renameStore, deleteStore, - getStoreSize, - getStoreLength, - getStoreHits, - getStoreMisses, + getStoreStats, tileExistsInStore, readTile, readLatestTile, @@ -22,15 +19,19 @@ enum _WorkerCmdType { deleteTile, removeOldestTilesAboveLimit, removeTilesOlderThan, + readMetadata, + setMetadata, + setBulkMetadata, + removeMetadata, + resetMetadata, } Future _worker( ({ SendPort sendPort, - String? rootDirectory, + Directory rootDirectory, int? maxDatabaseSize, String? macosApplicationGroup, - int? maxReaders, }) input, ) async { //! SETUP !// @@ -44,15 +45,10 @@ Future _worker( input.sendPort.send((id: id, data: data)); // Initialise database - final rootDirectory = await (input.rootDirectory == null - ? await getApplicationDocumentsDirectory() - : Directory(input.rootDirectory!) >> 'fmtc') - .create(recursive: true); final root = await openStore( - directory: rootDirectory.absolute.path, - maxDBSizeInKB: input.maxDatabaseSize, + directory: input.rootDirectory.absolute.path, + maxDBSizeInKB: input.maxDatabaseSize ?? 10000000, // Defaults to 10 GB macosApplicationGroup: input.macosApplicationGroup, - maxReaders: input.maxReaders, ); // Respond with comms channel for future cmds @@ -97,8 +93,8 @@ Future _worker( if (store.name != storeName) continue; stores.put( store - ..numberOfTiles -= 1 - ..numberOfBytes -= tile.bytes.lengthInBytes, + ..length -= 1 + ..size -= tile.bytes.lengthInBytes, mode: PutMode.update, ); break; @@ -111,7 +107,8 @@ Future _worker( if (tile.stores.isEmpty) return tiles.remove(tile.id); // Otherwise just update the tile - tiles.put(tile, mode: PutMode.update); + // TODO: Check this works + tile.stores.applyToDb(mode: PutMode.update); return false; }); } @@ -130,16 +127,38 @@ Future _worker( case _WorkerCmdType.destroy_: root.close(); if (cmd.args['deleteRoot'] == true) { - rootDirectory.deleteSync(recursive: true); + input.rootDirectory.deleteSync(recursive: true); } // TODO: Consider final message Isolate.exit(); case _WorkerCmdType.storeExists: + final query = root + .box() + .query( + ObjectBoxStore_.name.equals(cmd.args['storeName']! as String), + ) + .build(); + + sendRes(id: cmd.id, data: {'exists': query.count() == 1}); + + query.close(); + + break; + case _WorkerCmdType.getStoreStats: + final storeName = cmd.args['storeName']! as String; + final store = getStore(storeName) ?? + (throw StoreNotExists(storeName: storeName)); + sendRes( id: cmd.id, data: { - 'exists': getStore(cmd.args['storeName']! as String) != null, + 'stats': ( + size: store.size / 1024, // Convert to KiB + length: store.length, + hits: store.hits, + misses: store.misses, + ), }, ); @@ -151,14 +170,15 @@ Future _worker( root.box().put( ObjectBoxStore( name: storeName, - numberOfTiles: 0, - numberOfBytes: 0, + length: 0, + size: 0, hits: 0, misses: 0, ), mode: PutMode.insert, ); } catch (e) { + debugPrint(e.runtimeType.toString()); throw StoreAlreadyExists(storeName: storeName); } @@ -166,6 +186,7 @@ Future _worker( break; case _WorkerCmdType.resetStore: + // TODO: Consider just deleting then creating final storeName = cmd.args['storeName']! as String; final removeIds = []; @@ -201,9 +222,7 @@ Future _worker( ); tilesQuery.close(); - tiles.query(ObjectBoxTile_.id.oneOf(removeIds)).build() - ..remove() - ..close(); + tiles.removeMany(removeIds); final store = storeQuery.findUnique() ?? (throw StoreNotExists(storeName: storeName)); @@ -215,8 +234,8 @@ Future _worker( stores.put( store ..tiles.clear() - ..numberOfTiles = 0 - ..numberOfBytes = 0 + ..length = 0 + ..size = 0 ..hits = 0 ..misses = 0, ); @@ -230,11 +249,22 @@ Future _worker( final currentStoreName = cmd.args['currentStoreName']! as String; final newStoreName = cmd.args['newStoreName']! as String; - root.box().put( - getStore(currentStoreName) ?? - (throw StoreNotExists(storeName: currentStoreName)) - ..name = newStoreName, - ); + final stores = root.box(); + + final query = stores + .query(ObjectBoxStore_.name.equals(currentStoreName)) + .build(); + + root.runInTransaction( + TxMode.write, + () { + final store = query.findUnique() ?? + (throw StoreNotExists(storeName: currentStoreName)); + query.close(); + + stores.put(store..name = newStoreName, mode: PutMode.update); + }, + ); sendRes(id: cmd.id); @@ -249,60 +279,10 @@ Future _worker( ..remove() ..close(); - sendRes(id: cmd.id); - - break; - case _WorkerCmdType.getStoreSize: - final storeName = cmd.args['storeName']! as String; - - sendRes( - id: cmd.id, - data: { - 'size': (getStore(storeName) ?? - (throw StoreNotExists(storeName: storeName))) - .numberOfBytes / - 1024, - }, - ); - - break; - case _WorkerCmdType.getStoreLength: - final storeName = cmd.args['storeName']! as String; - - sendRes( - id: cmd.id, - data: { - 'length': (getStore(storeName) ?? - (throw StoreNotExists(storeName: storeName))) - .numberOfTiles, - }, - ); - - break; - case _WorkerCmdType.getStoreHits: - final storeName = cmd.args['storeName']! as String; - - sendRes( - id: cmd.id, - data: { - 'hits': (getStore(storeName) ?? - (throw StoreNotExists(storeName: storeName))) - .hits, - }, - ); - - break; - case _WorkerCmdType.getStoreMisses: - final storeName = cmd.args['storeName']! as String; + // TODO: Check tiles relations + // TODO: Integrate metadata - sendRes( - id: cmd.id, - data: { - 'misses': (getStore(storeName) ?? - (throw StoreNotExists(storeName: storeName))) - .misses, - }, - ); + sendRes(id: cmd.id); break; case _WorkerCmdType.tileExistsInStore: @@ -388,8 +368,8 @@ Future _worker( ); stores.put( store - ..numberOfTiles += 1 - ..numberOfBytes += bytes.lengthInBytes, + ..length += 1 + ..size += bytes.lengthInBytes, ); break; case (false, true): // Existing tile, no update @@ -398,8 +378,8 @@ Future _worker( tiles.put(existingTile..stores.add(store)); stores.put( store - ..numberOfTiles += 1 - ..numberOfBytes += existingTile.bytes.lengthInBytes, + ..length += 1 + ..size += existingTile.bytes.lengthInBytes, ); } break; @@ -413,7 +393,7 @@ Future _worker( existingTile.stores .map( (store) => store - ..numberOfBytes += (bytes.lengthInBytes - + ..size += (bytes.lengthInBytes - existingTile.bytes.lengthInBytes), ) .toList(), @@ -483,7 +463,7 @@ Future _worker( final store = storeQuery.findUnique() ?? (throw StoreNotExists(storeName: storeName)); - final numToRemove = store.numberOfTiles - tilesLimit; + final numToRemove = store.length - tilesLimit; return numToRemove <= 0 ? const Iterable.empty() @@ -533,6 +513,138 @@ Future _worker( tilesQuery.close(); + break; + case _WorkerCmdType.readMetadata: + final storeName = cmd.args['storeName']! as String; + final store = getStore(storeName) ?? + (throw StoreNotExists(storeName: storeName)); + + sendRes( + id: cmd.id, + data: {'metadata': jsonDecode(store.metadataJson)}, + ); + + break; + case _WorkerCmdType.setMetadata: + final storeName = cmd.args['storeName']! as String; + final key = cmd.args['key']! as String; + final value = cmd.args['value']! as String; + + final stores = root.box(); + + final query = + stores.query(ObjectBoxStore_.name.equals(storeName)).build(); + + root.runInTransaction( + TxMode.write, + () { + final store = query.findUnique() ?? + (throw StoreNotExists(storeName: storeName)); + query.close(); + + stores.put( + store + ..metadataJson = jsonEncode( + (jsonDecode(store.metadataJson) + as Map)[key] = value, + ), + mode: PutMode.update, + ); + }, + ); + + sendRes(id: cmd.id); + + break; + case _WorkerCmdType.setBulkMetadata: + final storeName = cmd.args['storeName']! as String; + final kvs = cmd.args['kvs']! as Map; + + final stores = root.box(); + + final query = + stores.query(ObjectBoxStore_.name.equals(storeName)).build(); + + root.runInTransaction( + TxMode.write, + () { + final store = query.findUnique() ?? + (throw StoreNotExists(storeName: storeName)); + query.close(); + + stores.put( + store + ..metadataJson = jsonEncode( + (jsonDecode(store.metadataJson) as Map) + ..addAll(kvs), + ), + mode: PutMode.update, + ); + }, + ); + + sendRes(id: cmd.id); + + break; + case _WorkerCmdType.removeMetadata: + final storeName = cmd.args['storeName']! as String; + final key = cmd.args['key']! as String; + + final stores = root.box(); + + final query = + stores.query(ObjectBoxStore_.name.equals(storeName)).build(); + + sendRes( + id: cmd.id, + data: { + 'removedValue': root.runInTransaction( + TxMode.write, + () { + final store = query.findUnique() ?? + (throw StoreNotExists(storeName: storeName)); + query.close(); + + final metadata = + jsonDecode(store.metadataJson) as Map; + final removedVal = metadata.remove(key); + + stores.put( + store..metadataJson = jsonEncode(metadata), + mode: PutMode.update, + ); + + return removedVal; + }, + ), + }, + ); + + break; + case _WorkerCmdType.resetMetadata: + final storeName = cmd.args['storeName']! as String; + + final stores = root.box(); + + final query = + stores.query(ObjectBoxStore_.name.equals(storeName)).build(); + + root.runInTransaction( + TxMode.write, + () { + final store = query.findUnique() ?? + (throw StoreNotExists(storeName: storeName)); + query.close(); + + stores.put( + store..metadataJson = jsonEncode({}), + mode: PutMode.update, + ); + }, + ); + + sendRes(id: cmd.id); + break; } } catch (e) { diff --git a/lib/src/backend/interfaces/backend.dart b/lib/src/backend/interfaces/backend.dart index 9d7fb130..7482c1be 100644 --- a/lib/src/backend/interfaces/backend.dart +++ b/lib/src/backend/interfaces/backend.dart @@ -2,12 +2,13 @@ // A full license can be found at .\LICENSE import 'dart:async'; +import 'dart:io'; import 'dart:typed_data'; import 'package:meta/meta.dart'; -import 'package:path_provider/path_provider.dart'; import '../../../flutter_map_tile_caching.dart'; +import 'models.dart'; /// An abstract interface that FMTC will use to communicate with a storage /// 'backend' (usually one root) @@ -24,10 +25,6 @@ import '../../../flutter_map_tile_caching.dart'; /// be accessed via [FMTCBackend.internal] /// * Prefer throwing included implementation-generic errors/exceptions /// * See the default [ObjectBoxBackend] implementation for an example -/// -/// To end-users: -/// * Use [FMTCSettings.backend] to set a custom backend -/// * Avoid calling the [internal] method of a backend abstract interface class FMTCBackend { const FMTCBackend(); @@ -46,27 +43,17 @@ abstract interface class FMTCBackendInternal { /// Generic description/name of this backend abstract final String friendlyIdentifier; - /// {@template fmtc.backend.initialise} /// Initialise this backend & create the root /// - /// [rootDirectory] defaults to '[getApplicationDocumentsDirectory]/fmtc'. - /// - /// [maxDatabaseSize] defaults to 1 GB shared across all stores. Specify the - /// amount in KB. + /// See [FlutterMapTileCaching.initialise] for more information. /// /// Some implementations may accept/require additional arguments that may /// be set through [implSpecificArgs]. See their documentation for more - /// information. - /// {@endtemplate} - /// - /// --- - /// - /// Note to implementers: if you accept implementation specific arguments, - /// ensure you properly document these. + /// information. Note to implementers: if you accept implementation specific + /// arguments, ensure you properly document these. Future initialise({ - String? rootDirectory, - int? maxDatabaseSize, - Map implSpecificArgs = const {}, + required Directory rootDirectory, + required Map implSpecificArgs, }); /// {@template fmtc.backend.destroy} @@ -128,36 +115,14 @@ abstract interface class FMTCBackendInternal { required String newStoreName, }); - /// {@template fmtc.backend.getStoreSize} - /// Retrieve the total size (in kibibytes KiB) of the image bytes of all the - /// tiles that belong to the specified store - /// - /// This does not return any other data that adds to the 'real' store size. - /// {@endtemplate} - Future getStoreSize({ - required String storeName, - }); - - /// {@template fmtc.backend.getStoreLength} - /// Retrieve the number of tiles that belong to the specified store + /// {@template fmtc.backend.getStoreStats} + /// Retrieve the following statistics about the specified store (all available): + /// * `size`: total number of KiBs of all tiles' bytes (not 'real total' size) + /// * `length`: number of tiles belonging + /// * `hits`: number of successful tile retrievals when browsing + /// * `misses`: number of unsuccessful tile retrievals when browsing /// {@endtemplate} - Future getStoreLength({ - required String storeName, - }); - - /// {@template fmtc.backend.getStoreHits} - /// Retrieve the number of times that a tile was successfully retrieved from - /// the specified store when browsing - /// {@endtemplate} - Future getStoreHits({ - required String storeName, - }); - - /// {@template fmtc.backend.getStoreMisses} - /// Retrieve the number of times that a tile was attempted to be retrieved from - /// the specified store when browsing, but was not present - /// {@endtemplate} - Future getStoreMisses({ + Future<({double size, int length, int hits, int misses})> getStoreStats({ required String storeName, }); @@ -167,7 +132,7 @@ abstract interface class FMTCBackendInternal { required String url, }); - /// Get a raw tile by URL + /// Retrieve a raw tile by the specified URL Future readTile({ required String url, }); @@ -236,4 +201,58 @@ abstract interface class FMTCBackendInternal { required String storeName, required DateTime expiry, }); + + /// {@template fmtc.backend.readMetadata} + /// Retrieve the stored metadata for the specified store + /// {@endtemplate} + Future> readMetadata({ + required String storeName, + }); + + /// {@template fmtc.backend.setMetadata} + /// Set a key-value pair in the metadata for the specified store + /// + /// Note that this operation will override the stored value if there is already + /// a matching key present. + /// + /// Prefer using [setBulkMetadata] when setting multiple keys. Only one backend + /// operation is required to set them all at once, and so is more efficient. + /// {@endtemplate} + Future setMetadata({ + required String storeName, + required String key, + required String value, + }); + + /// {@template fmtc.backend.setBulkMetadata} + /// Set multiple key-value pairs in the metadata for the specified store + /// + /// Note that this operation will override the stored value if there is already + /// a matching key present. + /// {@endtemplate} + Future setBulkMetadata({ + required String storeName, + required Map kvs, + }); + + /// {@template fmtc.backend.removeMetadata} + /// Remove the specified key from the metadata for the specified store + /// + /// Returns the value associated with key before it was removed, or `null` if + /// it was not present. + /// {@endtemplate} + Future removeMetadata({ + required String storeName, + required String key, + }); + + /// {@template fmtc.backend.resetMetadata} + /// Clear the metadata for the specified store + /// + /// This operation cannot be undone! Ensure you confirm with the user that + /// this action is expected. + /// {@endtemplate} + Future resetMetadata({ + required String storeName, + }); } diff --git a/lib/src/backend/interfaces/models.dart b/lib/src/backend/interfaces/models.dart index 5ef55a5c..6b0af9e4 100644 --- a/lib/src/backend/interfaces/models.dart +++ b/lib/src/backend/interfaces/models.dart @@ -5,11 +5,18 @@ import 'dart:typed_data'; import 'package:meta/meta.dart'; -abstract base class BackendStore>> { - abstract String name; - abstract int hits; - abstract int misses; - T get tiles; +import '../../../flutter_map_tile_caching.dart'; +import '../../misc/obscure_query_params.dart'; + +/// Represents a store (which is never directly exposed to the user) +/// +/// Note that the relationship between stores and tiles is many-to-many, and +/// backend implementations should fully support this. +abstract base class BackendStore { + /// The human-readable name for this store + /// + /// Note that this may contain any character, and may also be empty. + String get name; /// Uses [name] for equality comparisons only (unless the two objects are /// [identical]) @@ -26,11 +33,25 @@ abstract base class BackendStore>> { int get hashCode => name.hashCode; } -abstract base class BackendTile>> { - abstract String url; - abstract DateTime lastModified; - abstract Uint8List bytes; - S get stores; +/// Represents a tile (which is never directly exposed to the user) +/// +/// Note that the relationship between stores and tiles is many-to-many, and +/// backend implementations should fully support this. +abstract base class BackendTile { + /// The representative URL of the tile + /// + /// This is passed through [obscureQueryParams] before storage here, and so + /// may not be the same as the network URL. + String get url; + + /// The time at which the [bytes] of this tile were last changed + /// + /// This must be kept up to date, otherwise unexpected behaviour may occur + /// when the [FMTCTileProviderSettings.maxStoreLength] is exceeded. + DateTime get lastModified; + + /// The raw bytes of the image of this tile + Uint8List get bytes; /// Uses [url] for equality comparisons only (unless the two objects are /// [identical]) diff --git a/lib/src/fmtc.dart b/lib/src/fmtc.dart index 765d3739..09959a8b 100644 --- a/lib/src/fmtc.dart +++ b/lib/src/fmtc.dart @@ -21,115 +21,98 @@ class FlutterMapTileCaching { /// The directory which contains all databases required to use FMTC final RootDirectory rootDirectory; - /// Custom global 'flutter_map_tile_caching' settings + /// The database or other storage mechanism that FMTC will use as a cache + /// 'backend' /// - /// See [FMTCSettings]' properties for more information - final FMTCSettings settings; + /// Defaults to [ObjectBoxBackend], which uses the ObjectBox library and + /// database. + /// + /// See [FMTCBackend] for more information. + final FMTCBackend backend; - /// Whether FMTC should perform extra reporting and console logging + /// Default settings used when creating an [FMTCTileProvider] /// - /// Depends on [_debugMode] (set via [initialise]) and [kDebugMode]. - bool get debugMode => _debugMode && kDebugMode; - final bool _debugMode; + /// Can be overridden on a case-to-case basis when actually creating the tile + /// provider. + final FMTCTileProviderSettings defaultTileProviderSettings; /// Internal constructor, to be used by [initialise] const FlutterMapTileCaching._({ required this.rootDirectory, - required this.settings, - required bool debugMode, - }) : _debugMode = debugMode; + required this.backend, + required this.defaultTileProviderSettings, + }); /// {@macro fmtc.backend.initialise} /// + /// + /// {@macro fmtc.backend.objectbox.initialise} + /// /// Initialise and prepare FMTC, by creating all necessary directories/files /// and configuring the [FlutterMapTileCaching] singleton /// - /// Prefer to leave [rootDirectory] as `null`, which will use - /// `getApplicationDocumentsDirectory()`. Alternatively, pass a custom - /// directory - it is recommended to not use a cache directory, as the OS can - /// clear these without notice at any time. - /// /// You must construct using this before using [FlutterMapTileCaching.instance], /// otherwise a [StateError] will be thrown. /// - /// The initialisation safety system ensures that a corrupted database cannot - /// prevent the app from launching. However, one fatal crash is necessary for - /// each corrupted database, as this allows each one to be individually located - /// and deleted, due to limitations in dependencies. Note that any triggering - /// of the safety system will also reset the recovery database, meaning - /// recovery information will be lost. + /// Returns a configured [FlutterMapTileCaching] instance, and assigns it to + /// [instance]. /// - /// [errorHandler] must not (re)throw an error, as this interferes with the - /// initialisation safety system, and may result in unnecessary data loss. + /// Note that [FMTC] is an alias for [FlutterMapTileCaching]. /// - /// Setting [disableInitialisationSafety] `true` will disable the - /// initialisation safety system, and is not recommended, as this may leave the - /// application unable to launch if any database becomes corrupted. + /// --- /// - /// Setting [debugMode] `true` can be useful to diagnose issues, either within - /// your application or FMTC itself. It enables the Isar inspector and causes - /// extra console logging in important areas. Prefer to leave disabled to - /// prevent console pollution and to maximise performance. Whether FMTC chooses - /// to listen to this value is also dependent on [kDebugMode] - see - /// [FlutterMapTileCaching.debugMode] for more information. - /// _Extra logging is currently limited._ + /// Prefer to leave [rootDirectory] as `null`, which will use + /// `getApplicationDocumentsDirectory()`. Alternatively, pass a custom + /// directory - it is recommended to not use a cache directory, as the OS can + /// clear these without notice at any time. + /// + /// Optionally set a custom storage [backend] instead of [ObjectBoxBackend]. + /// Some implementations may accept/require additional arguments that may be + /// set through [backendImplArgs]. See their documentation for more + /// information. If provided, they must be of the specified type. /// - /// This returns a configured [FlutterMapTileCaching], the same object as - /// [FlutterMapTileCaching.instance]. Note that [FMTC] is an alias for this - /// object. + /// > The default [ObjectBoxBackend] accepts the following optional + /// > [backendImplArgs] (note that other implementations may support different + /// > args). For ease of use, they have been provided in this method as typed + /// > arguments - these may be useless in other implementations (although they + /// > will be forwarded to any). + /// > + /// > * [macosApplicationGroup] : when creating a sandboxed macOS app, + /// > use to specify the application group (of less than 20 chars). See + /// > [the ObjectBox docs](https://docs.objectbox.io/getting-started) for + /// > details. + /// > * [maxDatabaseSize] : the maximum size the database file can grow + /// > to. Exceeding it throws `DbFullException`. Defaults to 10 GB. static Future initialise({ String? rootDirectory, - FMTCSettings? settings, - void Function(FMTCInitialisationException error)? errorHandler, - bool disableInitialisationSafety = false, - bool debugMode = false, + FMTCBackend? backend, + Map backendImplArgs = const {}, + String? macosApplicationGroup, + int? maxDatabaseSize, + FMTCTileProviderSettings? defaultTileProviderSettings, }) async { - final directory = await ((rootDirectory == null - ? await getApplicationDocumentsDirectory() - : Directory(rootDirectory)) >> - 'fmtc') + final dir = await (rootDirectory == null + ? await getApplicationDocumentsDirectory() + : Directory(rootDirectory) >> 'fmtc') .create(recursive: true); - settings ??= FMTCSettings(); + backend ??= ObjectBoxBackend(); + defaultTileProviderSettings ??= FMTCTileProviderSettings(); - if (!disableInitialisationSafety) { - final initialisationSafetyFile = - directory >>> '.initialisationSafety.tmp'; - final needsRescue = await initialisationSafetyFile.exists(); - - await initialisationSafetyFile.create(); - final writeSink = - initialisationSafetyFile.openWrite(mode: FileMode.writeOnlyAppend); - - await FMTCRegistry.initialise( - directory: directory, - databaseMaxSize: settings.databaseMaxSize, - databaseCompactCondition: settings.databaseCompactCondition, - errorHandler: errorHandler, - initialisationSafetyWriteSink: writeSink, - safeModeSuccessfulIDs: - needsRescue ? await initialisationSafetyFile.readAsLines() : null, - debugMode: debugMode && kDebugMode, - ); - - await writeSink.close(); - await initialisationSafetyFile.delete(); - } else { - await FMTCRegistry.initialise( - directory: directory, - databaseMaxSize: settings.databaseMaxSize, - databaseCompactCondition: settings.databaseCompactCondition, - errorHandler: errorHandler, - initialisationSafetyWriteSink: null, - safeModeSuccessfulIDs: null, - debugMode: debugMode && kDebugMode, - ); - } + // ignore: invalid_use_of_protected_member + await backend.internal.initialise( + rootDirectory: dir, + implSpecificArgs: { + if (macosApplicationGroup != null) + 'macosApplicationGroup': macosApplicationGroup, + if (maxDatabaseSize != null) 'maxDatabaseSize': maxDatabaseSize, + }..addAll(backendImplArgs), + ); return _instance = FMTC._( - rootDirectory: RootDirectory._(directory), - settings: settings, - debugMode: debugMode, + rootDirectory: RootDirectory._(dir), + backend: backend, + defaultTileProviderSettings: defaultTileProviderSettings, ); } diff --git a/lib/src/misc/deprecations.dart b/lib/src/misc/deprecations.dart new file mode 100644 index 00000000..9bb1569e --- /dev/null +++ b/lib/src/misc/deprecations.dart @@ -0,0 +1,82 @@ +// Copyright © Luka S (JaffaKetchup) under GPL-v3 +// A full license can be found at .\LICENSE + +part of flutter_map_tile_caching; + +// TODO: Store management deprecations + +const _syncRemoval = ''' + +Synchronous operations have been removed throughout FMTC v9, therefore the distinction between sync and async operations has been removed. +This deprecated member will be removed in a future version. +'''; + +/// Provides deprecations where possible for previous methods in [StoreMetadata] +/// after the v9 release. +/// +/// Synchronous operations have been removed throughout FMTC v9, therefore the +/// distinction between sync and async operations has been removed. +/// +/// Provided in an extension method for easy differentiation and quick removal. +@Deprecated( + 'Migrate to the suggested replacements for each operation. $_syncRemoval', +) +extension StoreMetadataDeprecations on StoreMetadata { + /// {@macro fmtc.backend.readMetadata} + @Deprecated('Migrate to `read`. $_syncRemoval') + Future> get readAsync => read; + + /// {@macro fmtc.backend.setMetadata} + @Deprecated('Migrate to `set`. $_syncRemoval') + Future addAsync({required String key, required String value}) => + set(key: key, value: value); + + /// {@macro fmtc.backend.removeMetadata} + @Deprecated('Migrate to `remove`.$_syncRemoval') + Future removeAsync({required String key}) => remove(key: key); + + /// {@macro fmtc.backend.resetMetadata} + @Deprecated('Migrate to `reset`. $_syncRemoval') + Future resetAsync() => reset(); +} + +/// Provides deprecations where possible for previous methods in [StoreStats] +/// after the v9 release. +/// +/// Synchronous operations have been removed throughout FMTC v9, therefore the +/// distinction between sync and async operations has been removed. +/// +/// Provided in an extension method for easy differentiation and quick removal. +@Deprecated( + ''' +Migrate to the suggested replacements for each operation. +Synchronous operations have been removed throughout FMTC v9, therefore the distinction between sync and async operations has been removed. +This deprecated member will be removed in a future version. +''', +) +extension StoreStatsDeprecations on StoreStats { + /// Retrieve the total number of KiBs of all tiles' bytes (not 'real total' + /// size) + /// + /// {@macro fmtc.frontend.storestats.efficiency} + @Deprecated('Migrate to `size`. $_syncRemoval') + Future get storeSizeAsync => size; + + /// Retrieve the number of tiles belonging to this store + /// + /// {@macro fmtc.frontend.storestats.efficiency} + @Deprecated('Migrate to `length`. $_syncRemoval') + Future get storeLengthAsync => length; + + /// Retrieve the number of successful tile retrievals when browsing + /// + /// {@macro fmtc.frontend.storestats.efficiency} + @Deprecated('Migrate to `hits`.$_syncRemoval') + Future get cacheHitsAsync => hits; + + /// Retrieve the number of unsuccessful tile retrievals when browsing + /// + /// {@macro fmtc.frontend.storestats.efficiency} + @Deprecated('Migrate to `misses`. $_syncRemoval') + Future get cacheMissesAsync => misses; +} diff --git a/lib/src/misc/int_extremes.dart b/lib/src/misc/int_extremes.dart index 51587c62..1ad2b33b 100644 --- a/lib/src/misc/int_extremes.dart +++ b/lib/src/misc/int_extremes.dart @@ -1,5 +1,9 @@ // Copyright © Luka S (JaffaKetchup) under GPL-v3 // A full license can be found at .\LICENSE +import 'package:meta/meta.dart'; + +@internal const largestInt = 9223372036854775807; +@internal const smallestInt = -9223372036854775808; diff --git a/lib/src/misc/typedefs.dart b/lib/src/misc/typedefs.dart deleted file mode 100644 index f47653a0..00000000 --- a/lib/src/misc/typedefs.dart +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright © Luka S (JaffaKetchup) under GPL-v3 -// A full license can be found at .\LICENSE - -import 'package:isar/isar.dart'; - -import '../../flutter_map_tile_caching.dart'; - -/// See [FMTCSettings.databaseCompactCondition] and [CompactCondition]'s -/// documentation for more information -typedef DatabaseCompactCondition = CompactCondition; diff --git a/lib/src/misc/with_backend_access.dart b/lib/src/misc/with_backend_access.dart index adc1f5af..2897dc48 100644 --- a/lib/src/misc/with_backend_access.dart +++ b/lib/src/misc/with_backend_access.dart @@ -8,6 +8,6 @@ abstract base class _WithBackendAccess { final FMTCStore _store; // ignore: invalid_use_of_protected_member - FMTCBackendInternal get _backend => FMTC.instance.settings.backend.internal; + FMTCBackendInternal get _backend => FMTC.instance.backend.internal; String get _storeName => _store.storeName; } diff --git a/lib/src/providers/image_provider.dart b/lib/src/providers/image_provider.dart index 513a22f3..f447d758 100644 --- a/lib/src/providers/image_provider.dart +++ b/lib/src/providers/image_provider.dart @@ -16,6 +16,9 @@ import '../misc/obscure_query_params.dart'; /// A specialised [ImageProvider] dedicated to 'flutter_map_tile_caching' class FMTCImageProvider extends ImageProvider { + /// The name of the store associated with this provider + final String storeName; + /// An instance of the [FMTCTileProvider] in use final FMTCTileProvider provider; @@ -27,13 +30,14 @@ class FMTCImageProvider extends ImageProvider { /// Create a specialised [ImageProvider] dedicated to 'flutter_map_tile_caching' FMTCImageProvider({ + required this.storeName, required this.provider, required this.options, required this.coords, }); // ignore: invalid_use_of_protected_member - FMTCBackendInternal get _backend => FMTC.instance.settings.backend.internal; + FMTCBackendInternal get _backend => FMTC.instance.backend.internal; @override ImageStreamCompleter loadImage( @@ -47,7 +51,7 @@ class FMTCImageProvider extends ImageProvider { scale: 1, debugLabel: coords.toString(), informationCollector: () => [ - DiagnosticsProperty('Store name', provider.storeDirectory.storeName), + DiagnosticsProperty('Store name', storeName), DiagnosticsProperty('Tile coordinates', coords), DiagnosticsProperty('Current provider', key), ], @@ -205,7 +209,7 @@ class FMTCImageProvider extends ImageProvider { // Cache the tile retrieved from the network response unawaited( _backend.writeTile( - storeName: provider.storeDirectory.storeName, + storeName: storeName, url: matcherUrl, bytes: responseBytes, ), @@ -216,7 +220,7 @@ class FMTCImageProvider extends ImageProvider { // TODO: Check if performance is acceptable without checking limit excess first unawaited( _backend.removeOldestTilesAboveLimit( - storeName: provider.storeDirectory.storeName, + storeName: storeName, tilesLimit: provider.settings.maxStoreLength, ), ); diff --git a/lib/src/providers/tile_provider.dart b/lib/src/providers/tile_provider.dart index 3d2486fb..371029a4 100644 --- a/lib/src/providers/tile_provider.dart +++ b/lib/src/providers/tile_provider.dart @@ -8,12 +8,9 @@ part of flutter_map_tile_caching; /// Create from the store directory chain, eg. [FMTCStore.getTileProvider]. class FMTCTileProvider extends TileProvider { /// The store directory attached to this provider - final FMTCStore storeDirectory; + final FMTCStore _store; /// The tile provider settings to use - /// - /// Defaults to the one provided by [FMTCSettings] when initialising - /// [FlutterMapTileCaching]. final FMTCTileProviderSettings settings; /// [http.Client] (such as a [IOClient]) used to make all network requests @@ -21,13 +18,12 @@ class FMTCTileProvider extends TileProvider { /// Defaults to a standard [IOClient]/[HttpClient] for HTTP/1.1 servers. final http.Client httpClient; - FMTCTileProvider._({ - required this.storeDirectory, + FMTCTileProvider._( + this._store, { required FMTCTileProviderSettings? settings, - Map headers = const {}, - http.Client? httpClient, - }) : settings = - settings ?? FMTC.instance.settings.defaultTileProviderSettings, + required Map headers, + required http.Client? httpClient, + }) : settings = settings ?? FMTC.instance.defaultTileProviderSettings, httpClient = httpClient ?? IOClient(HttpClient()..userAgent = null), super( headers: { @@ -39,7 +35,7 @@ class FMTCTileProvider extends TileProvider { ); // ignore: invalid_use_of_protected_member - FMTCBackendInternal get _backend => FMTC.instance.settings.backend.internal; + FMTCBackendInternal get _backend => FMTC.instance.backend.internal; /// Closes the open [httpClient] - this will make the provider unable to /// perform network requests @@ -54,18 +50,21 @@ class FMTCTileProvider extends TileProvider { @override ImageProvider getImage(TileCoordinates coords, TileLayer options) => FMTCImageProvider( + storeName: _store.storeName, provider: this, options: options, coords: coords, ); + // TODO: Define deprecation for `Async` + /// Check whether a specified tile is cached in the current store Future checkTileCached({ required TileCoordinates coords, required TileLayer options, }) => _backend.tileExistsInStore( - storeName: storeDirectory.storeName, + storeName: _store.storeName, url: obscureQueryParams( url: getTileUrl(coords, options), obscuredQueryParams: settings.obscuredQueryParams, @@ -76,13 +75,11 @@ class FMTCTileProvider extends TileProvider { bool operator ==(Object other) => identical(this, other) || (other is FMTCTileProvider && - other.runtimeType == runtimeType && - other.httpClient == httpClient && + other._store == _store && + other.headers == headers && other.settings == settings && - other.storeDirectory == storeDirectory && - other.headers == headers); + other.httpClient == httpClient); @override - int get hashCode => - Object.hash(httpClient, settings, storeDirectory, headers); + int get hashCode => Object.hash(_store, settings, headers, httpClient); } diff --git a/lib/src/settings/tile_provider_settings.dart b/lib/src/providers/tile_provider_settings.dart similarity index 100% rename from lib/src/settings/tile_provider_settings.dart rename to lib/src/providers/tile_provider_settings.dart diff --git a/lib/src/settings/fmtc_settings.dart b/lib/src/settings/fmtc_settings.dart deleted file mode 100644 index c051b310..00000000 --- a/lib/src/settings/fmtc_settings.dart +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright © Luka S (JaffaKetchup) under GPL-v3 -// A full license can be found at .\LICENSE - -part of flutter_map_tile_caching; - -/// Global FMTC settings -class FMTCSettings { - /// The database or other storage mechanism that FMTC will use as a cache - /// 'backend' - /// - /// Defaults to [ObjectBoxBackend], which uses the ObjectBox library and - /// database. - /// - /// See [FMTCBackend] for more information. - final FMTCBackend backend; - - /// Default settings used when creating an [FMTCTileProvider] - /// - /// Can be overridden on a case-to-case basis when actually creating the tile - /// provider. - final FMTCTileProviderSettings defaultTileProviderSettings; - - /// Sets a strict upper size limit on each underlying database individually - /// (of which there are multiple) - /// - /// It is also recommended to set a limit on the number of tiles instead, using - /// [FMTCTileProviderSettings.maxStoreLength]. If using a generous number - /// there, use a larger number here as well. - /// - /// Setting this value too low may cause unexpected errors when writing to the - /// database. Setting this value too high may cause memory issues on certain - /// older devices or emulators. - /// - /// Defaults to 2GiB (2048MiB). - final int databaseMaxSize; - - /// Sets conditions that will trigger each underlying database (individually) - /// to compact/shrink - /// - /// Isar databases can contain unused space that will be reused for later - /// operations and storage. This operation can be expensive, as the entire - /// database must be copied. Ensure your chosen conditions do not trigger - /// compaction too often. - /// - /// Defaults to triggering compaction when the size of the database file can - /// be halved. - /// - /// Set to `null` to never automatically compact (not recommended). Note that - /// exporting a store will always compact it's underlying database. - final DatabaseCompactCondition? databaseCompactCondition; - - /// Create custom global FMTC settings - FMTCSettings({ - FMTCBackend? backend, - FMTCTileProviderSettings? defaultTileProviderSettings, - this.databaseMaxSize = 2048, - this.databaseCompactCondition = const CompactCondition(minRatio: 2), - }) : backend = backend ?? ObjectBoxBackend(), - defaultTileProviderSettings = - defaultTileProviderSettings ?? FMTCTileProviderSettings(); -} diff --git a/lib/src/store/directory.dart b/lib/src/store/directory.dart index e1f40b11..daaaf290 100644 --- a/lib/src/store/directory.dart +++ b/lib/src/store/directory.dart @@ -68,17 +68,13 @@ class FMTCStore { /// Get the [TileProvider] suitable to connect the [TileLayer] to FMTC's /// internals - /// - /// Uses [FMTCSettings.defaultTileProviderSettings] by default (and it's - /// default if unspecified). Alternatively, override [settings] for this get - /// only. FMTCTileProvider getTileProvider({ FMTCTileProviderSettings? settings, Map? headers, http.Client? httpClient, }) => FMTCTileProvider._( - storeDirectory: this, + this, settings: settings, headers: headers ?? {}, httpClient: httpClient, diff --git a/lib/src/store/download.dart b/lib/src/store/download.dart index 5a801151..ad4593fc 100644 --- a/lib/src/store/download.dart +++ b/lib/src/store/download.dart @@ -83,7 +83,7 @@ class DownloadManagement { /// /// For information about [obscuredQueryParams], see the /// [online documentation](https://fmtc.jaffaketchup.dev/usage/integration#obscuring-query-parameters). - /// Will default to the current value in [FMTCSettings]. + /// Will default to the value in the default [FMTCTileProviderSettings]. /// /// To set additional headers, set it via [TileProvider.headers] when /// constructing the [DownloadableRegion]. @@ -181,8 +181,7 @@ class DownloadManagement { rateLimit: rateLimit, obscuredQueryParams: obscuredQueryParams?.map((e) => RegExp('$e=[^&]*')) ?? - FMTC.instance.settings.defaultTileProviderSettings - .obscuredQueryParams, + FMTC.instance.defaultTileProviderSettings.obscuredQueryParams, ), onExit: receivePort.sendPort, debugName: '[FMTC] Master Bulk Download Thread', diff --git a/lib/src/store/manage.dart b/lib/src/store/manage.dart index 21a22f14..1452fedc 100644 --- a/lib/src/store/manage.dart +++ b/lib/src/store/manage.dart @@ -3,7 +3,7 @@ part of flutter_map_tile_caching; -/// Manages a [FMTCStore]'s representation on the filesystem, such as +/// Manages an [FMTCStore]'s representation on the filesystem, such as /// creation and deletion /// /// If the store is not in the expected state (of existence) when invoking an @@ -42,9 +42,11 @@ final class StoreManagement extends _WithBackendAccess { Future removeTilesOlderThan({required DateTime expiry}) => _backend.removeTilesOlderThan(storeName: _storeName, expiry: expiry); + // TODO: Define deprecation for `tileImageAsync` + /// {@macro fmtc.backend.readLatestTile} /// , then render the bytes to an [Image] - Future tileImageAsync({ + Future tileImage({ double? size, Key? key, double scale = 1.0, diff --git a/lib/src/store/metadata.dart b/lib/src/store/metadata.dart index 1a5242c8..1030ec17 100644 --- a/lib/src/store/metadata.dart +++ b/lib/src/store/metadata.dart @@ -3,74 +3,37 @@ part of flutter_map_tile_caching; -/// Manage custom miscellaneous information tied to a [FMTCStore] +/// Manage custom miscellaneous information tied to an [FMTCStore] /// /// Uses a key-value format where both key and value must be [String]. More /// advanced requirements should use another package, as this is a basic /// implementation. -class StoreMetadata extends _WithBackendAccess { +final class StoreMetadata extends _WithBackendAccess { const StoreMetadata._(super._store); - /// Add a new key-value pair to the store asynchronously - /// - /// Overwrites the value if the key already exists. - Future addAsync({required String key, required String value}) => - _db.writeTxn(() => _db.metadata.put(DbMetadata(name: key, data: value))); - - /// Add a new key-value pair to the store synchronously - /// - /// Overwrites the value if the key already exists. - /// - /// Prefer [addAsync] to avoid blocking the UI thread. Otherwise, this has - /// slightly better performance. - void add({required String key, required String value}) => _db.writeTxnSync( - () => _db.metadata.putSync(DbMetadata(name: key, data: value)), - ); - - /// Remove a new key-value pair from the store asynchronously - Future removeAsync({required String key}) => - _db.writeTxn(() => _db.metadata.delete(DatabaseTools.hash(key))); - - /// Remove a new key-value pair from the store synchronously - /// - /// Prefer [removeAsync] to avoid blocking the UI thread. Otherwise, this has - /// slightly better performance. - void remove({required String key}) => _db.writeTxnSync( - () => _db.metadata.deleteSync(DatabaseTools.hash(key)), - ); - - /// Remove all the key-value pairs from the store asynchronously - Future resetAsync() => _db.writeTxn( - () async => Future.wait( - (await _db.metadata.where().findAll()) - .map((m) => _db.metadata.delete(m.id)), - ), - ); - - /// Remove all the key-value pairs from the store synchronously - /// - /// Prefer [resetAsync] to avoid blocking the UI thread. Otherwise, this has - /// slightly better performance. - void reset() => _db.writeTxnSync( - () => Future.wait( - _db.metadata - .where() - .findAllSync() - .map((m) => _db.metadata.delete(m.id)), - ), - ); - - /// Read all the key-value pairs from the store asynchronously - Future> get readAsync async => Map.fromEntries( - (await _db.metadata.where().findAll()) - .map((m) => MapEntry(m.name, m.data)), - ); - - /// Read all the key-value pairs from the store synchronously - /// - /// Prefer [readAsync] to avoid blocking the UI thread. Otherwise, this has - /// slightly better performance. - Map get read => Map.fromEntries( - _db.metadata.where().findAllSync().map((m) => MapEntry(m.name, m.data)), - ); + /// {@macro fmtc.backend.readMetadata} + Future> get read => + _backend.readMetadata(storeName: _storeName); + + /// {@macro fmtc.backend.setMetadata} + Future set({ + required String key, + required String value, + }) => + _backend.setMetadata(storeName: _storeName, key: key, value: value); + + /// {@macro fmtc.backend.setBulkMetadata} + Future setBulk({ + required Map kvs, + }) => + _backend.setBulkMetadata(storeName: _storeName, kvs: kvs); + + /// {@macro fmtc.backend.removeMetadata} + Future remove({ + required String key, + }) => + _backend.removeMetadata(storeName: _storeName, key: key); + + /// {@macro fmtc.backend.resetMetadata} + Future reset() => _backend.resetMetadata(storeName: _storeName); } diff --git a/lib/src/store/statistics.dart b/lib/src/store/statistics.dart index dea986e4..a1289bfb 100644 --- a/lib/src/store/statistics.dart +++ b/lib/src/store/statistics.dart @@ -3,22 +3,45 @@ part of flutter_map_tile_caching; -/// Provides statistics about a [FMTCStore] +/// Provides statistics about an [FMTCStore] +/// +/// If the store is not in the expected state (of existence) when invoking an +/// operation, then an error will be thrown (likely [StoreNotExists] or +/// [StoreAlreadyExists]). It is recommended to check [StoreManagement.ready] +/// when necessary. final class StoreStats extends _WithBackendAccess { const StoreStats._(super._store); - /// {@macro fmtc.backend.getStoreSize} - Future get size => _backend.getStoreSize(storeName: _storeName); + /// {@macro fmtc.backend.getStoreStats} + /// + /// {@template fmtc.frontend.storestats.efficiency} + /// Prefer using [all] when multiple statistics are required instead of getting + /// them individually. Only one backend operation is required to get all the + /// stats, and so is more efficient. + /// {@endtemplate} + Future<({double size, int length, int hits, int misses})> get all => + _backend.getStoreStats(storeName: _storeName); + + /// Retrieve the total number of KiBs of all tiles' bytes (not 'real total' + /// size) + /// + /// {@macro fmtc.frontend.storestats.efficiency} + Future get size => all.then((a) => a.size); - /// {@macro fmtc.backend.getStoreLength} - Future get length => _backend.getStoreLength(storeName: _storeName); + /// Retrieve the number of tiles belonging to this store + /// + /// {@macro fmtc.frontend.storestats.efficiency} + Future get length => all.then((a) => a.length); - /// {@macro fmtc.backend.getStoreHits} - Future get cacheHits async => - _backend.getStoreHits(storeName: _storeName); + /// Retrieve the number of successful tile retrievals when browsing + /// + /// {@macro fmtc.frontend.storestats.efficiency} + Future get hits => all.then((a) => a.hits); - /// {@macro fmtc.backend.getStoreMisses} - Future get cacheMisses => _backend.getStoreMisses(storeName: _storeName); + /// Retrieve number of unsuccessful tile retrievals when browsing + /// + /// {@macro fmtc.frontend.storestats.efficiency} + Future get misses => all.then((a) => a.misses); /// Watch for changes in the current store /// From b2e984c6f61ace71e216d1eb24d1b07c7fc5bb02 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Wed, 14 Feb 2024 18:30:41 +0000 Subject: [PATCH 092/168] Removed `FMTC`/`FlutterMapTileCaching` Renamed `RootDirectory` to `FMTCRoot` Improved external visibility rules/setup Improved documentation1 Former-commit-id: 09a6d9e10763d62d14837a567d78efe45667ef52 [formerly 9cbaf67d83e92c70aec4a1d29dfc9552bc0754e4] Former-commit-id: 8084b7d0b5c48467a5dc0fb62420b9f7a18a6922 --- lib/src/backend/context.dart | 28 +++++ lib/src/backend/export_internal.dart | 2 +- lib/src/backend/impls/objectbox/backend.dart | 102 +++++++++++------- .../impls/objectbox/models/models.dart | 2 +- lib/src/backend/impls/objectbox/worker.dart | 4 +- lib/src/backend/interfaces/backend.dart | 75 +++++++------ lib/src/backend/interfaces/models.dart | 4 +- .../bulk_download/tile_loops/generate.dart | 2 + lib/src/fmtc.dart | 17 ++- lib/src/misc/with_backend_access.dart | 3 +- lib/src/providers/image_provider.dart | 1 - lib/src/providers/tile_provider.dart | 5 +- lib/src/root/directory.dart | 53 ++++----- lib/src/root/manage.dart | 2 +- lib/src/root/recovery.dart | 4 +- lib/src/root/statistics.dart | 2 +- lib/src/store/directory.dart | 15 ++- lib/src/store/download.dart | 10 +- 18 files changed, 183 insertions(+), 148 deletions(-) create mode 100644 lib/src/backend/context.dart diff --git a/lib/src/backend/context.dart b/lib/src/backend/context.dart new file mode 100644 index 00000000..f75c24c5 --- /dev/null +++ b/lib/src/backend/context.dart @@ -0,0 +1,28 @@ +// Copyright © Luka S (JaffaKetchup) under GPL-v3 +// A full license can be found at .\LICENSE + +import 'package:meta/meta.dart' as meta; + +import 'export_internal.dart'; + +/// Provides access to the backend in use throughout FMTC internals +/// +/// Designed to allow a single backend to set and control the context at once. +/// +/// Avoid using externally. Never set `context` externally. +@meta.internal +abstract mixin class FMTCBackendAccess { + static FMTCBackendInternal? _internal; + + @meta.internal + @meta.experimental + static FMTCBackendInternal get internal => + _internal ?? (throw RootUnavailable()); + + @meta.internal + @meta.protected + static set internal(FMTCBackendInternal? newInternal) { + if (_internal != null) throw RootAlreadyInitialised(); + _internal = newInternal; + } +} diff --git a/lib/src/backend/export_internal.dart b/lib/src/backend/export_internal.dart index 055972c9..10686dc0 100644 --- a/lib/src/backend/export_internal.dart +++ b/lib/src/backend/export_internal.dart @@ -1,6 +1,6 @@ // Copyright © Luka S (JaffaKetchup) under GPL-v3 // A full license can be found at .\LICENSE +export 'context.dart'; export 'export_external.dart'; - export 'interfaces/models.dart'; diff --git a/lib/src/backend/impls/objectbox/backend.dart b/lib/src/backend/impls/objectbox/backend.dart index b706e67c..2bdf9237 100644 --- a/lib/src/backend/impls/objectbox/backend.dart +++ b/lib/src/backend/impls/objectbox/backend.dart @@ -8,34 +8,72 @@ import 'dart:isolate'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; -import 'package:meta/meta.dart' as meta; +import 'package:path/path.dart' as path; +import 'package:path_provider/path_provider.dart'; -import '../../../../flutter_map_tile_caching.dart'; +import '../../export_internal.dart'; import 'models/generated/objectbox.g.dart'; import 'models/models.dart'; part 'worker.dart'; /// Implementation of [FMTCBackend] that uses ObjectBox as the storage database -final class ObjectBoxBackend implements FMTCBackend { - /// It is not recommended to access this method externally +final class FMTCObjectBoxBackend implements FMTCBackend { + /// {@macro fmtc.backend.inititialise} + /// + /// [maxDatabaseSize] is the maximum size the database file can grow + /// to, in KB. Exceeding it throws [DbFullException]. Defaults to 10 GB. + /// + /// [macosApplicationGroup] should be set when creating a sandboxed macOS app, + /// specify the application group (of less than 20 chars). See + /// [the ObjectBox docs](https://docs.objectbox.io/getting-started) for + /// details. + @override + Future initialise({ + String? rootDirectory, + int maxDatabaseSize = 10000000, + String? macosApplicationGroup, + }) => + FMTCObjectBoxBackendInternal._instance.initialise( + rootDirectory: rootDirectory, + maxDatabaseSize: maxDatabaseSize, + macosApplicationGroup: macosApplicationGroup, + ); + + /// {@macro fmtc.backend.uninitialise} + /// + /// If [immediate] is `true`, any operations currently underway will be lost. + /// If `false`, all operations currently underway will be allowed to complete, + /// but any operations started after this method call will be lost. A lost + /// operation may throw [RootUnavailable]. This parameter may not have a + /// noticable/any effect in some implementations. @override - @meta.internal - FMTCBackendInternal get internal => ObjectBoxBackendInternal._instance; + Future uninitialise({ + bool deleteRoot = false, + bool immediate = false, + }) => + FMTCObjectBoxBackendInternal._instance + .uninitialise(deleteRoot: deleteRoot, immediate: immediate); } /// Internal implementation of [FMTCBackend] that uses ObjectBox as the storage /// database -abstract interface class ObjectBoxBackendInternal +abstract interface class FMTCObjectBoxBackendInternal implements FMTCBackendInternal { static final _instance = _ObjectBoxBackendImpl._(); } -class _ObjectBoxBackendImpl implements ObjectBoxBackendInternal { +class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { _ObjectBoxBackendImpl._(); + @override + String get friendlyIdentifier => 'ObjectBox'; + void get expectInitialised => _sendPort ?? (throw RootUnavailable()); + @override + Directory? rootDirectory; + // Worker communication SendPort? _sendPort; final Map?>> _workerRes = {}; @@ -70,29 +108,10 @@ class _ObjectBoxBackendImpl implements ObjectBoxBackendInternal { throw err; } - @override - String get friendlyIdentifier => 'ObjectBox'; - - /// See [FMTCBackendInternal.initialise] & [FlutterMapTileCaching.initialise] - /// for more info. - /// - /// --- - /// - /// This implementation additionally accepts the following [implSpecificArgs]: - /// - /// * 'macosApplicationGroup' (`String`): when creating a sandboxed macOS app, - /// use to specify the application group (of less than 20 chars). See - /// [the ObjectBox docs](https://docs.objectbox.io/getting-started) for - /// details. - /// * 'maxDatabaseSize' (`int`): the maximum size the database file can grow - /// to. Exceeding it throws [DbFullException]. Defaults to 10 GB. - /// - /// These arguments are optional. However, failure to provide them in the - /// specified type will result in an uncaught type casting error. - @override Future initialise({ - required Directory rootDirectory, - required Map implSpecificArgs, + required String? rootDirectory, + required int maxDatabaseSize, + required String? macosApplicationGroup, }) async { if (_sendPort != null) throw RootAlreadyInitialised(); @@ -102,6 +121,11 @@ class _ObjectBoxBackendImpl implements ObjectBoxBackendInternal { //_rotalStore = null; //_rotalLength = 0; + this.rootDirectory = await (rootDirectory == null + ? await getApplicationDocumentsDirectory() + : Directory(path.join(rootDirectory, 'fmtc'))) + .create(recursive: true); + // Prepare to recieve `SendPort` from worker _workerRes[0] = Completer(); unawaited( @@ -127,20 +151,20 @@ class _ObjectBoxBackendImpl implements ObjectBoxBackendInternal { _worker, ( sendPort: receivePort.sendPort, - rootDirectory: rootDirectory, - maxDatabaseSize: implSpecificArgs['maxDatabaseSize'] as int?, - macosApplicationGroup: - implSpecificArgs['macosApplicationGroup'] as String?, + rootDirectory: this.rootDirectory!, + maxDatabaseSize: maxDatabaseSize, + macosApplicationGroup: macosApplicationGroup, ), onExit: receivePort.sendPort, debugName: '[FMTC] ObjectBox Backend Worker', ); + + FMTCBackendAccess.internal = this; } - @override - Future destroy({ - bool deleteRoot = false, - bool immediate = false, + Future uninitialise({ + required bool deleteRoot, + required bool immediate, }) async { expectInitialised; @@ -171,6 +195,8 @@ class _ObjectBoxBackendImpl implements ObjectBoxBackendInternal { for (final completer in _workerRes.values) { completer.complete({'error': RootUnavailable()}); } + + FMTCBackendAccess.internal = null; } @override diff --git a/lib/src/backend/impls/objectbox/models/models.dart b/lib/src/backend/impls/objectbox/models/models.dart index 9e2935d3..1fa5dca6 100644 --- a/lib/src/backend/impls/objectbox/models/models.dart +++ b/lib/src/backend/impls/objectbox/models/models.dart @@ -18,7 +18,7 @@ base class ObjectBoxStore extends BackendStore { String name; @Index() - @Backlink() + @Backlink('stores') final tiles = ToMany(); int length; diff --git a/lib/src/backend/impls/objectbox/worker.dart b/lib/src/backend/impls/objectbox/worker.dart index b7a6da90..67dbae7a 100644 --- a/lib/src/backend/impls/objectbox/worker.dart +++ b/lib/src/backend/impls/objectbox/worker.dart @@ -30,7 +30,7 @@ Future _worker( ({ SendPort sendPort, Directory rootDirectory, - int? maxDatabaseSize, + int maxDatabaseSize, String? macosApplicationGroup, }) input, ) async { @@ -47,7 +47,7 @@ Future _worker( // Initialise database final root = await openStore( directory: input.rootDirectory.absolute.path, - maxDBSizeInKB: input.maxDatabaseSize ?? 10000000, // Defaults to 10 GB + maxDBSizeInKB: input.maxDatabaseSize, // Defaults to 10 GB macosApplicationGroup: input.macosApplicationGroup, ); diff --git a/lib/src/backend/interfaces/backend.dart b/lib/src/backend/interfaces/backend.dart index 7482c1be..ddb6db3c 100644 --- a/lib/src/backend/interfaces/backend.dart +++ b/lib/src/backend/interfaces/backend.dart @@ -5,11 +5,9 @@ import 'dart:async'; import 'dart:io'; import 'dart:typed_data'; -import 'package:meta/meta.dart'; - -import '../../../flutter_map_tile_caching.dart'; -import 'models.dart'; +import '../export_internal.dart'; +/// {@template fmtc.backend.backend} /// An abstract interface that FMTC will use to communicate with a storage /// 'backend' (usually one root) /// @@ -19,17 +17,38 @@ import 'models.dart'; /// To implementers: /// * Provide a seperate [FMTCBackend] & [FMTCBackendInternal] implementation /// (both public scope), and a private scope `FMTCBackendImpl` -/// * Annotate your [FMTCBackend.internal] method with '@internal' /// * Always make [FMTCBackendInternal] a singleton 'cover-up' for -/// `FMTCBackendImpl`, without a constructor, as the `FMTCBackendImpl` will -/// be accessed via [FMTCBackend.internal] +/// `FMTCBackendImpl`, without a constructor /// * Prefer throwing included implementation-generic errors/exceptions -/// * See the default [ObjectBoxBackend] implementation for an example +/// * Ensure the [FMTCBackendInternal]/impl can be sent through isolates +/// * Always set the [FMTCBackendAccess.internal] property as necessary +/// +/// See the default [FMTCObjectBoxBackend] implementation for an example. +/// {@endtemplate} abstract interface class FMTCBackend { + /// {@macro fmtc.backend.backend} const FMTCBackend(); - @protected - Internal get internal; + /// {@template fmtc.backend.inititialise} + /// Initialise this backend, and create the root + /// + /// Prefer to leave [rootDirectory] as null, which will use + /// `getApplicationDocumentsDirectory()`. Alternatively, pass a custom + /// directory - it is recommended to not use a typical cache directory, as the + /// OS can clear these without notice at any time. + /// {@endtemplate} + Future initialise({ + String? rootDirectory, + }); + + /// {@template fmtc.backend.uninitialise} + /// Uninitialise this backend, and release whatever resources it is consuming + /// + /// If [deleteRoot] is `true`, then the root will be permanently deleted. + /// {@endtemplate} + Future uninitialise({ + bool deleteRoot = false, + }); } /// An abstract interface that FMTC will use to communicate with a storage @@ -39,39 +58,17 @@ abstract interface class FMTCBackend { /// invocation. /// /// See [FMTCBackend] for more information. -abstract interface class FMTCBackendInternal { +abstract interface class FMTCBackendInternal with FMTCBackendAccess { + const FMTCBackendInternal._(); + /// Generic description/name of this backend abstract final String friendlyIdentifier; - /// Initialise this backend & create the root - /// - /// See [FlutterMapTileCaching.initialise] for more information. - /// - /// Some implementations may accept/require additional arguments that may - /// be set through [implSpecificArgs]. See their documentation for more - /// information. Note to implementers: if you accept implementation specific - /// arguments, ensure you properly document these. - Future initialise({ - required Directory rootDirectory, - required Map implSpecificArgs, - }); - - /// {@template fmtc.backend.destroy} - /// Uninitialise this backend, and release whatever resources it is consuming - /// - /// If [deleteRoot] is `true`, then the storage medium will be permanently - /// deleted. + /// The filesystem directory in use /// - /// If [immediate] is `true`, any operations currently underway will be lost. - /// If `false`, all operations currently underway will be allowed to complete, - /// but any operations started after this method call will be lost. A lost - /// operation may throw [RootUnavailable]. This parameter may not have a - /// noticable/any effect in some implementations. - /// {@endtemplate} - Future destroy({ - required bool deleteRoot, - required bool immediate, - }); + /// May also be used as an indicator as to whether the root has been + /// initialised. + Directory? get rootDirectory; /// {@template fmtc.backend.storeExists} /// Check whether the specified store currently exists diff --git a/lib/src/backend/interfaces/models.dart b/lib/src/backend/interfaces/models.dart index 6b0af9e4..942567d6 100644 --- a/lib/src/backend/interfaces/models.dart +++ b/lib/src/backend/interfaces/models.dart @@ -25,7 +25,7 @@ abstract base class BackendStore { /// therefore not recommended. @override @nonVirtual - bool operator ==(Object? other) => + bool operator ==(Object other) => identical(this, other) || (other is BackendStore && name == other.name); @override @@ -60,7 +60,7 @@ abstract base class BackendTile { /// therefore not recommended. @override @nonVirtual - bool operator ==(Object? other) => + bool operator ==(Object other) => identical(this, other) || (other is BackendTile && url == other.url); @override diff --git a/lib/src/bulk_download/tile_loops/generate.dart b/lib/src/bulk_download/tile_loops/generate.dart index dcff6816..59403fcd 100644 --- a/lib/src/bulk_download/tile_loops/generate.dart +++ b/lib/src/bulk_download/tile_loops/generate.dart @@ -47,6 +47,8 @@ class TilesGenerator { // 4. Loop over these XY values and add them to the list // Theoretically, this could have been done using the same method as `lineTiles`, but `lineTiles` was built after this algorithm and this makes more sense for a circle + // TODO: https://en.wikipedia.org/wiki/Midpoint_circle_algorithm + final region = input.region as DownloadableRegion; final circleOutline = region.originalRegion.toOutline(); diff --git a/lib/src/fmtc.dart b/lib/src/fmtc.dart index 09959a8b..2ed03343 100644 --- a/lib/src/fmtc.dart +++ b/lib/src/fmtc.dart @@ -3,7 +3,7 @@ part of flutter_map_tile_caching; -/// Direct alias of [FlutterMapTileCaching] for easier development +/*/// Direct alias of [FlutterMapTileCaching] for easier development /// /// Prefer use of full 'FlutterMapTileCaching' when initialising to ensure /// readability and understanding in other code. @@ -19,7 +19,7 @@ typedef FMTC = FlutterMapTileCaching; /// [FMTC] is an alias for this object. class FlutterMapTileCaching { /// The directory which contains all databases required to use FMTC - final RootDirectory rootDirectory; + final FMTCRoot rootDirectory; /// The database or other storage mechanism that FMTC will use as a cache /// 'backend' @@ -43,11 +43,6 @@ class FlutterMapTileCaching { required this.defaultTileProviderSettings, }); - /// {@macro fmtc.backend.initialise} - /// - /// - /// {@macro fmtc.backend.objectbox.initialise} - /// /// Initialise and prepare FMTC, by creating all necessary directories/files /// and configuring the [FlutterMapTileCaching] singleton /// @@ -97,9 +92,7 @@ class FlutterMapTileCaching { .create(recursive: true); backend ??= ObjectBoxBackend(); - defaultTileProviderSettings ??= FMTCTileProviderSettings(); - // ignore: invalid_use_of_protected_member await backend.internal.initialise( rootDirectory: dir, implSpecificArgs: { @@ -110,9 +103,10 @@ class FlutterMapTileCaching { ); return _instance = FMTC._( - rootDirectory: RootDirectory._(dir), + rootDirectory: FMTCRoot._(dir), backend: backend, - defaultTileProviderSettings: defaultTileProviderSettings, + defaultTileProviderSettings: + defaultTileProviderSettings ?? FMTCTileProviderSettings(), ); } @@ -136,3 +130,4 @@ Use `FlutterMapTileCaching.initialise()` before getting /// provided for backwards-compatibility. FMTCStore call(String storeName) => FMTCStore(storeName); } +*/ \ No newline at end of file diff --git a/lib/src/misc/with_backend_access.dart b/lib/src/misc/with_backend_access.dart index 2897dc48..55f9be41 100644 --- a/lib/src/misc/with_backend_access.dart +++ b/lib/src/misc/with_backend_access.dart @@ -7,7 +7,6 @@ abstract base class _WithBackendAccess { const _WithBackendAccess(this._store); final FMTCStore _store; - // ignore: invalid_use_of_protected_member - FMTCBackendInternal get _backend => FMTC.instance.backend.internal; + FMTCBackendInternal get _backend => FMTCBackendAccess.internal; String get _storeName => _store.storeName; } diff --git a/lib/src/providers/image_provider.dart b/lib/src/providers/image_provider.dart index f447d758..f28eb1d3 100644 --- a/lib/src/providers/image_provider.dart +++ b/lib/src/providers/image_provider.dart @@ -36,7 +36,6 @@ class FMTCImageProvider extends ImageProvider { required this.coords, }); - // ignore: invalid_use_of_protected_member FMTCBackendInternal get _backend => FMTC.instance.backend.internal; @override diff --git a/lib/src/providers/tile_provider.dart b/lib/src/providers/tile_provider.dart index 371029a4..b9ec4de8 100644 --- a/lib/src/providers/tile_provider.dart +++ b/lib/src/providers/tile_provider.dart @@ -34,9 +34,6 @@ class FMTCTileProvider extends TileProvider { }, ); - // ignore: invalid_use_of_protected_member - FMTCBackendInternal get _backend => FMTC.instance.backend.internal; - /// Closes the open [httpClient] - this will make the provider unable to /// perform network requests @override @@ -63,7 +60,7 @@ class FMTCTileProvider extends TileProvider { required TileCoordinates coords, required TileLayer options, }) => - _backend.tileExistsInStore( + FMTCBackendAccess.internal.tileExistsInStore( storeName: _store.storeName, url: obscureQueryParams( url: getTileUrl(coords, options), diff --git a/lib/src/root/directory.dart b/lib/src/root/directory.dart index dde2afe3..8e03967a 100644 --- a/lib/src/root/directory.dart +++ b/lib/src/root/directory.dart @@ -3,53 +3,46 @@ part of flutter_map_tile_caching; -/// Represents the root directory and root databases +/// Equivalent to [FMTCRoot], provided to ease migration only /// -/// Note that this does not provide direct access to any [FMTCStore]s. +/// The name refers to earlier versions of this library where the filesystem +/// was used for storage, instead of a database. /// -/// The name originates from previous versions of this library, where it -/// represented a real directory instead of a database. +/// This deprecation typedef will be removed in a future release: migrate to +/// [FMTCRoot]. +@Deprecated( + ''' +Migrate to `FMTCRoot`. This deprecation typedef is provided to ease migration +only. It will be removed in a future version. +''', +) +typedef RootDirectory = FMTCRoot; + +/// Provides access to management, statistics, recovery, migration (and the +/// import functionality) on the intitialised root. /// -/// Reach through [FlutterMapTileCaching.rootDirectory]. -class RootDirectory { - const RootDirectory._(this.directory); - - /// The real directory beneath which FMTC places all data - usually located - /// within the application's directories - /// - /// Provides low level access. Use with caution, and prefer built-in methods! - /// Corrupting some databases, for example the registry, can lead to data - /// loss from multiple stores. - @internal - @protected - final Directory directory; +/// Note that this does not provide direct access to any [FMTCStore]s. +abstract class FMTCRoot { + const FMTCRoot._(); /// Manage the root's representation on the filesystem /// /// To create, initialise FMTC. Assume that FMTC is ready after initialisation /// and before [RootManagement.delete] is called. - RootManagement get manage => const RootManagement._(); + static RootManagement get manage => const RootManagement._(); /// Get statistics about this root (and all sub-stores) - RootStats get stats => const RootStats._(); + static RootStats get stats => const RootStats._(); /// Manage the download recovery of all sub-stores - RootRecovery get recovery => RootRecovery.instance ?? RootRecovery._(); + static RootRecovery get recovery => RootRecovery.instance ?? RootRecovery._(); /// Manage migration for file structure across FMTC versions - RootMigrator get migrator => const RootMigrator._(); + static RootMigrator get migrator => const RootMigrator._(); /// Provides store import functionality for this root /// /// The 'fmtc_plus_sharing' module must be installed to add the functionality, /// without it, this object provides no functionality. - RootImport get import => const RootImport._(); - - @override - bool operator ==(Object other) => - identical(this, other) || - (other is RootDirectory && other.directory == directory); - - @override - int get hashCode => directory.hashCode; + static RootImport get import => const RootImport._(); } diff --git a/lib/src/root/manage.dart b/lib/src/root/manage.dart index ccf19ded..e372ce1d 100644 --- a/lib/src/root/manage.dart +++ b/lib/src/root/manage.dart @@ -3,7 +3,7 @@ part of flutter_map_tile_caching; -/// Manages a [RootDirectory]'s representation on the filesystem, such as +/// Manages a [FMTCRoot]'s representation on the filesystem, such as /// creation and deletion class RootManagement { const RootManagement._(); diff --git a/lib/src/root/recovery.dart b/lib/src/root/recovery.dart index 88a0cae4..a6f2e239 100644 --- a/lib/src/root/recovery.dart +++ b/lib/src/root/recovery.dart @@ -3,7 +3,7 @@ part of flutter_map_tile_caching; -/// Manages the download recovery of all sub-stores of this [RootDirectory] +/// Manages the download recovery of all sub-stores of this [FMTCRoot] /// /// Is a singleton to ensure functioning as expected. class RootRecovery { @@ -13,7 +13,7 @@ class RootRecovery { Isar get _recovery => FMTCRegistry.instance.recoveryDatabase; - /// Manages the download recovery of all sub-stores of this [RootDirectory] + /// Manages the download recovery of all sub-stores of this [FMTCRoot] /// /// Is a singleton to ensure functioning as expected. static RootRecovery? instance; diff --git a/lib/src/root/statistics.dart b/lib/src/root/statistics.dart index 100f1cca..f38a32ea 100644 --- a/lib/src/root/statistics.dart +++ b/lib/src/root/statistics.dart @@ -3,7 +3,7 @@ part of flutter_map_tile_caching; -/// Provides statistics about a [RootDirectory] +/// Provides statistics about a [FMTCRoot] class RootStats { const RootStats._(); diff --git a/lib/src/store/directory.dart b/lib/src/store/directory.dart index daaaf290..d8203a1e 100644 --- a/lib/src/store/directory.dart +++ b/lib/src/store/directory.dart @@ -18,24 +18,21 @@ only. It will be removed in a future version. ) typedef StoreDirectory = FMTCStore; -/// Container for a [storeName] which includes methods and getters to access -/// functionality based on the specified store using resources from the ambient -/// initialised [FlutterMapTileCaching] +/// Provides access to management, statistics, metadata, bulk download, +/// the tile provider (and the export functionality) on the store named +/// [storeName] /// /// {@template fmtc.fmtcstore.sub.noautocreate} /// Note that constructing an instance of this class will not automatically /// create it, as this is an asynchronous operation. To create this store, use /// [manage] > [StoreManagement.create]. /// {@endtemplate} -/// -/// May be constructed via [FlutterMapTileCaching.call], or directly. class FMTCStore { - /// Container for a [storeName] which includes methods and getters to access - /// functionality based on the specified store + /// Provides access to management, statistics, metadata, bulk download, + /// the tile provider (and the export functionality) on the store named + /// [storeName] /// /// {@macro fmtc.fmtcstore.sub.noautocreate} - /// - /// May be constructed via [FlutterMapTileCaching.call], or directly. const FMTCStore(this.storeName); /// The user-friendly name of the store directory diff --git a/lib/src/store/download.dart b/lib/src/store/download.dart index ad4593fc..fdcacea5 100644 --- a/lib/src/store/download.dart +++ b/lib/src/store/download.dart @@ -104,6 +104,8 @@ class DownloadManagement { List? obscuredQueryParams, Object instanceId = 0, }) async* { + FMTCBackendAccess.internal; // Verify intialisation + // Check input arguments for suitability if (!(region.options.wmsOptions != null || region.options.urlTemplate != null)) { @@ -157,7 +159,7 @@ class DownloadManagement { final recoveryId = Object.hash(instanceId, DateTime.now().millisecondsSinceEpoch); if (!disableRecovery) { - await FMTC.instance.rootDirectory.recovery._start( + await FMTCRoot.recovery._start( id: recoveryId, storeName: _storeDirectory.storeName, region: region, @@ -170,7 +172,7 @@ class DownloadManagement { _downloadManager, ( sendPort: receivePort.sendPort, - rootDirectory: FMTC.instance.rootDirectory.directory.absolute.path, + rootDirectory: FMTCBackendAccess.internal.rootDirectory!.absolute.path, region: region, storeName: _storeDirectory.storeName, parallelThreads: parallelThreads, @@ -181,7 +183,7 @@ class DownloadManagement { rateLimit: rateLimit, obscuredQueryParams: obscuredQueryParams?.map((e) => RegExp('$e=[^&]*')) ?? - FMTC.instance.defaultTileProviderSettings.obscuredQueryParams, + FMTC.defaultTileProviderSettings.obscuredQueryParams, ), onExit: receivePort.sendPort, debugName: '[FMTC] Master Bulk Download Thread', @@ -229,7 +231,7 @@ class DownloadManagement { // Handle shutdown (both normal and cancellation) receivePort.close(); - await FMTC.instance.rootDirectory.recovery.cancel(recoveryId); + await FMTCRoot.recovery.cancel(recoveryId); DownloadInstance.unregister(instanceId); cancelCompleter.complete(); } From 3e23c9a10f8bdb53f54d7c01bac19e3eb491cb5d Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Wed, 14 Feb 2024 23:02:13 +0000 Subject: [PATCH 093/168] Add support to ObjectBox backend for root statistics Removed v6 -> v7 migrator Removed `RootManagement` Former-commit-id: ec98a2e99ef438af1e88ad6e8f5fa951a0f0d1fb [formerly 08d6dfc3e7920169df8d1d7f4aa92eef6ddb1479] Former-commit-id: f8e99ec9e29bd786b22ab00a39965da1a8ffd440 --- lib/flutter_map_tile_caching.dart | 3 - lib/src/backend/impls/objectbox/backend.dart | 14 +- lib/src/backend/impls/objectbox/worker.dart | 38 +++ lib/src/backend/interfaces/backend.dart | 16 ++ lib/src/misc/exts.dart | 29 -- lib/src/root/directory.dart | 13 +- lib/src/root/manage.dart | 45 ---- lib/src/root/migrator.dart | 266 +------------------ lib/src/root/statistics.dart | 59 +--- 9 files changed, 81 insertions(+), 402 deletions(-) delete mode 100644 lib/src/misc/exts.dart delete mode 100644 lib/src/root/manage.dart diff --git a/lib/flutter_map_tile_caching.dart b/lib/flutter_map_tile_caching.dart index 0161d451..98d8a2c1 100644 --- a/lib/flutter_map_tile_caching.dart +++ b/lib/flutter_map_tile_caching.dart @@ -29,7 +29,6 @@ import 'package:isar/isar.dart'; import 'package:latlong2/latlong.dart'; import 'package:meta/meta.dart'; import 'package:path/path.dart' as path; -import 'package:path_provider/path_provider.dart'; import 'package:stream_transform/stream_transform.dart'; import 'package:watcher/watcher.dart'; @@ -38,7 +37,6 @@ import 'src/bulk_download/instance.dart'; import 'src/bulk_download/rate_limited_stream.dart'; import 'src/bulk_download/tile_loops/shared.dart'; import 'src/errors/browsing.dart'; -import 'src/misc/exts.dart'; import 'src/misc/int_extremes.dart'; import 'src/misc/obscure_query_params.dart'; import 'src/providers/image_provider.dart'; @@ -64,7 +62,6 @@ part 'src/regions/recovered_region.dart'; part 'src/regions/rectangle.dart'; part 'src/root/directory.dart'; part 'src/root/import.dart'; -part 'src/root/manage.dart'; part 'src/root/migrator.dart'; part 'src/root/recovery.dart'; part 'src/root/statistics.dart'; diff --git a/lib/src/backend/impls/objectbox/backend.dart b/lib/src/backend/impls/objectbox/backend.dart index 2bdf9237..e889a1b2 100644 --- a/lib/src/backend/impls/objectbox/backend.dart +++ b/lib/src/backend/impls/objectbox/backend.dart @@ -89,7 +89,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { Future?> _sendCmd({ required _WorkerCmdType type, - required Map args, + Map args = const {}, }) async { expectInitialised; @@ -199,6 +199,18 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { FMTCBackendAccess.internal = null; } + @override + Future> listStores() async => + (await _sendCmd(type: _WorkerCmdType.storeExists))!['stores']; + + @override + Future rootSize() async => + (await _sendCmd(type: _WorkerCmdType.rootSize))!['size']; + + @override + Future rootLength() async => + (await _sendCmd(type: _WorkerCmdType.rootLength))!['length']; + @override Future storeExists({ required String storeName, diff --git a/lib/src/backend/impls/objectbox/worker.dart b/lib/src/backend/impls/objectbox/worker.dart index 67dbae7a..e5df4fba 100644 --- a/lib/src/backend/impls/objectbox/worker.dart +++ b/lib/src/backend/impls/objectbox/worker.dart @@ -6,6 +6,9 @@ part of 'backend.dart'; enum _WorkerCmdType { initialise_, // Only valid as a response destroy_, // Only valid as a request + listStores, + rootSize, + rootLength, storeExists, createStore, resetStore, @@ -132,6 +135,41 @@ Future _worker( // TODO: Consider final message Isolate.exit(); + case _WorkerCmdType.listStores: + final query = root + .box() + .query() + .build() + .property(ObjectBoxStore_.name); + + sendRes(id: cmd.id, data: {'stores': query.find()}); + + query.close(); + + break; + case _WorkerCmdType.rootSize: + final query = root + .box() + .query() + .build() + .property(ObjectBoxStore_.size); + + sendRes( + id: cmd.id, + data: {'size': query.find().sum / 1024}, // Convert to KiB + ); + + query.close(); + + break; + case _WorkerCmdType.rootLength: + final query = root.box().query().build(); + + sendRes(id: cmd.id, data: {'length': query.count()}); + + query.close(); + + break; case _WorkerCmdType.storeExists: final query = root .box() diff --git a/lib/src/backend/interfaces/backend.dart b/lib/src/backend/interfaces/backend.dart index ddb6db3c..1986d954 100644 --- a/lib/src/backend/interfaces/backend.dart +++ b/lib/src/backend/interfaces/backend.dart @@ -70,6 +70,22 @@ abstract interface class FMTCBackendInternal with FMTCBackendAccess { /// initialised. Directory? get rootDirectory; + /// {@template fmtc.backend.listStores} + /// List all the available stores + /// {@endtemplate} + Future> listStores(); + + /// {@template fmtc.backend.rootSize} + /// Retrieve the total number of KiBs of all tiles' bytes (not 'real total' + /// size) from all stores + /// {@endtemplate} + Future rootSize(); + + /// {@template fmtc.backend.rootLength} + /// Retrieve the total number of tiles in all stores + /// {@endtemplate} + Future rootLength(); + /// {@template fmtc.backend.storeExists} /// Check whether the specified store currently exists /// {@endtemplate} diff --git a/lib/src/misc/exts.dart b/lib/src/misc/exts.dart deleted file mode 100644 index d9484144..00000000 --- a/lib/src/misc/exts.dart +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright © Luka S (JaffaKetchup) under GPL-v3 -// A full license can be found at .\LICENSE - -import 'dart:io'; - -import 'package:meta/meta.dart'; -import 'package:path/path.dart' as p; - -@internal -extension DirectoryExtensions on Directory { - String operator >(String sub) => p.join( - absolute.path, - sub, - ); - - Directory operator >>(String sub) => Directory( - p.join( - absolute.path, - sub, - ), - ); - - File operator >>>(String name) => File( - p.join( - absolute.path, - name, - ), - ); -} diff --git a/lib/src/root/directory.dart b/lib/src/root/directory.dart index 8e03967a..30e639d2 100644 --- a/lib/src/root/directory.dart +++ b/lib/src/root/directory.dart @@ -18,19 +18,16 @@ only. It will be removed in a future version. ) typedef RootDirectory = FMTCRoot; -/// Provides access to management, statistics, recovery, migration (and the -/// import functionality) on the intitialised root. +/// Provides access to statistics, recovery, migration (and the import +/// functionality) on the intitialised root. +/// +/// Management services are not provided here, instead use methods on the backend +/// directly. /// /// Note that this does not provide direct access to any [FMTCStore]s. abstract class FMTCRoot { const FMTCRoot._(); - /// Manage the root's representation on the filesystem - /// - /// To create, initialise FMTC. Assume that FMTC is ready after initialisation - /// and before [RootManagement.delete] is called. - static RootManagement get manage => const RootManagement._(); - /// Get statistics about this root (and all sub-stores) static RootStats get stats => const RootStats._(); diff --git a/lib/src/root/manage.dart b/lib/src/root/manage.dart deleted file mode 100644 index e372ce1d..00000000 --- a/lib/src/root/manage.dart +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright © Luka S (JaffaKetchup) under GPL-v3 -// A full license can be found at .\LICENSE - -part of flutter_map_tile_caching; - -/// Manages a [FMTCRoot]'s representation on the filesystem, such as -/// creation and deletion -class RootManagement { - const RootManagement._(); - - /// Unintialise/close open databases, and delete the root directory and its - /// contents - /// - /// This will remove all traces of this root from the user's device. Use with - /// caution! - Future delete() async { - await FMTCRegistry.instance.uninitialise(delete: true); - await FMTC.instance.rootDirectory.directory.delete(recursive: true); - FMTC._instance = null; - } - - /// Reset the root directory, database, and stores - /// - /// Internally calls [delete] then re-initialises FMTC with the same root - /// directory, [FMTCSettings], and debug mode. Other setup is lost: need to - /// further customise the [FlutterMapTileCaching.initialise]? Use [delete], - /// then re-initialise yourself. - /// - /// This will remove all traces of this root from the user's device. Use with - /// caution! - /// - /// Returns the new [FlutterMapTileCaching] instance. - Future reset() async { - final directory = FMTC.instance.rootDirectory.directory.absolute.path; - final settings = FMTC.instance.settings; - final debugMode = FMTC.instance.debugMode; - - await delete(); - return FMTC.initialise( - rootDirectory: directory, - settings: settings, - debugMode: debugMode, - ); - } -} diff --git a/lib/src/root/migrator.dart b/lib/src/root/migrator.dart index 68a93fd0..fd3159cc 100644 --- a/lib/src/root/migrator.dart +++ b/lib/src/root/migrator.dart @@ -1,273 +1,11 @@ // Copyright © Luka S (JaffaKetchup) under GPL-v3 // A full license can be found at .\LICENSE -// ignore_for_file: comment_references - part of flutter_map_tile_caching; /// Manage migration for file structure across FMTC versions +/// +/// There is no migration available to v9. class RootMigrator { const RootMigrator._(); - - /// Migrates a v6 file structure to a v7 structure - /// - /// Note that this method can be slow on large tilesets, so it's best to offer - /// a choice to your users as to whether they would like to migrate, or just - /// lose all stored tiles. - /// - /// Checks within `getApplicationDocumentsDirectory()` and - /// `getTemporaryDirectory()` for a directory named 'fmtc'. Alternatively, - /// specify a [customDirectory] to search for 'fmtc' within. - /// - /// In order to migrate the tiles to the new format, [urlTemplates] must be - /// used. Pass every URL template used to store any of the tiles that might be - /// in the store. Specifying an empty list will use the preset OSM tile servers - /// only. - /// - /// Set [deleteOldStructure] to `false` to keep the old structure. If a store - /// exists with the same name, it will not be overwritten, and the - /// [deleteOldStructure] parameter will be followed regardless. - /// - /// Only supports placeholders in the normal flutter_map form, those that meet - /// the RegEx: `\{ *([\w_-]+) *\}`. Only supports tiles that were sanitised - /// with the default sanitiser included in FMTC. - /// - /// Recovery information and cached statistics will be lost. - /// - /// Returns `null` if no structure root was found, otherwise a [Map] of the - /// store names to the number of failed tiles (tiles that could not be matched - /// to any of the [urlTemplates]), or `null` if it was skipped because there - /// was an existing store with the same name. A successful migration will have - /// all values 0. - Future?> fromV6({ - required List urlTemplates, - Directory? customDirectory, - bool deleteOldStructure = true, - }) async { - // Prepare the migration regular expressions - final placeholderRegex = RegExp(r'\{ *([\w_-]+) *\}'); - final matchables = [ - ...[ - 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', - 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', - ...urlTemplates, - ].map((url) { - final sanitised = _defaultFilesystemSanitiser(url).validOutput; - - return [ - sanitised.replaceAll('.', r'\.').replaceAll(placeholderRegex, '.+?'), - sanitised, - url, - ]; - }), - ]; - - // Search for the previous structure - final normal = (await getApplicationDocumentsDirectory()) >> 'fmtc'; - final temporary = (await getTemporaryDirectory()) >> 'fmtc'; - final custom = customDirectory == null ? null : customDirectory >> 'fmtc'; - final root = await normal.exists() - ? normal - : await temporary.exists() - ? temporary - : custom == null - ? null - : await custom.exists() - ? custom - : null; - if (root == null) return null; - - // Delete recovery files and cached statistics - if (deleteOldStructure) { - final oldRecovery = root >> 'recovery'; - if (await oldRecovery.exists()) await oldRecovery.delete(recursive: true); - final oldStats = root >> 'stats'; - if (await oldStats.exists()) await oldStats.delete(recursive: true); - } - - // Don't continue migration if there are no stores - final oldStores = root >> 'stores'; - if (!await oldStores.exists()) return {}; - - // Prepare results map - final Map results = {}; - - // Migrate stores - await for (final storeDirectory - in oldStores.list().whereType()) { - final name = path.basename(storeDirectory.absolute.path); - results[name] = 0; - - // Ignore this store if a counterpart already exists - if (FMTC.instance(name).manage.ready) { - results[name] = null; - continue; - } - await FMTC.instance(name).manage.createAsync(); - final store = FMTCRegistry.instance(name); - - // Migrate tiles in transaction batches of 250 - await for (final List tiles - in (storeDirectory >> 'tiles').list().whereType().slices(250)) { - await store.writeTxn( - () async => store.tiles.putAll( - (await Future.wait( - tiles.map( - (f) async { - final filename = path.basename(f.absolute.path); - final Map placeholderValues = {}; - - for (final e in matchables) { - if (!RegExp('^${e[0]}\$', multiLine: true) - .hasMatch(filename)) { - continue; - } - - String filenameChangable = filename; - List filenameSplit = filename.split('')..add(''); - - for (final match in placeholderRegex.allMatches(e[1])) { - final templateValue = - e[1].substring(match.start, match.end); - final afterChar = (e[1].split('')..add(''))[match.end]; - - final memory = StringBuffer(); - int i = match.start; - for (; filenameSplit[i] != afterChar; i++) { - memory.write(filenameSplit[i]); - } - filenameChangable = filenameChangable.replaceRange( - match.start, - i, - templateValue, - ); - filenameSplit = filenameChangable.split('')..add(''); - - placeholderValues[templateValue.substring( - 1, - templateValue.length - 1, - )] = memory.toString(); - } - - return DbTile( - url: e[2].replaceAllMapped( - TileProvider.templatePlaceholderElement, - (match) { - final value = placeholderValues[match.group(1)!]; - if (value != null) return value; - throw ArgumentError( - 'Missing value for placeholder: {${match.group(1)}}', - ); - }, - ), - bytes: await f.readAsBytes(), - ); - } - - results[name] = results[name]! + 1; - return null; - }, - ), - )) - .nonNulls - .toList(), - ), - ); - } - - // Migrate metadata - await store.writeTxn( - () async => store.metadata.putAll( - await (storeDirectory >> 'metadata') - .list() - .whereType() - .asyncMap( - (f) async => DbMetadata( - name: path.basename(f.absolute.path).split('.metadata')[0], - data: await f.readAsString(), - ), - ) - .toList(), - ), - ); - } - - // Delete store files - if (deleteOldStructure && await oldStores.exists()) { - await oldStores.delete(recursive: true); - } - - return results; - } -} - -//! OLD FILESYSTEM SANITISER CODE !// - -_FilesystemSanitiserResult _defaultFilesystemSanitiser(String input) { - final List errorMessages = []; - String validOutput = input; - - // Apply other character rules with general RegExp - validOutput = validOutput.replaceAll(RegExp(r'[\\\\/\:\*\?\"\<\>\|]'), '_'); - if (validOutput != input) { - errorMessages - .add('The name cannot contain invalid characters: \'[NUL]\\/:*?"<>|\''); - } - - // Trim - validOutput = validOutput.trim(); - if (validOutput != input) { - errorMessages.add('The name cannot contain leading and/or trailing spaces'); - } - - // Ensure is not empty - if (validOutput.isEmpty) { - errorMessages.add('The name cannot be empty'); - validOutput = '_'; - } - - // Ensure is not just '.' - if (validOutput.replaceAll('.', '').isEmpty) { - errorMessages.add('The name cannot consist of only periods (.)'); - validOutput = validOutput.replaceAll('.', '_'); - } - - // Reduce string to under 255 chars (keeps end) - if (validOutput.length > 255) { - validOutput = validOutput.substring(validOutput.length - 255); - if (validOutput != input) { - errorMessages.add('The name cannot contain more than 255 characters'); - } - } - - return _FilesystemSanitiserResult( - validOutput: validOutput, - errorMessages: errorMessages, - ); -} - -class _FilesystemSanitiserResult { - final String validOutput; - final List errorMessages; - - _FilesystemSanitiserResult({ - required this.validOutput, - this.errorMessages = const [], - }); - - @override - String toString() => - 'FilesystemSanitiserResult(validOutput: $validOutput, errorMessages: $errorMessages)'; - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - - return other is _FilesystemSanitiserResult && - other.validOutput == validOutput && - listEquals(other.errorMessages, errorMessages); - } - - @override - int get hashCode => Object.hash(validOutput, errorMessages); } diff --git a/lib/src/root/statistics.dart b/lib/src/root/statistics.dart index f38a32ea..527d0e0a 100644 --- a/lib/src/root/statistics.dart +++ b/lib/src/root/statistics.dart @@ -7,60 +7,15 @@ part of flutter_map_tile_caching; class RootStats { const RootStats._(); - FMTCRegistry get _registry => FMTCRegistry.instance; + /// {@macro fmtc.backend.listStores} + Future> get storesAvailable async => + FMTCBackendAccess.internal.listStores().then((s) => s.map(FMTCStore.new)); - /// List all the available [FMTCStore]s synchronously - /// - /// Prefer [storesAvailableAsync] to avoid blocking the UI thread. Otherwise, - /// this has slightly better performance. - List get storesAvailable => _registry.storeDatabases.values - .map((e) => FMTCStore._(e.descriptorSync.name, autoCreate: false)) - .toList(); - - /// List all the available [FMTCStore]s asynchronously - Future> get storesAvailableAsync => Future.wait( - _registry.storeDatabases.values.map( - (e) async => - FMTCStore._((await e.descriptor).name, autoCreate: false), - ), - ); - - /// Retrieve the total size of all stored tiles in kibibytes (KiB) - /// synchronously - /// - /// Prefer [rootSizeAsync] to avoid blocking the UI thread. Otherwise, this - /// has slightly better performance. - /// - /// Internally sums up the size of all stores (using [StoreStats.storeSize]). - double get rootSize => - storesAvailable.map((e) => e.stats.storeSize).sum / 1024; + /// {@macro fmtc.backend.rootSize} + Future get rootSize async => FMTCBackendAccess.internal.rootSize(); - /// Retrieve the total size of all stored tiles in kibibytes (KiB) - /// asynchronously - /// - /// Internally sums up the size of all stores (using - /// [StoreStats.storeSizeAsync]). - Future get rootSizeAsync async => - (await Future.wait(storesAvailable.map((e) => e.stats.storeSizeAsync))) - .sum / - 1024; - - /// Retrieve the number of all stored tiles synchronously - /// - /// Prefer [rootLengthAsync] to avoid blocking the UI thread. Otherwise, this - /// has slightly better performance. - /// - /// Internally sums up the length of all stores (using - /// [StoreStats.storeLength]). - int get rootLength => storesAvailable.map((e) => e.stats.storeLength).sum; - - /// Retrieve the number of all stored tiles asynchronously - /// - /// Internally sums up the length of all stores (using - /// [StoreStats.storeLengthAsync]). - Future get rootLengthAsync async => - (await Future.wait(storesAvailable.map((e) => e.stats.storeLengthAsync))) - .sum; + /// {@macro fmtc.backend.rootLength} + Future get rootLength async => FMTCBackendAccess.internal.rootLength(); /// Watch for changes in the current root /// From 2171e9e8fcb23487c72c28ccefc52364d2def2dc Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 15 Feb 2024 12:35:49 +0000 Subject: [PATCH 094/168] Start of implementation of recovery system in ObjectBox Former-commit-id: 5be6684b070f5565055c51579eacac113e6dc07f [formerly 48a6f7fdf5a63024dc477354850f04f4eabcc6d7] Former-commit-id: e12fe16b06a772bf7495f178d41bb7ae62ecb78a --- .../{context.dart => backend_access.dart} | 0 lib/src/backend/export_internal.dart | 2 +- lib/src/backend/impls/objectbox/backend.dart | 22 ++- .../models/generated/objectbox-model.json | 183 ++++++++++++++---- .../impls/objectbox/models/src/recovery.dart | 144 ++++++++++++++ .../impls/objectbox/models/src/store.dart | 34 ++++ .../models/{models.dart => src/tile.dart} | 32 +-- lib/src/backend/impls/objectbox/worker.dart | 29 +++ lib/src/backend/interfaces/backend.dart | 12 ++ lib/src/backend/interfaces/models.dart | 25 --- 10 files changed, 391 insertions(+), 92 deletions(-) rename lib/src/backend/{context.dart => backend_access.dart} (100%) create mode 100644 lib/src/backend/impls/objectbox/models/src/recovery.dart create mode 100644 lib/src/backend/impls/objectbox/models/src/store.dart rename lib/src/backend/impls/objectbox/models/{models.dart => src/tile.dart} (55%) diff --git a/lib/src/backend/context.dart b/lib/src/backend/backend_access.dart similarity index 100% rename from lib/src/backend/context.dart rename to lib/src/backend/backend_access.dart diff --git a/lib/src/backend/export_internal.dart b/lib/src/backend/export_internal.dart index 10686dc0..afa59d7a 100644 --- a/lib/src/backend/export_internal.dart +++ b/lib/src/backend/export_internal.dart @@ -1,6 +1,6 @@ // Copyright © Luka S (JaffaKetchup) under GPL-v3 // A full license can be found at .\LICENSE -export 'context.dart'; +export 'backend_access.dart'; export 'export_external.dart'; export 'interfaces/models.dart'; diff --git a/lib/src/backend/impls/objectbox/backend.dart b/lib/src/backend/impls/objectbox/backend.dart index e889a1b2..0b0ee615 100644 --- a/lib/src/backend/impls/objectbox/backend.dart +++ b/lib/src/backend/impls/objectbox/backend.dart @@ -11,9 +11,12 @@ import 'package:flutter/foundation.dart'; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; +import '../../../../flutter_map_tile_caching.dart'; import '../../export_internal.dart'; import 'models/generated/objectbox.g.dart'; -import 'models/models.dart'; +import 'models/src/recovery.dart'; +import 'models/src/store.dart'; +import 'models/src/tile.dart'; part 'worker.dart'; @@ -425,4 +428,21 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { type: _WorkerCmdType.resetMetadata, args: {'storeName': storeName}, ); + + @override + Future startRecovery({ + required int id, + required String storeName, + required DownloadableRegion region, + }) => + _sendCmd( + type: _WorkerCmdType.startRecovery, + args: {'id': id, 'storeName': storeName, 'region': region}, + ); + + @override + Future endRecovery({ + required int id, + }) => + _sendCmd(type: _WorkerCmdType.endRecovery, args: {'id': id}); } diff --git a/lib/src/backend/impls/objectbox/models/generated/objectbox-model.json b/lib/src/backend/impls/objectbox/models/generated/objectbox-model.json index 59dabaac..9d651f9c 100644 --- a/lib/src/backend/impls/objectbox/models/generated/objectbox-model.json +++ b/lib/src/backend/impls/objectbox/models/generated/objectbox-model.json @@ -4,103 +4,216 @@ "_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.", "entities": [ { - "id": "1:4818990512469112793", - "lastPropertyId": "9:7001905095427330194", - "name": "ObjectBoxStore", + "id": "1:5852113147755082214", + "lastPropertyId": "21:4175806851713282292", + "name": "ObjectBoxRecovery", "properties": [ { - "id": "1:3336028670326288161", + "id": "1:4049260877710714778", "name": "id", "type": 6, "flags": 1 }, { - "id": "2:4687139407104104001", - "name": "name", - "type": 9, - "flags": 2080, - "indexId": "1:7472642561499264209" + "id": "2:514228763227462265", + "name": "refId", + "type": 6, + "flags": 40, + "indexId": "1:1915947614835147959" }, { - "id": "5:6058775585602184944", - "name": "hits", + "id": "3:8338900024311175845", + "name": "storeName", + "type": 9 + }, + { + "id": "4:6716912532653788634", + "name": "creationTime", + "type": 10 + }, + { + "id": "5:1519845599652561210", + "name": "minZoom", "type": 6 }, { - "id": "6:6933472703948745593", - "name": "misses", + "id": "6:5013395781301985972", + "name": "maxZoom", "type": 6 }, { - "id": "7:3575235946528079942", - "name": "metadataJson", - "type": 9 + "id": "7:5813475472850223709", + "name": "startTile", + "type": 6 + }, + { + "id": "8:62434241352879189", + "name": "endTile", + "type": 6 + }, + { + "id": "9:1377762685943168852", + "name": "typeId", + "type": 6 + }, + { + "id": "10:7827529863528185814", + "name": "rectNwLat", + "type": 8 + }, + { + "id": "11:4768940403554990885", + "name": "rectNwLng", + "type": 8 + }, + { + "id": "12:7059818053329608732", + "name": "rectSeLat", + "type": 8 + }, + { + "id": "13:7887700468628261283", + "name": "rectSeLng", + "type": 8 + }, + { + "id": "14:8392236879597342193", + "name": "circleCenterLat", + "type": 8 + }, + { + "id": "15:5063472498722773794", + "name": "circleCenterLng", + "type": 8 + }, + { + "id": "16:5319186299784201385", + "name": "circleRadius", + "type": 8 + }, + { + "id": "17:4479579774250144926", + "name": "lineLats", + "type": 29 }, { - "id": "8:9173249510215692378", + "id": "18:3149550933222350400", + "name": "lineLngs", + "type": 29 + }, + { + "id": "19:6474585775498338051", + "name": "lineRadius", + "type": 8 + }, + { + "id": "20:7965435653690834819", + "name": "customPolygonLats", + "type": 29 + }, + { + "id": "21:4175806851713282292", + "name": "customPolygonLngs", + "type": 29 + } + ], + "relations": [] + }, + { + "id": "2:8434848168440140631", + "lastPropertyId": "7:441658295997331328", + "name": "ObjectBoxStore", + "properties": [ + { + "id": "1:123601567385773864", + "name": "id", + "type": 6, + "flags": 1 + }, + { + "id": "2:9007911375527621682", + "name": "name", + "type": 9, + "flags": 2080, + "indexId": "2:5983120737825279673" + }, + { + "id": "3:8187748336899942109", "name": "length", "type": 6 }, { - "id": "9:7001905095427330194", + "id": "4:2385332217074178388", "name": "size", "type": 8 + }, + { + "id": "5:464625589558308463", + "name": "hits", + "type": 6 + }, + { + "id": "6:4049198995248939830", + "name": "misses", + "type": 6 + }, + { + "id": "7:441658295997331328", + "name": "metadataJson", + "type": 9 } ], "relations": [] }, { - "id": "2:5729851195378529265", - "lastPropertyId": "4:4688190119175680445", + "id": "3:8831490920600301075", + "lastPropertyId": "4:1909369032294267427", "name": "ObjectBoxTile", "properties": [ { - "id": "1:145404380988255117", + "id": "1:7557087112400213643", "name": "id", "type": 6, "flags": 1 }, { - "id": "2:5879597573334804833", + "id": "2:5772177928281361264", "name": "url", "type": 9, "flags": 34848, - "indexId": "2:916751376918221285" + "indexId": "3:554942103700819034" }, { - "id": "3:7079942909661621360", + "id": "3:925223426368624455", "name": "lastModified", "type": 10, "flags": 8, - "indexId": "3:1775764954149910715" + "indexId": "4:7943022627739396393" }, { - "id": "4:4688190119175680445", + "id": "4:1909369032294267427", "name": "bytes", "type": 23 } ], "relations": [ { - "id": "1:8266116100918510486", + "id": "1:6378815042102010365", "name": "stores", - "targetId": "1:4818990512469112793" + "targetId": "2:8434848168440140631" } ] } ], - "lastEntityId": "2:5729851195378529265", - "lastIndexId": "3:1775764954149910715", - "lastRelationId": "1:8266116100918510486", + "lastEntityId": "3:8831490920600301075", + "lastIndexId": "4:7943022627739396393", + "lastRelationId": "1:6378815042102010365", "lastSequenceId": "0:0", "modelVersion": 5, "modelVersionParserMinimum": 5, "retiredEntityUids": [], "retiredIndexUids": [], - "retiredPropertyUids": [ - 4684432242170874534, - 2520378516311313562 - ], + "retiredPropertyUids": [], "retiredRelationUids": [], "version": 1 } \ No newline at end of file diff --git a/lib/src/backend/impls/objectbox/models/src/recovery.dart b/lib/src/backend/impls/objectbox/models/src/recovery.dart new file mode 100644 index 00000000..fc9ab366 --- /dev/null +++ b/lib/src/backend/impls/objectbox/models/src/recovery.dart @@ -0,0 +1,144 @@ +// Copyright © Luka S (JaffaKetchup) under GPL-v3 +// A full license can be found at .\LICENSE + +import 'package:meta/meta.dart'; +import 'package:objectbox/objectbox.dart'; + +import '../../../../../../flutter_map_tile_caching.dart'; + +@Entity() +base class ObjectBoxRecovery { + @Id() + @internal + int id = 0; + + @Index() + @Unique() + int refId; + + String storeName; + @Property(type: PropertyType.date) + DateTime creationTime; + + int minZoom; + int maxZoom; + int startTile; + int? endTile; + + int typeId; // 0 - rect, 1 - circle, 2 - line, 3 - custom polygon + + double? rectNwLat; + double? rectNwLng; + double? rectSeLat; + double? rectSeLng; + + double? circleCenterLat; + double? circleCenterLng; + double? circleRadius; + + List? lineLats; + List? lineLngs; + double? lineRadius; + + List? customPolygonLats; + List? customPolygonLngs; + + ObjectBoxRecovery({ + required this.refId, + required this.storeName, + required this.creationTime, + required this.typeId, + required this.minZoom, + required this.maxZoom, + required this.startTile, + required this.endTile, + required this.rectNwLat, + required this.rectNwLng, + required this.rectSeLat, + required this.rectSeLng, + required this.circleCenterLat, + required this.circleCenterLng, + required this.circleRadius, + required this.lineLats, + required this.lineLngs, + required this.lineRadius, + required this.customPolygonLats, + required this.customPolygonLngs, + }); + + ObjectBoxRecovery.startFromRegion({ + required this.refId, + required this.storeName, + required DownloadableRegion region, + }) : creationTime = DateTime.now(), + typeId = region.when( + rectangle: (_) => 0, + circle: (_) => 1, + line: (_) => 2, + customPolygon: (_) => 3, + ), + minZoom = region.minZoom, + maxZoom = region.maxZoom, + startTile = region.start, + endTile = region.end, + rectNwLat = region.originalRegion is RectangleRegion + ? (region.originalRegion as RectangleRegion) + .bounds + .northWest + .latitude + : null, + rectNwLng = region.originalRegion is RectangleRegion + ? (region.originalRegion as RectangleRegion) + .bounds + .northWest + .longitude + : null, + rectSeLat = region.originalRegion is RectangleRegion + ? (region.originalRegion as RectangleRegion) + .bounds + .southEast + .latitude + : null, + rectSeLng = region.originalRegion is RectangleRegion + ? (region.originalRegion as RectangleRegion) + .bounds + .southEast + .longitude + : null, + circleCenterLat = region.originalRegion is CircleRegion + ? (region.originalRegion as CircleRegion).center.latitude + : null, + circleCenterLng = region.originalRegion is CircleRegion + ? (region.originalRegion as CircleRegion).center.longitude + : null, + circleRadius = region.originalRegion is CircleRegion + ? (region.originalRegion as CircleRegion).radius + : null, + lineLats = region.originalRegion is LineRegion + ? (region.originalRegion as LineRegion) + .line + .map((c) => c.latitude) + .toList() + : null, + lineLngs = region.originalRegion is LineRegion + ? (region.originalRegion as LineRegion) + .line + .map((c) => c.longitude) + .toList() + : null, + lineRadius = region.originalRegion is LineRegion + ? (region.originalRegion as LineRegion).radius + : null, + customPolygonLats = region.originalRegion is CustomPolygonRegion + ? (region.originalRegion as CustomPolygonRegion) + .outline + .map((c) => c.latitude) + .toList() + : null, + customPolygonLngs = region.originalRegion is CustomPolygonRegion + ? (region.originalRegion as CustomPolygonRegion) + .outline + .map((c) => c.longitude) + .toList() + : null; +} diff --git a/lib/src/backend/impls/objectbox/models/src/store.dart b/lib/src/backend/impls/objectbox/models/src/store.dart new file mode 100644 index 00000000..4bd5b3af --- /dev/null +++ b/lib/src/backend/impls/objectbox/models/src/store.dart @@ -0,0 +1,34 @@ +// Copyright © Luka S (JaffaKetchup) under GPL-v3 +// A full license can be found at .\LICENSE + +import 'package:objectbox/objectbox.dart'; + +import 'tile.dart'; + +@Entity() +class ObjectBoxStore { + @Id() + int id = 0; + + @Index() + @Unique() + String name; + + @Index() + @Backlink('stores') + final tiles = ToMany(); + + int length; + double size; + int hits; + int misses; + String metadataJson; + + ObjectBoxStore({ + required this.name, + required this.length, + required this.size, + required this.hits, + required this.misses, + }) : metadataJson = ''; +} diff --git a/lib/src/backend/impls/objectbox/models/models.dart b/lib/src/backend/impls/objectbox/models/src/tile.dart similarity index 55% rename from lib/src/backend/impls/objectbox/models/models.dart rename to lib/src/backend/impls/objectbox/models/src/tile.dart index 1fa5dca6..a361b856 100644 --- a/lib/src/backend/impls/objectbox/models/models.dart +++ b/lib/src/backend/impls/objectbox/models/src/tile.dart @@ -5,36 +5,8 @@ import 'dart:typed_data'; import 'package:objectbox/objectbox.dart'; -import '../../../interfaces/models.dart'; - -@Entity() -base class ObjectBoxStore extends BackendStore { - @Id() - int id = 0; - - @override - @Index() - @Unique() - String name; - - @Index() - @Backlink('stores') - final tiles = ToMany(); - - int length; - double size; - int hits; - int misses; - String metadataJson; - - ObjectBoxStore({ - required this.name, - required this.length, - required this.size, - required this.hits, - required this.misses, - }) : metadataJson = ''; -} +import '../../../../interfaces/models.dart'; +import 'store.dart'; @Entity() base class ObjectBoxTile extends BackendTile { diff --git a/lib/src/backend/impls/objectbox/worker.dart b/lib/src/backend/impls/objectbox/worker.dart index e5df4fba..088da7d6 100644 --- a/lib/src/backend/impls/objectbox/worker.dart +++ b/lib/src/backend/impls/objectbox/worker.dart @@ -27,6 +27,8 @@ enum _WorkerCmdType { setBulkMetadata, removeMetadata, resetMetadata, + startRecovery, + endRecovery, } Future _worker( @@ -683,6 +685,33 @@ Future _worker( sendRes(id: cmd.id); + break; + case _WorkerCmdType.startRecovery: + final id = cmd.args['id']! as int; + final storeName = cmd.args['storeName']! as String; + final region = cmd.args['region']! as DownloadableRegion; + + root.box().put( + ObjectBoxRecovery.startFromRegion( + refId: id, + storeName: storeName, + region: region, + ), + ); + + sendRes(id: cmd.id); + + break; + case _WorkerCmdType.endRecovery: + root + .box() + .query(ObjectBoxRecovery_.refId.equals(cmd.args['id']! as int)) + .build() + ..remove() + ..close(); + + sendRes(id: cmd.id); + break; } } catch (e) { diff --git a/lib/src/backend/interfaces/backend.dart b/lib/src/backend/interfaces/backend.dart index 1986d954..3c4db44f 100644 --- a/lib/src/backend/interfaces/backend.dart +++ b/lib/src/backend/interfaces/backend.dart @@ -5,6 +5,7 @@ import 'dart:async'; import 'dart:io'; import 'dart:typed_data'; +import '../../../flutter_map_tile_caching.dart'; import '../export_internal.dart'; /// {@template fmtc.backend.backend} @@ -130,6 +131,7 @@ abstract interface class FMTCBackendInternal with FMTCBackendAccess { /// {@template fmtc.backend.getStoreStats} /// Retrieve the following statistics about the specified store (all available): + /// /// * `size`: total number of KiBs of all tiles' bytes (not 'real total' size) /// * `length`: number of tiles belonging /// * `hits`: number of successful tile retrievals when browsing @@ -268,4 +270,14 @@ abstract interface class FMTCBackendInternal with FMTCBackendAccess { Future resetMetadata({ required String storeName, }); + + Future startRecovery({ + required int id, + required String storeName, + required DownloadableRegion region, + }); + + Future endRecovery({ + required int id, + }); } diff --git a/lib/src/backend/interfaces/models.dart b/lib/src/backend/interfaces/models.dart index 942567d6..cd764328 100644 --- a/lib/src/backend/interfaces/models.dart +++ b/lib/src/backend/interfaces/models.dart @@ -8,31 +8,6 @@ import 'package:meta/meta.dart'; import '../../../flutter_map_tile_caching.dart'; import '../../misc/obscure_query_params.dart'; -/// Represents a store (which is never directly exposed to the user) -/// -/// Note that the relationship between stores and tiles is many-to-many, and -/// backend implementations should fully support this. -abstract base class BackendStore { - /// The human-readable name for this store - /// - /// Note that this may contain any character, and may also be empty. - String get name; - - /// Uses [name] for equality comparisons only (unless the two objects are - /// [identical]) - /// - /// Overriding this in an implementation may cause FMTC logic to break, and is - /// therefore not recommended. - @override - @nonVirtual - bool operator ==(Object other) => - identical(this, other) || (other is BackendStore && name == other.name); - - @override - @nonVirtual - int get hashCode => name.hashCode; -} - /// Represents a tile (which is never directly exposed to the user) /// /// Note that the relationship between stores and tiles is many-to-many, and From f43c886d25bcaef84089f0f1d9394ff097afdc75 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Mon, 19 Feb 2024 14:05:25 +0000 Subject: [PATCH 095/168] Start migration of example application Removed Isar dependency Former-commit-id: 7326281aad889f852990e9d6009a7f970e274db8 [formerly 73a1a6f2363511a7a63ede6cff57a7d721a33f83] Former-commit-id: 76abbf3c8d72bdea2fd305e605ac1bda0d6ca65a --- example/lib/main.dart | 30 +++------------- .../components/region_information.dart | 2 +- .../components/store_selector.dart | 6 ++-- .../screens/import_store/import_store.dart | 2 +- example/lib/screens/main/main.dart | 32 +++-------------- .../main/pages/map/components/map_view.dart | 36 +++++++++---------- .../recovery/components/recovery_list.dart | 19 +++++----- .../components/recovery_start_button.dart | 5 ++- .../screens/main/pages/recovery/recovery.dart | 8 ++--- .../region_selection/region_selection.dart | 5 ++- .../pages/stores/components/store_tile.dart | 2 +- .../lib/screens/main/pages/stores/stores.dart | 18 +++++----- .../store_editor/components/header.dart | 11 +++--- .../screens/store_editor/store_editor.dart | 11 +++--- lib/flutter_map_tile_caching.dart | 1 - lib/src/backend/interfaces/backend.dart | 3 ++ lib/src/providers/image_provider.dart | 10 +++--- pubspec.yaml | 12 +++---- 18 files changed, 81 insertions(+), 132 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index f4dd2271..7e5f9944 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,10 +1,7 @@ -import 'dart:io'; - import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; import 'package:google_fonts/google_fonts.dart'; -import 'package:path/path.dart' as p; import 'package:provider/provider.dart'; import 'screens/configure_download/state/configure_download_provider.dart'; @@ -23,30 +20,13 @@ void main() async { ), ); - String? damagedDatabaseDeleted; - await FlutterMapTileCaching.initialise(); - - await FMTC.instance.rootDirectory.migrator.fromV6(urlTemplates: []); - - final newAppVersionFile = File( - p.join( - // ignore: invalid_use_of_internal_member, invalid_use_of_protected_member - FMTC.instance.rootDirectory.directory.absolute.path, - 'newAppVersion.${Platform.isWindows ? 'exe' : 'apk'}', - ), - ); - if (await newAppVersionFile.exists()) await newAppVersionFile.delete(); + await FMTCObjectBoxBackend().initialise(); - runApp(AppContainer(damagedDatabaseDeleted: damagedDatabaseDeleted)); + runApp(const _AppContainer()); } -class AppContainer extends StatelessWidget { - const AppContainer({ - super.key, - required this.damagedDatabaseDeleted, - }); - - final String? damagedDatabaseDeleted; +class _AppContainer extends StatelessWidget { + const _AppContainer(); @override Widget build(BuildContext context) => MultiProvider( @@ -87,7 +67,7 @@ class AppContainer extends StatelessWidget { ), ), debugShowCheckedModeBanner: false, - home: MainScreen(damagedDatabaseDeleted: damagedDatabaseDeleted), + home: const MainScreen(), ), ); } diff --git a/example/lib/screens/configure_download/components/region_information.dart b/example/lib/screens/configure_download/components/region_information.dart index c984bfdb..6c1c1527 100644 --- a/example/lib/screens/configure_download/components/region_information.dart +++ b/example/lib/screens/configure_download/components/region_information.dart @@ -32,7 +32,7 @@ class _RegionInformationState extends State { @override void initState() { super.initState(); - numOfTiles = FMTC.instance('').download.check( + numOfTiles = const FMTCStore('').download.check( widget.region.toDownloadable( minZoom: widget.minZoom, maxZoom: widget.maxZoom, diff --git a/example/lib/screens/configure_download/components/store_selector.dart b/example/lib/screens/configure_download/components/store_selector.dart index 4d56ad1c..ba28610f 100644 --- a/example/lib/screens/configure_download/components/store_selector.dart +++ b/example/lib/screens/configure_download/components/store_selector.dart @@ -21,8 +21,8 @@ class _StoreSelectorState extends State { IntrinsicWidth( child: Consumer2( builder: (context, downloadProvider, generalProvider, _) => - FutureBuilder>( - future: FMTC.instance.rootDirectory.stats.storesAvailableAsync, + FutureBuilder>( + future: FMTCRoot.stats.storesAvailable, builder: (context, snapshot) => DropdownButton( items: snapshot.data ?.map( @@ -37,7 +37,7 @@ class _StoreSelectorState extends State { value: downloadProvider.selectedStore ?? (generalProvider.currentStore == null ? null - : FMTC.instance(generalProvider.currentStore!)), + : FMTCStore(generalProvider.currentStore!)), hint: Text( snapshot.data == null ? 'Loading...' diff --git a/example/lib/screens/import_store/import_store.dart b/example/lib/screens/import_store/import_store.dart index c6d3edff..44c78815 100644 --- a/example/lib/screens/import_store/import_store.dart +++ b/example/lib/screens/import_store/import_store.dart @@ -31,7 +31,7 @@ class _ImportStorePopupState extends State { subtitle: const Text('Select any valid store files (.fmtc)'), onTap: () async { importStores.addAll( - (await FMTC.instance.rootDirectory.import.withGUI( + (await FMTCRoot.import.withGUI( collisionHandler: (fn, sn) { setState( () => importStores[fn]!.collisionInfo = [ diff --git a/example/lib/screens/main/main.dart b/example/lib/screens/main/main.dart index 7ad63f69..bf3a11b4 100644 --- a/example/lib/screens/main/main.dart +++ b/example/lib/screens/main/main.dart @@ -11,19 +11,14 @@ import 'pages/region_selection/region_selection.dart'; import 'pages/stores/stores.dart'; class MainScreen extends StatefulWidget { - const MainScreen({ - super.key, - required this.damagedDatabaseDeleted, - }); - - final String? damagedDatabaseDeleted; + const MainScreen({super.key}); @override State createState() => _MainScreenState(); } class _MainScreenState extends State { - late final PageController _pageController; + late final _pageController = PageController(initialPage: _currentPageIndex); int _currentPageIndex = 0; bool extended = false; @@ -46,11 +41,9 @@ class _MainScreenState extends State { NavigationDestination( label: 'Recover', icon: StreamBuilder( - stream: FMTC.instance.rootDirectory.stats - .watchChanges() - .asBroadcastStream(), + stream: FMTCRoot.stats.watchChanges().asBroadcastStream(), builder: (context, _) => FutureBuilder>( - future: FMTC.instance.rootDirectory.recovery.failedRegions, + future: FMTCRoot.recovery.failedRegions, builder: (context, snapshot) => Badge( position: BadgePosition.topEnd(top: -5, end: -6), badgeAnimation: const BadgeAnimation.size( @@ -100,23 +93,6 @@ class _MainScreenState extends State { ); } - @override - void initState() { - _pageController = PageController(initialPage: _currentPageIndex); - if (widget.damagedDatabaseDeleted != null) { - WidgetsBinding.instance.addPostFrameCallback( - (_) => ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - 'At least one corrupted database has been deleted.\n${widget.damagedDatabaseDeleted}', - ), - ), - ), - ); - } - super.initState(); - } - @override void dispose() { _pageController.dispose(); diff --git a/example/lib/screens/main/pages/map/components/map_view.dart b/example/lib/screens/main/pages/map/components/map_view.dart index 1c6bf499..e6b69bb7 100644 --- a/example/lib/screens/main/pages/map/components/map_view.dart +++ b/example/lib/screens/main/pages/map/components/map_view.dart @@ -24,7 +24,7 @@ class MapView extends StatelessWidget { FutureBuilder?>( future: currentStore == null ? Future.sync(() => {}) - : FMTC.instance(currentStore).metadata.read, + : FMTCStore(currentStore).metadata.read, builder: (context, metadata) { if (!metadata.hasData || metadata.data == null || @@ -60,25 +60,25 @@ class MapView extends StatelessWidget { maxNativeZoom: 20, panBuffer: 5, tileProvider: currentStore != null - ? FMTC.instance(currentStore).getTileProvider( - settings: FMTCTileProviderSettings( - behavior: CacheBehavior.values - .byName(metadata.data!['behaviour']!), - cachedValidDuration: int.parse( - metadata.data!['validDuration']!, - ) == - 0 - ? Duration.zero - : Duration( - days: int.parse( - metadata.data!['validDuration']!, - ), + ? FMTCStore(currentStore).getTileProvider( + settings: FMTCTileProviderSettings( + behavior: CacheBehavior.values + .byName(metadata.data!['behaviour']!), + cachedValidDuration: int.parse( + metadata.data!['validDuration']!, + ) == + 0 + ? Duration.zero + : Duration( + days: int.parse( + metadata.data!['validDuration']!, ), - maxStoreLength: int.parse( - metadata.data!['maxLength']!, - ), + ), + maxStoreLength: int.parse( + metadata.data!['maxLength']!, ), - ) + ), + ) : NetworkTileProvider(), ) else ...[ diff --git a/example/lib/screens/main/pages/recovery/components/recovery_list.dart b/example/lib/screens/main/pages/recovery/components/recovery_list.dart index 9ac622e8..eac8b7e4 100644 --- a/example/lib/screens/main/pages/recovery/components/recovery_list.dart +++ b/example/lib/screens/main/pages/recovery/components/recovery_list.dart @@ -27,8 +27,7 @@ class _RecoveryListState extends State { return ListTile( leading: FutureBuilder( - future: FMTC.instance.rootDirectory.recovery - .getFailedRegion(region.id), + future: FMTCRoot.recovery.getFailedRegion(region.id), builder: (context, isFailed) => Icon( isFailed.data != null ? Icons.warning @@ -71,15 +70,13 @@ class _RecoveryListState extends State { IconButton( icon: const Icon(Icons.delete_forever, color: Colors.red), onPressed: () async { - await FMTC.instance.rootDirectory.recovery - .cancel(region.id); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Deleted Recovery Information'), - ), - ); - } + await FMTCRoot.recovery.cancel(region.id); + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Deleted Recovery Information'), + ), + ); }, ), const SizedBox(width: 10), diff --git a/example/lib/screens/main/pages/recovery/components/recovery_start_button.dart b/example/lib/screens/main/pages/recovery/components/recovery_start_button.dart index 19ee6ab4..6849a315 100644 --- a/example/lib/screens/main/pages/recovery/components/recovery_start_button.dart +++ b/example/lib/screens/main/pages/recovery/components/recovery_start_button.dart @@ -18,10 +18,9 @@ class RecoveryStartButton extends StatelessWidget { @override Widget build(BuildContext context) => FutureBuilder( - future: FMTC.instance.rootDirectory.recovery.getFailedRegion(region.id), + future: FMTCRoot.recovery.getFailedRegion(region.id), builder: (context, isFailed) => FutureBuilder( - future: FMTC - .instance('') + future: const FMTCStore('') .download .check(region.toDownloadable(TileLayer())), builder: (context, tiles) => tiles.hasData diff --git a/example/lib/screens/main/pages/recovery/recovery.dart b/example/lib/screens/main/pages/recovery/recovery.dart index 5fe51200..3be21ffc 100644 --- a/example/lib/screens/main/pages/recovery/recovery.dart +++ b/example/lib/screens/main/pages/recovery/recovery.dart @@ -25,13 +25,11 @@ class _RecoveryPageState extends State { void initState() { super.initState(); - void listRecoverableRegions() => _recoverableRegions = - FMTC.instance.rootDirectory.recovery.recoverableRegions; + void listRecoverableRegions() => + _recoverableRegions = FMTCRoot.recovery.recoverableRegions; listRecoverableRegions(); - FMTC.instance.rootDirectory.stats - .watchChanges(watchRecovery: true) - .listen((_) { + FMTCRoot.stats.watchChanges(watchRecovery: true).listen((_) { if (mounted) { listRecoverableRegions(); setState(() {}); diff --git a/example/lib/screens/main/pages/region_selection/region_selection.dart b/example/lib/screens/main/pages/region_selection/region_selection.dart index 9484da26..fae985c1 100644 --- a/example/lib/screens/main/pages/region_selection/region_selection.dart +++ b/example/lib/screens/main/pages/region_selection/region_selection.dart @@ -225,7 +225,7 @@ class _RegionSelectionPageState extends State { FutureBuilder?>( future: currentStore == null ? Future.value() - : FMTC.instance(currentStore).metadata.read, + : FMTCStore(currentStore).metadata.read, builder: (context, metadata) { if (currentStore != null && metadata.data == null) { return const LoadingIndicator('Preparing Map'); @@ -259,8 +259,7 @@ class _RegionSelectionPageState extends State { FutureBuilder( future: currentStore == null ? Future.value() - : FMTC - .instance(currentStore) + : FMTCStore(currentStore) .getTileProvider() .checkTileCached( coords: tile.coordinates, diff --git a/example/lib/screens/main/pages/stores/components/store_tile.dart b/example/lib/screens/main/pages/stores/components/store_tile.dart index d33f3e68..71422bc5 100644 --- a/example/lib/screens/main/pages/stores/components/store_tile.dart +++ b/example/lib/screens/main/pages/stores/components/store_tile.dart @@ -33,7 +33,7 @@ class _StoreTileState extends State { bool _emptyingProgress = false; bool _exportingProgress = false; - late final _store = FMTC.instance(widget.storeName); + late final _store = FMTCStore(widget.storeName); void _loadStatistics() { final stats = _store.stats.all; diff --git a/example/lib/screens/main/pages/stores/stores.dart b/example/lib/screens/main/pages/stores/stores.dart index 940b86fc..7f34dd60 100644 --- a/example/lib/screens/main/pages/stores/stores.dart +++ b/example/lib/screens/main/pages/stores/stores.dart @@ -17,17 +17,16 @@ class StoresPage extends StatefulWidget { } class _StoresPageState extends State { - late Future> _stores; + late Future> _stores; @override void initState() { super.initState(); - void listStores() => - _stores = FMTC.instance.rootDirectory.stats.storesAvailableAsync; + void listStores() => _stores = FMTCRoot.stats.storesAvailable; listStores(); - FMTC.instance.rootDirectory.stats.watchChanges().listen((_) { + FMTCRoot.stats.watchChanges().listen((_) { if (mounted) { listStores(); setState(() {}); @@ -46,7 +45,7 @@ class _StoresPageState extends State { const Header(), const SizedBox(height: 12), Expanded( - child: FutureBuilder>( + child: FutureBuilder>( future: _stores, builder: (context, snapshot) => snapshot.hasError ? throw snapshot.error! as FMTCDamagedStoreException @@ -57,10 +56,13 @@ class _StoresPageState extends State { itemCount: snapshot.data!.length, itemBuilder: (context, index) => StoreTile( context: context, - storeName: - snapshot.data![index].storeName, + storeName: snapshot.data! + .elementAt(index) + .storeName, key: ValueKey( - snapshot.data![index].storeName, + snapshot.data! + .elementAt(index) + .storeName, ), ), ) diff --git a/example/lib/screens/store_editor/components/header.dart b/example/lib/screens/store_editor/components/header.dart index 46422687..bc4083e2 100644 --- a/example/lib/screens/store_editor/components/header.dart +++ b/example/lib/screens/store_editor/components/header.dart @@ -41,11 +41,11 @@ AppBar buildHeader({ if (formKey.currentState!.validate()) { formKey.currentState!.save(); - final FMTCStore? existingStore = widget.existingStoreName == null + final existingStore = widget.existingStoreName == null ? null - : FMTC.instance(widget.existingStoreName!); - final FMTCStore newStore = existingStore == null - ? FMTC.instance(newValues['storeName']!) + : FMTCStore(widget.existingStoreName!); + final newStore = existingStore == null + ? FMTCStore(newValues['storeName']!) : await existingStore.manage.rename(newValues['storeName']!); if (!mounted) return; @@ -78,7 +78,7 @@ AppBar buildHeader({ ); } - if (!mounted) return; + if (!context.mounted) return; if (widget.isStoreInUse && widget.existingStoreName != null) { Provider.of(context, listen: false) .currentStore = newValues['storeName']; @@ -90,6 +90,7 @@ AppBar buildHeader({ const SnackBar(content: Text('Saved successfully')), ); } else { + if (!context.mounted) return; ScaffoldMessenger.of(context).hideCurrentSnackBar(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( diff --git a/example/lib/screens/store_editor/store_editor.dart b/example/lib/screens/store_editor/store_editor.dart index c36a7190..821dd07d 100644 --- a/example/lib/screens/store_editor/store_editor.dart +++ b/example/lib/screens/store_editor/store_editor.dart @@ -67,7 +67,7 @@ class _StoreEditorPopupState extends State { child: FutureBuilder?>( future: widget.existingStoreName == null ? Future.sync(() => {}) - : FMTC.instance(widget.existingStoreName!).metadata.read, + : FMTCStore(widget.existingStoreName!).metadata.read, builder: (context, metadata) { if (!metadata.hasData || metadata.data == null) { return const LoadingIndicator('Retrieving Settings'); @@ -84,12 +84,9 @@ class _StoreEditorPopupState extends State { isDense: true, ), onChanged: (input) async { - _storeNameIsDuplicate = (await FMTC - .instance - .rootDirectory - .stats - .storesAvailableAsync) - .contains(FMTC.instance(input)); + _storeNameIsDuplicate = + (await FMTCRoot.stats.storesAvailable) + .contains(FMTCStore(input)); setState(() {}); }, validator: (input) => diff --git a/lib/flutter_map_tile_caching.dart b/lib/flutter_map_tile_caching.dart index 98d8a2c1..c7c67d5b 100644 --- a/lib/flutter_map_tile_caching.dart +++ b/lib/flutter_map_tile_caching.dart @@ -25,7 +25,6 @@ import 'package:flutter/services.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:http/http.dart' as http; import 'package:http/io_client.dart'; -import 'package:isar/isar.dart'; import 'package:latlong2/latlong.dart'; import 'package:meta/meta.dart'; import 'package:path/path.dart' as path; diff --git a/lib/src/backend/interfaces/backend.dart b/lib/src/backend/interfaces/backend.dart index 3c4db44f..24165d8d 100644 --- a/lib/src/backend/interfaces/backend.dart +++ b/lib/src/backend/interfaces/backend.dart @@ -28,6 +28,9 @@ import '../export_internal.dart'; /// {@endtemplate} abstract interface class FMTCBackend { /// {@macro fmtc.backend.backend} + /// + /// This constructor does not initialise this backend, also invoke + /// [initialise]. const FMTCBackend(); /// {@template fmtc.backend.inititialise} diff --git a/lib/src/providers/image_provider.dart b/lib/src/providers/image_provider.dart index f28eb1d3..20253470 100644 --- a/lib/src/providers/image_provider.dart +++ b/lib/src/providers/image_provider.dart @@ -12,6 +12,7 @@ import 'package:flutter_map/flutter_map.dart'; import 'package:http/http.dart'; import '../../flutter_map_tile_caching.dart'; +import '../backend/export_internal.dart'; import '../misc/obscure_query_params.dart'; /// A specialised [ImageProvider] dedicated to 'flutter_map_tile_caching' @@ -36,8 +37,6 @@ class FMTCImageProvider extends ImageProvider { required this.coords, }); - FMTCBackendInternal get _backend => FMTC.instance.backend.internal; - @override ImageStreamCompleter loadImage( FMTCImageProvider key, @@ -89,7 +88,8 @@ class FMTCImageProvider extends ImageProvider { obscuredQueryParams: provider.settings.obscuredQueryParams, ); - final existingTile = await _backend.readTile(url: matcherUrl); + final existingTile = + await FMTCBackendAccess.internal.readTile(url: matcherUrl); final needsCreating = existingTile == null; final needsUpdating = !needsCreating && @@ -207,7 +207,7 @@ class FMTCImageProvider extends ImageProvider { // Cache the tile retrieved from the network response unawaited( - _backend.writeTile( + FMTCBackendAccess.internal.writeTile( storeName: storeName, url: matcherUrl, bytes: responseBytes, @@ -218,7 +218,7 @@ class FMTCImageProvider extends ImageProvider { if (needsCreating && provider.settings.maxStoreLength != 0) { // TODO: Check if performance is acceptable without checking limit excess first unawaited( - _backend.removeOldestTilesAboveLimit( + FMTCBackendAccess.internal.removeOldestTilesAboveLimit( storeName: storeName, tilesLimit: provider.settings.maxStoreLength, ), diff --git a/pubspec.yaml b/pubspec.yaml index 29ddb2a4..3003ab75 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -29,19 +29,17 @@ environment: dependencies: async: ^2.11.0 collection: ^1.18.0 - dart_earcut: ^1.0.1 + dart_earcut: ^1.1.0 flutter: sdk: flutter flutter_map: ^6.1.0 - http: ^1.1.2 - isar: ^3.1.0+1 - isar_flutter_libs: ^3.1.0+1 + http: ^1.2.1 latlong2: ^0.9.0 - meta: ^1.11.0 - objectbox: ^2.4.0 + meta: any + objectbox: ^2.5.0 objectbox_flutter_libs: any path: ^1.9.0 - path_provider: ^2.1.1 + path_provider: ^2.1.2 stream_transform: ^2.1.0 watcher: ^1.1.0 From 42cb06b2325ef38e253f99d4049dafca71bf917d Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Tue, 20 Feb 2024 18:30:50 +0000 Subject: [PATCH 096/168] Made `FMTCTileProviderSettings` an independent singleton Former-commit-id: e15618b6865ee7f7bd364bdfcc57f272078a17ee [formerly 37ef5e504b3e73881c311fe24382a2c19b60f826] Former-commit-id: 2f574c07231854d4ae6e02953c8c27e023089be8 --- lib/src/bulk_download/manager.dart | 3 +- lib/src/bulk_download/thread.dart | 2 +- lib/src/providers/tile_provider.dart | 2 +- lib/src/providers/tile_provider_settings.dart | 45 ++++++++++++++----- lib/src/store/download.dart | 2 +- 5 files changed, 39 insertions(+), 15 deletions(-) diff --git a/lib/src/bulk_download/manager.dart b/lib/src/bulk_download/manager.dart index dde188c8..88ea90d0 100644 --- a/lib/src/bulk_download/manager.dart +++ b/lib/src/bulk_download/manager.dart @@ -19,7 +19,6 @@ Future _downloadManager( }) input, ) async { // Precalculate shared inputs for all threads - final storeId = DatabaseTools.hash(input.storeName).toString(); final threadBufferLength = (input.maxBufferLength / input.parallelThreads).floor(); final headers = { @@ -188,7 +187,7 @@ Future _downloadManager( _singleDownloadThread, ( sendPort: downloadThreadReceivePort.sendPort, - storeId: storeId, + storeName: input.storeName, rootDirectory: input.rootDirectory, options: input.region.options, maxBufferLength: threadBufferLength, diff --git a/lib/src/bulk_download/thread.dart b/lib/src/bulk_download/thread.dart index 5b9a9d81..044c19df 100644 --- a/lib/src/bulk_download/thread.dart +++ b/lib/src/bulk_download/thread.dart @@ -6,7 +6,7 @@ part of flutter_map_tile_caching; Future _singleDownloadThread( ({ SendPort sendPort, - String storeId, + String storeName, String rootDirectory, TileLayer options, int maxBufferLength, diff --git a/lib/src/providers/tile_provider.dart b/lib/src/providers/tile_provider.dart index b9ec4de8..404199d5 100644 --- a/lib/src/providers/tile_provider.dart +++ b/lib/src/providers/tile_provider.dart @@ -23,7 +23,7 @@ class FMTCTileProvider extends TileProvider { required FMTCTileProviderSettings? settings, required Map headers, required http.Client? httpClient, - }) : settings = settings ?? FMTC.instance.defaultTileProviderSettings, + }) : settings = settings ?? FMTCTileProviderSettings.instance, httpClient = httpClient ?? IOClient(HttpClient()..userAgent = null), super( headers: { diff --git a/lib/src/providers/tile_provider_settings.dart b/lib/src/providers/tile_provider_settings.dart index baff876d..fb17499a 100644 --- a/lib/src/providers/tile_provider_settings.dart +++ b/lib/src/providers/tile_provider_settings.dart @@ -3,6 +3,9 @@ part of flutter_map_tile_caching; +/// Callback type that takes an [FMTCBrowsingError] exception +typedef FMTCBrowsingErrorHandler = void Function(FMTCBrowsingError exception); + /// Behaviours dictating how and when browse caching should be carried out enum CacheBehavior { /// Only get tiles from the local cache @@ -24,6 +27,11 @@ enum CacheBehavior { /// Settings for an [FMTCTileProvider] class FMTCTileProviderSettings { + /// Get an existing instance, if one has been constructed, or get the default + /// intial configuration + static FMTCTileProviderSettings get instance => _instance; + static var _instance = FMTCTileProviderSettings(); + /// The behavior method to get and cache a tile /// /// Defaults to [CacheBehavior.cacheFirst] - get tiles from the local cache, @@ -44,8 +52,7 @@ class FMTCTileProviderSettings { /// Only applies to 'browse caching', ie. downloading regions will bypass this /// limit. /// - /// Note that the actual store has a size limit of - /// [FMTCSettings.databaseMaxSize], irrespective of this value. + /// Note that the database maximum size may be set by the backend. /// /// Defaults to 0 disabled. final int maxStoreLength; @@ -54,7 +61,8 @@ class FMTCTileProviderSettings { /// a URL's query parameter list /// /// If using this property, it is recommended to set it globally on - /// initialisation with [FMTCSettings], to ensure it gets applied throughout. + /// initialisation with [FMTCTileProviderSettings], to ensure it gets applied + /// throughout. /// /// Used by [obscureQueryParams] to apply to a URL. /// @@ -67,14 +75,31 @@ class FMTCTileProviderSettings { /// Even if this is defined, the error will still be (re)thrown. void Function(FMTCBrowsingError exception)? errorHandler; - /// Create settings for an [FMTCTileProvider] - FMTCTileProviderSettings({ - this.behavior = CacheBehavior.cacheFirst, - this.cachedValidDuration = const Duration(days: 16), - this.maxStoreLength = 0, + /// Create new settings for an [FMTCTileProvider], and set the [instance] + /// + /// To access the existing settings, if any, get [instance]. + factory FMTCTileProviderSettings({ + CacheBehavior behavior = CacheBehavior.cacheFirst, + Duration cachedValidDuration = const Duration(days: 16), + int maxStoreLength = 0, List obscuredQueryParams = const [], - this.errorHandler, - }) : obscuredQueryParams = obscuredQueryParams.map((e) => RegExp('$e=[^&]*')); + FMTCBrowsingErrorHandler? errorHandler, + }) => + _instance = FMTCTileProviderSettings._( + behavior: behavior, + cachedValidDuration: cachedValidDuration, + maxStoreLength: maxStoreLength, + obscuredQueryParams: obscuredQueryParams.map((e) => RegExp('$e=[^&]*')), + errorHandler: errorHandler, + ); + + FMTCTileProviderSettings._({ + required this.behavior, + required this.cachedValidDuration, + required this.maxStoreLength, + required this.obscuredQueryParams, + required this.errorHandler, + }); @override bool operator ==(Object other) => diff --git a/lib/src/store/download.dart b/lib/src/store/download.dart index fdcacea5..beeeaa65 100644 --- a/lib/src/store/download.dart +++ b/lib/src/store/download.dart @@ -183,7 +183,7 @@ class DownloadManagement { rateLimit: rateLimit, obscuredQueryParams: obscuredQueryParams?.map((e) => RegExp('$e=[^&]*')) ?? - FMTC.defaultTileProviderSettings.obscuredQueryParams, + FMTCTileProviderSettings.instance.obscuredQueryParams, ), onExit: receivePort.sendPort, debugName: '[FMTC] Master Bulk Download Thread', From bfa1ef2029db0464d457edbf3de653ee36256dc9 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Tue, 20 Feb 2024 21:42:28 +0000 Subject: [PATCH 097/168] Finished `ObjectBoxBackend` implementation of recovery system and integration Updated & removed dependencies Removed internal `_WithBackendAccess` Former-commit-id: 34090ba2172fffedb243c565bd9fc012e61934f5 [formerly 797b34f732c29861160e9548c78342fb075e247a] Former-commit-id: 2158514a505303d1f3f42e3137a6d54ef26d8978 --- lib/flutter_map_tile_caching.dart | 5 - lib/src/backend/impls/objectbox/backend.dart | 28 ++- lib/src/backend/impls/objectbox/worker.dart | 81 ++++++-- lib/src/backend/interfaces/backend.dart | 31 ++- lib/src/misc/with_backend_access.dart | 12 -- lib/src/regions/recovered_region.dart | 83 +++----- lib/src/root/directory.dart | 6 +- lib/src/root/migrator.dart | 11 - lib/src/root/recovery.dart | 202 +++++-------------- lib/src/root/statistics.dart | 16 +- lib/src/store/manage.dart | 26 ++- lib/src/store/metadata.dart | 20 +- lib/src/store/statistics.dart | 9 +- pubspec.yaml | 2 - tile_server/pubspec.yaml | 2 +- 15 files changed, 240 insertions(+), 294 deletions(-) delete mode 100644 lib/src/misc/with_backend_access.dart delete mode 100644 lib/src/root/migrator.dart diff --git a/lib/flutter_map_tile_caching.dart b/lib/flutter_map_tile_caching.dart index c7c67d5b..b2617b69 100644 --- a/lib/flutter_map_tile_caching.dart +++ b/lib/flutter_map_tile_caching.dart @@ -27,9 +27,6 @@ import 'package:http/http.dart' as http; import 'package:http/io_client.dart'; import 'package:latlong2/latlong.dart'; import 'package:meta/meta.dart'; -import 'package:path/path.dart' as path; -import 'package:stream_transform/stream_transform.dart'; -import 'package:watcher/watcher.dart'; import 'src/backend/export_internal.dart'; import 'src/bulk_download/instance.dart'; @@ -50,7 +47,6 @@ part 'src/bulk_download/thread.dart'; part 'src/bulk_download/tile_event.dart'; part 'src/fmtc.dart'; part 'src/misc/deprecations.dart'; -part 'src/misc/with_backend_access.dart'; part 'src/providers/tile_provider.dart'; part 'src/regions/base_region.dart'; part 'src/regions/circle.dart'; @@ -61,7 +57,6 @@ part 'src/regions/recovered_region.dart'; part 'src/regions/rectangle.dart'; part 'src/root/directory.dart'; part 'src/root/import.dart'; -part 'src/root/migrator.dart'; part 'src/root/recovery.dart'; part 'src/root/statistics.dart'; part 'src/providers/tile_provider_settings.dart'; diff --git a/lib/src/backend/impls/objectbox/backend.dart b/lib/src/backend/impls/objectbox/backend.dart index 0b0ee615..a3ec8459 100644 --- a/lib/src/backend/impls/objectbox/backend.dart +++ b/lib/src/backend/impls/objectbox/backend.dart @@ -8,6 +8,8 @@ import 'dart:isolate'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:latlong2/latlong.dart'; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; @@ -202,10 +204,6 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { FMTCBackendAccess.internal = null; } - @override - Future> listStores() async => - (await _sendCmd(type: _WorkerCmdType.storeExists))!['stores']; - @override Future rootSize() async => (await _sendCmd(type: _WorkerCmdType.rootSize))!['size']; @@ -214,6 +212,10 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { Future rootLength() async => (await _sendCmd(type: _WorkerCmdType.rootLength))!['length']; + @override + Future> listStores() async => + (await _sendCmd(type: _WorkerCmdType.storeExists))!['stores']; + @override Future storeExists({ required String storeName, @@ -429,6 +431,20 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { args: {'storeName': storeName}, ); + @override + Future> listRecoverableRegions() async => + (await _sendCmd( + type: _WorkerCmdType.listRecoverableRegions, + ))!['recoverableRegions']; + + @override + Future getRecoverableRegion({ + required int id, + }) async => + (await _sendCmd( + type: _WorkerCmdType.getRecoverableRegion, + ))!['recoverableRegion']; + @override Future startRecovery({ required int id, @@ -441,8 +457,8 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { ); @override - Future endRecovery({ + Future cancelRecovery({ required int id, }) => - _sendCmd(type: _WorkerCmdType.endRecovery, args: {'id': id}); + _sendCmd(type: _WorkerCmdType.cancelRecovery, args: {'id': id}); } diff --git a/lib/src/backend/impls/objectbox/worker.dart b/lib/src/backend/impls/objectbox/worker.dart index 088da7d6..20639c6b 100644 --- a/lib/src/backend/impls/objectbox/worker.dart +++ b/lib/src/backend/impls/objectbox/worker.dart @@ -6,9 +6,9 @@ part of 'backend.dart'; enum _WorkerCmdType { initialise_, // Only valid as a response destroy_, // Only valid as a request - listStores, rootSize, rootLength, + listStores, storeExists, createStore, resetStore, @@ -27,8 +27,10 @@ enum _WorkerCmdType { setBulkMetadata, removeMetadata, resetMetadata, + listRecoverableRegions, + getRecoverableRegion, startRecovery, - endRecovery, + cancelRecovery, } Future _worker( @@ -137,18 +139,6 @@ Future _worker( // TODO: Consider final message Isolate.exit(); - case _WorkerCmdType.listStores: - final query = root - .box() - .query() - .build() - .property(ObjectBoxStore_.name); - - sendRes(id: cmd.id, data: {'stores': query.find()}); - - query.close(); - - break; case _WorkerCmdType.rootSize: final query = root .box() @@ -171,6 +161,15 @@ Future _worker( query.close(); + break; + case _WorkerCmdType.listStores: + sendRes( + id: cmd.id, + data: { + 'stores': root.box().getAll().map((e) => e.name), + }, + ); + break; case _WorkerCmdType.storeExists: final query = root @@ -685,6 +684,58 @@ Future _worker( sendRes(id: cmd.id); + break; + case _WorkerCmdType.listRecoverableRegions: + sendRes( + id: cmd.id, + data: { + 'recoverableRegions': root.box().getAll().map( + (r) => RecoveredRegion( + id: r.id, + storeName: r.storeName, + time: r.creationTime, + bounds: r.typeId == 0 + ? LatLngBounds( + LatLng(r.rectNwLat!, r.rectNwLng!), + LatLng(r.rectSeLat!, r.rectSeLng!), + ) + : null, + center: r.typeId == 1 + ? LatLng(r.circleCenterLat!, r.circleCenterLng!) + : null, + line: r.typeId == 2 || r.typeId == 3 + ? List.generate( + r.lineLats!.length, + (i) => LatLng(r.lineLats![i], r.lineLngs![i]), + ) + : null, + radius: r.typeId == 1 + ? r.circleRadius! + : r.typeId == 2 + ? r.lineRadius! + : null, + minZoom: r.minZoom, + maxZoom: r.maxZoom, + start: r.startTile, + end: r.endTile, + ), + ), + }, + ); + + break; + case _WorkerCmdType.getRecoverableRegion: + final id = cmd.args['id']! as int; + + final query = root + .box() + .query(ObjectBoxRecovery_.refId.equals(id)) + .build(); + + sendRes(id: cmd.id, data: {'recoverableRegion': query.findUnique()}); + + query.close(); + break; case _WorkerCmdType.startRecovery: final id = cmd.args['id']! as int; @@ -702,7 +753,7 @@ Future _worker( sendRes(id: cmd.id); break; - case _WorkerCmdType.endRecovery: + case _WorkerCmdType.cancelRecovery: root .box() .query(ObjectBoxRecovery_.refId.equals(cmd.args['id']! as int)) diff --git a/lib/src/backend/interfaces/backend.dart b/lib/src/backend/interfaces/backend.dart index 24165d8d..b4b82c26 100644 --- a/lib/src/backend/interfaces/backend.dart +++ b/lib/src/backend/interfaces/backend.dart @@ -74,11 +74,6 @@ abstract interface class FMTCBackendInternal with FMTCBackendAccess { /// initialised. Directory? get rootDirectory; - /// {@template fmtc.backend.listStores} - /// List all the available stores - /// {@endtemplate} - Future> listStores(); - /// {@template fmtc.backend.rootSize} /// Retrieve the total number of KiBs of all tiles' bytes (not 'real total' /// size) from all stores @@ -90,6 +85,11 @@ abstract interface class FMTCBackendInternal with FMTCBackendAccess { /// {@endtemplate} Future rootLength(); + /// {@template fmtc.backend.listStores} + /// List all the available stores + /// {@endtemplate} + Future> listStores(); + /// {@template fmtc.backend.storeExists} /// Check whether the specified store currently exists /// {@endtemplate} @@ -274,13 +274,32 @@ abstract interface class FMTCBackendInternal with FMTCBackendAccess { required String storeName, }); + /// List all registered recovery regions + /// + /// Not all regions are failed, requires the [RootRecovery] object to + /// determine this. + Future> listRecoverableRegions(); + + /// Retrieve the specified registered recovery region + /// + /// Not all regions are failed, requires the [RootRecovery] object to + /// determine this. + Future getRecoverableRegion({ + required int id, + }); + + /// Create a recovery store with a recoverable region from the specified + /// components Future startRecovery({ required int id, required String storeName, required DownloadableRegion region, }); - Future endRecovery({ + /// {@template fmtc.backend.cancelRecovery} + /// Safely cancel the specified recoverable region + /// {@endtemplate} + Future cancelRecovery({ required int id, }); } diff --git a/lib/src/misc/with_backend_access.dart b/lib/src/misc/with_backend_access.dart deleted file mode 100644 index 55f9be41..00000000 --- a/lib/src/misc/with_backend_access.dart +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright © Luka S (JaffaKetchup) under GPL-v3 -// A full license can be found at .\LICENSE - -part of flutter_map_tile_caching; - -abstract base class _WithBackendAccess { - const _WithBackendAccess(this._store); - - final FMTCStore _store; - FMTCBackendInternal get _backend => FMTCBackendAccess.internal; - String get _storeName => _store.storeName; -} diff --git a/lib/src/regions/recovered_region.dart b/lib/src/regions/recovered_region.dart index aa48326d..4dab0ada 100644 --- a/lib/src/regions/recovered_region.dart +++ b/lib/src/regions/recovered_region.dart @@ -6,19 +6,12 @@ part of flutter_map_tile_caching; /// A mixture between [BaseRegion] and [DownloadableRegion] containing all the /// salvaged data from a recovered download /// -/// How does recovery work? At the start of a download, a file is created -/// including information about the download. At the end of a download or when a -/// download is correctly cancelled, this file is deleted. However, if there is -/// no ongoing download (controlled by an internal variable) and the recovery -/// file exists, the download has obviously been stopped incorrectly, meaning it -/// can be recovered using the information within the recovery file. +/// See [RootRecovery] for information about the recovery system. /// /// The availability of [bounds], [line], [center] & [radius] depend on the -/// [_type] of the recovered region. -/// -/// Should avoid manual construction. Use [toDownloadable] to restore a valid +/// type [R] of the recovered region. Use [toDownloadable] to restore a valid /// [DownloadableRegion]. -class RecoveredRegion { +class RecoveredRegion { /// A unique ID created for every bulk download operation /// /// Not actually used when converting to [DownloadableRegion]. @@ -34,7 +27,17 @@ class RecoveredRegion { /// Not actually used when converting to [DownloadableRegion]. final DateTime time; - final RegionType _type; + /// The minimum zoom level to fetch tiles for + final int minZoom; + + /// The maximum zoom level to fetch tiles for + final int maxZoom; + + /// Optionally skip past a number of tiles 'at the start' of a region + final int start; + + /// Optionally skip a number of tiles 'at the end' of a region + final int? end; /// The bounds for a rectangular region final LatLngBounds? bounds; @@ -49,61 +52,37 @@ class RecoveredRegion { /// The radius of a circular region final double? radius; - /// The minimum zoom level to fetch tiles for - final int minZoom; - - /// The maximum zoom level to fetch tiles for - final int maxZoom; - - /// Optionally skip past a number of tiles 'at the start' of a region - final int start; - - /// Optionally skip a number of tiles 'at the end' of a region - final int? end; - - RecoveredRegion._({ + @internal + RecoveredRegion({ required this.id, required this.storeName, required this.time, - required RegionType type, - required this.bounds, - required this.center, - required this.line, - required this.radius, required this.minZoom, required this.maxZoom, required this.start, required this.end, - }) : _type = type; + required this.bounds, + required this.center, + required this.line, + required this.radius, + }); - /// Convert this region into it's original [BaseRegion], calling the respective - /// callback with it - T toRegion({ - required T Function(RectangleRegion rectangle) rectangle, - required T Function(CircleRegion circle) circle, - required T Function(LineRegion line) line, - required T Function(CustomPolygonRegion customPolygon) customPolygon, - }) => - switch (_type) { - RegionType.rectangle => rectangle(RectangleRegion(bounds!)), - RegionType.circle => circle(CircleRegion(center!, radius!)), - RegionType.line => line(LineRegion(this.line!, radius!)), - RegionType.customPolygon => - customPolygon(CustomPolygonRegion(this.line!)), - }; + /// Convert this region into a [BaseRegion] + R toRegion() => switch (R) { + RectangleRegion => RectangleRegion(bounds!), + CircleRegion => CircleRegion(center!, radius!), + LineRegion => LineRegion(this.line!, radius!), + CustomPolygonRegion => CustomPolygonRegion(this.line!), + _ => throw UnimplementedError(), + } as R; /// Convert this region into a [DownloadableRegion] - DownloadableRegion toDownloadable( + DownloadableRegion toDownloadable( TileLayer options, { Crs crs = const Epsg3857(), }) => DownloadableRegion._( - toRegion( - rectangle: (r) => r, - circle: (c) => c, - line: (l) => l, - customPolygon: (p) => p, - ), + toRegion(), minZoom: minZoom, maxZoom: maxZoom, options: options, diff --git a/lib/src/root/directory.dart b/lib/src/root/directory.dart index 30e639d2..cc0456e7 100644 --- a/lib/src/root/directory.dart +++ b/lib/src/root/directory.dart @@ -32,10 +32,8 @@ abstract class FMTCRoot { static RootStats get stats => const RootStats._(); /// Manage the download recovery of all sub-stores - static RootRecovery get recovery => RootRecovery.instance ?? RootRecovery._(); - - /// Manage migration for file structure across FMTC versions - static RootMigrator get migrator => const RootMigrator._(); + static RootRecovery get recovery => + RootRecovery._instance ?? RootRecovery._(); /// Provides store import functionality for this root /// diff --git a/lib/src/root/migrator.dart b/lib/src/root/migrator.dart deleted file mode 100644 index fd3159cc..00000000 --- a/lib/src/root/migrator.dart +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright © Luka S (JaffaKetchup) under GPL-v3 -// A full license can be found at .\LICENSE - -part of flutter_map_tile_caching; - -/// Manage migration for file structure across FMTC versions -/// -/// There is no migration available to v9. -class RootMigrator { - const RootMigrator._(); -} diff --git a/lib/src/root/recovery.dart b/lib/src/root/recovery.dart index a6f2e239..3a1228e0 100644 --- a/lib/src/root/recovery.dart +++ b/lib/src/root/recovery.dart @@ -5,82 +5,57 @@ part of flutter_map_tile_caching; /// Manages the download recovery of all sub-stores of this [FMTCRoot] /// -/// Is a singleton to ensure functioning as expected. +/// --- +/// +/// When a download is started, a recovery region is stored in a database, and +/// the download ID is stored in memory (in a singleton to ensure it is never +/// disposed except when the application is closed). +/// +/// If the download finishes normally, both entries are removed, otherwise, the +/// memory is cleared when the app is closed, but the database record is not +/// removed. +/// +/// {@template fmtc.rootRecovery.failedDefinition} +/// A failed download is one that was found in the recovery database, but not +/// in the application memory. It can therefore be assumed that the download +/// is also no longer in memory, and therefore stopped unexpectedly. +/// {@endtemplate} class RootRecovery { + static RootRecovery? _instance; RootRecovery._() { - instance = this; + _instance = this; } - Isar get _recovery => FMTCRegistry.instance.recoveryDatabase; + /// Determines which downloads are known to be on-going, and therefore + /// can be ignored when fetching [recoverableRegions] + final Set _downloadsOngoing = {}; - /// Manages the download recovery of all sub-stores of this [FMTCRoot] + /// List all recoverable regions, and whether each one has failed /// - /// Is a singleton to ensure functioning as expected. - static RootRecovery? instance; - - /// Keeps a list of downloads that are ongoing, so they are not recoverable - /// unnecessarily - final List _downloadsOngoing = []; - - /// Get a list of all recoverable regions - /// - /// See [failedRegions] for regions that correspond to failed/stopped downloads. - Future> get recoverableRegions async => - (await _recovery.recovery.where().findAll()) - .map( - (r) => RecoveredRegion._( - id: r.id, - storeName: r.storeName, - time: r.time, - type: r.type, - bounds: r.type == RegionType.rectangle - ? LatLngBounds( - LatLng(r.nwLat!, r.nwLng!), - LatLng(r.seLat!, r.seLng!), - ) - : null, - center: r.type == RegionType.circle - ? LatLng(r.centerLat!, r.centerLng!) - : null, - line: r.type == RegionType.line || - r.type == RegionType.customPolygon - ? List.generate( - r.linePointsLat!.length, - (i) => LatLng(r.linePointsLat![i], r.linePointsLng![i]), - ) - : null, - radius: r.type != RegionType.rectangle - ? r.type == RegionType.circle - ? r.circleRadius! - : r.lineRadius! - : null, - minZoom: r.minZoom, - maxZoom: r.maxZoom, - start: r.start, - end: r.end, - ), - ) - .toList(); - - /// Get a list of all recoverable regions that correspond to failed/stopped downloads + /// Result can be filtered to only include failed downloads using the + /// [FMTCRecoveryGetFailedExts.failedOnly] extension. /// - /// See [recoverableRegions] for all regions. - Future> get failedRegions async => - (await recoverableRegions) - .where((r) => !_downloadsOngoing.contains(r.id)) - .toList(); + /// {@macro fmtc.rootRecovery.failedDefinition} + Future> + get recoverableRegions async => + FMTCBackendAccess.internal.listRecoverableRegions().then( + (rs) => rs.map( + (r) => + (isFailed: !_downloadsOngoing.contains(r.id), region: r), + ), + ); /// Get a specific region, even if it doesn't need recovering /// /// Returns `Future` if there was no region found - Future getRecoverableRegion(int id) async => - (await recoverableRegions).singleWhereOrNull((r) => r.id == id); + Future<({bool isFailed, RecoveredRegion region})?> getRecoverableRegion( + int id, + ) async { + final region = + await FMTCBackendAccess.internal.getRecoverableRegion(id: id); - /// Get a specific region, only if it needs recovering - /// - /// Returns `Future` if there was no region found - Future getFailedRegion(int id) async => - (await failedRegions).singleWhereOrNull((r) => r.id == id); + return (isFailed: !_downloadsOngoing.contains(region.id), region: region); + } Future _start({ required int id, @@ -88,89 +63,22 @@ class RootRecovery { required DownloadableRegion region, }) async { _downloadsOngoing.add(id); - return _recovery.writeTxn( - () => _recovery.recovery.put( - DbRecoverableRegion( - id: id, - storeName: storeName, - time: DateTime.now(), - type: region.when( - rectangle: (_) => RegionType.rectangle, - circle: (_) => RegionType.circle, - line: (_) => RegionType.line, - customPolygon: (_) => RegionType.customPolygon, - ), - minZoom: region.minZoom, - maxZoom: region.maxZoom, - start: region.start, - end: region.end, - nwLat: region.originalRegion is RectangleRegion - ? (region.originalRegion as RectangleRegion) - .bounds - .northWest - .latitude - : null, - nwLng: region.originalRegion is RectangleRegion - ? (region.originalRegion as RectangleRegion) - .bounds - .northWest - .longitude - : null, - seLat: region.originalRegion is RectangleRegion - ? (region.originalRegion as RectangleRegion) - .bounds - .southEast - .latitude - : null, - seLng: region.originalRegion is RectangleRegion - ? (region.originalRegion as RectangleRegion) - .bounds - .southEast - .longitude - : null, - centerLat: region.originalRegion is CircleRegion - ? (region.originalRegion as CircleRegion).center.latitude - : null, - centerLng: region.originalRegion is CircleRegion - ? (region.originalRegion as CircleRegion).center.longitude - : null, - linePointsLat: region.originalRegion is LineRegion - ? (region.originalRegion as LineRegion) - .line - .map((c) => c.latitude) - .toList() - : region.originalRegion is CustomPolygonRegion - ? (region.originalRegion as CustomPolygonRegion) - .outline - .map((c) => c.latitude) - .toList() - : null, - linePointsLng: region.originalRegion is LineRegion - ? (region.originalRegion as LineRegion) - .line - .map((c) => c.longitude) - .toList() - : region.originalRegion is CustomPolygonRegion - ? (region.originalRegion as CustomPolygonRegion) - .outline - .map((c) => c.longitude) - .toList() - : null, - circleRadius: region.originalRegion is CircleRegion - ? (region.originalRegion as CircleRegion).radius - : null, - lineRadius: region.originalRegion is LineRegion - ? (region.originalRegion as LineRegion).radius - : null, - ), - ), - ); + await FMTCBackendAccess.internal + .startRecovery(id: id, storeName: storeName, region: region); } - /// Safely cancel a recoverable region - Future cancel(int id) async { - if (_downloadsOngoing.remove(id)) { - return _recovery.writeTxn(() => _recovery.recovery.delete(id)); - } - } + /// {@macro fmtc.backend.cancelRecovery} + Future cancel(int id) async => + FMTCBackendAccess.internal.cancelRecovery(id: id); +} + +/// Contains [failedOnly] extension for [RootRecovery.recoverableRegions] +/// +/// See documentation on those methods for more information. +extension FMTCRecoveryGetFailedExts + on Iterable<({bool isFailed, RecoveredRegion region})> { + /// Filter the [RootRecovery.recoverableRegions] result to include only + /// failed downloads + Iterable get failedOnly => + where((r) => r.isFailed).map((r) => r.region); } diff --git a/lib/src/root/statistics.dart b/lib/src/root/statistics.dart index 527d0e0a..621665bf 100644 --- a/lib/src/root/statistics.dart +++ b/lib/src/root/statistics.dart @@ -50,18 +50,6 @@ class RootStats { StoreParts.stats, ], }) => - StreamGroup.merge([ - DirectoryWatcher(FMTC.instance.rootDirectory.directory.absolute.path) - .events - .where((e) => !path.dirname(e.path).endsWith('import')), - if (watchRecovery) - _registry.recoveryDatabase.recovery - .watchLazy(fireImmediately: fireImmediately), - ...recursive.map( - (s) => s.stats.watchChanges( - fireImmediately: fireImmediately, - storeParts: storeParts, - ), - ), - ]).debounce(debounce ?? Duration.zero); + // TODO: Implement + throw UnimplementedError(); } diff --git a/lib/src/store/manage.dart b/lib/src/store/manage.dart index 1452fedc..3b82d15b 100644 --- a/lib/src/store/manage.dart +++ b/lib/src/store/manage.dart @@ -9,20 +9,26 @@ part of flutter_map_tile_caching; /// If the store is not in the expected state (of existence) when invoking an /// operation, then an error will be thrown (likely [StoreNotExists] or /// [StoreAlreadyExists]). It is recommended to check [ready] when necessary. -final class StoreManagement extends _WithBackendAccess { - const StoreManagement._(super.store); +class StoreManagement { + StoreManagement._(FMTCStore store) : _storeName = store.storeName; + + final String _storeName; /// {@macro fmtc.backend.storeExists} - Future get ready => _backend.storeExists(storeName: _storeName); + Future get ready => + FMTCBackendAccess.internal.storeExists(storeName: _storeName); /// {@macro fmtc.backend.createStore} - Future create() => _backend.createStore(storeName: _storeName); + Future create() => + FMTCBackendAccess.internal.createStore(storeName: _storeName); /// {@macro fmtc.backend.deleteStore} - Future delete() => _backend.deleteStore(storeName: _storeName); + Future delete() => + FMTCBackendAccess.internal.deleteStore(storeName: _storeName); /// {@macro fmtc.backend.resetStore} - Future reset() => _backend.resetStore(storeName: _storeName); + Future reset() => + FMTCBackendAccess.internal.resetStore(storeName: _storeName); /// {@macro fmtc.backend.renameStore} /// @@ -30,7 +36,7 @@ final class StoreManagement extends _WithBackendAccess { /// always use the new returned value instead: returns a new [FMTCStore] /// after a successful renaming operation. Future rename(String newStoreName) async { - await _backend.renameStore( + await FMTCBackendAccess.internal.renameStore( currentStoreName: _storeName, newStoreName: newStoreName, ); @@ -40,7 +46,8 @@ final class StoreManagement extends _WithBackendAccess { /// {@macro fmtc.backend.removeTilesOlderThan} Future removeTilesOlderThan({required DateTime expiry}) => - _backend.removeTilesOlderThan(storeName: _storeName, expiry: expiry); + FMTCBackendAccess.internal + .removeTilesOlderThan(storeName: _storeName, expiry: expiry); // TODO: Define deprecation for `tileImageAsync` @@ -68,7 +75,8 @@ final class StoreManagement extends _WithBackendAccess { int? cacheWidth, int? cacheHeight, }) async { - final latestTile = await _backend.readLatestTile(storeName: _storeName); + final latestTile = + await FMTCBackendAccess.internal.readLatestTile(storeName: _storeName); if (latestTile == null) return null; return Image.memory( diff --git a/lib/src/store/metadata.dart b/lib/src/store/metadata.dart index 1030ec17..ed49f203 100644 --- a/lib/src/store/metadata.dart +++ b/lib/src/store/metadata.dart @@ -8,32 +8,38 @@ part of flutter_map_tile_caching; /// Uses a key-value format where both key and value must be [String]. More /// advanced requirements should use another package, as this is a basic /// implementation. -final class StoreMetadata extends _WithBackendAccess { - const StoreMetadata._(super._store); +class StoreMetadata { + StoreMetadata._(FMTCStore store) : _storeName = store.storeName; + + final String _storeName; /// {@macro fmtc.backend.readMetadata} Future> get read => - _backend.readMetadata(storeName: _storeName); + FMTCBackendAccess.internal.readMetadata(storeName: _storeName); /// {@macro fmtc.backend.setMetadata} Future set({ required String key, required String value, }) => - _backend.setMetadata(storeName: _storeName, key: key, value: value); + FMTCBackendAccess.internal + .setMetadata(storeName: _storeName, key: key, value: value); /// {@macro fmtc.backend.setBulkMetadata} Future setBulk({ required Map kvs, }) => - _backend.setBulkMetadata(storeName: _storeName, kvs: kvs); + FMTCBackendAccess.internal + .setBulkMetadata(storeName: _storeName, kvs: kvs); /// {@macro fmtc.backend.removeMetadata} Future remove({ required String key, }) => - _backend.removeMetadata(storeName: _storeName, key: key); + FMTCBackendAccess.internal + .removeMetadata(storeName: _storeName, key: key); /// {@macro fmtc.backend.resetMetadata} - Future reset() => _backend.resetMetadata(storeName: _storeName); + Future reset() => + FMTCBackendAccess.internal.resetMetadata(storeName: _storeName); } diff --git a/lib/src/store/statistics.dart b/lib/src/store/statistics.dart index a1289bfb..7718b24f 100644 --- a/lib/src/store/statistics.dart +++ b/lib/src/store/statistics.dart @@ -9,8 +9,10 @@ part of flutter_map_tile_caching; /// operation, then an error will be thrown (likely [StoreNotExists] or /// [StoreAlreadyExists]). It is recommended to check [StoreManagement.ready] /// when necessary. -final class StoreStats extends _WithBackendAccess { - const StoreStats._(super._store); +class StoreStats { + StoreStats._(FMTCStore store) : _storeName = store.storeName; + + final String _storeName; /// {@macro fmtc.backend.getStoreStats} /// @@ -20,7 +22,7 @@ final class StoreStats extends _WithBackendAccess { /// stats, and so is more efficient. /// {@endtemplate} Future<({double size, int length, int hits, int misses})> get all => - _backend.getStoreStats(storeName: _storeName); + FMTCBackendAccess.internal.getStoreStats(storeName: _storeName); /// Retrieve the total number of KiBs of all tiles' bytes (not 'real total' /// size) @@ -69,6 +71,7 @@ final class StoreStats extends _WithBackendAccess { StoreParts.stats, ], }) => + // TODO: Implement throw UnimplementedError(); } diff --git a/pubspec.yaml b/pubspec.yaml index 3003ab75..17a8589a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -40,8 +40,6 @@ dependencies: objectbox_flutter_libs: any path: ^1.9.0 path_provider: ^2.1.2 - stream_transform: ^2.1.0 - watcher: ^1.1.0 dev_dependencies: build_runner: ^2.4.7 diff --git a/tile_server/pubspec.yaml b/tile_server/pubspec.yaml index 2c802e3c..b1bec82d 100644 --- a/tile_server/pubspec.yaml +++ b/tile_server/pubspec.yaml @@ -8,4 +8,4 @@ environment: dependencies: dart_console: ^1.2.0 jaguar: ^3.1.3 - path: ^1.8.3 + path: ^1.9.0 From 872cde059f48aa414b3e209cfc2b2d1b23fad818 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Wed, 21 Feb 2024 11:29:15 +0000 Subject: [PATCH 098/168] Implemented initial cache hit/miss logic Former-commit-id: cf2b572ce556ed6ae8c396b3da74a01cdb7f562a [formerly 5e4e5ccacb3505c205b40a2b15f603fed924c318] Former-commit-id: 6a6ef825618bd9bec3b70b3a4856aa9390e04bab --- lib/src/backend/impls/objectbox/backend.dart | 12 ++++++++- lib/src/backend/impls/objectbox/worker.dart | 28 ++++++++++++++++++++ lib/src/backend/interfaces/backend.dart | 6 +++++ lib/src/providers/image_provider.dart | 21 +++------------ 4 files changed, 49 insertions(+), 18 deletions(-) diff --git a/lib/src/backend/impls/objectbox/backend.dart b/lib/src/backend/impls/objectbox/backend.dart index a3ec8459..5f59a11a 100644 --- a/lib/src/backend/impls/objectbox/backend.dart +++ b/lib/src/backend/impls/objectbox/backend.dart @@ -315,14 +315,24 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { @override Future deleteTile({ - required String url, required String storeName, + required String url, }) async => (await _sendCmd( type: _WorkerCmdType.deleteStore, args: {'storeName': storeName, 'url': url}, ))!['wasOrphan']; + @override + Future registerHitOrMiss({ + required String storeName, + required bool hit, + }) => + _sendCmd( + type: _WorkerCmdType.deleteStore, + args: {'storeName': storeName, 'hit': hit}, + ); + @override Future removeOldestTilesAboveLimit({ required String storeName, diff --git a/lib/src/backend/impls/objectbox/worker.dart b/lib/src/backend/impls/objectbox/worker.dart index 20639c6b..92444a84 100644 --- a/lib/src/backend/impls/objectbox/worker.dart +++ b/lib/src/backend/impls/objectbox/worker.dart @@ -20,6 +20,7 @@ enum _WorkerCmdType { readLatestTile, writeTile, deleteTile, + registerHitOrMiss, removeOldestTilesAboveLimit, removeTilesOlderThan, readMetadata, @@ -472,6 +473,33 @@ Future _worker( query.close(); + break; + case _WorkerCmdType.registerHitOrMiss: + final storeName = cmd.args['storeName']! as String; + final hit = cmd.args['hit']! as bool; + + final stores = root.box(); + + final query = + stores.query(ObjectBoxStore_.name.equals(storeName)).build(); + + root.runInTransaction( + TxMode.write, + () { + final store = query.findUnique() ?? + (throw StoreNotExists(storeName: storeName)); + query.close(); + + stores.put( + store + ..hits += hit ? 1 : 0 + ..misses += hit ? 0 : 1, + ); + }, + ); + + sendRes(id: cmd.id); + break; case _WorkerCmdType.removeOldestTilesAboveLimit: final storeName = cmd.args['storeName']! as String; diff --git a/lib/src/backend/interfaces/backend.dart b/lib/src/backend/interfaces/backend.dart index b4b82c26..2f5f00b8 100644 --- a/lib/src/backend/interfaces/backend.dart +++ b/lib/src/backend/interfaces/backend.dart @@ -194,6 +194,12 @@ abstract interface class FMTCBackendInternal with FMTCBackendAccess { required String url, }); + /// Register a cache hit or miss on the specified store + Future registerHitOrMiss({ + required String storeName, + required bool hit, + }); + // TODO: Verify below and add to belower doc string // // It is recommended to invoke this operation as few times as possible, for diff --git a/lib/src/providers/image_provider.dart b/lib/src/providers/image_provider.dart index 20253470..0d639a42 100644 --- a/lib/src/providers/image_provider.dart +++ b/lib/src/providers/image_provider.dart @@ -78,7 +78,10 @@ class FMTCImageProvider extends ImageProvider { unawaited(chunkEvents.close()); await evict(); - unawaited(_cacheHitMiss(hit: cacheHit)); + unawaited( + FMTCBackendAccess.internal + .registerHitOrMiss(storeName: storeName, hit: cacheHit), + ); return decode(await ImmutableBuffer.fromUint8List(bytes)); } @@ -228,22 +231,6 @@ class FMTCImageProvider extends ImageProvider { return finishSuccessfully(bytes: responseBytes, cacheHit: false); } - Future _cacheHitMiss({required bool hit}) => - (hit ? _cacheHitsQueue : _cacheMissesQueue).add(() async { - if (db.isOpen) { - await db.writeTxn(() async { - final store = db.isOpen ? await db.descriptor : null; - if (store == null) return; - if (hit) { - store.hits += 1; - } else { - store.misses += 1; - } - await db.storeDescriptor.put(store); - }); - } - }); - @override Future obtainKey(ImageConfiguration configuration) => SynchronousFuture(this); From 6f13a767c50760fcd308cd85db2ab4a89c954afb Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Wed, 21 Feb 2024 17:40:57 +0000 Subject: [PATCH 099/168] =?UTF-8?q?**0=20syntax=20issues=20=20=F0=9F=8E=89?= =?UTF-8?q?**=20Implement=20bulk=20downloading=20backend=20support=20(atte?= =?UTF-8?q?mpt)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Former-commit-id: 19ea7d2772f94fed1558dcebd6fb7d4be7d83f59 [formerly 447225dae45b1e67f3770b3b5132627377b1c85a] Former-commit-id: 47c57ca531febac0899813859af0cdbfd1abb601 --- example/lib/screens/main/main.dart | 6 +- .../recovery/components/recovery_list.dart | 38 +-- .../components/recovery_start_button.dart | 112 ++++--- .../screens/main/pages/recovery/recovery.dart | 5 +- .../pages/stores/components/store_tile.dart | 277 ++++++++++-------- lib/src/backend/impls/objectbox/backend.dart | 11 + .../impls/objectbox/models/src/recovery.dart | 2 +- lib/src/backend/impls/objectbox/worker.dart | 94 ++++++ lib/src/backend/interfaces/backend.dart | 13 + lib/src/bulk_download/manager.dart | 7 +- lib/src/bulk_download/thread.dart | 47 +-- lib/src/store/download.dart | 3 +- 12 files changed, 374 insertions(+), 241 deletions(-) diff --git a/example/lib/screens/main/main.dart b/example/lib/screens/main/main.dart index bf3a11b4..e37b0aa5 100644 --- a/example/lib/screens/main/main.dart +++ b/example/lib/screens/main/main.dart @@ -42,15 +42,15 @@ class _MainScreenState extends State { label: 'Recover', icon: StreamBuilder( stream: FMTCRoot.stats.watchChanges().asBroadcastStream(), - builder: (context, _) => FutureBuilder>( - future: FMTCRoot.recovery.failedRegions, + builder: (context, _) => FutureBuilder( + future: FMTCRoot.recovery.recoverableRegions, builder: (context, snapshot) => Badge( position: BadgePosition.topEnd(top: -5, end: -6), badgeAnimation: const BadgeAnimation.size( animationDuration: Duration(milliseconds: 100), ), showBadge: _currentPageIndex != 3 && - (snapshot.data?.isNotEmpty ?? false), + (snapshot.data?.failedOnly.isNotEmpty ?? false), child: const Icon(Icons.support), ), ), diff --git a/example/lib/screens/main/pages/recovery/components/recovery_list.dart b/example/lib/screens/main/pages/recovery/components/recovery_list.dart index eac8b7e4..3be7f7d1 100644 --- a/example/lib/screens/main/pages/recovery/components/recovery_list.dart +++ b/example/lib/screens/main/pages/recovery/components/recovery_list.dart @@ -11,7 +11,7 @@ class RecoveryList extends StatefulWidget { required this.moveToDownloadPage, }); - final List all; + final Iterable<({bool isFailed, RecoveredRegion region})> all; final void Function() moveToDownloadPage; @override @@ -23,30 +23,22 @@ class _RecoveryListState extends State { Widget build(BuildContext context) => ListView.builder( itemCount: widget.all.length, itemBuilder: (context, index) { - final region = widget.all[index]; + final result = widget.all.elementAt(index); + final region = result.region; + final isFailed = result.isFailed; return ListTile( - leading: FutureBuilder( - future: FMTCRoot.recovery.getFailedRegion(region.id), - builder: (context, isFailed) => Icon( - isFailed.data != null - ? Icons.warning - : region.toRegion( - rectangle: (_) => Icons.square_outlined, - circle: (_) => Icons.circle_outlined, - line: (_) => Icons.polyline_outlined, - customPolygon: (_) => Icons.pentagon_outlined, - ), - color: isFailed.data != null ? Colors.red : null, - ), + leading: Icon( + isFailed ? Icons.warning : Icons.pending_actions, + color: isFailed ? Colors.red : null, ), title: Text( - '${region.storeName} - ${region.toRegion( - rectangle: (_) => 'Rectangle', - circle: (_) => 'Circle', - line: (_) => 'Line', - customPolygon: (_) => 'Custom Polygon', - )} Type', + '${region.storeName} - ${switch (region.toRegion()) { + RectangleRegion() => 'Rectangle', + CircleRegion() => 'Circle', + LineRegion() => 'Line', + CustomPolygonRegion() => 'Custom Polygon', + }} Type', ), subtitle: FutureBuilder( future: Nominatim.reverseSearch( @@ -60,7 +52,7 @@ class _RecoveryListState extends State { addressDetails: true, ), builder: (context, response) => Text( - 'Started at ${region.time} (~${DateTime.now().difference(region.time).inMinutes} minutes ago)\n${response.hasData ? 'Center near ${response.data!.address!['postcode']}, ${response.data!.address!['country']}' : response.hasError ? 'Unable To Reverse Geocode Location' : 'Please Wait...'}', + 'Started at ${region.time} (~${DateTime.timestamp().difference(region.time).inMinutes} minutes ago)\n${response.hasData ? 'Center near ${response.data!.address!['postcode']}, ${response.data!.address!['country']}' : response.hasError ? 'Unable To Reverse Geocode Location' : 'Please Wait...'}', ), ), onTap: () {}, @@ -82,7 +74,7 @@ class _RecoveryListState extends State { const SizedBox(width: 10), RecoveryStartButton( moveToDownloadPage: widget.moveToDownloadPage, - region: region, + result: result, ), ], ), diff --git a/example/lib/screens/main/pages/recovery/components/recovery_start_button.dart b/example/lib/screens/main/pages/recovery/components/recovery_start_button.dart index 6849a315..836ac40a 100644 --- a/example/lib/screens/main/pages/recovery/components/recovery_start_button.dart +++ b/example/lib/screens/main/pages/recovery/components/recovery_start_button.dart @@ -1,77 +1,71 @@ import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; -//import 'package:provider/provider.dart'; - -//import '../../../../../shared/state/download_provider.dart'; -//import '../../../../download_region/download_region.dart'; class RecoveryStartButton extends StatelessWidget { const RecoveryStartButton({ super.key, required this.moveToDownloadPage, - required this.region, + required this.result, }); final void Function() moveToDownloadPage; - final RecoveredRegion region; + final ({bool isFailed, RecoveredRegion region}) result; @override - Widget build(BuildContext context) => FutureBuilder( - future: FMTCRoot.recovery.getFailedRegion(region.id), - builder: (context, isFailed) => FutureBuilder( - future: const FMTCStore('') - .download - .check(region.toDownloadable(TileLayer())), - builder: (context, tiles) => tiles.hasData - ? IconButton( - icon: Icon( - Icons.download, - color: isFailed.data != null ? Colors.green : null, - ), - onPressed: isFailed.data == null - ? null - : () async { - /* final DownloaderProvider downloadProvider = - Provider.of( - context, - listen: false, + Widget build(BuildContext context) => FutureBuilder( + future: const FMTCStore('') + .download + .check(result.region.toDownloadable(TileLayer())), + builder: (context, tiles) => tiles.hasData + ? IconButton( + icon: Icon( + Icons.download, + color: result.isFailed ? Colors.green : null, + ), + onPressed: !result.isFailed + ? null + : () async { + //TODO: Implement + /* final DownloaderProvider downloadProvider = + Provider.of( + context, + listen: false, + ) + ..region = region.toRegion( + rectangle: (r) => r, + circle: (c) => c, + line: (l) => l, ) - ..region = region.toRegion( - rectangle: (r) => r, - circle: (c) => c, - line: (l) => l, - ) - ..minZoom = region.minZoom - ..maxZoom = region.maxZoom - ..setSelectedStore( - FMTC.instance(region.storeName), - ) - ..regionTiles = tiles.data; - - await Navigator.of(context).push( - MaterialPageRoute( - builder: (BuildContext context) => - DownloadRegionPopup( - region: downloadProvider.region!, - ), - fullscreenDialog: true, - ), - ); - - moveToDownloadPage();*/ - }, - ) - : const Padding( - padding: EdgeInsets.all(8), - child: SizedBox( - height: 24, - width: 24, - child: CircularProgressIndicator( - strokeWidth: 3, - ), + ..minZoom = region.minZoom + ..maxZoom = region.maxZoom + ..setSelectedStore( + FMTC.instance(region.storeName), + ) + ..regionTiles = tiles.data; + + await Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) => + DownloadRegionPopup( + region: downloadProvider.region!, + ), + fullscreenDialog: true, + ), + ); + + moveToDownloadPage();*/ + }, + ) + : const Padding( + padding: EdgeInsets.all(8), + child: SizedBox( + height: 24, + width: 24, + child: CircularProgressIndicator( + strokeWidth: 3, ), ), - ), + ), ); } diff --git a/example/lib/screens/main/pages/recovery/recovery.dart b/example/lib/screens/main/pages/recovery/recovery.dart index 3be21ffc..c09e263a 100644 --- a/example/lib/screens/main/pages/recovery/recovery.dart +++ b/example/lib/screens/main/pages/recovery/recovery.dart @@ -19,7 +19,8 @@ class RecoveryPage extends StatefulWidget { } class _RecoveryPageState extends State { - late Future> _recoverableRegions; + late Future region})>> + _recoverableRegions; @override void initState() { @@ -48,7 +49,7 @@ class _RecoveryPageState extends State { const Header(), const SizedBox(height: 12), Expanded( - child: FutureBuilder>( + child: FutureBuilder( future: _recoverableRegions, builder: (context, all) => all.hasData ? all.data!.isEmpty diff --git a/example/lib/screens/main/pages/stores/components/store_tile.dart b/example/lib/screens/main/pages/stores/components/store_tile.dart index 71422bc5..1a3f5ce8 100644 --- a/example/lib/screens/main/pages/stores/components/store_tile.dart +++ b/example/lib/screens/main/pages/stores/components/store_tile.dart @@ -138,153 +138,170 @@ class _StoreTileState extends State { style: TextStyle( fontWeight: isCurrentStore ? FontWeight.bold : FontWeight.normal, - color: _store.manage.ready == false ? Colors.red : null, + // color: _store.manage.ready == false ? Colors.red : null, ), ), subtitle: _deletingProgress ? const Text('Deleting...') : null, - leading: _store.manage.ready == false - ? const Icon( - Icons.error, - color: Colors.red, - ) - : null, + // leading: _store.manage.ready == false + // ? const Icon( + // Icons.error, + // color: Colors.red, + // ) + // : null, onExpansionChanged: (e) { if (e) _loadStatistics(); }, children: [ - SizedBox( - width: double.infinity, - child: Padding( - padding: const EdgeInsets.only(left: 18, bottom: 10), - child: _store.manage.ready - ? Column( - children: [ - statistics!, - const SizedBox(height: 15), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, + FutureBuilder( + future: _store.manage.ready, + builder: (context, snapshot) { + if (snapshot.data == null) { + return const CircularProgressIndicator.adaptive(); + } + + return SizedBox( + width: double.infinity, + child: Padding( + padding: const EdgeInsets.only(left: 18, bottom: 10), + child: snapshot.data! + ? Column( children: [ - deleteStoreButton( - isCurrentStore: isCurrentStore, - ), - IconButton( - icon: _emptyingProgress - ? const CircularProgressIndicator( - strokeWidth: 3, - ) - : const Icon(Icons.delete), - tooltip: 'Empty Store', - onPressed: _emptyingProgress - ? null - : () async { - setState( - () => _emptyingProgress = true, - ); - await _store.manage.reset(); - setState( - () => _emptyingProgress = false, - ); + statistics!, + const SizedBox(height: 15), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceEvenly, + children: [ + deleteStoreButton( + isCurrentStore: isCurrentStore, + ), + IconButton( + icon: _emptyingProgress + ? const CircularProgressIndicator( + strokeWidth: 3, + ) + : const Icon(Icons.delete), + tooltip: 'Empty Store', + onPressed: _emptyingProgress + ? null + : () async { + setState( + () => _emptyingProgress = true, + ); + await _store.manage.reset(); + setState( + () => _emptyingProgress = false, + ); - _loadStatistics(); - }, - ), - IconButton( - icon: _exportingProgress - ? const CircularProgressIndicator( - strokeWidth: 3, - ) - : const Icon(Icons.upload_file_rounded), - tooltip: 'Export Store', - onPressed: _exportingProgress - ? null - : () async { - setState( - () => _exportingProgress = true, - ); - final bool result = await _store - .export - .withGUI(context: context); - setState( - () => _exportingProgress = false, - ); - if (mounted) { - ScaffoldMessenger.of(context) - .showSnackBar( - SnackBar( - content: Text( - result - ? 'Exported Sucessfully' - : 'Export Cancelled', - ), - ), - ); - } - }, - ), - IconButton( - icon: const Icon(Icons.edit), - tooltip: 'Edit Store', - onPressed: () => Navigator.of(context).push( - MaterialPageRoute( - builder: (BuildContext context) => - StoreEditorPopup( - existingStoreName: widget.storeName, - isStoreInUse: isCurrentStore, + _loadStatistics(); + }, + ), + IconButton( + icon: _exportingProgress + ? const CircularProgressIndicator( + strokeWidth: 3, + ) + : const Icon( + Icons.upload_file_rounded), + tooltip: 'Export Store', + onPressed: _exportingProgress + ? null + : () async { + setState( + () => _exportingProgress = true, + ); + final bool result = await _store + .export + .withGUI(context: context); + setState( + () => + _exportingProgress = false, + ); + if (mounted) { + ScaffoldMessenger.of(context) + .showSnackBar( + SnackBar( + content: Text( + result + ? 'Exported Sucessfully' + : 'Export Cancelled', + ), + ), + ); + } + }, + ), + IconButton( + icon: const Icon(Icons.edit), + tooltip: 'Edit Store', + onPressed: () => + Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) => + StoreEditorPopup( + existingStoreName: widget.storeName, + isStoreInUse: isCurrentStore, + ), + fullscreenDialog: true, + ), ), - fullscreenDialog: true, ), - ), + IconButton( + icon: const Icon(Icons.refresh), + tooltip: 'Force Refresh Statistics', + onPressed: _loadStatistics, + ), + IconButton( + icon: Icon( + Icons.done, + color: isCurrentStore + ? Colors.green + : null, + ), + tooltip: 'Use Store', + onPressed: isCurrentStore + ? null + : () { + provider + ..currentStore = + widget.storeName + ..resetMap(); + }, + ), + ], ), - IconButton( - icon: const Icon(Icons.refresh), - tooltip: 'Force Refresh Statistics', - onPressed: _loadStatistics, + ], + ) + : Column( + children: [ + const SizedBox(height: 10), + const Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.broken_image, size: 34), + Icon(Icons.error, size: 34), + ], ), - IconButton( - icon: Icon( - Icons.done, - color: isCurrentStore ? Colors.green : null, + const SizedBox(height: 8), + const Text( + 'Invalid Store', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, ), - tooltip: 'Use Store', - onPressed: isCurrentStore - ? null - : () { - provider - ..currentStore = widget.storeName - ..resetMap(); - }, ), + const Text( + "This store's directory structure appears to have been corrupted. You must delete the store to resolve the issue.", + textAlign: TextAlign.center, + ), + const SizedBox(height: 5), + deleteStoreButton( + isCurrentStore: isCurrentStore), ], ), - ], - ) - : Column( - children: [ - const SizedBox(height: 10), - const Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.broken_image, size: 34), - Icon(Icons.error, size: 34), - ], - ), - const SizedBox(height: 8), - const Text( - 'Invalid Store', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - ), - ), - const Text( - "This store's directory structure appears to have been corrupted. You must delete the store to resolve the issue.", - textAlign: TextAlign.center, - ), - const SizedBox(height: 5), - deleteStoreButton(isCurrentStore: isCurrentStore), - ], - ), - ), + ), + ); + }, ), ], ); diff --git a/lib/src/backend/impls/objectbox/backend.dart b/lib/src/backend/impls/objectbox/backend.dart index 5f59a11a..0579e02d 100644 --- a/lib/src/backend/impls/objectbox/backend.dart +++ b/lib/src/backend/impls/objectbox/backend.dart @@ -313,6 +313,17 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { args: {'storeName': storeName, 'url': url, 'bytes': bytes}, ); + @override + Future writeTilesDirect({ + required String storeName, + required List urls, + required List bytess, + }) => + _sendCmd( + type: _WorkerCmdType.writeTilesDirect, + args: {'storeName': storeName, 'urls': urls, 'bytess': bytess}, + ); + @override Future deleteTile({ required String storeName, diff --git a/lib/src/backend/impls/objectbox/models/src/recovery.dart b/lib/src/backend/impls/objectbox/models/src/recovery.dart index fc9ab366..96cb5645 100644 --- a/lib/src/backend/impls/objectbox/models/src/recovery.dart +++ b/lib/src/backend/impls/objectbox/models/src/recovery.dart @@ -70,7 +70,7 @@ base class ObjectBoxRecovery { required this.refId, required this.storeName, required DownloadableRegion region, - }) : creationTime = DateTime.now(), + }) : creationTime = DateTime.timestamp(), typeId = region.when( rectangle: (_) => 0, circle: (_) => 1, diff --git a/lib/src/backend/impls/objectbox/worker.dart b/lib/src/backend/impls/objectbox/worker.dart index 92444a84..631c1dee 100644 --- a/lib/src/backend/impls/objectbox/worker.dart +++ b/lib/src/backend/impls/objectbox/worker.dart @@ -19,6 +19,7 @@ enum _WorkerCmdType { readTile, readLatestTile, writeTile, + writeTilesDirect, deleteTile, registerHitOrMiss, removeOldestTilesAboveLimit, @@ -428,6 +429,7 @@ Future _worker( existingTile! ..lastModified = DateTime.timestamp() ..bytes = bytes!, + mode: PutMode.update, ); stores.putMany( existingTile.stores @@ -449,6 +451,98 @@ Future _worker( sendRes(id: cmd.id); + break; + case _WorkerCmdType.writeTilesDirect: + final storeName = cmd.args['storeName']! as String; + final urls = cmd.args['urls']! as List; + final bytess = cmd.args['bytess']! as List; + + final tiles = root.box(); + final stores = root.box(); + + final tilesQuery = tiles.query(ObjectBoxTile_.url.equals('')).build(); + final storeQuery = + stores.query(ObjectBoxStore_.name.equals('')).build(); + + final storeAdjustments = + {}; + + root.runInTransaction( + TxMode.write, + () { + tiles.putMany( + List.generate( + urls.length, + (i) { + final existingTile = (tilesQuery + ..param(ObjectBoxTile_.url).value = urls[i]) + .findUnique(); + + if (existingTile == null) { + storeAdjustments[storeName] = + storeAdjustments[storeName] == null + ? ( + deltaLength: 1, + deltaSize: bytess[i].lengthInBytes + ) + : ( + deltaLength: + storeAdjustments[storeName]!.deltaLength + + 1, + deltaSize: + storeAdjustments[storeName]!.deltaSize + + bytess[i].lengthInBytes, + ); + } else { + for (final store in existingTile.stores) { + storeAdjustments[store.name] = + storeAdjustments[store.name] == null + ? ( + deltaLength: 0, + deltaSize: + -existingTile.bytes.lengthInBytes + + bytess[i].lengthInBytes, + ) + : ( + deltaLength: storeAdjustments[store.name]! + .deltaLength, + deltaSize: storeAdjustments[store.name]! + .deltaSize + + (-existingTile.bytes.lengthInBytes + + bytess[i].lengthInBytes), + ); + } + } + + return ObjectBoxTile( + url: urls[i], + lastModified: DateTime.timestamp(), + bytes: bytess[i], + ); + }, + growable: false, + ), + ); + + stores.putMany( + storeAdjustments.entries + .map( + (e) => (storeQuery + ..param(ObjectBoxStore_.name).value = e.key) + .findUnique()! + ..length += e.value.deltaLength + ..size += e.value.deltaSize, + ) + .toList(), + ); + }, + ); + + sendRes(id: cmd.id); + + tilesQuery.close(); + storeQuery.close(); + break; case _WorkerCmdType.deleteTile: final storeName = cmd.args['storeName']! as String; diff --git a/lib/src/backend/interfaces/backend.dart b/lib/src/backend/interfaces/backend.dart index 2f5f00b8..0e632ed9 100644 --- a/lib/src/backend/interfaces/backend.dart +++ b/lib/src/backend/interfaces/backend.dart @@ -178,6 +178,19 @@ abstract interface class FMTCBackendInternal with FMTCBackendAccess { required Uint8List? bytes, }); + /// Create multiple tiles (given given their respective [urls] and [bytess]) in + /// the specified store + /// + /// Logic is much simpler than [writeTile] and designed to be faster to allow + /// for high bulk downloading throughputs. + /// + /// Existing tiles will always be overwritten if they exist. + Future writeTilesDirect({ + required String storeName, + required List urls, + required List bytess, + }); + /// Remove the tile from the specified store, deleting it if was orphaned /// /// As tiles can belong to multiple stores, a tile cannot be safely 'truly' diff --git a/lib/src/bulk_download/manager.dart b/lib/src/bulk_download/manager.dart index 88ea90d0..ac35d4af 100644 --- a/lib/src/bulk_download/manager.dart +++ b/lib/src/bulk_download/manager.dart @@ -16,6 +16,7 @@ Future _downloadManager( Duration? maxReportInterval, int? rateLimit, Iterable obscuredQueryParams, + FMTCBackendInternal backend, }) input, ) async { // Precalculate shared inputs for all threads @@ -104,9 +105,10 @@ Future _downloadManager( final tpsSmoothingStorage = [null]; int currentTPSSmoothingIndex = 0; double getCurrentTPS({required bool registerNewTPS}) { - if (registerNewTPS) tileCompletionTimestamps.add(DateTime.now()); + if (registerNewTPS) tileCompletionTimestamps.add(DateTime.timestamp()); tileCompletionTimestamps.removeWhere( - (e) => e.isBefore(DateTime.now().subtract(const Duration(seconds: 1))), + (e) => + e.isBefore(DateTime.timestamp().subtract(const Duration(seconds: 1))), ); currentTPSSmoothingIndex++; tpsSmoothingStorage[currentTPSSmoothingIndex % tpsSmoothingStorage.length] = @@ -195,6 +197,7 @@ Future _downloadManager( seaTileBytes: seaTileBytes, obscuredQueryParams: input.obscuredQueryParams, headers: headers, + backend: input.backend, ), onExit: downloadThreadReceivePort.sendPort, debugName: '[FMTC] Bulk Download Thread #$threadNo', diff --git a/lib/src/bulk_download/thread.dart b/lib/src/bulk_download/thread.dart index 044c19df..0fb066e6 100644 --- a/lib/src/bulk_download/thread.dart +++ b/lib/src/bulk_download/thread.dart @@ -14,6 +14,7 @@ Future _singleDownloadThread( Uint8List? seaTileBytes, Iterable obscuredQueryParams, Map headers, + FMTCBackendInternal backend, }) input, ) async { // Setup two-way communications @@ -24,19 +25,12 @@ Future _singleDownloadThread( // Setup tile queue final tileQueue = StreamQueue(receivePort); - // Open a reference to the Isar DB for the current store - final db = Isar.openSync( - [DbStoreDescriptorSchema, DbTileSchema, DbMetadataSchema], - name: input.storeId, - directory: input.rootDirectory, - inspector: false, - ); - // Initialise a long lasting HTTP client final httpClient = http.Client(); - // Initialise the tile buffer array - final tileBuffer = []; + // Initialise the tile buffer arrays + final tileUrlsBuffer = []; + final tileBytesBuffer = []; while (true) { // Request new tile coords @@ -50,10 +44,13 @@ Future _singleDownloadThread( httpClient.close(); - if (tileBuffer.isNotEmpty) { - db.writeTxnSync(() => db.tiles.putAllSync(tileBuffer)); + if (tileUrlsBuffer.isNotEmpty) { + await input.backend.writeTilesDirect( + storeName: input.storeName, + urls: tileUrlsBuffer, + bytess: tileBytesBuffer, + ); } - await db.close(); Isolate.exit(); } @@ -69,7 +66,7 @@ Future _singleDownloadThread( url: networkUrl, obscuredQueryParams: input.obscuredQueryParams, ); - final existingTile = await db.tiles.get(DatabaseTools.hash(matcherUrl)); + final existingTile = await input.backend.readTile(url: matcherUrl); // Skip if tile already exists and user demands existing tile pruning if (input.skipExistingTiles && existingTile != null) { @@ -130,18 +127,28 @@ Future _singleDownloadThread( } // Write tile directly to database or place in buffer queue - final tile = DbTile(url: matcherUrl, bytes: response.bodyBytes); + //final tile = DbTile(url: matcherUrl, bytes: response.bodyBytes); if (input.maxBufferLength == 0) { - db.writeTxnSync(() => db.tiles.putSync(tile)); + await input.backend.writeTile( + storeName: input.storeName, + url: matcherUrl, + bytes: response.bodyBytes, + ); } else { - tileBuffer.add(tile); + tileUrlsBuffer.add(matcherUrl); + tileBytesBuffer.add(response.bodyBytes); } // Write buffer to database if necessary - final wasBufferReset = tileBuffer.length >= input.maxBufferLength; + final wasBufferReset = tileUrlsBuffer.length >= input.maxBufferLength; if (wasBufferReset) { - db.writeTxnSync(() => db.tiles.putAllSync(tileBuffer)); - tileBuffer.clear(); + await input.backend.writeTilesDirect( + storeName: input.storeName, + urls: tileUrlsBuffer, + bytess: tileBytesBuffer, + ); + tileUrlsBuffer.clear(); + tileBytesBuffer.clear(); } // Return successful response to user diff --git a/lib/src/store/download.dart b/lib/src/store/download.dart index beeeaa65..1b207a27 100644 --- a/lib/src/store/download.dart +++ b/lib/src/store/download.dart @@ -157,7 +157,7 @@ class DownloadManagement { // Start recovery system (unless disabled) final recoveryId = - Object.hash(instanceId, DateTime.now().millisecondsSinceEpoch); + Object.hash(instanceId, DateTime.timestamp().millisecondsSinceEpoch); if (!disableRecovery) { await FMTCRoot.recovery._start( id: recoveryId, @@ -184,6 +184,7 @@ class DownloadManagement { obscuredQueryParams: obscuredQueryParams?.map((e) => RegExp('$e=[^&]*')) ?? FMTCTileProviderSettings.instance.obscuredQueryParams, + backend: FMTCBackendAccess.internal, ), onExit: receivePort.sendPort, debugName: '[FMTC] Master Bulk Download Thread', From 3a5e5fadceddc1cf03aee16d8ee9f90790870d1a Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Wed, 21 Feb 2024 20:52:02 +0000 Subject: [PATCH 100/168] Implemented watching Fixed bugs Enabled compilation of example app by temporary removal of import/export plugin Former-commit-id: 7c0e5cbe5bb1b0104473e8daeff86c789bb6f28b [formerly bfde1971b0bc06dcc01d4c2ce3326538ecae0421] Former-commit-id: f1b08e0a5f9f14d7b9f2bacdead55ea9b1e81bd3 --- .../screens/import_store/import_store.dart | 20 +-- example/lib/screens/main/main.dart | 2 +- .../screens/main/pages/recovery/recovery.dart | 2 +- .../pages/stores/components/store_tile.dart | 6 +- .../lib/screens/main/pages/stores/stores.dart | 5 +- example/pubspec.yaml | 2 +- example/windows/flutter/CMakeLists.txt | 7 +- .../flutter/generated_plugin_registrant.cc | 9 -- .../windows/flutter/generated_plugins.cmake | 3 - lib/flutter_map_tile_caching.dart | 1 - lib/src/backend/impls/objectbox/backend.dart | 49 +++++-- .../impls/objectbox/models/src/recovery.dart | 32 ++++- lib/src/backend/impls/objectbox/worker.dart | 92 ++++++------ lib/src/backend/interfaces/backend.dart | 27 ++++ lib/src/fmtc.dart | 133 ------------------ lib/src/root/statistics.dart | 56 +++----- lib/src/store/statistics.dart | 50 ++----- 17 files changed, 205 insertions(+), 291 deletions(-) delete mode 100644 lib/src/fmtc.dart diff --git a/example/lib/screens/import_store/import_store.dart b/example/lib/screens/import_store/import_store.dart index 44c78815..c8ecec2e 100644 --- a/example/lib/screens/import_store/import_store.dart +++ b/example/lib/screens/import_store/import_store.dart @@ -1,8 +1,8 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; -import 'package:fmtc_plus_sharing/fmtc_plus_sharing.dart'; +//import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; +//import 'package:fmtc_plus_sharing/fmtc_plus_sharing.dart'; class ImportStorePopup extends StatefulWidget { const ImportStorePopup({super.key}); @@ -14,14 +14,15 @@ class ImportStorePopup extends StatefulWidget { class _ImportStorePopupState extends State { final Map importStores = {}; +// TODO: Implement @override Widget build(BuildContext context) => Scaffold( appBar: AppBar( title: const Text('Import Stores'), ), - body: Padding( - padding: const EdgeInsets.all(12), - child: ListView.separated( + body: const Padding( + padding: EdgeInsets.all(12), + /*child: ListView.separated( itemCount: importStores.length + 1, itemBuilder: (context, i) { if (i == importStores.length) { @@ -30,7 +31,7 @@ class _ImportStorePopupState extends State { title: const Text('Choose New Store(s)'), subtitle: const Text('Select any valid store files (.fmtc)'), onTap: () async { - importStores.addAll( + /*importStores.addAll( (await FMTCRoot.import.withGUI( collisionHandler: (fn, sn) { setState( @@ -51,7 +52,8 @@ class _ImportStorePopupState extends State { _ImportStore(status, collisionInfo: null), ), ), - ); + );*/ + if (mounted) setState(() {}); }, ); @@ -131,13 +133,13 @@ class _ImportStorePopupState extends State { separatorBuilder: (context, i) => i == importStores.length - 1 ? const Divider() : const SizedBox.shrink(), - ), + ),*/ ), ); } class _ImportStore { - final Future result; + final Future result; List? collisionInfo; Completer collisionResolution; diff --git a/example/lib/screens/main/main.dart b/example/lib/screens/main/main.dart index e37b0aa5..9abedda4 100644 --- a/example/lib/screens/main/main.dart +++ b/example/lib/screens/main/main.dart @@ -41,7 +41,7 @@ class _MainScreenState extends State { NavigationDestination( label: 'Recover', icon: StreamBuilder( - stream: FMTCRoot.stats.watchChanges().asBroadcastStream(), + stream: FMTCRoot.stats.watchRecovery(), builder: (context, _) => FutureBuilder( future: FMTCRoot.recovery.recoverableRegions, builder: (context, snapshot) => Badge( diff --git a/example/lib/screens/main/pages/recovery/recovery.dart b/example/lib/screens/main/pages/recovery/recovery.dart index c09e263a..e8212d35 100644 --- a/example/lib/screens/main/pages/recovery/recovery.dart +++ b/example/lib/screens/main/pages/recovery/recovery.dart @@ -30,7 +30,7 @@ class _RecoveryPageState extends State { _recoverableRegions = FMTCRoot.recovery.recoverableRegions; listRecoverableRegions(); - FMTCRoot.stats.watchChanges(watchRecovery: true).listen((_) { + FMTCRoot.stats.watchRecovery().listen((_) { if (mounted) { listRecoverableRegions(); setState(() {}); diff --git a/example/lib/screens/main/pages/stores/components/store_tile.dart b/example/lib/screens/main/pages/stores/components/store_tile.dart index 1a3f5ce8..675bf199 100644 --- a/example/lib/screens/main/pages/stores/components/store_tile.dart +++ b/example/lib/screens/main/pages/stores/components/store_tile.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; -import 'package:fmtc_plus_sharing/fmtc_plus_sharing.dart'; import 'package:provider/provider.dart'; import '../../../../../shared/misc/exts/size_formatter.dart'; @@ -207,7 +206,8 @@ class _StoreTileState extends State { onPressed: _exportingProgress ? null : () async { - setState( + // TODO: Implement + /* setState( () => _exportingProgress = true, ); final bool result = await _store @@ -228,7 +228,7 @@ class _StoreTileState extends State { ), ), ); - } + }*/ }, ), IconButton( diff --git a/example/lib/screens/main/pages/stores/stores.dart b/example/lib/screens/main/pages/stores/stores.dart index 7f34dd60..24741da7 100644 --- a/example/lib/screens/main/pages/stores/stores.dart +++ b/example/lib/screens/main/pages/stores/stores.dart @@ -26,7 +26,7 @@ class _StoresPageState extends State { void listStores() => _stores = FMTCRoot.stats.storesAvailable; listStores(); - FMTCRoot.stats.watchChanges().listen((_) { + FMTCRoot.stats.watchStores().listen((_) { if (mounted) { listStores(); setState(() {}); @@ -48,7 +48,8 @@ class _StoresPageState extends State { child: FutureBuilder>( future: _stores, builder: (context, snapshot) => snapshot.hasError - ? throw snapshot.error! as FMTCDamagedStoreException + // ignore: only_throw_errors + ? throw snapshot.error! : snapshot.hasData ? snapshot.data!.isEmpty ? const EmptyIndicator() diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 8d9ec7c6..b9b21a12 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -23,7 +23,7 @@ dependencies: flutter_map_animations: ^0.5.3 flutter_map_tile_caching: ^9.0.0-dev.5 flutter_speed_dial: ^7.0.0 - fmtc_plus_sharing: ^8.0.0 + #fmtc_plus_sharing: ^8.0.0 google_fonts: ^6.1.0 gpx: ^2.2.1 http: ^1.1.2 diff --git a/example/windows/flutter/CMakeLists.txt b/example/windows/flutter/CMakeLists.txt index 930d2071..903f4899 100644 --- a/example/windows/flutter/CMakeLists.txt +++ b/example/windows/flutter/CMakeLists.txt @@ -10,6 +10,11 @@ include(${EPHEMERAL_DIR}/generated_config.cmake) # https://github.com/flutter/flutter/issues/57146. set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + # === Flutter Library === set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") @@ -92,7 +97,7 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" - windows-x64 $ + ${FLUTTER_TARGET_PLATFORM} $ VERBATIM ) add_custom_target(flutter_assemble DEPENDS diff --git a/example/windows/flutter/generated_plugin_registrant.cc b/example/windows/flutter/generated_plugin_registrant.cc index 8700c13c..a84779d7 100644 --- a/example/windows/flutter/generated_plugin_registrant.cc +++ b/example/windows/flutter/generated_plugin_registrant.cc @@ -6,18 +6,9 @@ #include "generated_plugin_registrant.h" -#include #include -#include -#include void RegisterPlugins(flutter::PluginRegistry* registry) { - IsarFlutterLibsPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("IsarFlutterLibsPlugin")); ObjectboxFlutterLibsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("ObjectboxFlutterLibsPlugin")); - SharePlusWindowsPluginCApiRegisterWithRegistrar( - registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); - UrlLauncherWindowsRegisterWithRegistrar( - registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/example/windows/flutter/generated_plugins.cmake b/example/windows/flutter/generated_plugins.cmake index 57dfe6f1..9f0138ed 100644 --- a/example/windows/flutter/generated_plugins.cmake +++ b/example/windows/flutter/generated_plugins.cmake @@ -3,10 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST - isar_flutter_libs objectbox_flutter_libs - share_plus - url_launcher_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/lib/flutter_map_tile_caching.dart b/lib/flutter_map_tile_caching.dart index b2617b69..c81e65c6 100644 --- a/lib/flutter_map_tile_caching.dart +++ b/lib/flutter_map_tile_caching.dart @@ -45,7 +45,6 @@ part 'src/bulk_download/download_progress.dart'; part 'src/bulk_download/manager.dart'; part 'src/bulk_download/thread.dart'; part 'src/bulk_download/tile_event.dart'; -part 'src/fmtc.dart'; part 'src/misc/deprecations.dart'; part 'src/providers/tile_provider.dart'; part 'src/regions/base_region.dart'; diff --git a/lib/src/backend/impls/objectbox/backend.dart b/lib/src/backend/impls/objectbox/backend.dart index 0579e02d..bb039c57 100644 --- a/lib/src/backend/impls/objectbox/backend.dart +++ b/lib/src/backend/impls/objectbox/backend.dart @@ -8,8 +8,6 @@ import 'dart:isolate'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter_map/flutter_map.dart'; -import 'package:latlong2/latlong.dart'; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; @@ -103,14 +101,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { _sendPort!.send((id: id, type: type, args: args)); final res = await _workerRes[id]!.future; _workerRes.remove(id); - - final err = res?['error']; - if (err == null) return res; - - if (err is FMTCBackendError) throw err; - debugPrint('An unexpected error in the FMTC backend occurred:'); - // ignore: only_throw_errors - throw err; + return res; } Future initialise({ @@ -146,6 +137,22 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { _workerHandler = receivePort.listen( (evt) { evt as ({int id, Map? data}); + + final err = evt.data?['error']; + if (err != null) { + if (err is FMTCBackendError) throw err; + + debugPrint('An unexpected error in the FMTC backend occurred:'); + if (err is Error) { + debugPrint(err.stackTrace.toString()); + throw err; + } else { + debugPrint( + 'But it was not of type `Error`, it was type ${err.runtimeType}', + ); + } + } + _workerRes[evt.id]!.complete(evt.data); }, onDone: () => _workerComplete.complete(), @@ -482,4 +489,26 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { required int id, }) => _sendCmd(type: _WorkerCmdType.cancelRecovery, args: {'id': id}); + + @override + Future> watchRecovery({ + required bool triggerImmediately, + }) async => + (await _sendCmd( + type: _WorkerCmdType.watchRecovery, + args: {'triggerImmediately': triggerImmediately}, + ))!['stream']; + + @override + Future> watchStores({ + required List storeNames, + required bool triggerImmediately, + }) async => + (await _sendCmd( + type: _WorkerCmdType.watchStores, + args: { + 'storeNames': storeNames, + 'triggerImmediately': triggerImmediately, + }, + ))!['stream']; } diff --git a/lib/src/backend/impls/objectbox/models/src/recovery.dart b/lib/src/backend/impls/objectbox/models/src/recovery.dart index 96cb5645..bd888336 100644 --- a/lib/src/backend/impls/objectbox/models/src/recovery.dart +++ b/lib/src/backend/impls/objectbox/models/src/recovery.dart @@ -1,6 +1,8 @@ // Copyright © Luka S (JaffaKetchup) under GPL-v3 // A full license can be found at .\LICENSE +import 'package:flutter_map/flutter_map.dart'; +import 'package:latlong2/latlong.dart'; import 'package:meta/meta.dart'; import 'package:objectbox/objectbox.dart'; @@ -66,7 +68,7 @@ base class ObjectBoxRecovery { required this.customPolygonLngs, }); - ObjectBoxRecovery.startFromRegion({ + ObjectBoxRecovery.fromRegion({ required this.refId, required this.storeName, required DownloadableRegion region, @@ -141,4 +143,32 @@ base class ObjectBoxRecovery { .map((c) => c.longitude) .toList() : null; + + RecoveredRegion toRegion() => RecoveredRegion( + id: id, + storeName: storeName, + time: creationTime, + bounds: typeId == 0 + ? LatLngBounds( + LatLng(rectNwLat!, rectNwLng!), + LatLng(rectSeLat!, rectSeLng!), + ) + : null, + center: typeId == 1 ? LatLng(circleCenterLat!, circleCenterLng!) : null, + line: typeId == 2 || typeId == 3 + ? List.generate( + lineLats!.length, + (i) => LatLng(lineLats![i], lineLngs![i]), + ) + : null, + radius: typeId == 1 + ? circleRadius! + : typeId == 2 + ? lineRadius! + : null, + minZoom: minZoom, + maxZoom: maxZoom, + start: startTile, + end: endTile, + ); } diff --git a/lib/src/backend/impls/objectbox/worker.dart b/lib/src/backend/impls/objectbox/worker.dart index 631c1dee..12015ee8 100644 --- a/lib/src/backend/impls/objectbox/worker.dart +++ b/lib/src/backend/impls/objectbox/worker.dart @@ -33,6 +33,8 @@ enum _WorkerCmdType { getRecoverableRegion, startRecovery, cancelRecovery, + watchRecovery, + watchStores, } Future _worker( @@ -811,37 +813,10 @@ Future _worker( sendRes( id: cmd.id, data: { - 'recoverableRegions': root.box().getAll().map( - (r) => RecoveredRegion( - id: r.id, - storeName: r.storeName, - time: r.creationTime, - bounds: r.typeId == 0 - ? LatLngBounds( - LatLng(r.rectNwLat!, r.rectNwLng!), - LatLng(r.rectSeLat!, r.rectSeLng!), - ) - : null, - center: r.typeId == 1 - ? LatLng(r.circleCenterLat!, r.circleCenterLng!) - : null, - line: r.typeId == 2 || r.typeId == 3 - ? List.generate( - r.lineLats!.length, - (i) => LatLng(r.lineLats![i], r.lineLngs![i]), - ) - : null, - radius: r.typeId == 1 - ? r.circleRadius! - : r.typeId == 2 - ? r.lineRadius! - : null, - minZoom: r.minZoom, - maxZoom: r.maxZoom, - start: r.startTile, - end: r.endTile, - ), - ), + 'recoverableRegions': root + .box() + .getAll() + .map((r) => r.toRegion()), }, ); @@ -849,14 +824,18 @@ Future _worker( case _WorkerCmdType.getRecoverableRegion: final id = cmd.args['id']! as int; - final query = root - .box() - .query(ObjectBoxRecovery_.refId.equals(id)) - .build(); - - sendRes(id: cmd.id, data: {'recoverableRegion': query.findUnique()}); - - query.close(); + sendRes( + id: cmd.id, + data: { + 'recoverableRegion': (root + .box() + .query(ObjectBoxRecovery_.refId.equals(id)) + .build() + ..close()) + .findUnique() + ?.toRegion(), + }, + ); break; case _WorkerCmdType.startRecovery: @@ -865,7 +844,7 @@ Future _worker( final region = cmd.args['region']! as DownloadableRegion; root.box().put( - ObjectBoxRecovery.startFromRegion( + ObjectBoxRecovery.fromRegion( refId: id, storeName: storeName, region: region, @@ -885,6 +864,39 @@ Future _worker( sendRes(id: cmd.id); + break; + case _WorkerCmdType.watchRecovery: + final triggerImmediately = cmd.args['triggerImmediately']! as bool; + + sendRes( + id: cmd.id, + data: { + 'stream': root + .box() + .query() + .watch(triggerImmediately: triggerImmediately), + }, + ); + + break; + case _WorkerCmdType.watchStores: + final storeNames = cmd.args['storeNames']! as List; + final triggerImmediately = cmd.args['triggerImmediately']! as bool; + + sendRes( + id: cmd.id, + data: { + 'stream': root + .box() + .query( + storeNames.isEmpty + ? null + : ObjectBoxStore_.name.oneOf(storeNames), + ) + .watch(triggerImmediately: triggerImmediately), + }, + ); + break; } } catch (e) { diff --git a/lib/src/backend/interfaces/backend.dart b/lib/src/backend/interfaces/backend.dart index 0e632ed9..8e1ff881 100644 --- a/lib/src/backend/interfaces/backend.dart +++ b/lib/src/backend/interfaces/backend.dart @@ -321,4 +321,31 @@ abstract interface class FMTCBackendInternal with FMTCBackendAccess { Future cancelRecovery({ required int id, }); + + /// {@template fmtc.backend.watchRecovery} + /// Watch for changes to the recovery system + /// + /// Useful to update UI only when required, for example, in a `StreamBuilder`. + /// Whenever this has an event, it is likely the other statistics will have + /// changed. + /// {@endtemplate} + Future> watchRecovery({ + required bool triggerImmediately, + }); + + /// {@template fmtc.backend.watchStores} + /// Watch for changes in the specified stores, or all stores if no stores + /// are provided + /// + /// Useful to update UI only when required, for example, in a `StreamBuilder`. + /// Whenever this has an event, it is likely the other statistics will have + /// changed. + /// + /// Emits an event every time a change is made to a store (every time a + /// statistic changes, which should include every time a tile is changed). + /// {@endtemplate} + Future> watchStores({ + required List storeNames, + required bool triggerImmediately, + }); } diff --git a/lib/src/fmtc.dart b/lib/src/fmtc.dart deleted file mode 100644 index 2ed03343..00000000 --- a/lib/src/fmtc.dart +++ /dev/null @@ -1,133 +0,0 @@ -// Copyright © Luka S (JaffaKetchup) under GPL-v3 -// A full license can be found at .\LICENSE - -part of flutter_map_tile_caching; - -/*/// Direct alias of [FlutterMapTileCaching] for easier development -/// -/// Prefer use of full 'FlutterMapTileCaching' when initialising to ensure -/// readability and understanding in other code. -typedef FMTC = FlutterMapTileCaching; - -/// Main singleton access point for 'flutter_map_tile_caching' -/// -/// You must construct using [FlutterMapTileCaching.initialise] before using -/// [FlutterMapTileCaching.instance], otherwise a [StateError] will be thrown. -/// Note that the singleton can be re-initialised/changed by calling the -/// aforementioned constructor again. -/// -/// [FMTC] is an alias for this object. -class FlutterMapTileCaching { - /// The directory which contains all databases required to use FMTC - final FMTCRoot rootDirectory; - - /// The database or other storage mechanism that FMTC will use as a cache - /// 'backend' - /// - /// Defaults to [ObjectBoxBackend], which uses the ObjectBox library and - /// database. - /// - /// See [FMTCBackend] for more information. - final FMTCBackend backend; - - /// Default settings used when creating an [FMTCTileProvider] - /// - /// Can be overridden on a case-to-case basis when actually creating the tile - /// provider. - final FMTCTileProviderSettings defaultTileProviderSettings; - - /// Internal constructor, to be used by [initialise] - const FlutterMapTileCaching._({ - required this.rootDirectory, - required this.backend, - required this.defaultTileProviderSettings, - }); - - /// Initialise and prepare FMTC, by creating all necessary directories/files - /// and configuring the [FlutterMapTileCaching] singleton - /// - /// You must construct using this before using [FlutterMapTileCaching.instance], - /// otherwise a [StateError] will be thrown. - /// - /// Returns a configured [FlutterMapTileCaching] instance, and assigns it to - /// [instance]. - /// - /// Note that [FMTC] is an alias for [FlutterMapTileCaching]. - /// - /// --- - /// - /// Prefer to leave [rootDirectory] as `null`, which will use - /// `getApplicationDocumentsDirectory()`. Alternatively, pass a custom - /// directory - it is recommended to not use a cache directory, as the OS can - /// clear these without notice at any time. - /// - /// Optionally set a custom storage [backend] instead of [ObjectBoxBackend]. - /// Some implementations may accept/require additional arguments that may be - /// set through [backendImplArgs]. See their documentation for more - /// information. If provided, they must be of the specified type. - /// - /// > The default [ObjectBoxBackend] accepts the following optional - /// > [backendImplArgs] (note that other implementations may support different - /// > args). For ease of use, they have been provided in this method as typed - /// > arguments - these may be useless in other implementations (although they - /// > will be forwarded to any). - /// > - /// > * [macosApplicationGroup] : when creating a sandboxed macOS app, - /// > use to specify the application group (of less than 20 chars). See - /// > [the ObjectBox docs](https://docs.objectbox.io/getting-started) for - /// > details. - /// > * [maxDatabaseSize] : the maximum size the database file can grow - /// > to. Exceeding it throws `DbFullException`. Defaults to 10 GB. - static Future initialise({ - String? rootDirectory, - FMTCBackend? backend, - Map backendImplArgs = const {}, - String? macosApplicationGroup, - int? maxDatabaseSize, - FMTCTileProviderSettings? defaultTileProviderSettings, - }) async { - final dir = await (rootDirectory == null - ? await getApplicationDocumentsDirectory() - : Directory(rootDirectory) >> 'fmtc') - .create(recursive: true); - - backend ??= ObjectBoxBackend(); - - await backend.internal.initialise( - rootDirectory: dir, - implSpecificArgs: { - if (macosApplicationGroup != null) - 'macosApplicationGroup': macosApplicationGroup, - if (maxDatabaseSize != null) 'maxDatabaseSize': maxDatabaseSize, - }..addAll(backendImplArgs), - ); - - return _instance = FMTC._( - rootDirectory: FMTCRoot._(dir), - backend: backend, - defaultTileProviderSettings: - defaultTileProviderSettings ?? FMTCTileProviderSettings(), - ); - } - - /// Get the configured instance of [FlutterMapTileCaching], after - /// [FlutterMapTileCaching.initialise] has been called, for further actions - static FlutterMapTileCaching get instance => - _instance ?? - (throw StateError( - ''' -Use `FlutterMapTileCaching.initialise()` before getting -`FlutterMapTileCaching.instance` (or a method which requires an instance). - ''', - )); - static FlutterMapTileCaching? _instance; - - /// Construct a [FMTCStore] by name - /// - /// {@macro fmtc.fmtcstore.sub.noautocreate} - /// - /// Equivalent to constructing the [FMTCStore] directly. This method is - /// provided for backwards-compatibility. - FMTCStore call(String storeName) => FMTCStore(storeName); -} -*/ \ No newline at end of file diff --git a/lib/src/root/statistics.dart b/lib/src/root/statistics.dart index 621665bf..97be41d7 100644 --- a/lib/src/root/statistics.dart +++ b/lib/src/root/statistics.dart @@ -17,39 +17,25 @@ class RootStats { /// {@macro fmtc.backend.rootLength} Future get rootLength async => FMTCBackendAccess.internal.rootLength(); - /// Watch for changes in the current root - /// - /// Useful to update UI only when required, for example, in a `StreamBuilder`. - /// Whenever this has an event, it is likely the other statistics will have - /// changed. - /// - /// Recursively watch specific stores (using [StoreStats.watchChanges]) by - /// providing them as a list of [FMTCStore]s to [recursive]. To watch all - /// stores, use the [storesAvailable]/[storesAvailableAsync] getter as the - /// argument. By default, no sub-stores are watched (empty list), meaning only - /// events that affect the actual store database (eg. store creations) will be - /// caught. Control where changes are caught from using [storeParts]. See - /// documentation on those parts for their scope. - /// - /// Enable debouncing to prevent unnecessary events for small changes in detail - /// using [debounce]. Defaults to 200ms, or set to null to disable debouncing. - /// - /// Debouncing example (dash roughly represents [debounce]): - /// ```dart - /// input: 1-2-3---4---5-6-| - /// output: ------3---4-----6| - /// ``` - Stream watchChanges({ - Duration? debounce = const Duration(milliseconds: 200), - bool fireImmediately = false, - List recursive = const [], - bool watchRecovery = false, - List storeParts = const [ - StoreParts.metadata, - StoreParts.tiles, - StoreParts.stats, - ], - }) => - // TODO: Implement - throw UnimplementedError(); + /// {@macro fmtc.backend.watchRecovery} + Stream watchRecovery({ + bool triggerImmediately = false, + }) async* { + final stream = await FMTCBackendAccess.internal.watchRecovery( + triggerImmediately: triggerImmediately, + ); + yield* stream; + } + + /// {@macro fmtc.backend.watchStores} + Stream watchStores({ + List storeNames = const [], + bool triggerImmediately = false, + }) async* { + final stream = await FMTCBackendAccess.internal.watchStores( + storeNames: storeNames, + triggerImmediately: triggerImmediately, + ); + yield* stream; + } } diff --git a/lib/src/store/statistics.dart b/lib/src/store/statistics.dart index 7718b24f..50155064 100644 --- a/lib/src/store/statistics.dart +++ b/lib/src/store/statistics.dart @@ -45,46 +45,14 @@ class StoreStats { /// {@macro fmtc.frontend.storestats.efficiency} Future get misses => all.then((a) => a.misses); - /// Watch for changes in the current store - /// - /// Useful to update UI only when required, for example, in a `StreamBuilder`. - /// Whenever this has an event, it is likely the other statistics will have - /// changed. - /// - /// Control where changes are caught from using [storeParts]. See documentation - /// on those parts for their scope. - /// - /// Enable debouncing to prevent unnecessary events for small changes in detail - /// using [debounce]. Defaults to 200ms, or set to null to disable debouncing. - /// - /// Debouncing example (dash roughly represents [debounce]): - /// ```dart - /// input: 1-2-3---4---5-6-| - /// output: ------3---4-----6| - /// ``` + /// {@macro fmtc.backend.watchStores} Stream watchChanges({ - Duration? debounce = const Duration(milliseconds: 200), - bool fireImmediately = false, - List storeParts = const [ - StoreParts.metadata, - StoreParts.tiles, - StoreParts.stats, - ], - }) => - // TODO: Implement - throw UnimplementedError(); -} - -/// Parts of a store which can be watched -enum StoreParts { - /// Include changes to the store's metadata objects - metadata, - - /// Includes changes to the store's tile objects, including those which will - /// make some statistics change (eg. store size) - tiles, - - /// Includes changes to the store's descriptor object, which will change with - /// the cache hit and miss statistics - stats, + bool triggerImmediately = false, + }) async* { + final stream = await FMTCBackendAccess.internal.watchStores( + storeNames: [_storeName], + triggerImmediately: triggerImmediately, + ); + yield* stream; + } } From b2f901747507781ee1d265a450f6160bb06201fb Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Wed, 21 Feb 2024 22:09:37 +0000 Subject: [PATCH 101/168] Fixed bugs Former-commit-id: 5121e12b20350dffc3e75aa60db301a3c12bf5a5 [formerly 5d1f701c151f11509636b1a9bd219cf2e2485246] Former-commit-id: c437266ba3ce6349042bd5f7804324ccc90cda01 --- .../lib/screens/main/pages/stores/stores.dart | 2 +- .../store_editor/components/header.dart | 23 +++---- lib/src/backend/backend_access.dart | 4 +- lib/src/backend/impls/objectbox/backend.dart | 64 ++++++++++++++----- lib/src/backend/impls/objectbox/worker.dart | 43 ++++++++----- lib/src/root/statistics.dart | 6 +- 6 files changed, 92 insertions(+), 50 deletions(-) diff --git a/example/lib/screens/main/pages/stores/stores.dart b/example/lib/screens/main/pages/stores/stores.dart index 24741da7..b4aa3c7c 100644 --- a/example/lib/screens/main/pages/stores/stores.dart +++ b/example/lib/screens/main/pages/stores/stores.dart @@ -45,7 +45,7 @@ class _StoresPageState extends State { const Header(), const SizedBox(height: 12), Expanded( - child: FutureBuilder>( + child: FutureBuilder( future: _stores, builder: (context, snapshot) => snapshot.hasError // ignore: only_throw_errors diff --git a/example/lib/screens/store_editor/components/header.dart b/example/lib/screens/store_editor/components/header.dart index bc4083e2..5cf809db 100644 --- a/example/lib/screens/store_editor/components/header.dart +++ b/example/lib/screens/store_editor/components/header.dart @@ -58,26 +58,21 @@ AppBar buildHeader({ await newStore.manage.create(); + // Designed to test both methods, even though only bulk would be + // more efficient await newStore.metadata.set( key: 'sourceURL', value: newValues['sourceURL']!, ); - await newStore.metadata.set( - key: 'validDuration', - value: newValues['validDuration']!, - ); - await newStore.metadata.set( - key: 'maxLength', - value: newValues['maxLength']!, + await newStore.metadata.setBulk( + kvs: { + 'validDuration': newValues['validDuration']!, + 'maxLength': newValues['maxLength']!, + if (widget.existingStoreName == null || useNewCacheModeValue) + 'behaviour': cacheModeValue ?? 'cacheFirst', + }, ); - if (widget.existingStoreName == null || useNewCacheModeValue) { - await newStore.metadata.set( - key: 'behaviour', - value: cacheModeValue ?? 'cacheFirst', - ); - } - if (!context.mounted) return; if (widget.isStoreInUse && widget.existingStoreName != null) { Provider.of(context, listen: false) diff --git a/lib/src/backend/backend_access.dart b/lib/src/backend/backend_access.dart index f75c24c5..eb343032 100644 --- a/lib/src/backend/backend_access.dart +++ b/lib/src/backend/backend_access.dart @@ -22,7 +22,9 @@ abstract mixin class FMTCBackendAccess { @meta.internal @meta.protected static set internal(FMTCBackendInternal? newInternal) { - if (_internal != null) throw RootAlreadyInitialised(); + if (newInternal != null && _internal != null) { + throw RootAlreadyInitialised(); + } _internal = newInternal; } } diff --git a/lib/src/backend/impls/objectbox/backend.dart b/lib/src/backend/impls/objectbox/backend.dart index bb039c57..8615910b 100644 --- a/lib/src/backend/impls/objectbox/backend.dart +++ b/lib/src/backend/impls/objectbox/backend.dart @@ -45,11 +45,12 @@ final class FMTCObjectBoxBackend implements FMTCBackend { /// {@macro fmtc.backend.uninitialise} /// - /// If [immediate] is `true`, any operations currently underway will be lost. + /// If [immediate] is `true`, any operations currently underway will be lost, + /// as the worker will be killed as quickly as possible (not necessarily + /// instantly). /// If `false`, all operations currently underway will be allowed to complete, /// but any operations started after this method call will be lost. A lost - /// operation may throw [RootUnavailable]. This parameter may not have a - /// noticable/any effect in some implementations. + /// operation may throw [RootUnavailable]. @override Future uninitialise({ bool deleteRoot = false, @@ -101,6 +102,26 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { _sendPort!.send((id: id, type: type, args: args)); final res = await _workerRes[id]!.future; _workerRes.remove(id); + + final err = res?['error']; + if (err != null) { + if (err is FMTCBackendError) throw err; + + debugPrint('An unexpected error in the FMTC backend occurred:'); + if (err is Error) { + debugPrint(err.toString()); + debugPrint(err.stackTrace.toString()); + throw err; + } else if (err is Exception) { + debugPrint(err.toString()); + } else { + debugPrint( + 'But it was not of type `Error` or `Exception`, it was type ${err.runtimeType}', + ); + } + } + + print('[FMTC] cmd finished: $id'); return res; } @@ -117,10 +138,13 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { //_rotalStore = null; //_rotalLength = 0; - this.rootDirectory = await (rootDirectory == null - ? await getApplicationDocumentsDirectory() - : Directory(path.join(rootDirectory, 'fmtc'))) - .create(recursive: true); + this.rootDirectory = await Directory( + path.join( + rootDirectory ?? + (await getApplicationDocumentsDirectory()).absolute.path, + 'fmtc', + ), + ).create(recursive: true); // Prepare to recieve `SendPort` from worker _workerRes[0] = Completer(); @@ -136,7 +160,14 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { _workerComplete = Completer(); _workerHandler = receivePort.listen( (evt) { - evt as ({int id, Map? data}); + evt as ({int id, Map? data})?; + + // Killed forcefully by environment (eg. hot restart) + if (evt == null) { + _workerComplete.complete(); + _workerHandler.cancel(); + return; + } final err = evt.data?['error']; if (err != null) { @@ -144,6 +175,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { debugPrint('An unexpected error in the FMTC backend occurred:'); if (err is Error) { + debugPrint(err.toString()); debugPrint(err.stackTrace.toString()); throw err; } else { @@ -200,8 +232,6 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { await _workerHandler.cancel(); //_rotalDebouncer.cancel(); - print('passed _workerHandler cancel'); - // Kill any remaining operations with an error (they'll never recieve a // response from the worker) for (final completer in _workerRes.values) { @@ -221,7 +251,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { @override Future> listStores() async => - (await _sendCmd(type: _WorkerCmdType.storeExists))!['stores']; + (await _sendCmd(type: _WorkerCmdType.listStores))!['stores']; @override Future storeExists({ @@ -347,7 +377,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { required bool hit, }) => _sendCmd( - type: _WorkerCmdType.deleteStore, + type: _WorkerCmdType.registerHitOrMiss, args: {'storeName': storeName, 'hit': hit}, ); @@ -494,21 +524,23 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { Future> watchRecovery({ required bool triggerImmediately, }) async => - (await _sendCmd( + /*(await _sendCmd( type: _WorkerCmdType.watchRecovery, args: {'triggerImmediately': triggerImmediately}, - ))!['stream']; + ))!['stream']*/ + Stream.periodic(const Duration(seconds: 5)); @override Future> watchStores({ required List storeNames, required bool triggerImmediately, }) async => - (await _sendCmd( + /*(await _sendCmd( type: _WorkerCmdType.watchStores, args: { 'storeNames': storeNames, 'triggerImmediately': triggerImmediately, }, - ))!['stream']; + ))!['stream']*/ + Stream.periodic(const Duration(seconds: 5)); } diff --git a/lib/src/backend/impls/objectbox/worker.dart b/lib/src/backend/impls/objectbox/worker.dart index 12015ee8..16e4ce51 100644 --- a/lib/src/backend/impls/objectbox/worker.dart +++ b/lib/src/backend/impls/objectbox/worker.dart @@ -131,6 +131,9 @@ Future _worker( _WorkerCmdType type, Map args, }) cmd in receivePort) { + // TODO: Remove + debugPrint('[FMTC] cmd recieved: $cmd'); + try { switch (cmd.type) { case _WorkerCmdType.initialise_: @@ -141,7 +144,6 @@ Future _worker( input.rootDirectory.deleteSync(recursive: true); } - // TODO: Consider final message Isolate.exit(); case _WorkerCmdType.rootSize: final query = root @@ -170,7 +172,11 @@ Future _worker( sendRes( id: cmd.id, data: { - 'stores': root.box().getAll().map((e) => e.name), + 'stores': root + .box() + .getAll() + .map((e) => e.name) + .toList(), }, ); @@ -705,12 +711,13 @@ Future _worker( (throw StoreNotExists(storeName: storeName)); query.close(); + final Map json = store.metadataJson == '' + ? {} + : jsonDecode(store.metadataJson); + json[key] = value; + stores.put( - store - ..metadataJson = jsonEncode( - (jsonDecode(store.metadataJson) - as Map)[key] = value, - ), + store..metadataJson = jsonEncode(json), mode: PutMode.update, ); }, @@ -735,12 +742,14 @@ Future _worker( (throw StoreNotExists(storeName: storeName)); query.close(); + final Map json = store.metadataJson == '' + ? {} + : jsonDecode(store.metadataJson); + // ignore: cascade_invocations + json.addAll(kvs); + stores.put( - store - ..metadataJson = jsonEncode( - (jsonDecode(store.metadataJson) as Map) - ..addAll(kvs), - ), + store..metadataJson = jsonEncode(json), mode: PutMode.update, ); }, @@ -871,10 +880,11 @@ Future _worker( sendRes( id: cmd.id, data: { - 'stream': root + 'stream': /* root .box() .query() - .watch(triggerImmediately: triggerImmediately), + .watch(triggerImmediately: triggerImmediately)*/ + null, }, ); @@ -886,14 +896,15 @@ Future _worker( sendRes( id: cmd.id, data: { - 'stream': root + 'stream': /*root .box() .query( storeNames.isEmpty ? null : ObjectBoxStore_.name.oneOf(storeNames), ) - .watch(triggerImmediately: triggerImmediately), + .watch(triggerImmediately: triggerImmediately)*/ + null, }, ); diff --git a/lib/src/root/statistics.dart b/lib/src/root/statistics.dart index 97be41d7..536e8207 100644 --- a/lib/src/root/statistics.dart +++ b/lib/src/root/statistics.dart @@ -8,8 +8,10 @@ class RootStats { const RootStats._(); /// {@macro fmtc.backend.listStores} - Future> get storesAvailable async => - FMTCBackendAccess.internal.listStores().then((s) => s.map(FMTCStore.new)); + Future> get storesAvailable async => + FMTCBackendAccess.internal + .listStores() + .then((s) => s.map(FMTCStore.new).toList()); /// {@macro fmtc.backend.rootSize} Future get rootSize async => FMTCBackendAccess.internal.rootSize(); From bace7247cae6a342627d74e93e9186f3c0bb527a Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Wed, 21 Feb 2024 22:11:06 +0000 Subject: [PATCH 102/168] Fixed bug Former-commit-id: 8e366c7530953a3559421f6634e23928d67b5a23 [formerly 24c21935b68b4d6a3249df9a1be51ccf3edb1292] Former-commit-id: 1dd13427f25e5a6362880731956993f93a9efe9e --- lib/src/backend/impls/objectbox/backend.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/backend/impls/objectbox/backend.dart b/lib/src/backend/impls/objectbox/backend.dart index 8615910b..c9d4dd88 100644 --- a/lib/src/backend/impls/objectbox/backend.dart +++ b/lib/src/backend/impls/objectbox/backend.dart @@ -309,7 +309,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { (await _sendCmd( type: _WorkerCmdType.getStoreStats, args: {'storeName': storeName}, - ))!['store']; + ))!['stats']; @override Future tileExistsInStore({ From 8bf52f82390d2ec4d593bcd68435e7b1545512d2 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 22 Feb 2024 13:24:26 +0000 Subject: [PATCH 103/168] Fixed bugs Improved error handling & reporting Former-commit-id: 9c0350d75f783638fdb6045f84b64ee102cb97d8 [formerly 57bee4e800c286c59891c79f120c34d84fe6ffc7] Former-commit-id: 19a62eb3c7e194efcce778799daa3cf5df689bae --- .../main/pages/map/components/map_view.dart | 1 - .../store_editor/components/header.dart | 4 +- lib/src/backend/impls/objectbox/backend.dart | 42 +++++++++---------- lib/src/backend/impls/objectbox/worker.dart | 18 ++++---- 4 files changed, 32 insertions(+), 33 deletions(-) diff --git a/example/lib/screens/main/pages/map/components/map_view.dart b/example/lib/screens/main/pages/map/components/map_view.dart index e6b69bb7..d3a58deb 100644 --- a/example/lib/screens/main/pages/map/components/map_view.dart +++ b/example/lib/screens/main/pages/map/components/map_view.dart @@ -58,7 +58,6 @@ class MapView extends StatelessWidget { urlTemplate: urlTemplate, userAgentPackageName: 'dev.jaffaketchup.fmtc.demo', maxNativeZoom: 20, - panBuffer: 5, tileProvider: currentStore != null ? FMTCStore(currentStore).getTileProvider( settings: FMTCTileProviderSettings( diff --git a/example/lib/screens/store_editor/components/header.dart b/example/lib/screens/store_editor/components/header.dart index 5cf809db..8ce3704b 100644 --- a/example/lib/screens/store_editor/components/header.dart +++ b/example/lib/screens/store_editor/components/header.dart @@ -51,12 +51,12 @@ AppBar buildHeader({ /*final downloadProvider = Provider.of(context, listen: false); - if (existingStore != null && + if (existingStore != null && downloadProvider.selectedStore == existingStore) { downloadProvider.setSelectedStore(newStore); }*/ - await newStore.manage.create(); + if (existingStore == null) await newStore.manage.create(); // Designed to test both methods, even though only bulk would be // more efficient diff --git a/lib/src/backend/impls/objectbox/backend.dart b/lib/src/backend/impls/objectbox/backend.dart index c9d4dd88..1fa6cd17 100644 --- a/lib/src/backend/impls/objectbox/backend.dart +++ b/lib/src/backend/impls/objectbox/backend.dart @@ -99,8 +99,15 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { final id = ++_workerId; _workerRes[id] = Completer(); + + final stopwatch = Stopwatch()..start(); + _sendPort!.send((id: id, type: type, args: args)); final res = await _workerRes[id]!.future; + + print(stopwatch.elapsedMilliseconds); + stopwatch.stop(); + _workerRes.remove(id); final err = res?['error']; @@ -108,20 +115,15 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { if (err is FMTCBackendError) throw err; debugPrint('An unexpected error in the FMTC backend occurred:'); - if (err is Error) { - debugPrint(err.toString()); - debugPrint(err.stackTrace.toString()); - throw err; - } else if (err is Exception) { - debugPrint(err.toString()); - } else { - debugPrint( - 'But it was not of type `Error` or `Exception`, it was type ${err.runtimeType}', - ); - } + Error.throwWithStackTrace( + err, + StackTrace.fromString( + (res?['stackTrace']! as StackTrace).toString() + + StackTrace.current.toString(), + ), + ); } - print('[FMTC] cmd finished: $id'); return res; } @@ -174,15 +176,13 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { if (err is FMTCBackendError) throw err; debugPrint('An unexpected error in the FMTC backend occurred:'); - if (err is Error) { - debugPrint(err.toString()); - debugPrint(err.stackTrace.toString()); - throw err; - } else { - debugPrint( - 'But it was not of type `Error`, it was type ${err.runtimeType}', - ); - } + Error.throwWithStackTrace( + err, + StackTrace.fromString( + (evt.data?['stackTrace']! as StackTrace).toString() + + StackTrace.current.toString(), + ), + ); } _workerRes[evt.id]!.complete(evt.data); diff --git a/lib/src/backend/impls/objectbox/worker.dart b/lib/src/backend/impls/objectbox/worker.dart index 16e4ce51..efd8a740 100644 --- a/lib/src/backend/impls/objectbox/worker.dart +++ b/lib/src/backend/impls/objectbox/worker.dart @@ -131,9 +131,6 @@ Future _worker( _WorkerCmdType type, Map args, }) cmd in receivePort) { - // TODO: Remove - debugPrint('[FMTC] cmd recieved: $cmd'); - try { switch (cmd.type) { case _WorkerCmdType.initialise_: @@ -224,10 +221,9 @@ Future _worker( hits: 0, misses: 0, ), - mode: PutMode.insert, + mode: PutMode.update, ); - } catch (e) { - debugPrint(e.runtimeType.toString()); + } on UniqueViolationException { throw StoreAlreadyExists(storeName: storeName); } @@ -690,7 +686,11 @@ Future _worker( sendRes( id: cmd.id, - data: {'metadata': jsonDecode(store.metadataJson)}, + data: { + 'metadata': + (jsonDecode(store.metadataJson) as Map) + .cast() + }, ); break; @@ -910,8 +910,8 @@ Future _worker( break; } - } catch (e) { - sendRes(id: cmd.id, data: {'error': e}); + } catch (e, s) { + sendRes(id: cmd.id, data: {'error': e, 'stackTrace': s}); } } } From eaa1b63035d4e41d9a95aed33281c3ca6fcbbd2e Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 22 Feb 2024 14:51:33 +0000 Subject: [PATCH 104/168] Fixed bugs Former-commit-id: a004079ad128a859013c51ce0c756bcdbccf37da [formerly 7cff780b6874abf9510fb9ba5e65a5a5991a83a9] Former-commit-id: 5b322a6c612d95322662d8ad808af6209a4b0ff8 --- .../recovery/components/recovery_list.dart | 2 +- .../components/recovery_start_button.dart | 2 +- .../screens/main/pages/recovery/recovery.dart | 2 +- .../pages/stores/components/store_tile.dart | 6 +++-- lib/src/backend/impls/objectbox/backend.dart | 7 ------ .../impls/objectbox/models/src/recovery.dart | 2 +- lib/src/backend/impls/objectbox/worker.dart | 17 ++++++++------ lib/src/regions/recovered_region.dart | 23 ++++++++++--------- lib/src/store/statistics.dart | 2 ++ 9 files changed, 32 insertions(+), 31 deletions(-) diff --git a/example/lib/screens/main/pages/recovery/components/recovery_list.dart b/example/lib/screens/main/pages/recovery/components/recovery_list.dart index 3be7f7d1..fe423af4 100644 --- a/example/lib/screens/main/pages/recovery/components/recovery_list.dart +++ b/example/lib/screens/main/pages/recovery/components/recovery_list.dart @@ -11,7 +11,7 @@ class RecoveryList extends StatefulWidget { required this.moveToDownloadPage, }); - final Iterable<({bool isFailed, RecoveredRegion region})> all; + final Iterable<({bool isFailed, RecoveredRegion region})> all; final void Function() moveToDownloadPage; @override diff --git a/example/lib/screens/main/pages/recovery/components/recovery_start_button.dart b/example/lib/screens/main/pages/recovery/components/recovery_start_button.dart index 836ac40a..2c5317c9 100644 --- a/example/lib/screens/main/pages/recovery/components/recovery_start_button.dart +++ b/example/lib/screens/main/pages/recovery/components/recovery_start_button.dart @@ -10,7 +10,7 @@ class RecoveryStartButton extends StatelessWidget { }); final void Function() moveToDownloadPage; - final ({bool isFailed, RecoveredRegion region}) result; + final ({bool isFailed, RecoveredRegion region}) result; @override Widget build(BuildContext context) => FutureBuilder( diff --git a/example/lib/screens/main/pages/recovery/recovery.dart b/example/lib/screens/main/pages/recovery/recovery.dart index e8212d35..81b97607 100644 --- a/example/lib/screens/main/pages/recovery/recovery.dart +++ b/example/lib/screens/main/pages/recovery/recovery.dart @@ -19,7 +19,7 @@ class RecoveryPage extends StatefulWidget { } class _RecoveryPageState extends State { - late Future region})>> + late Future> _recoverableRegions; @override diff --git a/example/lib/screens/main/pages/stores/components/store_tile.dart b/example/lib/screens/main/pages/stores/components/store_tile.dart index 675bf199..cd1c7c44 100644 --- a/example/lib/screens/main/pages/stores/components/store_tile.dart +++ b/example/lib/screens/main/pages/stores/components/store_tile.dart @@ -201,7 +201,8 @@ class _StoreTileState extends State { strokeWidth: 3, ) : const Icon( - Icons.upload_file_rounded), + Icons.upload_file_rounded, + ), tooltip: 'Export Store', onPressed: _exportingProgress ? null @@ -296,7 +297,8 @@ class _StoreTileState extends State { ), const SizedBox(height: 5), deleteStoreButton( - isCurrentStore: isCurrentStore), + isCurrentStore: isCurrentStore, + ), ], ), ), diff --git a/lib/src/backend/impls/objectbox/backend.dart b/lib/src/backend/impls/objectbox/backend.dart index 1fa6cd17..856126a6 100644 --- a/lib/src/backend/impls/objectbox/backend.dart +++ b/lib/src/backend/impls/objectbox/backend.dart @@ -99,15 +99,8 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { final id = ++_workerId; _workerRes[id] = Completer(); - - final stopwatch = Stopwatch()..start(); - _sendPort!.send((id: id, type: type, args: args)); final res = await _workerRes[id]!.future; - - print(stopwatch.elapsedMilliseconds); - stopwatch.stop(); - _workerRes.remove(id); final err = res?['error']; diff --git a/lib/src/backend/impls/objectbox/models/src/recovery.dart b/lib/src/backend/impls/objectbox/models/src/recovery.dart index bd888336..4df563b6 100644 --- a/lib/src/backend/impls/objectbox/models/src/recovery.dart +++ b/lib/src/backend/impls/objectbox/models/src/recovery.dart @@ -145,7 +145,7 @@ base class ObjectBoxRecovery { : null; RecoveredRegion toRegion() => RecoveredRegion( - id: id, + id: refId, storeName: storeName, time: creationTime, bounds: typeId == 0 diff --git a/lib/src/backend/impls/objectbox/worker.dart b/lib/src/backend/impls/objectbox/worker.dart index efd8a740..c1f95e74 100644 --- a/lib/src/backend/impls/objectbox/worker.dart +++ b/lib/src/backend/impls/objectbox/worker.dart @@ -689,7 +689,7 @@ Future _worker( data: { 'metadata': (jsonDecode(store.metadataJson) as Map) - .cast() + .cast(), }, ); @@ -825,7 +825,8 @@ Future _worker( 'recoverableRegions': root .box() .getAll() - .map((r) => r.toRegion()), + .map((r) => r.toRegion()) + .toList(), }, ); @@ -864,15 +865,17 @@ Future _worker( break; case _WorkerCmdType.cancelRecovery: - root + final id = cmd.args['id']! as int; + + final query = root .box() - .query(ObjectBoxRecovery_.refId.equals(cmd.args['id']! as int)) - .build() - ..remove() - ..close(); + .query(ObjectBoxRecovery_.refId.equals(id)) + .build(); sendRes(id: cmd.id); + query.close(); + break; case _WorkerCmdType.watchRecovery: final triggerImmediately = cmd.args['triggerImmediately']! as bool; diff --git a/lib/src/regions/recovered_region.dart b/lib/src/regions/recovered_region.dart index 4dab0ada..292db84e 100644 --- a/lib/src/regions/recovered_region.dart +++ b/lib/src/regions/recovered_region.dart @@ -9,9 +9,9 @@ part of flutter_map_tile_caching; /// See [RootRecovery] for information about the recovery system. /// /// The availability of [bounds], [line], [center] & [radius] depend on the -/// type [R] of the recovered region. Use [toDownloadable] to restore a valid -/// [DownloadableRegion]. -class RecoveredRegion { +/// represented type of the recovered region. Use [toDownloadable] to restore a +/// valid [DownloadableRegion]. +class RecoveredRegion { /// A unique ID created for every bulk download operation /// /// Not actually used when converting to [DownloadableRegion]. @@ -68,16 +68,17 @@ class RecoveredRegion { }); /// Convert this region into a [BaseRegion] - R toRegion() => switch (R) { - RectangleRegion => RectangleRegion(bounds!), - CircleRegion => CircleRegion(center!, radius!), - LineRegion => LineRegion(this.line!, radius!), - CustomPolygonRegion => CustomPolygonRegion(this.line!), - _ => throw UnimplementedError(), - } as R; + /// + /// Determine which type of [BaseRegion] using [BaseRegion.when]. + BaseRegion toRegion() { + if (bounds != null) return RectangleRegion(bounds!); + if (center != null) return CircleRegion(center!, radius!); + if (line != null && radius != null) return LineRegion(line!, radius!); + return CustomPolygonRegion(line!); + } /// Convert this region into a [DownloadableRegion] - DownloadableRegion toDownloadable( + DownloadableRegion toDownloadable( TileLayer options, { Crs crs = const Epsg3857(), }) => diff --git a/lib/src/store/statistics.dart b/lib/src/store/statistics.dart index 50155064..0ecad2a2 100644 --- a/lib/src/store/statistics.dart +++ b/lib/src/store/statistics.dart @@ -1,6 +1,8 @@ // Copyright © Luka S (JaffaKetchup) under GPL-v3 // A full license can be found at .\LICENSE +// ignore_for_file: use_late_for_private_fields_and_variables + part of flutter_map_tile_caching; /// Provides statistics about an [FMTCStore] From 40dfd0ddc54156eae6ac68847c6f54db82fe2e24 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 22 Feb 2024 20:44:29 +0000 Subject: [PATCH 105/168] Fixed support for database watching Fixed bugs Former-commit-id: 161184b69cb1f4112e06adac169b572ba5887818 [formerly ae2193f55bb2ede90d4f0f3f2ef39559593edc43] Former-commit-id: 53d0e904c1513c2e24fd4d73447af637539d8ba2 --- .../screens/store_editor/store_editor.dart | 405 +++++++++--------- lib/src/backend/impls/objectbox/backend.dart | 175 +++++--- lib/src/backend/impls/objectbox/worker.dart | 73 ++-- lib/src/backend/interfaces/backend.dart | 4 +- 4 files changed, 346 insertions(+), 311 deletions(-) diff --git a/example/lib/screens/store_editor/store_editor.dart b/example/lib/screens/store_editor/store_editor.dart index 821dd07d..1605dffe 100644 --- a/example/lib/screens/store_editor/store_editor.dart +++ b/example/lib/screens/store_editor/store_editor.dart @@ -45,224 +45,215 @@ class _StoreEditorPopupState extends State { @override Widget build(BuildContext context) => Consumer( - builder: (context, downloadProvider, _) => PopScope( - onPopInvoked: (_) { - scaffoldMessenger.showSnackBar( - const SnackBar(content: Text('Changes not saved')), - ); - }, - child: Scaffold( - appBar: buildHeader( - widget: widget, - mounted: mounted, - formKey: _formKey, - newValues: _newValues, - useNewCacheModeValue: _useNewCacheModeValue, - cacheModeValue: _cacheModeValue, - context: context, - ), - body: Consumer( - builder: (context, provider, _) => Padding( - padding: const EdgeInsets.all(12), - child: FutureBuilder?>( - future: widget.existingStoreName == null - ? Future.sync(() => {}) - : FMTCStore(widget.existingStoreName!).metadata.read, - builder: (context, metadata) { - if (!metadata.hasData || metadata.data == null) { - return const LoadingIndicator('Retrieving Settings'); - } - return Form( - key: _formKey, - child: SingleChildScrollView( - child: Column( - children: [ - TextFormField( - decoration: const InputDecoration( - labelText: 'Store Name', - prefixIcon: Icon(Icons.text_fields), - isDense: true, - ), - onChanged: (input) async { - _storeNameIsDuplicate = - (await FMTCRoot.stats.storesAvailable) - .contains(FMTCStore(input)); - setState(() {}); - }, - validator: (input) => - input == null || input.isEmpty - ? 'Required' - : _storeNameIsDuplicate - ? 'Store already exists' - : null, - onSaved: (input) => - _newValues['storeName'] = input!, - autovalidateMode: - AutovalidateMode.onUserInteraction, - textCapitalization: TextCapitalization.words, - initialValue: widget.existingStoreName, - textInputAction: TextInputAction.next, + builder: (context, downloadProvider, _) => Scaffold( + appBar: buildHeader( + widget: widget, + mounted: mounted, + formKey: _formKey, + newValues: _newValues, + useNewCacheModeValue: _useNewCacheModeValue, + cacheModeValue: _cacheModeValue, + context: context, + ), + body: Consumer( + builder: (context, provider, _) => Padding( + padding: const EdgeInsets.all(12), + child: FutureBuilder?>( + future: widget.existingStoreName == null + ? Future.sync(() => {}) + : FMTCStore(widget.existingStoreName!).metadata.read, + builder: (context, metadata) { + if (!metadata.hasData || metadata.data == null) { + return const LoadingIndicator('Retrieving Settings'); + } + return Form( + key: _formKey, + child: SingleChildScrollView( + child: Column( + children: [ + TextFormField( + decoration: const InputDecoration( + labelText: 'Store Name', + prefixIcon: Icon(Icons.text_fields), + isDense: true, ), - const SizedBox(height: 5), - TextFormField( - decoration: const InputDecoration( - labelText: 'Map Source URL', - helperText: - "Use '{x}', '{y}', '{z}' as placeholders. Include protocol. Omit subdomain.", - prefixIcon: Icon(Icons.link), - isDense: true, - ), - onChanged: (i) async { - final uri = Uri.tryParse( - NetworkTileProvider().getTileUrl( - const TileCoordinates(0, 0, 0), - TileLayer(urlTemplate: i), - ), + onChanged: (input) async { + _storeNameIsDuplicate = + (await FMTCRoot.stats.storesAvailable) + .contains(FMTCStore(input)); + setState(() {}); + }, + validator: (input) => input == null || input.isEmpty + ? 'Required' + : _storeNameIsDuplicate + ? 'Store already exists' + : null, + onSaved: (input) => + _newValues['storeName'] = input!, + autovalidateMode: + AutovalidateMode.onUserInteraction, + textCapitalization: TextCapitalization.words, + initialValue: widget.existingStoreName, + textInputAction: TextInputAction.next, + ), + const SizedBox(height: 5), + TextFormField( + decoration: const InputDecoration( + labelText: 'Map Source URL', + helperText: + "Use '{x}', '{y}', '{z}' as placeholders. Include protocol. Omit subdomain.", + prefixIcon: Icon(Icons.link), + isDense: true, + ), + onChanged: (i) async { + final uri = Uri.tryParse( + NetworkTileProvider().getTileUrl( + const TileCoordinates(0, 0, 0), + TileLayer(urlTemplate: i), + ), + ); + + if (uri == null) { + setState( + () => _httpRequestFailed = 'Invalid URL', ); + return; + } - if (uri == null) { - setState( - () => _httpRequestFailed = 'Invalid URL', + _httpRequestFailed = await http.get(uri).then( + (res) => res.statusCode == 200 + ? null + : 'HTTP Request Failed', + onError: (_) => 'HTTP Request Failed', ); - return; - } - - _httpRequestFailed = await http.get(uri).then( - (res) => res.statusCode == 200 - ? null - : 'HTTP Request Failed', - onError: (_) => 'HTTP Request Failed', - ); - setState(() {}); - }, - validator: (i) { - final String input = i ?? ''; + setState(() {}); + }, + validator: (i) { + final String input = i ?? ''; - if (!validators.isURL( - input, - protocols: ['http', 'https'], - requireProtocol: true, - )) { - return 'Invalid URL'; - } - if (!input.contains('{x}') || - !input.contains('{y}') || - !input.contains('{z}')) { - return 'Missing placeholder(s)'; - } + if (!validators.isURL( + input, + protocols: ['http', 'https'], + requireProtocol: true, + )) { + return 'Invalid URL'; + } + if (!input.contains('{x}') || + !input.contains('{y}') || + !input.contains('{z}')) { + return 'Missing placeholder(s)'; + } - return _httpRequestFailed; - }, - onSaved: (input) => - _newValues['sourceURL'] = input!, - autovalidateMode: - AutovalidateMode.onUserInteraction, - keyboardType: TextInputType.url, - initialValue: metadata.data!.isEmpty - ? 'https://tile.openstreetmap.org/{z}/{x}/{y}.png' - : metadata.data!['sourceURL'], - textInputAction: TextInputAction.next, + return _httpRequestFailed; + }, + onSaved: (input) => + _newValues['sourceURL'] = input!, + autovalidateMode: + AutovalidateMode.onUserInteraction, + keyboardType: TextInputType.url, + initialValue: metadata.data!.isEmpty + ? 'https://tile.openstreetmap.org/{z}/{x}/{y}.png' + : metadata.data!['sourceURL'], + textInputAction: TextInputAction.next, + ), + const SizedBox(height: 5), + TextFormField( + decoration: const InputDecoration( + labelText: 'Valid Cache Duration', + helperText: 'Use 0 to disable expiry', + suffixText: 'days', + prefixIcon: Icon(Icons.timelapse), + isDense: true, ), - const SizedBox(height: 5), - TextFormField( - decoration: const InputDecoration( - labelText: 'Valid Cache Duration', - helperText: 'Use 0 to disable expiry', - suffixText: 'days', - prefixIcon: Icon(Icons.timelapse), - isDense: true, - ), - validator: (input) { - if (input == null || - input.isEmpty || - int.parse(input) < 0) { - return 'Must be 0 or more'; - } - return null; - }, - onSaved: (input) => - _newValues['validDuration'] = input!, - autovalidateMode: - AutovalidateMode.onUserInteraction, - keyboardType: TextInputType.number, - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly, - ], - initialValue: metadata.data!.isEmpty - ? '14' - : metadata.data!['validDuration'], - textInputAction: TextInputAction.done, + validator: (input) { + if (input == null || + input.isEmpty || + int.parse(input) < 0) { + return 'Must be 0 or more'; + } + return null; + }, + onSaved: (input) => + _newValues['validDuration'] = input!, + autovalidateMode: + AutovalidateMode.onUserInteraction, + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + ], + initialValue: metadata.data!.isEmpty + ? '14' + : metadata.data!['validDuration'], + textInputAction: TextInputAction.done, + ), + const SizedBox(height: 5), + TextFormField( + decoration: const InputDecoration( + labelText: 'Maximum Length', + helperText: 'Use 0 to disable limit', + suffixText: 'tiles', + prefixIcon: Icon(Icons.disc_full), + isDense: true, ), - const SizedBox(height: 5), - TextFormField( - decoration: const InputDecoration( - labelText: 'Maximum Length', - helperText: 'Use 0 to disable limit', - suffixText: 'tiles', - prefixIcon: Icon(Icons.disc_full), - isDense: true, - ), - validator: (input) { - if (input == null || - input.isEmpty || - int.parse(input) < 0) { - return 'Must be 0 or more'; - } - return null; - }, - onSaved: (input) => - _newValues['maxLength'] = input!, - autovalidateMode: - AutovalidateMode.onUserInteraction, - keyboardType: TextInputType.number, - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly, - ], - initialValue: metadata.data!.isEmpty - ? '20000' - : metadata.data!['maxLength'], - textInputAction: TextInputAction.done, - ), - Row( - children: [ - const Text('Cache Behaviour:'), - const SizedBox(width: 10), - Expanded( - child: DropdownButton( - value: _useNewCacheModeValue - ? _cacheModeValue! - : metadata.data!.isEmpty - ? 'cacheFirst' - : metadata.data!['behaviour'], - onChanged: (newVal) => setState( - () { - _cacheModeValue = - newVal ?? 'cacheFirst'; - _useNewCacheModeValue = true; - }, - ), - items: [ - 'cacheFirst', - 'onlineFirst', - 'cacheOnly', - ] - .map>( - (v) => DropdownMenuItem( - value: v, - child: Text(v), - ), - ) - .toList(), + validator: (input) { + if (input == null || + input.isEmpty || + int.parse(input) < 0) { + return 'Must be 0 or more'; + } + return null; + }, + onSaved: (input) => + _newValues['maxLength'] = input!, + autovalidateMode: + AutovalidateMode.onUserInteraction, + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + ], + initialValue: metadata.data!.isEmpty + ? '20000' + : metadata.data!['maxLength'], + textInputAction: TextInputAction.done, + ), + Row( + children: [ + const Text('Cache Behaviour:'), + const SizedBox(width: 10), + Expanded( + child: DropdownButton( + value: _useNewCacheModeValue + ? _cacheModeValue! + : metadata.data!.isEmpty + ? 'cacheFirst' + : metadata.data!['behaviour'], + onChanged: (newVal) => setState( + () { + _cacheModeValue = newVal ?? 'cacheFirst'; + _useNewCacheModeValue = true; + }, ), + items: [ + 'cacheFirst', + 'onlineFirst', + 'cacheOnly', + ] + .map>( + (v) => DropdownMenuItem( + value: v, + child: Text(v), + ), + ) + .toList(), ), - ], - ), - ], - ), + ), + ], + ), + ], ), - ); - }, - ), + ), + ); + }, ), ), ), diff --git a/lib/src/backend/impls/objectbox/backend.dart b/lib/src/backend/impls/objectbox/backend.dart index 856126a6..39d88c59 100644 --- a/lib/src/backend/impls/objectbox/backend.dart +++ b/lib/src/backend/impls/objectbox/backend.dart @@ -73,15 +73,14 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { @override String get friendlyIdentifier => 'ObjectBox'; - void get expectInitialised => _sendPort ?? (throw RootUnavailable()); - @override Directory? rootDirectory; - // Worker communication SendPort? _sendPort; - final Map?>> _workerRes = {}; - late int _workerId; + void get expectInitialised => _sendPort ?? (throw RootUnavailable()); + final _workerResOneShot = ?>>{}; + final _workerResStreamed = ?>>{}; + int _workerId = 0; late Completer _workerComplete; late StreamSubscription _workerHandler; @@ -91,18 +90,19 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { //late Timer _rotalDebouncer; //late String? _rotalStore; - Future?> _sendCmd({ + Future?> _sendCmdOneShot({ required _WorkerCmdType type, Map args = const {}, }) async { expectInitialised; - final id = ++_workerId; - _workerRes[id] = Completer(); - _sendPort!.send((id: id, type: type, args: args)); - final res = await _workerRes[id]!.future; - _workerRes.remove(id); + final id = ++_workerId; // Create new unique ID + _workerResOneShot[id] = Completer(); // Will be completed by direct handler + _sendPort!.send((id: id, type: type, args: args)); // Send cmd + final res = await _workerResOneShot[id]!.future; // Await response + _workerResOneShot.remove(id); // Free memory + // Handle errors (if missed by direct handler) final err = res?['error']; if (err != null) { if (err is FMTCBackendError) throw err; @@ -120,6 +120,34 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { return res; } + Stream?> _sendCmdStreamed({ + required _WorkerCmdType type, + Map args = const {}, + }) async* { + expectInitialised; + + final id = ++_workerId; // Create new unique ID + final controller = StreamController?>( + onCancel: () async { + _workerResStreamed.remove(id); // Free memory + // Cancel the worker stream if the worker is alive + if (!_workerComplete.isCompleted) { + await _sendCmdOneShot(type: type.streamCancel!, args: {'id': id}); + } + }, + ); + _workerResStreamed[id] = + controller.sink; // Will be inserted into by direct handler + _sendPort!.send((id: id, type: type, args: args)); // Send cmd + + try { + yield* controller.stream; // Return responses + } finally { + // Goto `onCancel` once output listening cancelled + await controller.close(); + } + } + Future initialise({ required String? rootDirectory, required int maxDatabaseSize, @@ -127,12 +155,6 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { }) async { if (_sendPort != null) throw RootAlreadyInitialised(); - // Reset non-comms-related non-resource-intensive state - _workerId = 0; - _workerRes.clear(); - //_rotalStore = null; - //_rotalLength = 0; - this.rootDirectory = await Directory( path.join( rootDirectory ?? @@ -142,11 +164,11 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { ).create(recursive: true); // Prepare to recieve `SendPort` from worker - _workerRes[0] = Completer(); + _workerResOneShot[0] = Completer(); unawaited( - _workerRes[0]!.future.then((res) { + _workerResOneShot[0]!.future.then((res) { _sendPort = res!['sendPort']; - _workerRes.remove(0); + _workerResOneShot.remove(0); }), ); @@ -164,6 +186,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { return; } + // Handle errors final err = evt.data?['error']; if (err != null) { if (err is FMTCBackendError) throw err; @@ -178,7 +201,11 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { ); } - _workerRes[evt.id]!.complete(evt.data); + if (evt.data?['expectStream'] == true) { + _workerResStreamed[evt.id]!.add(evt.data); + } else { + _workerResOneShot[evt.id]!.complete(evt.data); + } }, onDone: () => _workerComplete.complete(), ); @@ -207,50 +234,58 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { // Wait for all currently underway operations to complete before destroying // the isolate (if not `immediate`) - if (!immediate) await Future.wait(_workerRes.values.map((e) => e.future)); + if (!immediate) { + await Future.wait(_workerResOneShot.values.map((e) => e.future)); + } - // Send self-destruct cmd to worker, and don't wait for any response - unawaited( - _sendCmd( - type: _WorkerCmdType.destroy_, - args: {'deleteRoot': deleteRoot}, - ), + // Send self-destruct cmd to worker, and wait for response and exit + await _sendCmdOneShot( + type: _WorkerCmdType.destroy, + args: {'deleteRoot': deleteRoot}, ); - - // Wait for worker to exit (worker handler will exit and signal) await _workerComplete.future; - // Resource-intensive state cleanup only (other cleanup done during init) + // Destroy remaining worker refs _sendPort = null; // Indicate ready for re-init - await _workerHandler.cancel(); - //_rotalDebouncer.cancel(); + await _workerHandler.cancel(); // Stop response handler // Kill any remaining operations with an error (they'll never recieve a // response from the worker) - for (final completer in _workerRes.values) { + for (final completer in _workerResOneShot.values) { completer.complete({'error': RootUnavailable()}); } + for (final streamController in List.of(_workerResStreamed.values)) { + await streamController.close(); + } + + // Reset state + _workerId = 0; + _workerResOneShot.clear(); + _workerResStreamed.clear(); + //_rotalStore = null; + //_rotalLength = 0; + //_rotalDebouncer.cancel(); FMTCBackendAccess.internal = null; } @override Future rootSize() async => - (await _sendCmd(type: _WorkerCmdType.rootSize))!['size']; + (await _sendCmdOneShot(type: _WorkerCmdType.rootSize))!['size']; @override Future rootLength() async => - (await _sendCmd(type: _WorkerCmdType.rootLength))!['length']; + (await _sendCmdOneShot(type: _WorkerCmdType.rootLength))!['length']; @override Future> listStores() async => - (await _sendCmd(type: _WorkerCmdType.listStores))!['stores']; + (await _sendCmdOneShot(type: _WorkerCmdType.listStores))!['stores']; @override Future storeExists({ required String storeName, }) async => - (await _sendCmd( + (await _sendCmdOneShot( type: _WorkerCmdType.storeExists, args: {'storeName': storeName}, ))!['exists']; @@ -259,7 +294,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { Future createStore({ required String storeName, }) => - _sendCmd( + _sendCmdOneShot( type: _WorkerCmdType.createStore, args: {'storeName': storeName}, ); @@ -268,7 +303,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { Future resetStore({ required String storeName, }) => - _sendCmd( + _sendCmdOneShot( type: _WorkerCmdType.resetStore, args: {'storeName': storeName}, ); @@ -278,7 +313,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { required String currentStoreName, required String newStoreName, }) => - _sendCmd( + _sendCmdOneShot( type: _WorkerCmdType.renameStore, args: { 'currentStoreName': currentStoreName, @@ -290,7 +325,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { Future deleteStore({ required String storeName, }) => - _sendCmd( + _sendCmdOneShot( type: _WorkerCmdType.deleteStore, args: {'storeName': storeName}, ); @@ -299,7 +334,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { Future<({double size, int length, int hits, int misses})> getStoreStats({ required String storeName, }) async => - (await _sendCmd( + (await _sendCmdOneShot( type: _WorkerCmdType.getStoreStats, args: {'storeName': storeName}, ))!['stats']; @@ -309,7 +344,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { required String storeName, required String url, }) async => - (await _sendCmd( + (await _sendCmdOneShot( type: _WorkerCmdType.tileExistsInStore, args: {'storeName': storeName, 'url': url}, ))!['exists']; @@ -318,7 +353,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { Future readTile({ required String url, }) async => - (await _sendCmd( + (await _sendCmdOneShot( type: _WorkerCmdType.readTile, args: {'url': url}, ))!['tile']; @@ -327,7 +362,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { Future readLatestTile({ required String storeName, }) async => - (await _sendCmd( + (await _sendCmdOneShot( type: _WorkerCmdType.readLatestTile, args: {'storeName': storeName}, ))!['tile']; @@ -338,7 +373,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { required String url, required Uint8List? bytes, }) => - _sendCmd( + _sendCmdOneShot( type: _WorkerCmdType.writeTile, args: {'storeName': storeName, 'url': url, 'bytes': bytes}, ); @@ -349,7 +384,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { required List urls, required List bytess, }) => - _sendCmd( + _sendCmdOneShot( type: _WorkerCmdType.writeTilesDirect, args: {'storeName': storeName, 'urls': urls, 'bytess': bytess}, ); @@ -359,7 +394,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { required String storeName, required String url, }) async => - (await _sendCmd( + (await _sendCmdOneShot( type: _WorkerCmdType.deleteStore, args: {'storeName': storeName, 'url': url}, ))!['wasOrphan']; @@ -369,7 +404,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { required String storeName, required bool hit, }) => - _sendCmd( + _sendCmdOneShot( type: _WorkerCmdType.registerHitOrMiss, args: {'storeName': storeName, 'hit': hit}, ); @@ -379,7 +414,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { required String storeName, required int tilesLimit, }) async => - (await _sendCmd( + (await _sendCmdOneShot( type: _WorkerCmdType.removeOldestTilesAboveLimit, args: {'storeName': storeName, 'tilesLimit': tilesLimit}, ))!['numOrphans']; @@ -428,7 +463,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { required String storeName, required DateTime expiry, }) async => - (await _sendCmd( + (await _sendCmdOneShot( type: _WorkerCmdType.removeTilesOlderThan, args: {'storeName': storeName, 'expiry': expiry}, ))!['numOrphans']; @@ -437,7 +472,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { Future> readMetadata({ required String storeName, }) async => - (await _sendCmd( + (await _sendCmdOneShot( type: _WorkerCmdType.readMetadata, args: {'storeName': storeName}, ))!['metadata']; @@ -448,7 +483,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { required String key, required String value, }) => - _sendCmd( + _sendCmdOneShot( type: _WorkerCmdType.setMetadata, args: {'storeName': storeName, 'key': key, 'value': value}, ); @@ -458,7 +493,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { required String storeName, required Map kvs, }) => - _sendCmd( + _sendCmdOneShot( type: _WorkerCmdType.setBulkMetadata, args: {'storeName': storeName, 'kvs': kvs}, ); @@ -468,7 +503,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { required String storeName, required String key, }) async => - (await _sendCmd( + (await _sendCmdOneShot( type: _WorkerCmdType.removeMetadata, args: {'storeName': storeName, 'key': key}, ))!['removedValue']; @@ -477,14 +512,14 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { Future resetMetadata({ required String storeName, }) => - _sendCmd( + _sendCmdOneShot( type: _WorkerCmdType.resetMetadata, args: {'storeName': storeName}, ); @override Future> listRecoverableRegions() async => - (await _sendCmd( + (await _sendCmdOneShot( type: _WorkerCmdType.listRecoverableRegions, ))!['recoverableRegions']; @@ -492,7 +527,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { Future getRecoverableRegion({ required int id, }) async => - (await _sendCmd( + (await _sendCmdOneShot( type: _WorkerCmdType.getRecoverableRegion, ))!['recoverableRegion']; @@ -502,7 +537,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { required String storeName, required DownloadableRegion region, }) => - _sendCmd( + _sendCmdOneShot( type: _WorkerCmdType.startRecovery, args: {'id': id, 'storeName': storeName, 'region': region}, ); @@ -511,29 +546,27 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { Future cancelRecovery({ required int id, }) => - _sendCmd(type: _WorkerCmdType.cancelRecovery, args: {'id': id}); + _sendCmdOneShot(type: _WorkerCmdType.cancelRecovery, args: {'id': id}); @override - Future> watchRecovery({ + Stream watchRecovery({ required bool triggerImmediately, - }) async => - /*(await _sendCmd( + }) => + _sendCmdStreamed( type: _WorkerCmdType.watchRecovery, args: {'triggerImmediately': triggerImmediately}, - ))!['stream']*/ - Stream.periodic(const Duration(seconds: 5)); + ); @override - Future> watchStores({ + Stream watchStores({ required List storeNames, required bool triggerImmediately, - }) async => - /*(await _sendCmd( + }) => + _sendCmdStreamed( type: _WorkerCmdType.watchStores, args: { 'storeNames': storeNames, 'triggerImmediately': triggerImmediately, }, - ))!['stream']*/ - Stream.periodic(const Duration(seconds: 5)); + ); } diff --git a/lib/src/backend/impls/objectbox/worker.dart b/lib/src/backend/impls/objectbox/worker.dart index c1f95e74..cd7cf346 100644 --- a/lib/src/backend/impls/objectbox/worker.dart +++ b/lib/src/backend/impls/objectbox/worker.dart @@ -4,8 +4,8 @@ part of 'backend.dart'; enum _WorkerCmdType { - initialise_, // Only valid as a response - destroy_, // Only valid as a request + initialise_, // Only valid as a request + destroy, rootSize, rootLength, listStores, @@ -33,8 +33,14 @@ enum _WorkerCmdType { getRecoverableRegion, startRecovery, cancelRecovery, - watchRecovery, - watchStores, + watchRecovery(streamCancel: cancelWatch), + watchStores(streamCancel: cancelWatch), + cancelWatch; + + const _WorkerCmdType({this.streamCancel}); + + /// Command to execute when cancelling a streamed result + final _WorkerCmdType? streamCancel; } Future _worker( @@ -68,6 +74,9 @@ Future _worker( data: {'sendPort': receivePort.sendPort}, ); + // Create memory for streamed output subscription storage + final streamedOutputSubscriptions = {}; + //! UTIL FUNCTIONS !// /// Convert store name to database store object @@ -135,12 +144,15 @@ Future _worker( switch (cmd.type) { case _WorkerCmdType.initialise_: throw UnsupportedError('Invalid operation'); - case _WorkerCmdType.destroy_: + case _WorkerCmdType.destroy: root.close(); + if (cmd.args['deleteRoot'] == true) { input.rootDirectory.deleteSync(recursive: true); } + sendRes(id: cmd.id); + Isolate.exit(); case _WorkerCmdType.rootSize: final query = root @@ -221,7 +233,7 @@ Future _worker( hits: 0, misses: 0, ), - mode: PutMode.update, + mode: PutMode.insert, ); } on UniqueViolationException { throw StoreAlreadyExists(storeName: storeName); @@ -325,7 +337,6 @@ Future _worker( ..close(); // TODO: Check tiles relations - // TODO: Integrate metadata sendRes(id: cmd.id); @@ -880,36 +891,36 @@ Future _worker( case _WorkerCmdType.watchRecovery: final triggerImmediately = cmd.args['triggerImmediately']! as bool; - sendRes( - id: cmd.id, - data: { - 'stream': /* root - .box() - .query() - .watch(triggerImmediately: triggerImmediately)*/ - null, - }, - ); + streamedOutputSubscriptions[cmd.id] = root + .box() + .query() + .watch(triggerImmediately: triggerImmediately) + .listen((_) => sendRes(id: cmd.id, data: {'expectStream': true})); break; + case _WorkerCmdType.watchStores: final storeNames = cmd.args['storeNames']! as List; final triggerImmediately = cmd.args['triggerImmediately']! as bool; - sendRes( - id: cmd.id, - data: { - 'stream': /*root - .box() - .query( - storeNames.isEmpty - ? null - : ObjectBoxStore_.name.oneOf(storeNames), - ) - .watch(triggerImmediately: triggerImmediately)*/ - null, - }, - ); + streamedOutputSubscriptions[cmd.id] = root + .box() + .query( + storeNames.isEmpty + ? null + : ObjectBoxStore_.name.oneOf(storeNames), + ) + .watch(triggerImmediately: triggerImmediately) + .listen((_) => sendRes(id: cmd.id, data: {'expectStream': true})); + + break; + case _WorkerCmdType.cancelWatch: + final id = cmd.args['id']! as int; + + await streamedOutputSubscriptions[id]?.cancel(); + streamedOutputSubscriptions.remove(id); + + sendRes(id: cmd.id); break; } diff --git a/lib/src/backend/interfaces/backend.dart b/lib/src/backend/interfaces/backend.dart index 8e1ff881..38cc0e4d 100644 --- a/lib/src/backend/interfaces/backend.dart +++ b/lib/src/backend/interfaces/backend.dart @@ -329,7 +329,7 @@ abstract interface class FMTCBackendInternal with FMTCBackendAccess { /// Whenever this has an event, it is likely the other statistics will have /// changed. /// {@endtemplate} - Future> watchRecovery({ + Stream watchRecovery({ required bool triggerImmediately, }); @@ -344,7 +344,7 @@ abstract interface class FMTCBackendInternal with FMTCBackendAccess { /// Emits an event every time a change is made to a store (every time a /// statistic changes, which should include every time a tile is changed). /// {@endtemplate} - Future> watchStores({ + Stream watchStores({ required List storeNames, required bool triggerImmediately, }); From be89d2e55bdc5612f9823fcccfc84ca218bce791 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sat, 24 Feb 2024 11:20:14 +0000 Subject: [PATCH 106/168] Updated lints Former-commit-id: 278c497ad1e2dd009ca2ee3a485f09a0d0ad67d7 [formerly 2a3f55f5231f384b17b5ea5c98fec4f1f80f513d] Former-commit-id: 63fe7f3c58111cd34183a7f8868742a0691d4662 --- analysis_options.yaml | 2 + example/analysis_options.yaml | 5 ++ .../screens/import_store/import_store.dart | 8 +-- .../components/region_shape.dart | 10 ++- .../additional_panes/line_region_pane.dart | 7 +- .../components/side_panel/primary_pane.dart | 31 ++++---- .../region_selection/region_selection.dart | 6 -- .../pages/stores/components/stat_display.dart | 19 ++--- jaffa_lints.yaml | 31 ++++++-- .../impls/objectbox/models/src/recovery.dart | 70 +++++++++---------- .../impls/objectbox/models/src/store.dart | 16 ++--- .../impls/objectbox/models/src/tile.dart | 12 ++-- lib/src/backend/impls/objectbox/worker.dart | 64 ----------------- lib/src/backend/utils/errors.dart | 8 +-- lib/src/bulk_download/download_progress.dart | 66 ++++++++--------- lib/src/bulk_download/tile_event.dart | 22 +++--- lib/src/bulk_download/tile_loops/count.dart | 14 ++-- .../bulk_download/tile_loops/generate.dart | 14 ++-- lib/src/errors/browsing.dart | 40 +++++------ lib/src/errors/damaged_store.dart | 12 ++-- lib/src/providers/image_provider.dart | 16 ++--- lib/src/providers/tile_provider.dart | 22 +++--- lib/src/providers/tile_provider_settings.dart | 52 +++++++------- lib/src/regions/downloadable_region.dart | 32 ++++----- lib/src/regions/line.dart | 18 ++--- lib/src/regions/recovered_region.dart | 30 ++++---- lib/src/root/recovery.dart | 2 +- lib/src/root/statistics.dart | 4 +- lib/src/store/statistics.dart | 2 +- 29 files changed, 297 insertions(+), 338 deletions(-) create mode 100644 example/analysis_options.yaml diff --git a/analysis_options.yaml b/analysis_options.yaml index 59d8a4a8..9431a974 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -7,3 +7,5 @@ analyzer: linter: rules: avoid_slow_async_io: false + # TODO: Remove + public_member_api_docs: false \ No newline at end of file diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml new file mode 100644 index 00000000..cb1978b3 --- /dev/null +++ b/example/analysis_options.yaml @@ -0,0 +1,5 @@ +include: ../analysis_options.yaml + +linter: + rules: + public_member_api_docs: false diff --git a/example/lib/screens/import_store/import_store.dart b/example/lib/screens/import_store/import_store.dart index c8ecec2e..1786c92a 100644 --- a/example/lib/screens/import_store/import_store.dart +++ b/example/lib/screens/import_store/import_store.dart @@ -139,12 +139,12 @@ class _ImportStorePopupState extends State { } class _ImportStore { - final Future result; - List? collisionInfo; - Completer collisionResolution; - _ImportStore( this.result, { required this.collisionInfo, }) : collisionResolution = Completer(); + + final Future result; + List? collisionInfo; + Completer collisionResolution; } diff --git a/example/lib/screens/main/pages/region_selection/components/region_shape.dart b/example/lib/screens/main/pages/region_selection/components/region_shape.dart index 8056f045..9cc00621 100644 --- a/example/lib/screens/main/pages/region_selection/components/region_shape.dart +++ b/example/lib/screens/main/pages/region_selection/components/region_shape.dart @@ -51,7 +51,6 @@ class RegionShape extends StatelessWidget { bounds.southEast, bounds.southWest, ]; - break; case RegionType.circle: holePoints = CircleRegion( provider.coordinates[0], @@ -63,7 +62,6 @@ class RegionShape extends StatelessWidget { ) / 1000, ).toOutline().toList(); - break; case RegionType.line: throw Error(); case RegionType.customPolygon: @@ -71,11 +69,11 @@ class RegionShape extends StatelessWidget { ? provider.coordinates : [ ...provider.coordinates, - provider.customPolygonSnap - ? provider.coordinates.first - : provider.currentNewPointPos, + if (provider.customPolygonSnap) + provider.coordinates.first + else + provider.currentNewPointPos, ]; - break; } } diff --git a/example/lib/screens/main/pages/region_selection/components/side_panel/additional_panes/line_region_pane.dart b/example/lib/screens/main/pages/region_selection/components/side_panel/additional_panes/line_region_pane.dart index 475ded7d..894773c1 100644 --- a/example/lib/screens/main/pages/region_selection/components/side_panel/additional_panes/line_region_pane.dart +++ b/example/lib/screens/main/pages/region_selection/components/side_panel/additional_panes/line_region_pane.dart @@ -63,9 +63,10 @@ class LineRegionPane extends StatelessWidget { icon: const Icon(Icons.route), tooltip: 'Import from GPX', ), - layoutDirection == Axis.vertical - ? const Divider(height: 8) - : const VerticalDivider(width: 8), + if (layoutDirection == Axis.vertical) + const Divider(height: 8) + else + const VerticalDivider(width: 8), const SizedBox.square(dimension: 4), if (layoutDirection == Axis.vertical) ...[ Text('${provider.lineRadius.round()}m'), diff --git a/example/lib/screens/main/pages/region_selection/components/side_panel/primary_pane.dart b/example/lib/screens/main/pages/region_selection/components/side_panel/primary_pane.dart index 56a0804c..034cadc4 100644 --- a/example/lib/screens/main/pages/region_selection/components/side_panel/primary_pane.dart +++ b/example/lib/screens/main/pages/region_selection/components/side_panel/primary_pane.dart @@ -144,21 +144,22 @@ class _PrimaryPane extends StatelessWidget { direction: layoutDirection, mainAxisSize: MainAxisSize.min, children: [ - provider.openAdjustZoomLevelsSlider - ? IconButton.outlined( - icon: Icon( - layoutDirection == Axis.vertical - ? Icons.arrow_left - : Icons.arrow_drop_down, - ), - onPressed: () => - provider.openAdjustZoomLevelsSlider = false, - ) - : IconButton( - icon: const Icon(Icons.zoom_in), - onPressed: () => - provider.openAdjustZoomLevelsSlider = true, - ), + if (provider.openAdjustZoomLevelsSlider) + IconButton.outlined( + icon: Icon( + layoutDirection == Axis.vertical + ? Icons.arrow_left + : Icons.arrow_drop_down, + ), + onPressed: () => + provider.openAdjustZoomLevelsSlider = false, + ) + else + IconButton( + icon: const Icon(Icons.zoom_in), + onPressed: () => + provider.openAdjustZoomLevelsSlider = true, + ), const SizedBox.square(dimension: 12), IconButton.filled( icon: const Icon(Icons.done), diff --git a/example/lib/screens/main/pages/region_selection/region_selection.dart b/example/lib/screens/main/pages/region_selection/region_selection.dart index fae985c1..89e72aad 100644 --- a/example/lib/screens/main/pages/region_selection/region_selection.dart +++ b/example/lib/screens/main/pages/region_selection/region_selection.dart @@ -66,8 +66,6 @@ class _RegionSelectionPageState extends State { provider ..clearCoordinates() ..addCoordinate(provider.currentNewPointPos); - - break; case RegionType.circle: if (coords.length == 2) { provider.region = CircleRegion( @@ -81,15 +79,11 @@ class _RegionSelectionPageState extends State { provider ..clearCoordinates() ..addCoordinate(provider.currentNewPointPos); - - break; case RegionType.line: provider.region = LineRegion(coords, provider.lineRadius); - break; case RegionType.customPolygon: if (!provider.isCustomPolygonComplete) break; provider.region = CustomPolygonRegion(coords); - break; } }, onSecondaryTap: (_, __) => diff --git a/example/lib/screens/main/pages/stores/components/stat_display.dart b/example/lib/screens/main/pages/stores/components/stat_display.dart index cad14c5f..eeae3369 100644 --- a/example/lib/screens/main/pages/stores/components/stat_display.dart +++ b/example/lib/screens/main/pages/stores/components/stat_display.dart @@ -13,15 +13,16 @@ class StatDisplay extends StatelessWidget { @override Widget build(BuildContext context) => Column( children: [ - statistic == null - ? const CircularProgressIndicator() - : Text( - statistic!, - style: const TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), + if (statistic == null) + const CircularProgressIndicator() + else + Text( + statistic!, + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), Text( description, style: const TextStyle( diff --git a/jaffa_lints.yaml b/jaffa_lints.yaml index c7dab103..d6e149b1 100644 --- a/jaffa_lints.yaml +++ b/jaffa_lints.yaml @@ -1,9 +1,10 @@ linter: rules: - always_declare_return_types - - always_require_non_null_named_parameters - annotate_overrides + - annotate_redeclares - avoid_annotating_with_dynamic + - avoid_bool_literals_in_conditional_expressions - avoid_catching_errors - avoid_double_and_int_checks - avoid_dynamic_calls @@ -24,8 +25,6 @@ linter: - avoid_relative_lib_imports - avoid_renaming_method_parameters - avoid_return_types_on_setters - - avoid_returning_null - - avoid_returning_null_for_future - avoid_returning_null_for_void - avoid_returning_this - avoid_setters_without_getters @@ -45,13 +44,17 @@ linter: - cascade_invocations - cast_nullable_to_non_nullable - close_sinks + - collection_methods_unrelated_type + - combinators_ordering - comment_references - conditional_uri_does_not_exist - constant_identifier_names - control_flow_in_finally - curly_braces_in_flow_control_structures + - dangling_library_doc_comments - depend_on_referenced_packages - deprecated_consistency + - deprecated_member_use_from_same_package - directives_ordering - do_not_use_environment - empty_catches @@ -62,22 +65,28 @@ linter: - file_names - hash_and_equals - implementation_imports - - iterable_contains_unrelated_type + - implicit_call_tearoffs + - implicit_reopen + - invalid_case_patterns - join_return_with_assignment - leading_newlines_in_multiline_strings + - library_annotations - library_names - library_prefixes - library_private_types_in_public_api - - list_remove_unrelated_type - literal_only_boolean_expressions + - matching_super_parameters - missing_whitespace_between_adjacent_strings - no_adjacent_strings_in_list - no_default_cases - no_duplicate_case_values - no_leading_underscores_for_library_prefixes - no_leading_underscores_for_local_identifiers + - no_literal_bool_comparisons - no_logic_in_create_state - no_runtimeType_toString + - no_self_assignments + - no_wildcard_variable_uses - non_constant_identifier_names - noop_primitive_operations - null_check_on_nullable_type_parameter @@ -100,7 +109,6 @@ linter: - prefer_const_literals_to_create_immutables - prefer_constructors_over_static_methods - prefer_contains - - prefer_equal_for_default_values - prefer_expression_function_bodies - prefer_final_fields - prefer_final_in_for_each @@ -109,6 +117,7 @@ linter: - prefer_foreach - prefer_function_declarations_over_variables - prefer_generic_function_type_aliases + - prefer_if_elements_to_conditional_expressions - prefer_if_null_operators - prefer_initializing_formals - prefer_inlined_adds @@ -127,6 +136,7 @@ linter: - prefer_typing_uninitialized_variables - prefer_void_to_null - provide_deprecation_message + - public_member_api_docs - recursive_getters - require_trailing_commas - secure_pubspec_urls @@ -134,6 +144,7 @@ linter: - sized_box_shrink_expand - slash_for_doc_comments - sort_child_properties_last + - sort_constructors_first - sort_pub_dependencies - sort_unnamed_constructors_first - test_types_in_equals @@ -141,16 +152,20 @@ linter: - tighten_type_of_initializing_formals - type_annotate_public_apis - type_init_formals + - type_literal_in_constant_pattern - unawaited_futures - unnecessary_await_in_return - unnecessary_brace_in_string_interps + - unnecessary_breaks - unnecessary_const - unnecessary_constructor_name - unnecessary_getters_setters - unnecessary_lambdas - unnecessary_late + - unnecessary_library_directive - unnecessary_new - unnecessary_null_aware_assignments + - unnecessary_null_aware_operator_on_extension_on_nullable - unnecessary_null_checks - unnecessary_null_in_if_null_operators - unnecessary_nullable_for_final_variable_declarations @@ -161,6 +176,8 @@ linter: - unnecessary_string_escapes - unnecessary_string_interpolations - unnecessary_this + - unnecessary_to_list_in_spreads + - unreachable_from_main - unrelated_type_equality_checks - unsafe_html - use_build_context_synchronously @@ -182,4 +199,4 @@ linter: - use_test_throws_matchers - use_to_and_as_if_applicable - valid_regexps - - void_checks + - void_checks \ No newline at end of file diff --git a/lib/src/backend/impls/objectbox/models/src/recovery.dart b/lib/src/backend/impls/objectbox/models/src/recovery.dart index 4df563b6..12527f77 100644 --- a/lib/src/backend/impls/objectbox/models/src/recovery.dart +++ b/lib/src/backend/impls/objectbox/models/src/recovery.dart @@ -10,41 +10,6 @@ import '../../../../../../flutter_map_tile_caching.dart'; @Entity() base class ObjectBoxRecovery { - @Id() - @internal - int id = 0; - - @Index() - @Unique() - int refId; - - String storeName; - @Property(type: PropertyType.date) - DateTime creationTime; - - int minZoom; - int maxZoom; - int startTile; - int? endTile; - - int typeId; // 0 - rect, 1 - circle, 2 - line, 3 - custom polygon - - double? rectNwLat; - double? rectNwLng; - double? rectSeLat; - double? rectSeLng; - - double? circleCenterLat; - double? circleCenterLng; - double? circleRadius; - - List? lineLats; - List? lineLngs; - double? lineRadius; - - List? customPolygonLats; - List? customPolygonLngs; - ObjectBoxRecovery({ required this.refId, required this.storeName, @@ -144,6 +109,41 @@ base class ObjectBoxRecovery { .toList() : null; + @Id() + @internal + int id = 0; + + @Index() + @Unique() + int refId; + + String storeName; + @Property(type: PropertyType.date) + DateTime creationTime; + + int minZoom; + int maxZoom; + int startTile; + int? endTile; + + int typeId; // 0 - rect, 1 - circle, 2 - line, 3 - custom polygon + + double? rectNwLat; + double? rectNwLng; + double? rectSeLat; + double? rectSeLng; + + double? circleCenterLat; + double? circleCenterLng; + double? circleRadius; + + List? lineLats; + List? lineLngs; + double? lineRadius; + + List? customPolygonLats; + List? customPolygonLngs; + RecoveredRegion toRegion() => RecoveredRegion( id: refId, storeName: storeName, diff --git a/lib/src/backend/impls/objectbox/models/src/store.dart b/lib/src/backend/impls/objectbox/models/src/store.dart index 4bd5b3af..2618613c 100644 --- a/lib/src/backend/impls/objectbox/models/src/store.dart +++ b/lib/src/backend/impls/objectbox/models/src/store.dart @@ -7,6 +7,14 @@ import 'tile.dart'; @Entity() class ObjectBoxStore { + ObjectBoxStore({ + required this.name, + required this.length, + required this.size, + required this.hits, + required this.misses, + }) : metadataJson = ''; + @Id() int id = 0; @@ -23,12 +31,4 @@ class ObjectBoxStore { int hits; int misses; String metadataJson; - - ObjectBoxStore({ - required this.name, - required this.length, - required this.size, - required this.hits, - required this.misses, - }) : metadataJson = ''; } diff --git a/lib/src/backend/impls/objectbox/models/src/tile.dart b/lib/src/backend/impls/objectbox/models/src/tile.dart index a361b856..6cd0d235 100644 --- a/lib/src/backend/impls/objectbox/models/src/tile.dart +++ b/lib/src/backend/impls/objectbox/models/src/tile.dart @@ -10,6 +10,12 @@ import 'store.dart'; @Entity() base class ObjectBoxTile extends BackendTile { + ObjectBoxTile({ + required this.url, + required this.lastModified, + required this.bytes, + }); + @Id() int id = 0; @@ -28,10 +34,4 @@ base class ObjectBoxTile extends BackendTile { @Index() final stores = ToMany(); - - ObjectBoxTile({ - required this.url, - required this.lastModified, - required this.bytes, - }); } diff --git a/lib/src/backend/impls/objectbox/worker.dart b/lib/src/backend/impls/objectbox/worker.dart index cd7cf346..ec03073e 100644 --- a/lib/src/backend/impls/objectbox/worker.dart +++ b/lib/src/backend/impls/objectbox/worker.dart @@ -167,16 +167,12 @@ Future _worker( ); query.close(); - - break; case _WorkerCmdType.rootLength: final query = root.box().query().build(); sendRes(id: cmd.id, data: {'length': query.count()}); query.close(); - - break; case _WorkerCmdType.listStores: sendRes( id: cmd.id, @@ -188,8 +184,6 @@ Future _worker( .toList(), }, ); - - break; case _WorkerCmdType.storeExists: final query = root .box() @@ -201,8 +195,6 @@ Future _worker( sendRes(id: cmd.id, data: {'exists': query.count() == 1}); query.close(); - - break; case _WorkerCmdType.getStoreStats: final storeName = cmd.args['storeName']! as String; final store = getStore(storeName) ?? @@ -219,8 +211,6 @@ Future _worker( ), }, ); - - break; case _WorkerCmdType.createStore: final storeName = cmd.args['storeName']! as String; @@ -240,8 +230,6 @@ Future _worker( } sendRes(id: cmd.id); - - break; case _WorkerCmdType.resetStore: // TODO: Consider just deleting then creating final storeName = cmd.args['storeName']! as String; @@ -300,8 +288,6 @@ Future _worker( ); sendRes(id: cmd.id); - - break; case _WorkerCmdType.renameStore: final currentStoreName = cmd.args['currentStoreName']! as String; final newStoreName = cmd.args['newStoreName']! as String; @@ -324,8 +310,6 @@ Future _worker( ); sendRes(id: cmd.id); - - break; case _WorkerCmdType.deleteStore: root .box() @@ -339,8 +323,6 @@ Future _worker( // TODO: Check tiles relations sendRes(id: cmd.id); - - break; case _WorkerCmdType.tileExistsInStore: final storeName = cmd.args['storeName']! as String; final url = cmd.args['url']! as String; @@ -356,8 +338,6 @@ Future _worker( sendRes(id: cmd.id, data: {'exists': query.count() == 1}); query.close(); - - break; case _WorkerCmdType.readTile: final url = cmd.args['url']! as String; @@ -369,8 +349,6 @@ Future _worker( sendRes(id: cmd.id, data: {'tile': query.findUnique()}); query.close(); - - break; case _WorkerCmdType.readLatestTile: final storeName = cmd.args['storeName']! as String; @@ -387,8 +365,6 @@ Future _worker( sendRes(id: cmd.id, data: {'tile': query.findFirst()}); query.close(); - - break; case _WorkerCmdType.writeTile: final storeName = cmd.args['storeName']! as String; final url = cmd.args['url']! as String; @@ -427,7 +403,6 @@ Future _worker( ..length += 1 ..size += bytes.lengthInBytes, ); - break; case (false, true): // Existing tile, no update // Only take action if it's not already belonging to the store if (!existingTile!.stores.contains(store)) { @@ -438,7 +413,6 @@ Future _worker( ..size += existingTile.bytes.lengthInBytes, ); } - break; case (false, false): // Existing tile, update required tiles.put( existingTile! @@ -455,7 +429,6 @@ Future _worker( ) .toList(), ); - break; case (true, true): // FMTC internal error throw StateError( 'FMTC ObjectBox backend internal state error: $url', @@ -465,8 +438,6 @@ Future _worker( ); sendRes(id: cmd.id); - - break; case _WorkerCmdType.writeTilesDirect: final storeName = cmd.args['storeName']! as String; final urls = cmd.args['urls']! as List; @@ -557,8 +528,6 @@ Future _worker( tilesQuery.close(); storeQuery.close(); - - break; case _WorkerCmdType.deleteTile: final storeName = cmd.args['storeName']! as String; final url = cmd.args['url']! as String; @@ -581,8 +550,6 @@ Future _worker( ); query.close(); - - break; case _WorkerCmdType.registerHitOrMiss: final storeName = cmd.args['storeName']! as String; final hit = cmd.args['hit']! as bool; @@ -608,8 +575,6 @@ Future _worker( ); sendRes(id: cmd.id); - - break; case _WorkerCmdType.removeOldestTilesAboveLimit: final storeName = cmd.args['storeName']! as String; final tilesLimit = cmd.args['tilesLimit']! as int; @@ -656,8 +621,6 @@ Future _worker( storeQuery.close(); tilesQuery.close(); - - break; case _WorkerCmdType.removeTilesOlderThan: final storeName = cmd.args['storeName']! as String; final expiry = cmd.args['expiry']! as DateTime; @@ -688,8 +651,6 @@ Future _worker( ); tilesQuery.close(); - - break; case _WorkerCmdType.readMetadata: final storeName = cmd.args['storeName']! as String; final store = getStore(storeName) ?? @@ -703,8 +664,6 @@ Future _worker( .cast(), }, ); - - break; case _WorkerCmdType.setMetadata: final storeName = cmd.args['storeName']! as String; final key = cmd.args['key']! as String; @@ -735,8 +694,6 @@ Future _worker( ); sendRes(id: cmd.id); - - break; case _WorkerCmdType.setBulkMetadata: final storeName = cmd.args['storeName']! as String; final kvs = cmd.args['kvs']! as Map; @@ -767,8 +724,6 @@ Future _worker( ); sendRes(id: cmd.id); - - break; case _WorkerCmdType.removeMetadata: final storeName = cmd.args['storeName']! as String; final key = cmd.args['key']! as String; @@ -802,8 +757,6 @@ Future _worker( ), }, ); - - break; case _WorkerCmdType.resetMetadata: final storeName = cmd.args['storeName']! as String; @@ -827,8 +780,6 @@ Future _worker( ); sendRes(id: cmd.id); - - break; case _WorkerCmdType.listRecoverableRegions: sendRes( id: cmd.id, @@ -840,8 +791,6 @@ Future _worker( .toList(), }, ); - - break; case _WorkerCmdType.getRecoverableRegion: final id = cmd.args['id']! as int; @@ -857,8 +806,6 @@ Future _worker( ?.toRegion(), }, ); - - break; case _WorkerCmdType.startRecovery: final id = cmd.args['id']! as int; final storeName = cmd.args['storeName']! as String; @@ -873,8 +820,6 @@ Future _worker( ); sendRes(id: cmd.id); - - break; case _WorkerCmdType.cancelRecovery: final id = cmd.args['id']! as int; @@ -886,8 +831,6 @@ Future _worker( sendRes(id: cmd.id); query.close(); - - break; case _WorkerCmdType.watchRecovery: final triggerImmediately = cmd.args['triggerImmediately']! as bool; @@ -896,9 +839,6 @@ Future _worker( .query() .watch(triggerImmediately: triggerImmediately) .listen((_) => sendRes(id: cmd.id, data: {'expectStream': true})); - - break; - case _WorkerCmdType.watchStores: final storeNames = cmd.args['storeNames']! as List; final triggerImmediately = cmd.args['triggerImmediately']! as bool; @@ -912,8 +852,6 @@ Future _worker( ) .watch(triggerImmediately: triggerImmediately) .listen((_) => sendRes(id: cmd.id, data: {'expectStream': true})); - - break; case _WorkerCmdType.cancelWatch: final id = cmd.args['id']! as int; @@ -921,8 +859,6 @@ Future _worker( streamedOutputSubscriptions.remove(id); sendRes(id: cmd.id); - - break; } } catch (e, s) { sendRes(id: cmd.id, data: {'error': e, 'stackTrace': s}); diff --git a/lib/src/backend/utils/errors.dart b/lib/src/backend/utils/errors.dart index 82c05b45..19bcee56 100644 --- a/lib/src/backend/utils/errors.dart +++ b/lib/src/backend/utils/errors.dart @@ -30,10 +30,10 @@ final class RootAlreadyInitialised extends FMTCBackendError { /// Indicates that the specified store structure was not available for use in /// operations, likely because it didn't exist final class StoreNotExists extends FMTCBackendError { - final String storeName; - StoreNotExists({required this.storeName}); + final String storeName; + @override String toString() => 'StoreNotExists: The requested store "$storeName" did not exist'; @@ -42,10 +42,10 @@ final class StoreNotExists extends FMTCBackendError { /// Indicates that the specified store structure could not be created because it /// already existed final class StoreAlreadyExists extends FMTCBackendError { - final String storeName; - StoreAlreadyExists({required this.storeName}); + final String storeName; + @override String toString() => 'StoreAlreadyExists: The requested store "$storeName" already existed'; diff --git a/lib/src/bulk_download/download_progress.dart b/lib/src/bulk_download/download_progress.dart index a71f5a61..7e690ef5 100644 --- a/lib/src/bulk_download/download_progress.dart +++ b/lib/src/bulk_download/download_progress.dart @@ -12,6 +12,39 @@ part of flutter_map_tile_caching; /// See the documentation on each individual property for more information. @immutable class DownloadProgress { + const DownloadProgress.__({ + required TileEvent? latestTileEvent, + required this.cachedTiles, + required this.cachedSize, + required this.bufferedTiles, + required this.bufferedSize, + required this.skippedTiles, + required this.skippedSize, + required this.failedTiles, + required this.maxTiles, + required this.elapsedDuration, + required this.tilesPerSecond, + required this.isTPSArtificiallyCapped, + required this.isComplete, + }) : _latestTileEvent = latestTileEvent; + + factory DownloadProgress._initial({required int maxTiles}) => + DownloadProgress.__( + latestTileEvent: null, + cachedTiles: 0, + cachedSize: 0, + bufferedTiles: 0, + bufferedSize: 0, + skippedTiles: 0, + skippedSize: 0, + failedTiles: 0, + maxTiles: maxTiles, + elapsedDuration: Duration.zero, + tilesPerSecond: 0, + isTPSArtificiallyCapped: false, + isComplete: false, + ); + /// The result of the latest attempted tile /// /// Note that there a number of things to keep in mind when tracking the @@ -165,39 +198,6 @@ class DownloadProgress { ? Duration.zero : estTotalDuration - elapsedDuration; - const DownloadProgress.__({ - required TileEvent? latestTileEvent, - required this.cachedTiles, - required this.cachedSize, - required this.bufferedTiles, - required this.bufferedSize, - required this.skippedTiles, - required this.skippedSize, - required this.failedTiles, - required this.maxTiles, - required this.elapsedDuration, - required this.tilesPerSecond, - required this.isTPSArtificiallyCapped, - required this.isComplete, - }) : _latestTileEvent = latestTileEvent; - - factory DownloadProgress._initial({required int maxTiles}) => - DownloadProgress.__( - latestTileEvent: null, - cachedTiles: 0, - cachedSize: 0, - bufferedTiles: 0, - bufferedSize: 0, - skippedTiles: 0, - skippedSize: 0, - failedTiles: 0, - maxTiles: maxTiles, - elapsedDuration: Duration.zero, - tilesPerSecond: 0, - isTPSArtificiallyCapped: false, - isComplete: false, - ); - DownloadProgress _fallbackReportUpdate({ required Duration newDuration, required double tilesPerSecond, diff --git a/lib/src/bulk_download/tile_event.dart b/lib/src/bulk_download/tile_event.dart index 39c9d3c9..20bb3fca 100644 --- a/lib/src/bulk_download/tile_event.dart +++ b/lib/src/bulk_download/tile_event.dart @@ -66,6 +66,17 @@ enum TileEventResult { /// [DownloadProgress]' responsibility. @immutable class TileEvent { + const TileEvent._( + this.result, { + required this.url, + required this.coordinates, + this.tileImage, + this.fetchResponse, + this.fetchError, + this.isRepeat = false, + bool wasBufferReset = false, + }) : _wasBufferReset = wasBufferReset; + /// The status of this event, the result of attempting to cache this tile /// /// See [TileEventResult.category] ([TileEventResultCategory]) for @@ -121,17 +132,6 @@ class TileEvent { final bool _wasBufferReset; - const TileEvent._( - this.result, { - required this.url, - required this.coordinates, - this.tileImage, - this.fetchResponse, - this.fetchError, - this.isRepeat = false, - bool wasBufferReset = false, - }) : _wasBufferReset = wasBufferReset; - TileEvent _repeat() => TileEvent._( result, url: url, diff --git a/lib/src/bulk_download/tile_loops/count.dart b/lib/src/bulk_download/tile_loops/count.dart index ddd8e703..2f2ec777 100644 --- a/lib/src/bulk_download/tile_loops/count.dart +++ b/lib/src/bulk_download/tile_loops/count.dart @@ -57,12 +57,14 @@ class TilesCounter { outlineTileNums[zoomLvl]![tile.x] ??= [largestInt, smallestInt]; outlineTileNums[zoomLvl]![tile.x] = [ - tile.y < outlineTileNums[zoomLvl]![tile.x]![0] - ? tile.y - : outlineTileNums[zoomLvl]![tile.x]![0], - tile.y > outlineTileNums[zoomLvl]![tile.x]![1] - ? tile.y - : outlineTileNums[zoomLvl]![tile.x]![1], + if (tile.y < outlineTileNums[zoomLvl]![tile.x]![0]) + tile.y + else + outlineTileNums[zoomLvl]![tile.x]![0], + if (tile.y > outlineTileNums[zoomLvl]![tile.x]![1]) + tile.y + else + outlineTileNums[zoomLvl]![tile.x]![1], ]; } diff --git a/lib/src/bulk_download/tile_loops/generate.dart b/lib/src/bulk_download/tile_loops/generate.dart index 59403fcd..60cef68b 100644 --- a/lib/src/bulk_download/tile_loops/generate.dart +++ b/lib/src/bulk_download/tile_loops/generate.dart @@ -69,12 +69,14 @@ class TilesGenerator { outlineTileNums[zoomLvl]![tile.x] ??= [largestInt, smallestInt]; outlineTileNums[zoomLvl]![tile.x] = [ - tile.y < outlineTileNums[zoomLvl]![tile.x]![0] - ? tile.y - : outlineTileNums[zoomLvl]![tile.x]![0], - tile.y > outlineTileNums[zoomLvl]![tile.x]![1] - ? tile.y - : outlineTileNums[zoomLvl]![tile.x]![1], + if (tile.y < outlineTileNums[zoomLvl]![tile.x]![0]) + tile.y + else + outlineTileNums[zoomLvl]![tile.x]![0], + if (tile.y > outlineTileNums[zoomLvl]![tile.x]![1]) + tile.y + else + outlineTileNums[zoomLvl]![tile.x]![1], ]; } diff --git a/lib/src/errors/browsing.dart b/lib/src/errors/browsing.dart index cb3fffb9..e96fa196 100644 --- a/lib/src/errors/browsing.dart +++ b/lib/src/errors/browsing.dart @@ -19,6 +19,26 @@ import '../../flutter_map_tile_caching.dart'; /// [message] for a user-friendly English description of this exception. Also /// see the other properties for more information. class FMTCBrowsingError implements Exception { + /// An [Exception] indicating that there was an error retrieving tiles to be + /// displayed on the map + /// + /// These can usually be safely ignored, as they simply represent a fall + /// through of all valid/possible cases, but you may wish to handle them + /// anyway using [FMTCTileProviderSettings.errorHandler]. + /// + /// Use [type] to establish the condition that threw this exception, and + /// [message] for a user-friendly English description of this exception. Also + /// see the other properties for more information. + @internal + FMTCBrowsingError({ + required this.type, + required this.networkUrl, + required this.matcherUrl, + this.request, + this.response, + this.originalError, + }) : message = '${type.explanation} ${type.resolution}'; + /// Defines the condition that threw this exception /// /// See [message] for a user friendly description of this value. @@ -60,26 +80,6 @@ class FMTCBrowsingError implements Exception { /// [FMTCBrowsingErrorType.unknownFetchException]. final Object? originalError; - /// An [Exception] indicating that there was an error retrieving tiles to be - /// displayed on the map - /// - /// These can usually be safely ignored, as they simply represent a fall - /// through of all valid/possible cases, but you may wish to handle them - /// anyway using [FMTCTileProviderSettings.errorHandler]. - /// - /// Use [type] to establish the condition that threw this exception, and - /// [message] for a user-friendly English description of this exception. Also - /// see the other properties for more information. - @internal - FMTCBrowsingError({ - required this.type, - required this.networkUrl, - required this.matcherUrl, - this.request, - this.response, - this.originalError, - }) : message = '${type.explanation} ${type.resolution}'; - @override String toString() => 'FMTCBrowsingError ($type): $message'; } diff --git a/lib/src/errors/damaged_store.dart b/lib/src/errors/damaged_store.dart index 1fc4363b..ad92ba06 100644 --- a/lib/src/errors/damaged_store.dart +++ b/lib/src/errors/damaged_store.dart @@ -8,12 +8,6 @@ import 'package:meta/meta.dart'; /// Can be thrown for multiple reasons. See [type] and /// [FMTCDamagedStoreExceptionType] for more information. class FMTCDamagedStoreException implements Exception { - /// Friendly message - final String message; - - /// Programmatic error descriptor - final FMTCDamagedStoreExceptionType type; - /// An [Exception] indicating that an operation was attempted on a damaged store /// /// Can be thrown for multiple reasons. See [type] and @@ -21,6 +15,12 @@ class FMTCDamagedStoreException implements Exception { @internal FMTCDamagedStoreException(this.message, this.type); + /// Friendly message + final String message; + + /// Programmatic error descriptor + final FMTCDamagedStoreExceptionType type; + @override String toString() => 'FMTCDamagedStoreException: $message'; } diff --git a/lib/src/providers/image_provider.dart b/lib/src/providers/image_provider.dart index 0d639a42..1914a3d5 100644 --- a/lib/src/providers/image_provider.dart +++ b/lib/src/providers/image_provider.dart @@ -17,6 +17,14 @@ import '../misc/obscure_query_params.dart'; /// A specialised [ImageProvider] dedicated to 'flutter_map_tile_caching' class FMTCImageProvider extends ImageProvider { + /// Create a specialised [ImageProvider] dedicated to 'flutter_map_tile_caching' + FMTCImageProvider({ + required this.storeName, + required this.provider, + required this.options, + required this.coords, + }); + /// The name of the store associated with this provider final String storeName; @@ -29,14 +37,6 @@ class FMTCImageProvider extends ImageProvider { /// The coordinates of the tile to be fetched final TileCoordinates coords; - /// Create a specialised [ImageProvider] dedicated to 'flutter_map_tile_caching' - FMTCImageProvider({ - required this.storeName, - required this.provider, - required this.options, - required this.coords, - }); - @override ImageStreamCompleter loadImage( FMTCImageProvider key, diff --git a/lib/src/providers/tile_provider.dart b/lib/src/providers/tile_provider.dart index 404199d5..a48d1a14 100644 --- a/lib/src/providers/tile_provider.dart +++ b/lib/src/providers/tile_provider.dart @@ -7,17 +7,6 @@ part of flutter_map_tile_caching; /// /// Create from the store directory chain, eg. [FMTCStore.getTileProvider]. class FMTCTileProvider extends TileProvider { - /// The store directory attached to this provider - final FMTCStore _store; - - /// The tile provider settings to use - final FMTCTileProviderSettings settings; - - /// [http.Client] (such as a [IOClient]) used to make all network requests - /// - /// Defaults to a standard [IOClient]/[HttpClient] for HTTP/1.1 servers. - final http.Client httpClient; - FMTCTileProvider._( this._store, { required FMTCTileProviderSettings? settings, @@ -34,6 +23,17 @@ class FMTCTileProvider extends TileProvider { }, ); + /// The store directory attached to this provider + final FMTCStore _store; + + /// The tile provider settings to use + final FMTCTileProviderSettings settings; + + /// [http.Client] (such as a [IOClient]) used to make all network requests + /// + /// Defaults to a standard [IOClient]/[HttpClient] for HTTP/1.1 servers. + final http.Client httpClient; + /// Closes the open [httpClient] - this will make the provider unable to /// perform network requests @override diff --git a/lib/src/providers/tile_provider_settings.dart b/lib/src/providers/tile_provider_settings.dart index fb17499a..05ab0b43 100644 --- a/lib/src/providers/tile_provider_settings.dart +++ b/lib/src/providers/tile_provider_settings.dart @@ -27,6 +27,32 @@ enum CacheBehavior { /// Settings for an [FMTCTileProvider] class FMTCTileProviderSettings { + /// Create new settings for an [FMTCTileProvider], and set the [instance] + /// + /// To access the existing settings, if any, get [instance]. + factory FMTCTileProviderSettings({ + CacheBehavior behavior = CacheBehavior.cacheFirst, + Duration cachedValidDuration = const Duration(days: 16), + int maxStoreLength = 0, + List obscuredQueryParams = const [], + FMTCBrowsingErrorHandler? errorHandler, + }) => + _instance = FMTCTileProviderSettings._( + behavior: behavior, + cachedValidDuration: cachedValidDuration, + maxStoreLength: maxStoreLength, + obscuredQueryParams: obscuredQueryParams.map((e) => RegExp('$e=[^&]*')), + errorHandler: errorHandler, + ); + + FMTCTileProviderSettings._({ + required this.behavior, + required this.cachedValidDuration, + required this.maxStoreLength, + required this.obscuredQueryParams, + required this.errorHandler, + }); + /// Get an existing instance, if one has been constructed, or get the default /// intial configuration static FMTCTileProviderSettings get instance => _instance; @@ -75,32 +101,6 @@ class FMTCTileProviderSettings { /// Even if this is defined, the error will still be (re)thrown. void Function(FMTCBrowsingError exception)? errorHandler; - /// Create new settings for an [FMTCTileProvider], and set the [instance] - /// - /// To access the existing settings, if any, get [instance]. - factory FMTCTileProviderSettings({ - CacheBehavior behavior = CacheBehavior.cacheFirst, - Duration cachedValidDuration = const Duration(days: 16), - int maxStoreLength = 0, - List obscuredQueryParams = const [], - FMTCBrowsingErrorHandler? errorHandler, - }) => - _instance = FMTCTileProviderSettings._( - behavior: behavior, - cachedValidDuration: cachedValidDuration, - maxStoreLength: maxStoreLength, - obscuredQueryParams: obscuredQueryParams.map((e) => RegExp('$e=[^&]*')), - errorHandler: errorHandler, - ); - - FMTCTileProviderSettings._({ - required this.behavior, - required this.cachedValidDuration, - required this.maxStoreLength, - required this.obscuredQueryParams, - required this.errorHandler, - }); - @override bool operator ==(Object other) => identical(this, other) || diff --git a/lib/src/regions/downloadable_region.dart b/lib/src/regions/downloadable_region.dart index 08c635da..f0e585e5 100644 --- a/lib/src/regions/downloadable_region.dart +++ b/lib/src/regions/downloadable_region.dart @@ -7,6 +7,22 @@ part of flutter_map_tile_caching; /// /// Construct via [BaseRegion.toDownloadable]. class DownloadableRegion { + DownloadableRegion._( + this.originalRegion, { + required this.minZoom, + required this.maxZoom, + required this.options, + required this.start, + required this.end, + required this.crs, + }) { + if (minZoom > maxZoom) { + throw ArgumentError( + '`minZoom` should be less than or equal to `maxZoom`', + ); + } + } + /// A copy of the [BaseRegion] used to form this object /// /// To make decisions based on the type of this region, prefer [when] over @@ -35,22 +51,6 @@ class DownloadableRegion { /// The map projection to use to calculate tiles. Defaults to [Epsg3857]. final Crs crs; - DownloadableRegion._( - this.originalRegion, { - required this.minZoom, - required this.maxZoom, - required this.options, - required this.start, - required this.end, - required this.crs, - }) { - if (minZoom > maxZoom) { - throw ArgumentError( - '`minZoom` should be less than or equal to `maxZoom`', - ); - } - } - /// Cast [originalRegion] from [R] to [N] @optionalTypeArgs DownloadableRegion _cast() => DownloadableRegion._( diff --git a/lib/src/regions/line.dart b/lib/src/regions/line.dart index d19cf492..7abafe11 100644 --- a/lib/src/regions/line.dart +++ b/lib/src/regions/line.dart @@ -52,22 +52,22 @@ class LineRegion extends BaseRegion { final anticlockwiseRotation = (bearing - 90) < 0 ? 360 + (bearing - 90) : (bearing - 90); - final topRight = dist.offset(cp, rad, clockwiseRotation); - final bottomRight = dist.offset(np, rad, clockwiseRotation); - final bottomLeft = dist.offset(np, rad, anticlockwiseRotation); - final topLeft = dist.offset(cp, rad, anticlockwiseRotation); + final tr = dist.offset(cp, rad, clockwiseRotation); // Top right + final br = dist.offset(np, rad, clockwiseRotation); // Bottom right + final bl = dist.offset(np, rad, anticlockwiseRotation); // Bottom left + final tl = dist.offset(cp, rad, anticlockwiseRotation); // Top left - if (overlap == 0) yield [topRight, bottomRight, bottomLeft, topLeft]; + if (overlap == 0) yield [tr, br, bl, tl]; final r = overlap == -1; final os = i == 0; final oe = i == line.length - 2; yield [ - os ? topRight : dist.offset(topRight, r ? rad : -rad, bearing), - oe ? bottomRight : dist.offset(bottomRight, r ? -rad : rad, bearing), - oe ? bottomLeft : dist.offset(bottomLeft, r ? -rad : rad, bearing), - os ? topLeft : dist.offset(topLeft, r ? rad : -rad, bearing), + if (os) tr else dist.offset(tr, r ? rad : -rad, bearing), + if (oe) br else dist.offset(br, r ? -rad : rad, bearing), + if (oe) bl else dist.offset(bl, r ? -rad : rad, bearing), + if (os) tl else dist.offset(tl, r ? rad : -rad, bearing), ]; } } diff --git a/lib/src/regions/recovered_region.dart b/lib/src/regions/recovered_region.dart index 292db84e..98e9c563 100644 --- a/lib/src/regions/recovered_region.dart +++ b/lib/src/regions/recovered_region.dart @@ -12,6 +12,21 @@ part of flutter_map_tile_caching; /// represented type of the recovered region. Use [toDownloadable] to restore a /// valid [DownloadableRegion]. class RecoveredRegion { + @internal + RecoveredRegion({ + required this.id, + required this.storeName, + required this.time, + required this.minZoom, + required this.maxZoom, + required this.start, + required this.end, + required this.bounds, + required this.center, + required this.line, + required this.radius, + }); + /// A unique ID created for every bulk download operation /// /// Not actually used when converting to [DownloadableRegion]. @@ -52,21 +67,6 @@ class RecoveredRegion { /// The radius of a circular region final double? radius; - @internal - RecoveredRegion({ - required this.id, - required this.storeName, - required this.time, - required this.minZoom, - required this.maxZoom, - required this.start, - required this.end, - required this.bounds, - required this.center, - required this.line, - required this.radius, - }); - /// Convert this region into a [BaseRegion] /// /// Determine which type of [BaseRegion] using [BaseRegion.when]. diff --git a/lib/src/root/recovery.dart b/lib/src/root/recovery.dart index 3a1228e0..8bff8b69 100644 --- a/lib/src/root/recovery.dart +++ b/lib/src/root/recovery.dart @@ -21,10 +21,10 @@ part of flutter_map_tile_caching; /// is also no longer in memory, and therefore stopped unexpectedly. /// {@endtemplate} class RootRecovery { - static RootRecovery? _instance; RootRecovery._() { _instance = this; } + static RootRecovery? _instance; /// Determines which downloads are known to be on-going, and therefore /// can be ignored when fetching [recoverableRegions] diff --git a/lib/src/root/statistics.dart b/lib/src/root/statistics.dart index 536e8207..739f2fa9 100644 --- a/lib/src/root/statistics.dart +++ b/lib/src/root/statistics.dart @@ -23,7 +23,7 @@ class RootStats { Stream watchRecovery({ bool triggerImmediately = false, }) async* { - final stream = await FMTCBackendAccess.internal.watchRecovery( + final stream = FMTCBackendAccess.internal.watchRecovery( triggerImmediately: triggerImmediately, ); yield* stream; @@ -34,7 +34,7 @@ class RootStats { List storeNames = const [], bool triggerImmediately = false, }) async* { - final stream = await FMTCBackendAccess.internal.watchStores( + final stream = FMTCBackendAccess.internal.watchStores( storeNames: storeNames, triggerImmediately: triggerImmediately, ); diff --git a/lib/src/store/statistics.dart b/lib/src/store/statistics.dart index 0ecad2a2..bb43b5d8 100644 --- a/lib/src/store/statistics.dart +++ b/lib/src/store/statistics.dart @@ -51,7 +51,7 @@ class StoreStats { Stream watchChanges({ bool triggerImmediately = false, }) async* { - final stream = await FMTCBackendAccess.internal.watchStores( + final stream = FMTCBackendAccess.internal.watchStores( storeNames: [_storeName], triggerImmediately: triggerImmediately, ); From 3061e2cfff2fa5affdc6a3e4fcbe6b6a620a0568 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sat, 24 Feb 2024 16:43:28 +0000 Subject: [PATCH 107/168] Added support for bulk downloading to ObjectBox backend Fixed bugs & reimplemented some removed methods in example app Former-commit-id: d047e599b30f73acc33627c2c459b8be6e202350 [formerly 645abac91f60ef6ac1e306bee481877413a6ea92] Former-commit-id: 8405c9e6563083fa0030c9faebb994f6bb4d5373 --- .../configure_download.dart | 2 +- .../screens/import_store/import_store.dart | 2 +- .../components/recovery_start_button.dart | 59 ++--- lib/src/backend/backend_access.dart | 19 ++ lib/src/backend/export_external.dart | 6 +- .../impls/objectbox/backend/backend.dart | 63 +++++ .../{backend.dart => backend/internal.dart} | 91 ++----- .../backend/internal_thread_safe.dart | 230 ++++++++++++++++++ .../internal_worker.dart} | 186 +++++--------- .../impls/objectbox/models/src/recovery.dart | 9 +- .../backend/interfaces/backend/backend.dart | 46 ++++ .../{backend.dart => backend/internal.dart} | 78 ++---- .../backend/internal_thread_safe.dart | 68 ++++++ lib/src/bulk_download/download_progress.dart | 3 +- lib/src/bulk_download/manager.dart | 2 +- lib/src/bulk_download/thread.dart | 17 +- lib/src/providers/image_provider.dart | 6 +- lib/src/store/download.dart | 2 +- tile_server/bin/tile_server.dart | 3 +- 19 files changed, 577 insertions(+), 315 deletions(-) create mode 100644 lib/src/backend/impls/objectbox/backend/backend.dart rename lib/src/backend/impls/objectbox/{backend.dart => backend/internal.dart} (84%) create mode 100644 lib/src/backend/impls/objectbox/backend/internal_thread_safe.dart rename lib/src/backend/impls/objectbox/{worker.dart => backend/internal_worker.dart} (83%) create mode 100644 lib/src/backend/interfaces/backend/backend.dart rename lib/src/backend/interfaces/{backend.dart => backend/internal.dart} (78%) create mode 100644 lib/src/backend/interfaces/backend/internal_thread_safe.dart diff --git a/example/lib/screens/configure_download/configure_download.dart b/example/lib/screens/configure_download/configure_download.dart index 624450f1..d085dbf4 100644 --- a/example/lib/screens/configure_download/configure_download.dart +++ b/example/lib/screens/configure_download/configure_download.dart @@ -67,7 +67,7 @@ class ConfigureDownloadPopup extends StatelessWidget { suffixText: 'max. tps', value: (provider) => provider.rateLimit, min: 1, - max: 500, + max: 50000, maxEligibleTilesPreview: 20, onChanged: (provider, value) => provider.rateLimit = value, diff --git a/example/lib/screens/import_store/import_store.dart b/example/lib/screens/import_store/import_store.dart index 1786c92a..aa8c2fa8 100644 --- a/example/lib/screens/import_store/import_store.dart +++ b/example/lib/screens/import_store/import_store.dart @@ -14,7 +14,7 @@ class ImportStorePopup extends StatefulWidget { class _ImportStorePopupState extends State { final Map importStores = {}; -// TODO: Implement + // TODO: Implement @override Widget build(BuildContext context) => Scaffold( appBar: AppBar( diff --git a/example/lib/screens/main/pages/recovery/components/recovery_start_button.dart b/example/lib/screens/main/pages/recovery/components/recovery_start_button.dart index 2c5317c9..df5c7f74 100644 --- a/example/lib/screens/main/pages/recovery/components/recovery_start_button.dart +++ b/example/lib/screens/main/pages/recovery/components/recovery_start_button.dart @@ -1,6 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; +import 'package:provider/provider.dart'; + +import '../../../../configure_download/configure_download.dart'; +import '../../region_selection/state/region_selection_provider.dart'; class RecoveryStartButton extends StatelessWidget { const RecoveryStartButton({ @@ -26,35 +30,32 @@ class RecoveryStartButton extends StatelessWidget { onPressed: !result.isFailed ? null : () async { - //TODO: Implement - /* final DownloaderProvider downloadProvider = - Provider.of( - context, - listen: false, - ) - ..region = region.toRegion( - rectangle: (r) => r, - circle: (c) => c, - line: (l) => l, - ) - ..minZoom = region.minZoom - ..maxZoom = region.maxZoom - ..setSelectedStore( - FMTC.instance(region.storeName), - ) - ..regionTiles = tiles.data; - - await Navigator.of(context).push( - MaterialPageRoute( - builder: (BuildContext context) => - DownloadRegionPopup( - region: downloadProvider.region!, - ), - fullscreenDialog: true, - ), - ); - - moveToDownloadPage();*/ + final regionSelectionProvider = + Provider.of( + context, + listen: false, + ) + ..region = result.region.toRegion() + ..minZoom = result.region.minZoom + ..maxZoom = result.region.maxZoom + ..setSelectedStore( + FMTCStore(result.region.storeName), + ); + // TODO: Check + //..regionTiles = tiles.data; + + await Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => ConfigureDownloadPopup( + region: regionSelectionProvider.region!, + minZoom: result.region.minZoom, + maxZoom: result.region.maxZoom, + ), + fullscreenDialog: true, + ), + ); + + moveToDownloadPage(); }, ) : const Padding( diff --git a/lib/src/backend/backend_access.dart b/lib/src/backend/backend_access.dart index eb343032..e3ab2696 100644 --- a/lib/src/backend/backend_access.dart +++ b/lib/src/backend/backend_access.dart @@ -28,3 +28,22 @@ abstract mixin class FMTCBackendAccess { _internal = newInternal; } } + +@meta.internal +abstract mixin class FMTCBackendAccessThreadSafe { + static FMTCBackendInternalThreadSafe? _internal; + + @meta.internal + @meta.experimental + static FMTCBackendInternalThreadSafe get internal => + _internal ?? (throw RootUnavailable()); + + @meta.internal + @meta.protected + static set internal(FMTCBackendInternalThreadSafe? newInternal) { + if (newInternal != null && _internal != null) { + throw RootAlreadyInitialised(); + } + _internal = newInternal; + } +} diff --git a/lib/src/backend/export_external.dart b/lib/src/backend/export_external.dart index 66db6d46..47f063f3 100644 --- a/lib/src/backend/export_external.dart +++ b/lib/src/backend/export_external.dart @@ -1,6 +1,8 @@ // Copyright © Luka S (JaffaKetchup) under GPL-v3 // A full license can be found at .\LICENSE -export 'impls/objectbox/backend.dart'; -export 'interfaces/backend.dart'; +export 'impls/objectbox/backend/backend.dart'; +export 'interfaces/backend/backend.dart'; +export 'interfaces/backend/internal.dart'; +export 'interfaces/backend/internal_thread_safe.dart'; export 'utils/errors.dart'; diff --git a/lib/src/backend/impls/objectbox/backend/backend.dart b/lib/src/backend/impls/objectbox/backend/backend.dart new file mode 100644 index 00000000..1695de76 --- /dev/null +++ b/lib/src/backend/impls/objectbox/backend/backend.dart @@ -0,0 +1,63 @@ +// Copyright © Luka S (JaffaKetchup) under GPL-v3 +// A full license can be found at .\LICENSE + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:isolate'; + +import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; +import 'package:path/path.dart' as path; +import 'package:path_provider/path_provider.dart'; + +import '../../../../../flutter_map_tile_caching.dart'; +import '../../../export_internal.dart'; +import '../models/generated/objectbox.g.dart'; +import '../models/src/recovery.dart'; +import '../models/src/store.dart'; +import '../models/src/tile.dart'; + +part 'internal_thread_safe.dart'; +part 'internal.dart'; +part 'internal_worker.dart'; + +/// Implementation of [FMTCBackend] that uses ObjectBox as the storage database +final class FMTCObjectBoxBackend implements FMTCBackend { + /// {@macro fmtc.backend.initialise} + /// + /// [maxDatabaseSize] is the maximum size the database file can grow + /// to, in KB. Exceeding it throws [DbFullException]. Defaults to 10 GB. + /// + /// [macosApplicationGroup] should be set when creating a sandboxed macOS app, + /// specify the application group (of less than 20 chars). See + /// [the ObjectBox docs](https://docs.objectbox.io/getting-started) for + /// details. + @override + Future initialise({ + String? rootDirectory, + int maxDatabaseSize = 10000000, + String? macosApplicationGroup, + }) => + FMTCObjectBoxBackendInternal._instance.initialise( + rootDirectory: rootDirectory, + maxDatabaseSize: maxDatabaseSize, + macosApplicationGroup: macosApplicationGroup, + ); + + /// {@macro fmtc.backend.uninitialise} + /// + /// If [immediate] is `true`, any operations currently underway will be lost, + /// as the worker will be killed as quickly as possible (not necessarily + /// instantly). + /// If `false`, all operations currently underway will be allowed to complete, + /// but any operations started after this method call will be lost. A lost + /// operation may throw [RootUnavailable]. + @override + Future uninitialise({ + bool deleteRoot = false, + bool immediate = false, + }) => + FMTCObjectBoxBackendInternal._instance + .uninitialise(deleteRoot: deleteRoot, immediate: immediate); +} diff --git a/lib/src/backend/impls/objectbox/backend.dart b/lib/src/backend/impls/objectbox/backend/internal.dart similarity index 84% rename from lib/src/backend/impls/objectbox/backend.dart rename to lib/src/backend/impls/objectbox/backend/internal.dart index 39d88c59..f28f3887 100644 --- a/lib/src/backend/impls/objectbox/backend.dart +++ b/lib/src/backend/impls/objectbox/backend/internal.dart @@ -1,64 +1,7 @@ // Copyright © Luka S (JaffaKetchup) under GPL-v3 // A full license can be found at .\LICENSE -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; -import 'dart:isolate'; - -import 'package:collection/collection.dart'; -import 'package:flutter/foundation.dart'; -import 'package:path/path.dart' as path; -import 'package:path_provider/path_provider.dart'; - -import '../../../../flutter_map_tile_caching.dart'; -import '../../export_internal.dart'; -import 'models/generated/objectbox.g.dart'; -import 'models/src/recovery.dart'; -import 'models/src/store.dart'; -import 'models/src/tile.dart'; - -part 'worker.dart'; - -/// Implementation of [FMTCBackend] that uses ObjectBox as the storage database -final class FMTCObjectBoxBackend implements FMTCBackend { - /// {@macro fmtc.backend.inititialise} - /// - /// [maxDatabaseSize] is the maximum size the database file can grow - /// to, in KB. Exceeding it throws [DbFullException]. Defaults to 10 GB. - /// - /// [macosApplicationGroup] should be set when creating a sandboxed macOS app, - /// specify the application group (of less than 20 chars). See - /// [the ObjectBox docs](https://docs.objectbox.io/getting-started) for - /// details. - @override - Future initialise({ - String? rootDirectory, - int maxDatabaseSize = 10000000, - String? macosApplicationGroup, - }) => - FMTCObjectBoxBackendInternal._instance.initialise( - rootDirectory: rootDirectory, - maxDatabaseSize: maxDatabaseSize, - macosApplicationGroup: macosApplicationGroup, - ); - - /// {@macro fmtc.backend.uninitialise} - /// - /// If [immediate] is `true`, any operations currently underway will be lost, - /// as the worker will be killed as quickly as possible (not necessarily - /// instantly). - /// If `false`, all operations currently underway will be allowed to complete, - /// but any operations started after this method call will be lost. A lost - /// operation may throw [RootUnavailable]. - @override - Future uninitialise({ - bool deleteRoot = false, - bool immediate = false, - }) => - FMTCObjectBoxBackendInternal._instance - .uninitialise(deleteRoot: deleteRoot, immediate: immediate); -} +part of 'backend.dart'; /// Internal implementation of [FMTCBackend] that uses ObjectBox as the storage /// database @@ -164,13 +107,13 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { ).create(recursive: true); // Prepare to recieve `SendPort` from worker + late final ByteData storeReference; _workerResOneShot[0] = Completer(); - unawaited( - _workerResOneShot[0]!.future.then((res) { - _sendPort = res!['sendPort']; - _workerResOneShot.remove(0); - }), - ); + final workerInitialRes = _workerResOneShot[0]!.future.then((res) { + storeReference = res!['storeReference']; + _sendPort = res['sendPort']; + _workerResOneShot.remove(0); + }); // Setup worker comms/response handler final receivePort = ReceivePort(); @@ -223,7 +166,12 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { debugName: '[FMTC] ObjectBox Backend Worker', ); + await workerInitialRes; + FMTCBackendAccess.internal = this; + FMTCBackendAccessThreadSafe.internal = _ObjectBoxBackendThreadSafeImpl._( + storeReference: storeReference, + ); } Future uninitialise({ @@ -267,6 +215,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { //_rotalDebouncer.cancel(); FMTCBackendAccess.internal = null; + FMTCBackendAccessThreadSafe.internal = null; } @override @@ -352,10 +301,11 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { @override Future readTile({ required String url, + String? storeName, }) async => (await _sendCmdOneShot( type: _WorkerCmdType.readTile, - args: {'url': url}, + args: {'url': url, 'storeName': storeName}, ))!['tile']; @override @@ -378,17 +328,6 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { args: {'storeName': storeName, 'url': url, 'bytes': bytes}, ); - @override - Future writeTilesDirect({ - required String storeName, - required List urls, - required List bytess, - }) => - _sendCmdOneShot( - type: _WorkerCmdType.writeTilesDirect, - args: {'storeName': storeName, 'urls': urls, 'bytess': bytess}, - ); - @override Future deleteTile({ required String storeName, diff --git a/lib/src/backend/impls/objectbox/backend/internal_thread_safe.dart b/lib/src/backend/impls/objectbox/backend/internal_thread_safe.dart new file mode 100644 index 00000000..7720d047 --- /dev/null +++ b/lib/src/backend/impls/objectbox/backend/internal_thread_safe.dart @@ -0,0 +1,230 @@ +// Copyright © Luka S (JaffaKetchup) under GPL-v3 +// A full license can be found at .\LICENSE + +part of 'backend.dart'; + +class _ObjectBoxBackendThreadSafeImpl implements FMTCBackendInternalThreadSafe { + _ObjectBoxBackendThreadSafeImpl._({ + required this.storeReference, + }); + + final ByteData storeReference; + Store? root; + void get expectInitialised => root ?? (throw RootUnavailable()); + + @override + String get friendlyIdentifier => 'ObjectBox'; + + @override + void initialise() { + if (root != null) throw RootAlreadyInitialised(); + root = Store.fromReference(getObjectBoxModel(), storeReference); + } + + @override + void uninitialise() { + if (root == null) throw RootUnavailable(); + root!.close(); + root = null; + } + + @override + Future readTile({ + required String url, + String? storeName, + }) async { + expectInitialised; + + final stores = root!.box(); + + final query = storeName == null + ? stores.query(ObjectBoxTile_.url.equals(url)).build() + : (stores.query(ObjectBoxTile_.url.equals(url)) + ..linkMany( + ObjectBoxTile_.stores, + ObjectBoxStore_.name.equals(storeName), + )) + .build(); + + final tile = query.findUnique(); + + query.close(); + + return tile; + } + + @override + void htWriteTile({ + required String storeName, + required String url, + required Uint8List bytes, + }) { + expectInitialised; + + final tiles = root!.box(); + final stores = root!.box(); + + final tilesQuery = tiles.query(ObjectBoxTile_.url.equals(url)).build(); + final storeQuery = + stores.query(ObjectBoxStore_.name.equals(storeName)).build(); + + final storeAdjustments = {}; + + root!.runInTransaction( + TxMode.write, + () { + final store = storeQuery.findUnique()!; + + final existingTile = + (tilesQuery..param(ObjectBoxTile_.url).value = url).findUnique(); + + storeAdjustments[storeName] = storeAdjustments[storeName] == null + ? (deltaLength: 1, deltaSize: bytes.lengthInBytes) + : ( + deltaLength: storeAdjustments[storeName]!.deltaLength + 1, + deltaSize: storeAdjustments[storeName]!.deltaSize + + bytes.lengthInBytes, + ); + + if (existingTile != null) { + for (final store in existingTile.stores) { + storeAdjustments[store.name] = storeAdjustments[store.name] == null + ? ( + deltaLength: 0, + deltaSize: + -existingTile.bytes.lengthInBytes + bytes.lengthInBytes, + ) + : ( + deltaLength: storeAdjustments[store.name]!.deltaLength, + deltaSize: storeAdjustments[store.name]!.deltaSize + + (-existingTile.bytes.lengthInBytes + + bytes.lengthInBytes), + ); + } + } + + tiles.put( + ObjectBoxTile( + url: url, + lastModified: DateTime.timestamp(), + bytes: bytes, + )..stores.addAll( + { + store, + if (existingTile != null) ...existingTile.stores, + }, + ), + ); + + stores.putMany( + storeAdjustments.entries + .map( + (e) => store + ..length += e.value.deltaLength + ..size += e.value.deltaSize, + ) + .toList(), + ); + }, + ); + + tilesQuery.close(); + storeQuery.close(); + } + + @override + void htWriteTiles({ + required String storeName, + required List urls, + required List bytess, + }) { + expectInitialised; + + final tiles = root!.box(); + final stores = root!.box(); + + final tilesQuery = tiles.query(ObjectBoxTile_.url.equals('')).build(); + final storeQuery = stores.query(ObjectBoxStore_.name.equals('')).build(); + + final storeAdjustments = {}; + + root!.runInTransaction( + TxMode.write, + () { + final store = (storeQuery + ..param(ObjectBoxStore_.name).value = storeName) + .findUnique()!; + + tiles.putMany( + List.generate( + urls.length, + (i) { + final existingTile = (tilesQuery + ..param(ObjectBoxTile_.url).value = urls[i]) + .findUnique(); + + storeAdjustments[storeName] = storeAdjustments[storeName] == null + ? (deltaLength: 1, deltaSize: bytess[i].lengthInBytes) + : ( + deltaLength: storeAdjustments[storeName]!.deltaLength + 1, + deltaSize: storeAdjustments[storeName]!.deltaSize + + bytess[i].lengthInBytes, + ); + + if (existingTile != null) { + for (final store in existingTile.stores) { + storeAdjustments[store.name] = + storeAdjustments[store.name] == null + ? ( + deltaLength: 0, + deltaSize: -existingTile.bytes.lengthInBytes + + bytess[i].lengthInBytes, + ) + : ( + deltaLength: + storeAdjustments[store.name]!.deltaLength, + deltaSize: + storeAdjustments[store.name]!.deltaSize + + (-existingTile.bytes.lengthInBytes + + bytess[i].lengthInBytes), + ); + } + } + + return ObjectBoxTile( + url: urls[i], + lastModified: DateTime.timestamp(), + bytes: bytess[i], + )..stores.addAll( + { + store, + if (existingTile != null) ...existingTile.stores, + }, + ); + }, + growable: false, + ), + ); + + assert( + storeAdjustments.isNotEmpty, + '`storeAdjustments` should not be empty if relations are being set correctly', + ); + + stores.putMany( + storeAdjustments.entries + .map( + (e) => (storeQuery..param(ObjectBoxStore_.name).value = e.key) + .findUnique()! + ..length += e.value.deltaLength + ..size += e.value.deltaSize, + ) + .toList(), + ); + }, + ); + + tilesQuery.close(); + storeQuery.close(); + } +} diff --git a/lib/src/backend/impls/objectbox/worker.dart b/lib/src/backend/impls/objectbox/backend/internal_worker.dart similarity index 83% rename from lib/src/backend/impls/objectbox/worker.dart rename to lib/src/backend/impls/objectbox/backend/internal_worker.dart index ec03073e..81a30bc8 100644 --- a/lib/src/backend/impls/objectbox/worker.dart +++ b/lib/src/backend/impls/objectbox/backend/internal_worker.dart @@ -19,7 +19,6 @@ enum _WorkerCmdType { readTile, readLatestTile, writeTile, - writeTilesDirect, deleteTile, registerHitOrMiss, removeOldestTilesAboveLimit, @@ -71,7 +70,7 @@ Future _worker( // Respond with comms channel for future cmds sendRes( id: 0, - data: {'sendPort': receivePort.sendPort}, + data: {'sendPort': receivePort.sendPort, 'storeReference': root.reference}, ); // Create memory for streamed output subscription storage @@ -100,37 +99,38 @@ Future _worker( /// /// Returns whether each tile was actually deleted (whether it was an orphan), /// in iteration order of [tilesQuery.find]. - Iterable deleteTiles({ + List deleteTiles({ required String storeName, required Query tilesQuery, }) { final stores = root.box(); final tiles = root.box(); - return tilesQuery.find().map((tile) { - // For the correct store, adjust the statistics - for (final store in tile.stores) { - if (store.name != storeName) continue; - stores.put( - store - ..length -= 1 - ..size -= tile.bytes.lengthInBytes, - mode: PutMode.update, - ); - break; - } + return tilesQuery.find().map( + (tile) { + // For the correct store, adjust the statistics + for (final store in tile.stores) { + if (store.name != storeName) continue; + stores.put( + store + ..length -= 1 + ..size -= tile.bytes.lengthInBytes, + mode: PutMode.update, + ); + break; + } - // Remove the store relation from the tile - tile.stores.removeWhere((store) => store.name == storeName); + // Remove the store relation from the tile + tile.stores.removeWhere((store) => store.name == storeName); - // Delete the tile if it belongs to no stores - if (tile.stores.isEmpty) return tiles.remove(tile.id); + // Delete the tile if it belongs to no stores + if (tile.stores.isEmpty) return tiles.remove(tile.id); - // Otherwise just update the tile - // TODO: Check this works - tile.stores.applyToDb(mode: PutMode.update); - return false; - }); + // Otherwise just update the tile + tile.stores.applyToDb(mode: PutMode.update); + return false; + }, + ).toList(); } //! MAIN LOOP !// @@ -311,18 +311,26 @@ Future _worker( sendRes(id: cmd.id); case _WorkerCmdType.deleteStore: - root - .box() - .query( - ObjectBoxStore_.name.equals(cmd.args['storeName']! as String), - ) - .build() + final storeName = cmd.args['storeName']! as String; + + final stores = root.box(); + + final tilesQuery = (root.box().query() + ..linkMany( + ObjectBoxTile_.stores, + ObjectBoxStore_.name.equals(storeName), + )) + .build(); + + deleteTiles(storeName: storeName, tilesQuery: tilesQuery); + + stores.query(ObjectBoxStore_.name.equals(storeName)).build() ..remove() ..close(); - // TODO: Check tiles relations - sendRes(id: cmd.id); + + tilesQuery.close(); case _WorkerCmdType.tileExistsInStore: final storeName = cmd.args['storeName']! as String; final url = cmd.args['url']! as String; @@ -340,11 +348,18 @@ Future _worker( query.close(); case _WorkerCmdType.readTile: final url = cmd.args['url']! as String; + final storeName = cmd.args['storeName'] as String?; - final query = root - .box() - .query(ObjectBoxTile_.url.equals(url)) - .build(); + final stores = root.box(); + + final query = storeName == null + ? stores.query(ObjectBoxTile_.url.equals(url)).build() + : (stores.query(ObjectBoxTile_.url.equals(url)) + ..linkMany( + ObjectBoxTile_.stores, + ObjectBoxStore_.name.equals(storeName), + )) + .build(); sendRes(id: cmd.id, data: {'tile': query.findUnique()}); @@ -366,6 +381,7 @@ Future _worker( query.close(); case _WorkerCmdType.writeTile: + // TODO: Test all final storeName = cmd.args['storeName']! as String; final url = cmd.args['url']! as String; final bytes = cmd.args['bytes'] as Uint8List?; @@ -438,96 +454,6 @@ Future _worker( ); sendRes(id: cmd.id); - case _WorkerCmdType.writeTilesDirect: - final storeName = cmd.args['storeName']! as String; - final urls = cmd.args['urls']! as List; - final bytess = cmd.args['bytess']! as List; - - final tiles = root.box(); - final stores = root.box(); - - final tilesQuery = tiles.query(ObjectBoxTile_.url.equals('')).build(); - final storeQuery = - stores.query(ObjectBoxStore_.name.equals('')).build(); - - final storeAdjustments = - {}; - - root.runInTransaction( - TxMode.write, - () { - tiles.putMany( - List.generate( - urls.length, - (i) { - final existingTile = (tilesQuery - ..param(ObjectBoxTile_.url).value = urls[i]) - .findUnique(); - - if (existingTile == null) { - storeAdjustments[storeName] = - storeAdjustments[storeName] == null - ? ( - deltaLength: 1, - deltaSize: bytess[i].lengthInBytes - ) - : ( - deltaLength: - storeAdjustments[storeName]!.deltaLength + - 1, - deltaSize: - storeAdjustments[storeName]!.deltaSize + - bytess[i].lengthInBytes, - ); - } else { - for (final store in existingTile.stores) { - storeAdjustments[store.name] = - storeAdjustments[store.name] == null - ? ( - deltaLength: 0, - deltaSize: - -existingTile.bytes.lengthInBytes + - bytess[i].lengthInBytes, - ) - : ( - deltaLength: storeAdjustments[store.name]! - .deltaLength, - deltaSize: storeAdjustments[store.name]! - .deltaSize + - (-existingTile.bytes.lengthInBytes + - bytess[i].lengthInBytes), - ); - } - } - - return ObjectBoxTile( - url: urls[i], - lastModified: DateTime.timestamp(), - bytes: bytess[i], - ); - }, - growable: false, - ), - ); - - stores.putMany( - storeAdjustments.entries - .map( - (e) => (storeQuery - ..param(ObjectBoxStore_.name).value = e.key) - .findUnique()! - ..length += e.value.deltaLength - ..size += e.value.deltaSize, - ) - .toList(), - ); - }, - ); - - sendRes(id: cmd.id); - - tilesQuery.close(); - storeQuery.close(); case _WorkerCmdType.deleteTile: final storeName = cmd.args['storeName']! as String; final url = cmd.args['url']! as String; @@ -823,14 +749,14 @@ Future _worker( case _WorkerCmdType.cancelRecovery: final id = cmd.args['id']! as int; - final query = root + root .box() .query(ObjectBoxRecovery_.refId.equals(id)) - .build(); + .build() + ..remove() + ..close(); sendRes(id: cmd.id); - - query.close(); case _WorkerCmdType.watchRecovery: final triggerImmediately = cmd.args['triggerImmediately']! as bool; diff --git a/lib/src/backend/impls/objectbox/models/src/recovery.dart b/lib/src/backend/impls/objectbox/models/src/recovery.dart index 12527f77..3805f1b4 100644 --- a/lib/src/backend/impls/objectbox/models/src/recovery.dart +++ b/lib/src/backend/impls/objectbox/models/src/recovery.dart @@ -155,12 +155,17 @@ base class ObjectBoxRecovery { ) : null, center: typeId == 1 ? LatLng(circleCenterLat!, circleCenterLng!) : null, - line: typeId == 2 || typeId == 3 + line: typeId == 2 ? List.generate( lineLats!.length, (i) => LatLng(lineLats![i], lineLngs![i]), ) - : null, + : typeId == 3 + ? List.generate( + customPolygonLats!.length, + (i) => LatLng(customPolygonLats![i], customPolygonLngs![i]), + ) + : null, radius: typeId == 1 ? circleRadius! : typeId == 2 diff --git a/lib/src/backend/interfaces/backend/backend.dart b/lib/src/backend/interfaces/backend/backend.dart new file mode 100644 index 00000000..813dad78 --- /dev/null +++ b/lib/src/backend/interfaces/backend/backend.dart @@ -0,0 +1,46 @@ +// Copyright © Luka S (JaffaKetchup) under GPL-v3 +// A full license can be found at .\LICENSE + +import 'dart:async'; + +import '../../export_internal.dart'; + +/// {@template fmtc.backend.backend} +/// An abstract interface that FMTC will use to communicate with a storage +/// 'backend' (usually one root) +/// +/// See also [FMTCBackendInternal] and [FMTCBackendInternalThreadSafe], which +/// have the actual method signatures. This is provided as a public means to +/// initialise and uninitialise the backend. +/// +/// When creating a custom implementation, follow the same pattern as the +/// built-in ObjectBox backend ([FMTCObjectBoxBackend]). +/// {@endtemplate} +abstract interface class FMTCBackend { + /// {@macro fmtc.backend.backend} + /// + /// This constructor does not initialise this backend, also invoke + /// [initialise]. + const FMTCBackend(); + + /// {@template fmtc.backend.initialise} + /// Initialise this backend, and create the root + /// + /// Prefer to leave [rootDirectory] as null, which will use + /// `getApplicationDocumentsDirectory()`. Alternatively, pass a custom + /// directory - it is recommended to not use a typical cache directory, as the + /// OS can clear these without notice at any time. + /// {@endtemplate} + Future initialise({ + String? rootDirectory, + }); + + /// {@template fmtc.backend.uninitialise} + /// Uninitialise this backend, and release whatever resources it is consuming + /// + /// If [deleteRoot] is `true`, then the root will be permanently deleted. + /// {@endtemplate} + Future uninitialise({ + bool deleteRoot = false, + }); +} diff --git a/lib/src/backend/interfaces/backend.dart b/lib/src/backend/interfaces/backend/internal.dart similarity index 78% rename from lib/src/backend/interfaces/backend.dart rename to lib/src/backend/interfaces/backend/internal.dart index 38cc0e4d..1b542e31 100644 --- a/lib/src/backend/interfaces/backend.dart +++ b/lib/src/backend/interfaces/backend/internal.dart @@ -5,64 +5,27 @@ import 'dart:async'; import 'dart:io'; import 'dart:typed_data'; -import '../../../flutter_map_tile_caching.dart'; -import '../export_internal.dart'; +import '../../../../flutter_map_tile_caching.dart'; +import '../../export_internal.dart'; -/// {@template fmtc.backend.backend} /// An abstract interface that FMTC will use to communicate with a storage -/// 'backend' (usually one root) +/// 'backend' (usually one root), from a 'normal' thread (likely the UI thread) /// -/// See also [FMTCBackendInternal], which has the actual method signatures. This -/// is provided as a means to warn users to avoid using the backend directly. +/// Should implement methods that operate in another isolate/thread to avoid +/// blocking the normal thread. In this case, [FMTCBackendInternalThreadSafe] +/// must also be implemented, which should not operate in another thread, must be +/// sendable between isolates (because it will already be operated in another +/// thread), and must be suitable for simultaneous initialisation across multiple +/// threads. /// -/// To implementers: -/// * Provide a seperate [FMTCBackend] & [FMTCBackendInternal] implementation -/// (both public scope), and a private scope `FMTCBackendImpl` -/// * Always make [FMTCBackendInternal] a singleton 'cover-up' for -/// `FMTCBackendImpl`, without a constructor -/// * Prefer throwing included implementation-generic errors/exceptions -/// * Ensure the [FMTCBackendInternal]/impl can be sent through isolates -/// * Always set the [FMTCBackendAccess.internal] property as necessary -/// -/// See the default [FMTCObjectBoxBackend] implementation for an example. -/// {@endtemplate} -abstract interface class FMTCBackend { - /// {@macro fmtc.backend.backend} - /// - /// This constructor does not initialise this backend, also invoke - /// [initialise]. - const FMTCBackend(); - - /// {@template fmtc.backend.inititialise} - /// Initialise this backend, and create the root - /// - /// Prefer to leave [rootDirectory] as null, which will use - /// `getApplicationDocumentsDirectory()`. Alternatively, pass a custom - /// directory - it is recommended to not use a typical cache directory, as the - /// OS can clear these without notice at any time. - /// {@endtemplate} - Future initialise({ - String? rootDirectory, - }); - - /// {@template fmtc.backend.uninitialise} - /// Uninitialise this backend, and release whatever resources it is consuming - /// - /// If [deleteRoot] is `true`, then the root will be permanently deleted. - /// {@endtemplate} - Future uninitialise({ - bool deleteRoot = false, - }); -} - -/// An abstract interface that FMTC will use to communicate with a storage -/// 'backend' (usually one root) +/// Should be set in [FMTCBackendAccess] when ready to use, and unset when not. /// /// Methods with a doc template in the doc string are for 'direct' public /// invocation. /// /// See [FMTCBackend] for more information. -abstract interface class FMTCBackendInternal with FMTCBackendAccess { +abstract interface class FMTCBackendInternal + with FMTCBackendAccess, FMTCBackendAccessThreadSafe { const FMTCBackendInternal._(); /// Generic description/name of this backend @@ -151,8 +114,12 @@ abstract interface class FMTCBackendInternal with FMTCBackendAccess { }); /// Retrieve a raw tile by the specified URL + /// + /// If [storeName] is specified, the tile will be limited to the specified + /// store - if it exists in another store, it will not be returned. Future readTile({ required String url, + String? storeName, }); /// {@template fmtc.backend.readLatestTile} @@ -178,19 +145,6 @@ abstract interface class FMTCBackendInternal with FMTCBackendAccess { required Uint8List? bytes, }); - /// Create multiple tiles (given given their respective [urls] and [bytess]) in - /// the specified store - /// - /// Logic is much simpler than [writeTile] and designed to be faster to allow - /// for high bulk downloading throughputs. - /// - /// Existing tiles will always be overwritten if they exist. - Future writeTilesDirect({ - required String storeName, - required List urls, - required List bytess, - }); - /// Remove the tile from the specified store, deleting it if was orphaned /// /// As tiles can belong to multiple stores, a tile cannot be safely 'truly' diff --git a/lib/src/backend/interfaces/backend/internal_thread_safe.dart b/lib/src/backend/interfaces/backend/internal_thread_safe.dart new file mode 100644 index 00000000..646e59be --- /dev/null +++ b/lib/src/backend/interfaces/backend/internal_thread_safe.dart @@ -0,0 +1,68 @@ +// Copyright © Luka S (JaffaKetchup) under GPL-v3 +// A full license can be found at .\LICENSE + +import 'dart:async'; +import 'dart:typed_data'; + +import '../../export_internal.dart'; + +/// An abstract interface that FMTC will use to communicate with a storage +/// 'backend' (usually one root), from an existing bulk downloading thread +/// +/// Should implement methods that operate in the same thread. Must be sendable +/// between isolates, because it will be operated in another thread. Must be +/// suitable for simultaneous [initialise]ation across multiple threads. +/// +/// Should be set-up ready for intialisation, and set in the +/// [FMTCBackendAccessThreadSafe], from the initialisation of +/// [FMTCBackendInternal]. +/// +/// Methods with a doc template in the doc string are for 'direct' public +/// invocation. +/// +/// See [FMTCBackend] for more information. +abstract interface class FMTCBackendInternalThreadSafe + with FMTCBackendAccessThreadSafe { + const FMTCBackendInternalThreadSafe._(); + + /// Generic description/name of this backend + abstract final String friendlyIdentifier; + + /// Start this thread safe database operator + FutureOr initialise(); + + /// Stop this thread safe database operator + FutureOr uninitialise(); + + /// Retrieve a raw tile by the specified URL + /// + /// If [storeName] is specified, the tile will be limited to the specified + /// store - if it exists in another store, it will not be returned. + FutureOr readTile({ + required String url, + String? storeName, + }); + + /// Create or update a tile (given a [url] and its [bytes]) in the specified + /// store + /// + /// Logic is simpler than the respective [FMTCBackendInternal.writeTile] + /// method, and designed for high throughput: existing tiles will always be + /// overwritten (if they exist). + FutureOr htWriteTile({ + required String storeName, + required String url, + required Uint8List bytes, + }); + + /// Create or update multiple tiles (given given their respective [urls] and + /// [bytess]) in the specified store + /// + /// Designed for high throughput: existing tiles will always be overwritten + /// (if they exist). + FutureOr htWriteTiles({ + required String storeName, + required List urls, + required List bytess, + }); +} diff --git a/lib/src/bulk_download/download_progress.dart b/lib/src/bulk_download/download_progress.dart index 7e690ef5..a42e617c 100644 --- a/lib/src/bulk_download/download_progress.dart +++ b/lib/src/bulk_download/download_progress.dart @@ -138,8 +138,7 @@ class DownloadProgress { /// statistics. /// /// Prefer using this over checking any other statistics for completion. If all - /// threads have unexpectedly quit due to an error (for example, the store - /// becomes full to [FMTCSettings.databaseMaxSize]), the other statistics will + /// threads have unexpectedly quit due to an error, the other statistics will /// not indicate the the download has stopped/finished/completed, but this will /// be `true`. final bool isComplete; diff --git a/lib/src/bulk_download/manager.dart b/lib/src/bulk_download/manager.dart index ac35d4af..86709f5c 100644 --- a/lib/src/bulk_download/manager.dart +++ b/lib/src/bulk_download/manager.dart @@ -16,7 +16,7 @@ Future _downloadManager( Duration? maxReportInterval, int? rateLimit, Iterable obscuredQueryParams, - FMTCBackendInternal backend, + FMTCBackendInternalThreadSafe backend, }) input, ) async { // Precalculate shared inputs for all threads diff --git a/lib/src/bulk_download/thread.dart b/lib/src/bulk_download/thread.dart index 0fb066e6..79203c42 100644 --- a/lib/src/bulk_download/thread.dart +++ b/lib/src/bulk_download/thread.dart @@ -14,7 +14,7 @@ Future _singleDownloadThread( Uint8List? seaTileBytes, Iterable obscuredQueryParams, Map headers, - FMTCBackendInternal backend, + FMTCBackendInternalThreadSafe backend, }) input, ) async { // Setup two-way communications @@ -32,6 +32,8 @@ Future _singleDownloadThread( final tileUrlsBuffer = []; final tileBytesBuffer = []; + await input.backend.initialise(); + while (true) { // Request new tile coords send(0); @@ -45,13 +47,15 @@ Future _singleDownloadThread( httpClient.close(); if (tileUrlsBuffer.isNotEmpty) { - await input.backend.writeTilesDirect( + await input.backend.htWriteTiles( storeName: input.storeName, urls: tileUrlsBuffer, bytess: tileBytesBuffer, ); } + await input.backend.uninitialise(); + Isolate.exit(); } @@ -66,7 +70,10 @@ Future _singleDownloadThread( url: networkUrl, obscuredQueryParams: input.obscuredQueryParams, ); - final existingTile = await input.backend.readTile(url: matcherUrl); + final existingTile = await input.backend.readTile( + url: matcherUrl, + storeName: input.storeName, // TODO: Test + ); // Skip if tile already exists and user demands existing tile pruning if (input.skipExistingTiles && existingTile != null) { @@ -129,7 +136,7 @@ Future _singleDownloadThread( // Write tile directly to database or place in buffer queue //final tile = DbTile(url: matcherUrl, bytes: response.bodyBytes); if (input.maxBufferLength == 0) { - await input.backend.writeTile( + await input.backend.htWriteTile( storeName: input.storeName, url: matcherUrl, bytes: response.bodyBytes, @@ -142,7 +149,7 @@ Future _singleDownloadThread( // Write buffer to database if necessary final wasBufferReset = tileUrlsBuffer.length >= input.maxBufferLength; if (wasBufferReset) { - await input.backend.writeTilesDirect( + await input.backend.htWriteTiles( storeName: input.storeName, urls: tileUrlsBuffer, bytess: tileBytesBuffer, diff --git a/lib/src/providers/image_provider.dart b/lib/src/providers/image_provider.dart index 1914a3d5..eab72c9d 100644 --- a/lib/src/providers/image_provider.dart +++ b/lib/src/providers/image_provider.dart @@ -91,8 +91,10 @@ class FMTCImageProvider extends ImageProvider { obscuredQueryParams: provider.settings.obscuredQueryParams, ); - final existingTile = - await FMTCBackendAccess.internal.readTile(url: matcherUrl); + final existingTile = await FMTCBackendAccess.internal.readTile( + url: matcherUrl, + storeName: storeName, + ); final needsCreating = existingTile == null; final needsUpdating = !needsCreating && diff --git a/lib/src/store/download.dart b/lib/src/store/download.dart index 1b207a27..f0fce636 100644 --- a/lib/src/store/download.dart +++ b/lib/src/store/download.dart @@ -184,7 +184,7 @@ class DownloadManagement { obscuredQueryParams: obscuredQueryParams?.map((e) => RegExp('$e=[^&]*')) ?? FMTCTileProviderSettings.instance.obscuredQueryParams, - backend: FMTCBackendAccess.internal, + backend: FMTCBackendAccessThreadSafe.internal, ), onExit: receivePort.sendPort, debugName: '[FMTC] Master Bulk Download Thread', diff --git a/tile_server/bin/tile_server.dart b/tile_server/bin/tile_server.dart index aa1e3076..b4d76a66 100644 --- a/tile_server/bin/tile_server.dart +++ b/tile_server/bin/tile_server.dart @@ -50,6 +50,7 @@ Future main(List _) async { '[$requestTime] ${ctx.method} ${ctx.path}: ${ctx.response.statusCode}\t\t$servedSeaTiles sea tiles this session\t\t\t$lastRate tps - ${currentArtificialDelay.inMilliseconds} ms delay\n', ); }, + port: 7070, ); // Handle keyboard events @@ -141,7 +142,7 @@ Future main(List _) async { // Output basic console instructions console ..setTextStyle(italic: true) - ..write('Now serving tiles at 127.0.0.1:8080/{z}/{x}/{y}\n\n') + ..write('Now serving tiles at 127.0.0.1:7070/{z}/{x}/{y}\n\n') ..write("Press 'q' to kill server\n") ..write( 'Press UP or DOWN to manipulate artificial delay by ${artificialDelayChangeAmount.inMilliseconds} ms\n\n', From 63df69b43fe4f5760c02cffa1911f77f70dfe969 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sat, 24 Feb 2024 16:45:49 +0000 Subject: [PATCH 108/168] Minor fix in example app Former-commit-id: 6dc02904a437cbe872174d586fbdfea7af18e3ae [formerly 1397c8d9a6d91fe3e713deaa7f54a7e14b97ca7b] Former-commit-id: 91a001d89f173b2872d98df368db8228d3947f87 --- .../components/recovery_start_button.dart | 78 +++++++------------ 1 file changed, 30 insertions(+), 48 deletions(-) diff --git a/example/lib/screens/main/pages/recovery/components/recovery_start_button.dart b/example/lib/screens/main/pages/recovery/components/recovery_start_button.dart index df5c7f74..05b29932 100644 --- a/example/lib/screens/main/pages/recovery/components/recovery_start_button.dart +++ b/example/lib/screens/main/pages/recovery/components/recovery_start_button.dart @@ -17,56 +17,38 @@ class RecoveryStartButton extends StatelessWidget { final ({bool isFailed, RecoveredRegion region}) result; @override - Widget build(BuildContext context) => FutureBuilder( - future: const FMTCStore('') - .download - .check(result.region.toDownloadable(TileLayer())), - builder: (context, tiles) => tiles.hasData - ? IconButton( - icon: Icon( - Icons.download, - color: result.isFailed ? Colors.green : null, - ), - onPressed: !result.isFailed - ? null - : () async { - final regionSelectionProvider = - Provider.of( - context, - listen: false, - ) - ..region = result.region.toRegion() - ..minZoom = result.region.minZoom - ..maxZoom = result.region.maxZoom - ..setSelectedStore( - FMTCStore(result.region.storeName), - ); - // TODO: Check - //..regionTiles = tiles.data; + Widget build(BuildContext context) => IconButton( + icon: Icon( + Icons.download, + color: result.isFailed ? Colors.green : null, + ), + onPressed: !result.isFailed + ? null + : () async { + final regionSelectionProvider = + Provider.of(context, listen: false) + ..region = result.region.toRegion() + ..minZoom = result.region.minZoom + ..maxZoom = result.region.maxZoom + ..setSelectedStore( + FMTCStore(result.region.storeName), + ); - await Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => ConfigureDownloadPopup( - region: regionSelectionProvider.region!, - minZoom: result.region.minZoom, - maxZoom: result.region.maxZoom, - ), - fullscreenDialog: true, - ), - ); + // TODO: Check + //..regionTiles = tiles.data; - moveToDownloadPage(); - }, - ) - : const Padding( - padding: EdgeInsets.all(8), - child: SizedBox( - height: 24, - width: 24, - child: CircularProgressIndicator( - strokeWidth: 3, + await Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => ConfigureDownloadPopup( + region: regionSelectionProvider.region!, + minZoom: result.region.minZoom, + maxZoom: result.region.maxZoom, + ), + fullscreenDialog: true, ), - ), - ), + ); + + moveToDownloadPage(); + }, ); } From f1da734b4aaf9ac700bb8fb37d1948a95d2541c4 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sun, 25 Feb 2024 15:48:35 +0000 Subject: [PATCH 109/168] Minor performance improvements Former-commit-id: fafaf41544d70da80cd8df6e4f6c0a58400717f2 [formerly 86f2f1af88205b052920924089016016b4a4476d] Former-commit-id: 542d8ba7864c63b38971b6dd61bd3469e203d8f3 --- .../backend/internal_thread_safe.dart | 171 ++++++------------ .../objectbox/backend/internal_worker.dart | 165 +++++++++-------- .../models/generated/objectbox-model.json | 102 +++++------ .../impls/objectbox/models/src/store.dart | 3 +- .../impls/objectbox/models/src/tile.dart | 8 +- lib/src/bulk_download/thread.dart | 4 +- lib/src/providers/image_provider.dart | 1 + tile_server/static/generated/favicon.dart | 3 +- tile_server/static/generated/land.dart | 3 +- tile_server/static/generated/sea.dart | 3 +- 10 files changed, 208 insertions(+), 255 deletions(-) diff --git a/lib/src/backend/impls/objectbox/backend/internal_thread_safe.dart b/lib/src/backend/impls/objectbox/backend/internal_thread_safe.dart index 7720d047..72a046bf 100644 --- a/lib/src/backend/impls/objectbox/backend/internal_thread_safe.dart +++ b/lib/src/backend/impls/objectbox/backend/internal_thread_safe.dart @@ -68,38 +68,28 @@ class _ObjectBoxBackendThreadSafeImpl implements FMTCBackendInternalThreadSafe { final storeQuery = stores.query(ObjectBoxStore_.name.equals(storeName)).build(); - final storeAdjustments = {}; + final storesToUpdate = {}; root!.runInTransaction( TxMode.write, () { - final store = storeQuery.findUnique()!; - - final existingTile = - (tilesQuery..param(ObjectBoxTile_.url).value = url).findUnique(); - - storeAdjustments[storeName] = storeAdjustments[storeName] == null - ? (deltaLength: 1, deltaSize: bytes.lengthInBytes) - : ( - deltaLength: storeAdjustments[storeName]!.deltaLength + 1, - deltaSize: storeAdjustments[storeName]!.deltaSize + - bytes.lengthInBytes, - ); - - if (existingTile != null) { - for (final store in existingTile.stores) { - storeAdjustments[store.name] = storeAdjustments[store.name] == null - ? ( - deltaLength: 0, - deltaSize: - -existingTile.bytes.lengthInBytes + bytes.lengthInBytes, - ) - : ( - deltaLength: storeAdjustments[store.name]!.deltaLength, - deltaSize: storeAdjustments[store.name]!.deltaSize + - (-existingTile.bytes.lengthInBytes + - bytes.lengthInBytes), - ); + final existingTile = tilesQuery.findUnique(); + final store = storeQuery.findUnique() ?? + (throw StoreNotExists(storeName: storeName)); + + if (existingTile == null) { + storesToUpdate[storeName] = store + ..length += 1 + ..size += bytes.lengthInBytes; + } else { + storesToUpdate[storeName] = store + ..size += -existingTile.bytes.lengthInBytes + bytes.lengthInBytes; + + for (final relatedStore in existingTile.stores) { + storesToUpdate[relatedStore.name] = + (storesToUpdate[relatedStore.name] ?? relatedStore) + ..size += + -existingTile.bytes.lengthInBytes + bytes.lengthInBytes; } } @@ -108,23 +98,9 @@ class _ObjectBoxBackendThreadSafeImpl implements FMTCBackendInternalThreadSafe { url: url, lastModified: DateTime.timestamp(), bytes: bytes, - )..stores.addAll( - { - store, - if (existingTile != null) ...existingTile.stores, - }, - ), - ); - - stores.putMany( - storeAdjustments.entries - .map( - (e) => store - ..length += e.value.deltaLength - ..size += e.value.deltaSize, - ) - .toList(), + )..stores.addAll({store, ...?existingTile?.stores}), ); + stores.putMany(storesToUpdate.values.toList(), mode: PutMode.update); }, ); @@ -144,83 +120,52 @@ class _ObjectBoxBackendThreadSafeImpl implements FMTCBackendInternalThreadSafe { final stores = root!.box(); final tilesQuery = tiles.query(ObjectBoxTile_.url.equals('')).build(); - final storeQuery = stores.query(ObjectBoxStore_.name.equals('')).build(); + final storeQuery = + stores.query(ObjectBoxStore_.name.equals(storeName)).build(); - final storeAdjustments = {}; + final storesToUpdate = {}; root!.runInTransaction( TxMode.write, () { - final store = (storeQuery - ..param(ObjectBoxStore_.name).value = storeName) - .findUnique()!; - - tiles.putMany( - List.generate( - urls.length, - (i) { - final existingTile = (tilesQuery - ..param(ObjectBoxTile_.url).value = urls[i]) - .findUnique(); - - storeAdjustments[storeName] = storeAdjustments[storeName] == null - ? (deltaLength: 1, deltaSize: bytess[i].lengthInBytes) - : ( - deltaLength: storeAdjustments[storeName]!.deltaLength + 1, - deltaSize: storeAdjustments[storeName]!.deltaSize + - bytess[i].lengthInBytes, - ); - - if (existingTile != null) { - for (final store in existingTile.stores) { - storeAdjustments[store.name] = - storeAdjustments[store.name] == null - ? ( - deltaLength: 0, - deltaSize: -existingTile.bytes.lengthInBytes + - bytess[i].lengthInBytes, - ) - : ( - deltaLength: - storeAdjustments[store.name]!.deltaLength, - deltaSize: - storeAdjustments[store.name]!.deltaSize + - (-existingTile.bytes.lengthInBytes + - bytess[i].lengthInBytes), - ); - } + final store = storeQuery.findUnique() ?? + (throw StoreNotExists(storeName: storeName)); + + final tilesToUpdate = List.generate( + urls.length, + (i) { + final existingTile = (tilesQuery + ..param(ObjectBoxTile_.url).value = urls[i]) + .findUnique(); + + if (existingTile == null) { + storesToUpdate[storeName] = store + ..length += 1 + ..size += bytess[i].lengthInBytes; + } else { + storesToUpdate[storeName] = store + ..size += + -existingTile.bytes.lengthInBytes + bytess[i].lengthInBytes; + + for (final relatedStore in existingTile.stores) { + storesToUpdate[relatedStore.name] = + (storesToUpdate[relatedStore.name] ?? relatedStore) + ..size += -existingTile.bytes.lengthInBytes + + bytess[i].lengthInBytes; } - - return ObjectBoxTile( - url: urls[i], - lastModified: DateTime.timestamp(), - bytes: bytess[i], - )..stores.addAll( - { - store, - if (existingTile != null) ...existingTile.stores, - }, - ); - }, - growable: false, - ), - ); - - assert( - storeAdjustments.isNotEmpty, - '`storeAdjustments` should not be empty if relations are being set correctly', + } + + return ObjectBoxTile( + url: urls[i], + lastModified: DateTime.timestamp(), + bytes: bytess[i], + )..stores.addAll({store, ...?existingTile?.stores}); + }, + growable: false, ); - stores.putMany( - storeAdjustments.entries - .map( - (e) => (storeQuery..param(ObjectBoxStore_.name).value = e.key) - .findUnique()! - ..length += e.value.deltaLength - ..size += e.value.deltaSize, - ) - .toList(), - ); + tiles.putMany(tilesToUpdate); + stores.putMany(storesToUpdate.values.toList(), mode: PutMode.update); }, ); diff --git a/lib/src/backend/impls/objectbox/backend/internal_worker.dart b/lib/src/backend/impls/objectbox/backend/internal_worker.dart index 81a30bc8..e9943b42 100644 --- a/lib/src/backend/impls/objectbox/backend/internal_worker.dart +++ b/lib/src/backend/impls/objectbox/backend/internal_worker.dart @@ -76,61 +76,69 @@ Future _worker( // Create memory for streamed output subscription storage final streamedOutputSubscriptions = {}; - //! UTIL FUNCTIONS !// - - /// Convert store name to database store object - /// - /// Returns `null` if store not found. Throw the [StoreNotExists] error if it - /// was required. - ObjectBoxStore? getStore(String storeName) { - final query = root - .box() - .query(ObjectBoxStore_.name.equals(storeName)) - .build(); - final store = query.findUnique(); - query.close(); - return store; - } - /// Delete the specified tiles from the specified store /// /// Note that [tilesQuery] is not closed internally. Ensure it is closed after /// usage. /// - /// Returns whether each tile was actually deleted (whether it was an orphan), - /// in iteration order of [tilesQuery.find]. - List deleteTiles({ + /// Note that a transaction is used internally as necessary. + /// + /// Returns the number of orphaned (deleted) tiles. + int deleteTiles({ required String storeName, required Query tilesQuery, }) { final stores = root.box(); final tiles = root.box(); - return tilesQuery.find().map( - (tile) { - // For the correct store, adjust the statistics - for (final store in tile.stores) { - if (store.name != storeName) continue; - stores.put( - store - ..length -= 1 - ..size -= tile.bytes.lengthInBytes, - mode: PutMode.update, - ); - break; - } - - // Remove the store relation from the tile - tile.stores.removeWhere((store) => store.name == storeName); + final storeQuery = + stores.query(ObjectBoxStore_.name.equals(storeName)).build(); + + final tilesToUpdate = []; + + final deletedNum = root.runInTransaction( + TxMode.write, + () { + final queriedTiles = tilesQuery.find(); + final store = storeQuery.findUnique() ?? + (throw StoreNotExists(storeName: storeName)); + + final tilesToDelete = List.generate( + queriedTiles.length, + (i) { + final tile = queriedTiles[i]; + + // Linear search through related stores, and modify when located + for (final store in tile.stores) { + if (store.name != storeName) continue; + store + ..length -= 1 + ..size -= tile.bytes.lengthInBytes; + break; + } + + // Remove the store relation from the tile + tile.stores.removeWhere((store) => store.name == storeName); + + // Register the tile for deletion if it belongs to no stores + if (tile.stores.isEmpty) return tile.id; + + // Otherwise register the tile to be updated (the new relation applied) + tilesToUpdate.add(tile); + return -1; + }, + growable: false, + ); + + stores.put(store, mode: PutMode.update); + return (tiles..putMany(tilesToUpdate, mode: PutMode.update)) + .removeMany(tilesToDelete); + }, + ); - // Delete the tile if it belongs to no stores - if (tile.stores.isEmpty) return tiles.remove(tile.id); + storeQuery.close(); - // Otherwise just update the tile - tile.stores.applyToDb(mode: PutMode.update); - return false; - }, - ).toList(); + return deletedNum; } //! MAIN LOOP !// @@ -197,7 +205,13 @@ Future _worker( query.close(); case _WorkerCmdType.getStoreStats: final storeName = cmd.args['storeName']! as String; - final store = getStore(storeName) ?? + + final query = root + .box() + .query(ObjectBoxStore_.name.equals(storeName)) + .build(); + + final store = query.findUnique() ?? (throw StoreNotExists(storeName: storeName)); sendRes( @@ -211,6 +225,8 @@ Future _worker( ), }, ); + + query.close(); case _WorkerCmdType.createStore: final storeName = cmd.args['storeName']! as String; @@ -222,6 +238,7 @@ Future _worker( size: 0, hits: 0, misses: 0, + metadataJson: '', ), mode: PutMode.insert, ); @@ -466,12 +483,8 @@ Future _worker( sendRes( id: cmd.id, data: { - 'wasOrphan': root - .runInTransaction( - TxMode.write, - () => deleteTiles(storeName: storeName, tilesQuery: query), - ) - .singleOrNull, + 'wasOrphan': + deleteTiles(storeName: storeName, tilesQuery: query) == 1, }, ); @@ -520,28 +533,20 @@ Future _worker( .query(ObjectBoxStore_.name.equals(storeName)) .build(); + final store = storeQuery.findUnique() ?? + (throw StoreNotExists(storeName: storeName)); + + final numToRemove = store.length - tilesLimit; + sendRes( id: cmd.id, data: { - 'numOrphans': root - .runInTransaction( - TxMode.write, - () { - final store = storeQuery.findUnique() ?? - (throw StoreNotExists(storeName: storeName)); - - final numToRemove = store.length - tilesLimit; - - return numToRemove <= 0 - ? const Iterable.empty() - : deleteTiles( - storeName: storeName, - tilesQuery: tilesQuery..limit = numToRemove, - ); - }, - ) - .where((e) => e) - .length, + 'numOrphans': numToRemove <= 0 + ? 0 + : deleteTiles( + storeName: storeName, + tilesQuery: tilesQuery..limit = numToRemove, + ), }, ); @@ -563,23 +568,23 @@ Future _worker( sendRes( id: cmd.id, data: { - 'numOrphans': root - .runInTransaction( - TxMode.write, - () => deleteTiles( - storeName: storeName, - tilesQuery: tilesQuery, - ), - ) - .where((e) => e) - .length, + 'numOrphans': deleteTiles( + storeName: storeName, + tilesQuery: tilesQuery, + ), }, ); tilesQuery.close(); case _WorkerCmdType.readMetadata: final storeName = cmd.args['storeName']! as String; - final store = getStore(storeName) ?? + + final query = root + .box() + .query(ObjectBoxStore_.name.equals(storeName)) + .build(); + + final store = query.findUnique() ?? (throw StoreNotExists(storeName: storeName)); sendRes( @@ -590,6 +595,8 @@ Future _worker( .cast(), }, ); + + query.close(); case _WorkerCmdType.setMetadata: final storeName = cmd.args['storeName']! as String; final key = cmd.args['key']! as String; diff --git a/lib/src/backend/impls/objectbox/models/generated/objectbox-model.json b/lib/src/backend/impls/objectbox/models/generated/objectbox-model.json index 9d651f9c..564ca372 100644 --- a/lib/src/backend/impls/objectbox/models/generated/objectbox-model.json +++ b/lib/src/backend/impls/objectbox/models/generated/objectbox-model.json @@ -4,115 +4,115 @@ "_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.", "entities": [ { - "id": "1:5852113147755082214", - "lastPropertyId": "21:4175806851713282292", + "id": "1:5472631385587455945", + "lastPropertyId": "21:3590067577930145922", "name": "ObjectBoxRecovery", "properties": [ { - "id": "1:4049260877710714778", + "id": "1:3769282896877713230", "name": "id", "type": 6, "flags": 1 }, { - "id": "2:514228763227462265", + "id": "2:2496811483091029921", "name": "refId", "type": 6, "flags": 40, - "indexId": "1:1915947614835147959" + "indexId": "1:1036386105099927432" }, { - "id": "3:8338900024311175845", + "id": "3:3612512640999075849", "name": "storeName", "type": 9 }, { - "id": "4:6716912532653788634", + "id": "4:1095455913099058361", "name": "creationTime", "type": 10 }, { - "id": "5:1519845599652561210", + "id": "5:1138350672456876624", "name": "minZoom", "type": 6 }, { - "id": "6:5013395781301985972", + "id": "6:9040433791555820529", "name": "maxZoom", "type": 6 }, { - "id": "7:5813475472850223709", + "id": "7:6819230045021667310", "name": "startTile", "type": 6 }, { - "id": "8:62434241352879189", + "id": "8:8185724925875119436", "name": "endTile", "type": 6 }, { - "id": "9:1377762685943168852", + "id": "9:7217406424708558740", "name": "typeId", "type": 6 }, { - "id": "10:7827529863528185814", + "id": "10:5971465387225017460", "name": "rectNwLat", "type": 8 }, { - "id": "11:4768940403554990885", + "id": "11:6703340231106164623", "name": "rectNwLng", "type": 8 }, { - "id": "12:7059818053329608732", + "id": "12:741105584939284321", "name": "rectSeLat", "type": 8 }, { - "id": "13:7887700468628261283", + "id": "13:2939837278126242427", "name": "rectSeLng", "type": 8 }, { - "id": "14:8392236879597342193", + "id": "14:2393337671661697697", "name": "circleCenterLat", "type": 8 }, { - "id": "15:5063472498722773794", + "id": "15:8055510540122966413", "name": "circleCenterLng", "type": 8 }, { - "id": "16:5319186299784201385", + "id": "16:9110709438555760246", "name": "circleRadius", "type": 8 }, { - "id": "17:4479579774250144926", + "id": "17:8363656194353400366", "name": "lineLats", "type": 29 }, { - "id": "18:3149550933222350400", + "id": "18:7008680868853575786", "name": "lineLngs", "type": 29 }, { - "id": "19:6474585775498338051", + "id": "19:7670007285707179405", "name": "lineRadius", "type": 8 }, { - "id": "20:7965435653690834819", + "id": "20:490933261424375687", "name": "customPolygonLats", "type": 29 }, { - "id": "21:4175806851713282292", + "id": "21:3590067577930145922", "name": "customPolygonLngs", "type": 29 } @@ -120,45 +120,45 @@ "relations": [] }, { - "id": "2:8434848168440140631", - "lastPropertyId": "7:441658295997331328", + "id": "2:632249766926720928", + "lastPropertyId": "7:7028109958959828879", "name": "ObjectBoxStore", "properties": [ { - "id": "1:123601567385773864", + "id": "1:1672655555406818874", "name": "id", "type": 6, "flags": 1 }, { - "id": "2:9007911375527621682", + "id": "2:1060752758288526798", "name": "name", "type": 9, "flags": 2080, - "indexId": "2:5983120737825279673" + "indexId": "2:5602852847672696920" }, { - "id": "3:8187748336899942109", + "id": "3:7375048950056890678", "name": "length", "type": 6 }, { - "id": "4:2385332217074178388", + "id": "4:7781853256122686511", "name": "size", "type": 8 }, { - "id": "5:464625589558308463", + "id": "5:3183925806131180531", "name": "hits", "type": 6 }, { - "id": "6:4049198995248939830", + "id": "6:6484030110235711573", "name": "misses", "type": 6 }, { - "id": "7:441658295997331328", + "id": "7:7028109958959828879", "name": "metadataJson", "type": 9 } @@ -166,48 +166,48 @@ "relations": [] }, { - "id": "3:8831490920600301075", - "lastPropertyId": "4:1909369032294267427", + "id": "3:8691708694767276679", + "lastPropertyId": "4:1172878417733380836", "name": "ObjectBoxTile", "properties": [ { - "id": "1:7557087112400213643", + "id": "1:5356545328183635928", "name": "id", "type": 6, "flags": 1 }, { - "id": "2:5772177928281361264", + "id": "2:4115905667778721807", "name": "url", "type": 9, "flags": 34848, - "indexId": "3:554942103700819034" + "indexId": "3:4361441212367179043" }, { - "id": "3:925223426368624455", + "id": "3:7508139234299399524", + "name": "bytes", + "type": 23 + }, + { + "id": "4:1172878417733380836", "name": "lastModified", "type": 10, "flags": 8, - "indexId": "4:7943022627739396393" - }, - { - "id": "4:1909369032294267427", - "name": "bytes", - "type": 23 + "indexId": "4:4857742396480146668" } ], "relations": [ { - "id": "1:6378815042102010365", + "id": "1:7496298295217061586", "name": "stores", - "targetId": "2:8434848168440140631" + "targetId": "2:632249766926720928" } ] } ], - "lastEntityId": "3:8831490920600301075", - "lastIndexId": "4:7943022627739396393", - "lastRelationId": "1:6378815042102010365", + "lastEntityId": "3:8691708694767276679", + "lastIndexId": "4:4857742396480146668", + "lastRelationId": "1:7496298295217061586", "lastSequenceId": "0:0", "modelVersion": 5, "modelVersionParserMinimum": 5, diff --git a/lib/src/backend/impls/objectbox/models/src/store.dart b/lib/src/backend/impls/objectbox/models/src/store.dart index 2618613c..d3aa8a91 100644 --- a/lib/src/backend/impls/objectbox/models/src/store.dart +++ b/lib/src/backend/impls/objectbox/models/src/store.dart @@ -13,7 +13,8 @@ class ObjectBoxStore { required this.size, required this.hits, required this.misses, - }) : metadataJson = ''; + required this.metadataJson, + }); @Id() int id = 0; diff --git a/lib/src/backend/impls/objectbox/models/src/tile.dart b/lib/src/backend/impls/objectbox/models/src/tile.dart index 6cd0d235..974dd7d1 100644 --- a/lib/src/backend/impls/objectbox/models/src/tile.dart +++ b/lib/src/backend/impls/objectbox/models/src/tile.dart @@ -12,8 +12,8 @@ import 'store.dart'; base class ObjectBoxTile extends BackendTile { ObjectBoxTile({ required this.url, - required this.lastModified, required this.bytes, + required this.lastModified, }); @Id() @@ -24,14 +24,14 @@ base class ObjectBoxTile extends BackendTile { @Unique(onConflict: ConflictStrategy.replace) String url; + @override + Uint8List bytes; + @override @Index() @Property(type: PropertyType.date) DateTime lastModified; - @override - Uint8List bytes; - @Index() final stores = ToMany(); } diff --git a/lib/src/bulk_download/thread.dart b/lib/src/bulk_download/thread.dart index 79203c42..2f6b0d7b 100644 --- a/lib/src/bulk_download/thread.dart +++ b/lib/src/bulk_download/thread.dart @@ -70,9 +70,11 @@ Future _singleDownloadThread( url: networkUrl, obscuredQueryParams: input.obscuredQueryParams, ); + + // TODO: Work across stores in the event of an error final existingTile = await input.backend.readTile( url: matcherUrl, - storeName: input.storeName, // TODO: Test + storeName: input.storeName, ); // Skip if tile already exists and user demands existing tile pruning diff --git a/lib/src/providers/image_provider.dart b/lib/src/providers/image_provider.dart index eab72c9d..87a44572 100644 --- a/lib/src/providers/image_provider.dart +++ b/lib/src/providers/image_provider.dart @@ -91,6 +91,7 @@ class FMTCImageProvider extends ImageProvider { obscuredQueryParams: provider.settings.obscuredQueryParams, ); + // TODO: Work across stores in event of error final existingTile = await FMTCBackendAccess.internal.readTile( url: matcherUrl, storeName: storeName, diff --git a/tile_server/static/generated/favicon.dart b/tile_server/static/generated/favicon.dart index 7dd43c1b..259e34b4 100644 --- a/tile_server/static/generated/favicon.dart +++ b/tile_server/static/generated/favicon.dart @@ -1,2 +1 @@ -// Will be replaced automatically by GitHub Actions -final faviconTileBytes = []; +final faviconTileBytes = [0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 32, 0, 201, 45, 0, 0, 22, 0, 0, 0, 137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 1, 0, 0, 0, 1, 0, 8, 6, 0, 0, 0, 92, 114, 168, 102, 0, 0, 45, 144, 73, 68, 65, 84, 120, 218, 237, 157, 123, 124, 27, 229, 153, 239, 127, 26, 141, 70, 23, 91, 146, 109, 249, 126, 139, 147, 216, 9, 185, 185, 9, 16, 72, 40, 183, 64, 129, 246, 208, 238, 129, 237, 210, 82, 122, 202, 161, 91, 2, 11, 45, 44, 108, 161, 13, 112, 246, 0, 165, 52, 180, 205, 46, 109, 225, 244, 190, 103, 217, 148, 101, 89, 216, 93, 232, 133, 179, 101, 11, 9, 105, 32, 9, 73, 73, 76, 156, 224, 196, 151, 92, 124, 209, 197, 182, 44, 75, 178, 46, 51, 26, 73, 231, 15, 89, 178, 36, 75, 214, 109, 164, 153, 145, 222, 239, 231, 147, 79, 28, 105, 52, 126, 53, 153, 231, 55, 207, 251, 188, 207, 243, 188, 10, 254, 163, 23, 195, 40, 0, 191, 215, 9, 243, 88, 63, 204, 163, 199, 97, 29, 59, 1, 149, 190, 25, 205, 61, 215, 160, 121, 213, 39, 80, 219, 177, 9, 52, 83, 93, 200, 233, 9, 4, 66, 17, 81, 20, 42, 0, 201, 204, 76, 157, 195, 196, 249, 62, 88, 198, 250, 225, 152, 49, 163, 174, 227, 34, 52, 245, 92, 131, 166, 238, 109, 208, 55, 174, 18, 251, 251, 18, 8, 132, 56, 4, 23, 128, 120, 120, 158, 133, 101, 52, 226, 29, 152, 199, 250, 17, 166, 52, 104, 90, 181, 13, 77, 221, 219, 208, 216, 125, 37, 241, 14, 8, 4, 145, 201, 73, 0, 38, 236, 62, 184, 61, 172, 216, 99, 134, 182, 190, 153, 76, 47, 8, 4, 1, 160, 179, 61, 112, 194, 238, 131, 66, 93, 139, 21, 203, 87, 138, 61, 102, 76, 155, 71, 224, 24, 59, 134, 134, 149, 87, 136, 61, 20, 2, 65, 214, 80, 217, 30, 232, 246, 176, 168, 111, 21, 223, 248, 1, 160, 190, 117, 37, 124, 211, 86, 177, 135, 65, 32, 200, 158, 172, 5, 128, 64, 32, 148, 31, 178, 22, 0, 235, 224, 31, 192, 115, 115, 98, 15, 131, 64, 144, 45, 89, 199, 0, 164, 200, 200, 219, 79, 227, 200, 244, 121, 212, 117, 94, 130, 166, 158, 107, 208, 176, 242, 10, 24, 155, 214, 138, 61, 44, 2, 65, 54, 228, 45, 0, 111, 255, 231, 191, 225, 143, 111, 255, 22, 0, 160, 209, 106, 209, 220, 186, 12, 87, 95, 127, 19, 150, 175, 92, 19, 59, 230, 223, 94, 252, 49, 250, 251, 222, 143, 253, 123, 109, 239, 197, 248, 252, 237, 247, 1, 0, 206, 142, 12, 224, 159, 255, 225, 239, 97, 53, 143, 97, 237, 134, 139, 241, 63, 255, 234, 155, 168, 170, 210, 3, 0, 142, 31, 61, 128, 87, 95, 252, 9, 156, 179, 51, 184, 120, 203, 85, 184, 253, 174, 111, 164, 28, 195, 117, 55, 61, 6, 158, 103, 97, 29, 63, 9, 203, 232, 126, 28, 57, 240, 99, 4, 194, 10, 52, 245, 92, 131, 198, 149, 87, 162, 177, 231, 106, 48, 154, 26, 177, 175, 49, 129, 32, 89, 242, 22, 0, 135, 125, 18, 77, 45, 29, 248, 243, 47, 109, 199, 156, 195, 137, 211, 39, 251, 240, 204, 223, 222, 139, 47, 221, 245, 16, 46, 191, 250, 70, 0, 192, 208, 233, 227, 184, 230, 147, 159, 197, 138, 158, 200, 83, 89, 171, 173, 2, 0, 112, 156, 31, 127, 247, 212, 131, 248, 226, 95, 62, 136, 222, 139, 46, 195, 203, 255, 247, 7, 120, 101, 247, 243, 248, 242, 61, 143, 192, 238, 180, 227, 255, 236, 122, 12, 247, 239, 248, 30, 58, 186, 186, 241, 179, 103, 255, 55, 222, 248, 143, 221, 184, 241, 207, 111, 79, 253, 5, 104, 53, 218, 187, 46, 68, 123, 215, 133, 0, 0, 183, 203, 6, 243, 249, 15, 49, 241, 254, 79, 113, 236, 245, 7, 161, 111, 90, 131, 166, 149, 87, 161, 185, 231, 19, 168, 237, 188, 72, 236, 235, 77, 32, 72, 138, 130, 166, 0, 90, 93, 21, 154, 27, 151, 1, 141, 64, 247, 234, 94, 116, 116, 117, 227, 185, 239, 237, 192, 37, 151, 93, 11, 134, 209, 192, 53, 235, 192, 138, 158, 181, 232, 88, 214, 157, 240, 185, 161, 83, 253, 48, 24, 106, 176, 245, 202, 27, 0, 0, 55, 221, 126, 47, 118, 220, 125, 51, 190, 248, 149, 7, 209, 119, 96, 47, 214, 125, 108, 51, 214, 245, 110, 6, 0, 252, 217, 95, 124, 25, 191, 124, 254, 219, 105, 5, 32, 25, 189, 161, 9, 171, 55, 92, 143, 213, 27, 174, 7, 207, 179, 176, 219, 206, 96, 226, 124, 31, 250, 254, 253, 85, 120, 125, 30, 52, 173, 186, 38, 226, 33, 116, 95, 5, 117, 85, 189, 216, 215, 159, 64, 16, 21, 65, 99, 0, 189, 23, 94, 6, 138, 82, 98, 232, 84, 63, 214, 245, 110, 134, 219, 237, 68, 223, 159, 246, 227, 248, 7, 7, 208, 209, 213, 141, 222, 11, 47, 3, 0, 88, 39, 206, 163, 61, 78, 20, 76, 70, 19, 0, 192, 53, 235, 128, 213, 60, 138, 214, 182, 174, 216, 123, 237, 93, 61, 152, 180, 78, 228, 247, 229, 104, 53, 154, 218, 214, 160, 169, 109, 13, 112, 217, 23, 224, 247, 58, 49, 113, 190, 15, 230, 99, 187, 113, 252, 119, 223, 132, 198, 216, 129, 166, 85, 215, 162, 105, 229, 85, 36, 177, 136, 80, 145, 8, 30, 4, 172, 51, 53, 194, 237, 114, 0, 0, 62, 245, 103, 183, 197, 94, 255, 247, 151, 126, 134, 55, 127, 251, 47, 120, 248, 241, 231, 224, 247, 121, 161, 102, 212, 9, 159, 211, 234, 244, 112, 187, 103, 193, 113, 44, 106, 106, 23, 158, 204, 85, 85, 122, 4, 2, 28, 60, 30, 119, 44, 70, 144, 47, 26, 157, 17, 43, 215, 92, 133, 149, 107, 174, 2, 0, 76, 217, 134, 97, 29, 59, 129, 83, 255, 185, 3, 179, 179, 54, 152, 150, 109, 65, 211, 170, 107, 208, 188, 234, 90, 84, 213, 46, 43, 222, 85, 39, 16, 36, 130, 224, 2, 48, 61, 101, 129, 222, 80, 11, 0, 9, 110, 251, 117, 159, 254, 28, 190, 122, 251, 245, 24, 59, 63, 140, 106, 67, 13, 216, 179, 131, 9, 159, 243, 121, 221, 208, 84, 49, 208, 235, 141, 240, 121, 23, 150, 246, 60, 30, 55, 0, 20, 108, 252, 169, 104, 104, 234, 70, 67, 83, 55, 54, 92, 124, 19, 252, 94, 39, 108, 230, 83, 176, 12, 255, 30, 251, 247, 124, 15, 148, 218, 128, 166, 85, 159, 64, 211, 170, 109, 168, 239, 218, 74, 188, 3, 66, 89, 66, 191, 251, 135, 31, 163, 181, 179, 23, 173, 29, 27, 160, 209, 25, 11, 58, 217, 161, 119, 255, 11, 20, 165, 68, 207, 5, 27, 22, 189, 199, 48, 26, 104, 117, 122, 240, 124, 0, 109, 29, 93, 120, 243, 55, 47, 197, 222, 179, 59, 237, 224, 88, 22, 166, 186, 54, 52, 183, 47, 195, 209, 247, 247, 197, 222, 27, 63, 55, 132, 150, 214, 206, 162, 95, 8, 141, 206, 136, 101, 221, 151, 98, 89, 247, 165, 0, 0, 215, 172, 5, 227, 231, 142, 225, 204, 158, 157, 56, 50, 117, 22, 198, 214, 141, 104, 238, 185, 6, 77, 171, 175, 37, 75, 141, 132, 178, 129, 174, 93, 115, 51, 206, 13, 239, 197, 7, 7, 94, 65, 85, 149, 30, 45, 29, 189, 104, 237, 236, 141, 204, 155, 51, 224, 243, 122, 96, 157, 60, 15, 231, 148, 29, 253, 199, 14, 225, 205, 223, 189, 140, 237, 247, 63, 30, 9, 0, 186, 28, 24, 57, 221, 143, 85, 107, 55, 1, 0, 222, 121, 243, 53, 168, 213, 106, 180, 117, 44, 7, 195, 104, 16, 8, 112, 120, 247, 157, 55, 176, 105, 243, 149, 120, 125, 247, 143, 113, 233, 229, 215, 129, 97, 52, 216, 180, 249, 74, 188, 244, 15, 207, 226, 228, 241, 35, 232, 232, 234, 198, 111, 254, 237, 31, 99, 193, 194, 82, 98, 168, 105, 193, 218, 141, 45, 88, 187, 241, 191, 197, 150, 26, 173, 227, 135, 113, 228, 240, 47, 17, 8, 43, 208, 184, 242, 42, 52, 245, 108, 67, 195, 138, 203, 73, 48, 145, 32, 91, 20, 46, 135, 45, 86, 13, 232, 24, 253, 0, 214, 161, 183, 96, 27, 217, 7, 183, 109, 0, 77, 173, 23, 160, 181, 243, 99, 104, 237, 236, 197, 248, 172, 10, 43, 214, 127, 60, 246, 193, 228, 60, 128, 182, 142, 21, 216, 118, 195, 159, 199, 34, 254, 30, 143, 27, 255, 240, 252, 83, 56, 63, 114, 26, 148, 82, 137, 229, 221, 107, 241, 185, 47, 125, 21, 245, 141, 45, 0, 128, 177, 243, 195, 248, 167, 159, 125, 23, 147, 86, 51, 46, 88, 183, 41, 33, 15, 224, 228, 241, 35, 120, 249, 133, 31, 97, 110, 206, 137, 141, 23, 95, 142, 47, 220, 113, 63, 24, 70, 147, 48, 240, 51, 39, 222, 195, 5, 157, 53, 162, 92, 180, 57, 215, 84, 164, 196, 121, 244, 67, 216, 204, 167, 200, 82, 35, 65, 182, 36, 8, 64, 60, 172, 103, 26, 83, 103, 222, 133, 109, 104, 47, 172, 131, 111, 99, 195, 182, 199, 19, 4, 64, 108, 196, 20, 128, 100, 108, 19, 3, 176, 140, 245, 195, 60, 250, 33, 60, 30, 55, 76, 203, 183, 160, 169, 123, 27, 154, 87, 93, 11, 173, 177, 77, 236, 225, 17, 8, 105, 73, 43, 0, 201, 140, 190, 255, 42, 17, 128, 44, 32, 45, 210, 8, 114, 66, 214, 181, 0, 82, 68, 163, 51, 98, 197, 234, 203, 177, 98, 245, 229, 0, 22, 90, 164, 157, 126, 243, 49, 56, 102, 204, 48, 45, 219, 130, 198, 149, 87, 144, 22, 105, 4, 73, 64, 4, 160, 200, 212, 53, 116, 161, 174, 161, 11, 27, 46, 190, 105, 161, 69, 218, 249, 119, 112, 224, 221, 231, 72, 139, 52, 130, 232, 100, 61, 5, 152, 26, 217, 15, 85, 8, 146, 104, 10, 50, 109, 30, 65, 152, 117, 160, 205, 164, 21, 123, 40, 5, 17, 93, 106, 180, 140, 245, 99, 218, 54, 2, 99, 75, 47, 154, 87, 125, 2, 77, 221, 87, 195, 216, 186, 161, 240, 95, 64, 32, 100, 32, 107, 1, 224, 185, 57, 56, 198, 142, 73, 162, 19, 143, 190, 74, 141, 38, 35, 5, 154, 86, 23, 126, 50, 137, 192, 243, 44, 166, 204, 167, 99, 241, 3, 63, 199, 161, 169, 251, 42, 82, 183, 64, 40, 42, 89, 11, 128, 80, 36, 47, 53, 54, 182, 172, 70, 107, 231, 6, 180, 116, 108, 128, 161, 166, 69, 236, 235, 33, 25, 230, 92, 83, 176, 77, 124, 20, 105, 177, 62, 126, 18, 213, 166, 110, 52, 175, 254, 4, 26, 86, 92, 137, 250, 229, 91, 197, 30, 30, 161, 76, 40, 185, 0, 196, 195, 249, 103, 49, 125, 230, 0, 172, 131, 111, 193, 54, 180, 7, 84, 152, 71, 107, 199, 6, 180, 117, 109, 68, 115, 251, 186, 178, 122, 194, 23, 202, 148, 109, 24, 230, 115, 125, 48, 143, 126, 136, 57, 247, 52, 76, 43, 34, 129, 196, 166, 158, 109, 208, 213, 116, 136, 61, 60, 130, 76, 17, 85, 0, 146, 113, 218, 62, 194, 212, 200, 126, 216, 134, 246, 96, 102, 244, 48, 234, 234, 151, 161, 165, 115, 3, 90, 59, 122, 81, 215, 208, 37, 246, 240, 36, 67, 116, 169, 209, 58, 118, 2, 214, 241, 147, 80, 86, 53, 160, 113, 197, 21, 164, 110, 129, 144, 51, 146, 18, 128, 120, 120, 110, 14, 211, 231, 14, 98, 106, 228, 93, 88, 135, 246, 32, 224, 182, 162, 181, 179, 23, 205, 29, 235, 5, 169, 91, 40, 39, 102, 166, 206, 193, 60, 118, 28, 150, 209, 126, 204, 144, 22, 105, 132, 28, 144, 172, 0, 36, 227, 157, 29, 131, 109, 104, 47, 108, 195, 123, 97, 63, 179, 31, 213, 250, 250, 72, 154, 114, 215, 70, 52, 52, 117, 23, 254, 11, 202, 132, 133, 22, 105, 253, 176, 140, 245, 147, 22, 105, 132, 37, 145, 141, 0, 36, 51, 125, 246, 32, 166, 206, 252, 17, 214, 211, 111, 97, 206, 62, 140, 150, 246, 117, 104, 110, 95, 143, 214, 206, 94, 84, 27, 26, 196, 30, 158, 100, 136, 182, 72, 51, 143, 246, 99, 210, 114, 154, 212, 45, 16, 18, 144, 173, 0, 196, 195, 122, 166, 49, 57, 188, 15, 182, 161, 61, 176, 13, 239, 131, 134, 97, 98, 37, 206, 13, 173, 171, 73, 48, 113, 158, 248, 22, 105, 150, 209, 227, 164, 69, 26, 161, 60, 4, 32, 25, 167, 185, 31, 182, 225, 119, 96, 29, 124, 11, 78, 203, 113, 212, 55, 173, 68, 75, 199, 6, 180, 119, 109, 34, 75, 141, 113, 196, 90, 164, 141, 246, 195, 58, 113, 146, 180, 72, 171, 64, 202, 82, 0, 226, 225, 185, 57, 76, 14, 255, 17, 182, 225, 189, 176, 13, 238, 133, 34, 228, 71, 107, 199, 6, 180, 118, 246, 162, 165, 115, 3, 241, 14, 226, 136, 182, 72, 51, 159, 239, 35, 45, 210, 42, 132, 178, 23, 128, 100, 220, 147, 131, 17, 49, 152, 95, 106, 172, 53, 117, 160, 165, 99, 3, 218, 150, 109, 36, 75, 141, 113, 68, 91, 164, 153, 199, 142, 195, 114, 254, 120, 66, 139, 52, 125, 195, 42, 208, 76, 21, 153, 50, 148, 1, 21, 39, 0, 241, 68, 211, 155, 173, 131, 111, 193, 58, 180, 7, 156, 219, 130, 150, 121, 239, 128, 44, 53, 38, 50, 51, 117, 14, 214, 137, 143, 96, 62, 255, 33, 188, 115, 118, 176, 172, 7, 1, 206, 15, 5, 165, 4, 173, 214, 67, 165, 174, 134, 74, 173, 135, 82, 93, 13, 70, 87, 3, 90, 165, 131, 74, 99, 4, 163, 53, 66, 169, 210, 65, 165, 53, 130, 102, 170, 160, 210, 26, 64, 171, 170, 161, 82, 87, 131, 214, 26, 136, 144, 136, 76, 69, 11, 64, 50, 62, 231, 4, 172, 131, 111, 71, 150, 26, 207, 30, 202, 185, 69, 90, 37, 194, 243, 44, 120, 206, 143, 0, 239, 71, 128, 245, 33, 192, 249, 16, 8, 248, 192, 177, 94, 4, 56, 47, 2, 129, 200, 235, 28, 235, 137, 252, 204, 249, 16, 224, 188, 224, 3, 126, 176, 108, 228, 239, 69, 66, 162, 49, 66, 165, 49, 128, 214, 84, 167, 20, 18, 149, 198, 0, 90, 93, 5, 149, 218, 0, 90, 163, 135, 74, 173, 7, 173, 209, 147, 37, 206, 60, 32, 2, 176, 4, 75, 181, 72, 35, 75, 141, 194, 146, 74, 72, 22, 68, 195, 11, 142, 245, 130, 15, 176, 139, 132, 36, 192, 122, 193, 5, 252, 224, 3, 126, 112, 172, 7, 180, 74, 3, 74, 85, 181, 72, 72, 84, 234, 136, 183, 65, 132, 36, 17, 34, 0, 89, 18, 223, 34, 109, 114, 100, 31, 84, 138, 48, 90, 58, 54, 160, 165, 115, 3, 169, 91, 144, 16, 81, 33, 225, 184, 136, 96, 68, 133, 132, 99, 61, 224, 121, 118, 222, 51, 137, 122, 42, 139, 133, 132, 99, 61, 224, 3, 126, 208, 42, 13, 104, 141, 17, 180, 90, 63, 239, 133, 44, 8, 9, 163, 173, 1, 205, 232, 202, 66, 72, 136, 0, 228, 137, 211, 246, 17, 108, 167, 223, 134, 117, 104, 15, 156, 230, 62, 152, 26, 150, 163, 117, 217, 199, 208, 220, 182, 150, 4, 19, 203, 128, 120, 33, 97, 89, 15, 120, 214, 11, 158, 231, 82, 10, 9, 199, 121, 192, 7, 184, 140, 66, 162, 154, 23, 7, 37, 163, 75, 16, 18, 149, 182, 6, 74, 149, 54, 38, 36, 106, 77, 29, 40, 181, 22, 140, 198, 56, 31, 59, 41, 222, 114, 44, 17, 0, 1, 136, 214, 45, 216, 6, 247, 194, 54, 248, 22, 66, 172, 11, 45, 203, 122, 209, 218, 209, 139, 166, 214, 11, 72, 48, 177, 130, 89, 74, 72, 88, 214, 131, 32, 207, 45, 8, 9, 235, 141, 136, 9, 231, 3, 199, 249, 22, 4, 39, 224, 7, 205, 84, 129, 86, 87, 47, 18, 18, 70, 91, 27, 241, 56, 210, 8, 9, 173, 209, 71, 188, 147, 52, 66, 66, 4, 160, 8, 120, 28, 231, 35, 193, 196, 193, 61, 176, 159, 63, 132, 154, 154, 38, 180, 46, 219, 136, 230, 142, 245, 164, 110, 129, 144, 23, 169, 132, 132, 227, 124, 243, 193, 212, 136, 144, 112, 172, 7, 1, 214, 139, 0, 239, 143, 196, 76, 230, 133, 36, 242, 26, 155, 82, 72, 136, 0, 20, 153, 232, 82, 163, 109, 100, 31, 108, 131, 111, 195, 239, 28, 67, 115, 219, 58, 180, 118, 70, 114, 15, 136, 119, 64, 40, 37, 60, 207, 194, 239, 117, 33, 20, 226, 193, 178, 30, 34, 0, 165, 198, 231, 156, 192, 244, 185, 67, 145, 186, 133, 193, 61, 208, 105, 171, 208, 210, 217, 139, 150, 246, 117, 164, 110, 129, 80, 114, 136, 0, 136, 12, 105, 145, 70, 16, 19, 34, 0, 18, 130, 243, 207, 98, 114, 232, 29, 76, 142, 252, 17, 182, 161, 61, 100, 169, 145, 80, 116, 136, 0, 72, 24, 210, 34, 141, 80, 108, 136, 0, 200, 132, 132, 165, 198, 161, 61, 8, 249, 103, 209, 220, 190, 142, 180, 72, 35, 20, 4, 17, 0, 153, 226, 113, 156, 143, 52, 65, 33, 45, 210, 8, 5, 64, 4, 160, 76, 152, 62, 123, 16, 214, 161, 183, 49, 53, 188, 47, 214, 34, 173, 109, 217, 70, 52, 181, 173, 37, 117, 11, 132, 180, 16, 1, 40, 67, 72, 139, 52, 66, 182, 16, 1, 168, 0, 72, 139, 52, 66, 58, 136, 0, 84, 24, 164, 69, 26, 33, 30, 34, 0, 21, 78, 66, 139, 180, 177, 15, 80, 91, 215, 74, 90, 164, 85, 16, 68, 0, 8, 49, 146, 91, 164, 5, 220, 86, 52, 119, 172, 71, 93, 67, 23, 12, 53, 205, 208, 85, 213, 129, 86, 169, 193, 48, 58, 208, 140, 134, 120, 11, 101, 0, 17, 0, 66, 90, 162, 117, 11, 179, 230, 227, 112, 79, 143, 128, 157, 155, 66, 192, 239, 68, 128, 157, 67, 40, 176, 80, 239, 206, 168, 171, 192, 168, 52, 80, 169, 117, 80, 49, 58, 168, 24, 45, 84, 42, 77, 228, 111, 70, 11, 38, 250, 250, 252, 177, 42, 70, 75, 132, 68, 34, 16, 1, 32, 20, 4, 231, 159, 5, 239, 119, 35, 192, 186, 231, 255, 118, 129, 103, 61, 8, 248, 93, 8, 248, 156, 8, 6, 188, 224, 124, 78, 240, 156, 39, 242, 158, 127, 46, 242, 222, 188, 144, 240, 172, 27, 225, 80, 16, 42, 70, 19, 17, 147, 20, 66, 194, 168, 171, 34, 130, 145, 66, 72, 84, 106, 45, 84, 180, 134, 8, 73, 158, 16, 1, 32, 72, 2, 214, 51, 13, 158, 243, 68, 254, 204, 11, 73, 192, 231, 138, 8, 71, 156, 144, 4, 252, 78, 240, 1, 47, 56, 239, 44, 130, 156, 55, 173, 144, 168, 213, 58, 208, 42, 77, 76, 72, 212, 234, 170, 200, 191, 213, 81, 239, 68, 23, 17, 20, 149, 182, 162, 133, 132, 8, 0, 161, 172, 136, 9, 137, 207, 21, 17, 134, 192, 92, 130, 144, 112, 126, 103, 76, 56, 162, 158, 73, 144, 157, 139, 8, 9, 231, 77, 16, 18, 245, 188, 231, 17, 21, 17, 38, 234, 149, 48, 90, 208, 140, 166, 44, 132, 132, 8, 0, 129, 144, 4, 207, 205, 33, 24, 240, 39, 8, 73, 128, 117, 130, 103, 61, 17, 1, 137, 254, 157, 78, 72, 230, 167, 68, 0, 192, 196, 98, 32, 218, 121, 143, 100, 177, 144, 48, 76, 220, 20, 71, 21, 141, 155, 84, 129, 166, 153, 162, 215, 120, 16, 1, 32, 16, 138, 68, 182, 66, 18, 152, 23, 19, 206, 231, 4, 207, 186, 17, 240, 187, 16, 228, 60, 105, 133, 68, 197, 232, 98, 65, 212, 168, 88, 40, 85, 76, 94, 66, 66, 4, 128, 64, 144, 56, 201, 66, 194, 249, 102, 17, 96, 221, 8, 6, 124, 25, 133, 132, 103, 61, 145, 159, 211, 8, 9, 17, 0, 2, 161, 66, 136, 10, 9, 231, 153, 65, 136, 103, 193, 249, 102, 137, 0, 16, 8, 149, 12, 37, 246, 0, 8, 4, 130, 120, 16, 1, 32, 16, 42, 24, 34, 0, 4, 66, 5, 67, 4, 128, 64, 168, 96, 136, 0, 16, 8, 21, 12, 17, 0, 2, 161, 130, 161, 197, 30, 0, 129, 16, 101, 192, 113, 2, 78, 191, 3, 0, 64, 81, 74, 208, 97, 26, 148, 82, 1, 53, 173, 5, 173, 160, 161, 85, 234, 208, 174, 239, 20, 123, 152, 101, 5, 201, 3, 32, 136, 206, 128, 227, 4, 166, 125, 147, 240, 243, 190, 172, 142, 167, 20, 20, 40, 5, 5, 5, 148, 80, 82, 20, 40, 80, 80, 42, 148, 160, 40, 37, 40, 80, 160, 41, 37, 104, 74, 5, 37, 69, 67, 163, 212, 194, 200, 212, 160, 78, 99, 18, 251, 107, 74, 18, 226, 1, 16, 68, 35, 87, 195, 143, 18, 10, 135, 16, 10, 135, 0, 240, 8, 132, 178, 255, 28, 77, 69, 110, 119, 74, 161, 140, 136, 72, 156, 112, 68, 189, 13, 154, 82, 65, 173, 212, 84, 140, 183, 65, 60, 0, 66, 201, 201, 215, 240, 197, 32, 87, 111, 163, 90, 85, 141, 6, 109, 147, 216, 195, 206, 26, 226, 1, 16, 74, 134, 156, 12, 63, 74, 185, 123, 27, 196, 3, 32, 20, 29, 57, 26, 190, 24, 228, 228, 109, 64, 141, 106, 141, 161, 96, 111, 131, 120, 0, 132, 162, 81, 206, 134, 207, 7, 120, 76, 78, 78, 161, 177, 177, 1, 180, 138, 70, 40, 20, 134, 195, 49, 3, 214, 207, 65, 167, 211, 193, 104, 212, 67, 65, 229, 182, 202, 158, 179, 183, 17, 169, 240, 205, 217, 219, 48, 168, 141, 48, 48, 145, 254, 0, 196, 3, 32, 8, 78, 57, 27, 126, 148, 57, 183, 27, 44, 199, 65, 205, 48, 168, 214, 235, 49, 235, 112, 2, 0, 12, 70, 3, 92, 78, 23, 0, 160, 166, 86, 186, 59, 54, 71, 189, 13, 146, 8, 68, 16, 140, 1, 199, 9, 236, 55, 239, 193, 184, 251, 124, 89, 27, 63, 0, 120, 61, 126, 24, 13, 70, 120, 61, 254, 200, 191, 189, 94, 24, 140, 6, 80, 148, 2, 6, 163, 1, 94, 175, 87, 236, 33, 46, 73, 40, 28, 2, 31, 226, 201, 20, 128, 80, 56, 149, 240, 196, 143, 135, 15, 240, 80, 170, 40, 208, 42, 26, 74, 21, 5, 62, 192, 47, 58, 134, 202, 209, 253, 23, 11, 34, 0, 132, 188, 17, 218, 240, 139, 49, 175, 46, 6, 126, 191, 15, 106, 134, 1, 0, 168, 25, 6, 126, 191, 15, 58, 157, 14, 46, 167, 43, 54, 5, 208, 104, 52, 98, 15, 51, 43, 196, 191, 154, 4, 217, 81, 44, 87, 223, 239, 247, 65, 173, 137, 24, 20, 0, 184, 156, 46, 40, 41, 26, 205, 45, 205, 0, 0, 167, 211, 45, 246, 87, 7, 16, 113, 255, 53, 26, 45, 0, 64, 163, 209, 194, 235, 241, 195, 96, 52, 32, 24, 226, 97, 181, 88, 17, 12, 241, 48, 24, 13, 98, 15, 51, 43, 136, 7, 64, 200, 154, 98, 187, 250, 94, 143, 31, 117, 166, 90, 204, 216, 29, 168, 214, 235, 225, 245, 122, 209, 220, 210, 28, 155, 87, 91, 45, 86, 73, 4, 214, 248, 96, 196, 83, 137, 135, 162, 20, 48, 153, 76, 48, 79, 88, 96, 50, 201, 39, 237, 152, 8, 0, 33, 35, 165, 152, 227, 203, 105, 94, 221, 218, 214, 2, 0, 48, 79, 88, 98, 63, 203, 21, 34, 0, 132, 180, 148, 50, 184, 87, 78, 243, 106, 57, 65, 4, 128, 176, 8, 49, 162, 250, 81, 247, 31, 136, 204, 171, 103, 236, 14, 212, 55, 214, 195, 225, 152, 129, 213, 98, 133, 90, 195, 160, 182, 182, 78, 236, 75, 83, 118, 144, 68, 32, 66, 12, 49, 151, 243, 204, 19, 150, 69, 175, 149, 147, 171, 45, 4, 233, 86, 69, 146, 87, 79, 114, 129, 120, 0, 4, 73, 172, 227, 19, 99, 207, 204, 194, 170, 72, 29, 92, 78, 23, 156, 78, 55, 106, 106, 141, 9, 171, 39, 213, 42, 125, 78, 231, 148, 70, 84, 133, 32, 10, 149, 148, 185, 87, 14, 164, 203, 54, 76, 206, 74, 204, 5, 226, 1, 84, 32, 82, 120, 226, 19, 10, 135, 154, 119, 255, 147, 87, 79, 114, 153, 6, 144, 24, 64, 5, 65, 12, 95, 222, 164, 42, 56, 162, 233, 136, 19, 95, 173, 215, 99, 206, 237, 142, 253, 156, 45, 68, 0, 42, 0, 98, 248, 229, 65, 124, 16, 48, 186, 42, 50, 61, 57, 141, 58, 83, 45, 104, 21, 13, 62, 192, 99, 198, 238, 64, 99, 115, 67, 194, 231, 194, 161, 16, 154, 221, 141, 9, 175, 241, 170, 32, 0, 50, 5, 40, 107, 136, 225, 151, 23, 169, 178, 13, 83, 101, 37, 46, 194, 27, 68, 107, 221, 10, 212, 183, 45, 8, 3, 207, 205, 97, 206, 21, 34, 2, 80, 142, 16, 195, 175, 28, 178, 90, 61, 209, 41, 17, 240, 37, 222, 11, 52, 83, 141, 154, 122, 226, 1, 148, 21, 196, 240, 9, 169, 80, 80, 20, 248, 32, 159, 242, 61, 34, 0, 101, 0, 49, 124, 66, 190, 16, 1, 144, 57, 135, 44, 251, 225, 14, 184, 196, 30, 6, 161, 132, 164, 114, 245, 51, 37, 79, 133, 66, 169, 61, 0, 146, 8, 36, 115, 124, 65, 105, 183, 158, 34, 72, 27, 34, 0, 50, 102, 220, 61, 10, 62, 141, 178, 19, 8, 217, 64, 4, 64, 198, 204, 114, 51, 98, 15, 129, 32, 115, 136, 0, 200, 24, 63, 159, 123, 238, 55, 161, 50, 225, 130, 108, 202, 215, 137, 0, 200, 24, 54, 72, 4, 32, 19, 161, 80, 24, 118, 187, 29, 230, 9, 11, 102, 29, 78, 132, 67, 145, 29, 55, 248, 0, 15, 243, 132, 37, 101, 231, 161, 74, 130, 8, 128, 140, 153, 117, 146, 27, 58, 19, 233, 26, 139, 38, 55, 32, 173, 84, 136, 0, 200, 24, 46, 228, 35, 55, 116, 6, 138, 81, 66, 43, 71, 248, 112, 32, 229, 235, 68, 0, 100, 202, 89, 215, 8, 116, 85, 213, 21, 123, 67, 231, 75, 186, 18, 218, 114, 39, 0, 34, 0, 101, 133, 39, 16, 121, 242, 87, 234, 13, 157, 45, 209, 198, 162, 161, 80, 56, 214, 88, 52, 85, 3, 210, 74, 133, 8, 128, 76, 241, 242, 94, 114, 67, 103, 65, 170, 13, 59, 82, 109, 236, 81, 238, 240, 170, 32, 102, 167, 23, 103, 140, 18, 1, 144, 41, 211, 147, 230, 138, 190, 161, 179, 37, 90, 66, 11, 0, 38, 147, 9, 20, 165, 136, 149, 208, 154, 39, 44, 152, 156, 156, 74, 91, 40, 83, 78, 76, 135, 38, 83, 190, 78, 106, 1, 100, 138, 39, 196, 229, 86, 19, 94, 36, 138, 209, 169, 182, 216, 84, 100, 3, 82, 154, 2, 207, 46, 206, 5, 32, 30, 128, 76, 241, 251, 23, 158, 242, 173, 109, 45, 177, 27, 57, 254, 231, 82, 64, 150, 217, 100, 2, 163, 128, 223, 51, 183, 232, 101, 34, 0, 50, 100, 200, 121, 26, 161, 249, 245, 127, 177, 33, 203, 108, 242, 64, 65, 81, 8, 135, 23, 119, 255, 35, 2, 32, 67, 60, 156, 116, 203, 127, 165, 186, 42, 145, 79, 9, 109, 33, 72, 49, 3, 49, 85, 73, 48, 17, 0, 25, 194, 133, 2, 37, 191, 161, 211, 65, 150, 217, 82, 35, 151, 169, 17, 17, 0, 25, 34, 165, 26, 0, 178, 204, 150, 26, 185, 76, 141, 164, 21, 158, 37, 100, 5, 31, 10, 20, 126, 18, 129, 200, 187, 83, 109, 133, 33, 196, 38, 30, 197, 128, 8, 128, 204, 152, 241, 219, 37, 223, 4, 164, 34, 151, 217, 146, 72, 181, 181, 121, 170, 169, 81, 174, 123, 249, 21, 66, 170, 146, 96, 50, 5, 144, 25, 54, 159, 165, 240, 147, 16, 138, 142, 92, 166, 70, 196, 3, 144, 25, 44, 233, 252, 43, 11, 228, 50, 53, 34, 2, 32, 51, 164, 218, 5, 72, 42, 171, 18, 82, 70, 236, 169, 81, 170, 146, 96, 50, 5, 144, 25, 129, 176, 116, 2, 128, 4, 121, 145, 170, 36, 152, 8, 128, 204, 144, 210, 10, 0, 65, 254, 16, 1, 144, 17, 22, 239, 132, 228, 87, 0, 8, 137, 72, 105, 106, 196, 171, 130, 224, 185, 196, 122, 0, 34, 0, 50, 194, 230, 49, 139, 61, 4, 130, 140, 9, 168, 188, 152, 115, 37, 214, 144, 144, 32, 160, 76, 56, 106, 61, 12, 59, 39, 173, 8, 50, 65, 94, 56, 217, 185, 69, 37, 193, 68, 0, 100, 192, 97, 219, 1, 56, 57, 135, 216, 195, 32, 72, 156, 76, 189, 25, 26, 76, 245, 139, 183, 9, 23, 123, 208, 132, 165, 169, 228, 205, 63, 127, 254, 244, 175, 48, 126, 102, 233, 105, 79, 199, 202, 54, 108, 127, 244, 127, 136, 61, 84, 73, 176, 80, 128, 84, 7, 151, 211, 5, 167, 211, 141, 154, 90, 99, 172, 0, 137, 13, 248, 193, 211, 137, 49, 36, 34, 0, 18, 197, 197, 57, 241, 145, 253, 120, 197, 26, 127, 40, 20, 198, 96, 255, 8, 206, 14, 140, 46, 121, 92, 128, 35, 65, 209, 40, 94, 175, 23, 205, 45, 205, 177, 2, 36, 171, 197, 138, 154, 218, 72, 225, 81, 157, 169, 22, 51, 118, 7, 66, 122, 34, 0, 146, 103, 198, 111, 199, 192, 76, 63, 188, 188, 71, 236, 161, 136, 134, 203, 233, 130, 66, 161, 0, 0, 236, 220, 185, 19, 91, 182, 108, 73, 120, 255, 224, 193, 131, 120, 244, 209, 71, 197, 30, 166, 164, 73, 85, 128, 148, 12, 17, 0, 137, 97, 241, 78, 96, 120, 246, 52, 252, 21, 158, 242, 235, 245, 122, 161, 84, 42, 1, 0, 87, 95, 125, 245, 34, 1, 80, 171, 213, 98, 15, 81, 114, 100, 83, 128, 148, 92, 16, 68, 4, 64, 66, 140, 187, 71, 49, 226, 26, 76, 187, 145, 35, 129, 176, 20, 6, 163, 1, 14, 199, 12, 172, 22, 43, 212, 26, 6, 181, 181, 117, 152, 158, 156, 70, 157, 169, 22, 64, 164, 0, 105, 118, 102, 22, 227, 131, 103, 65, 205, 183, 7, 19, 77, 0, 44, 222, 9, 76, 184, 199, 176, 194, 216, 131, 58, 141, 73, 236, 107, 39, 58, 103, 93, 35, 56, 231, 26, 38, 137, 62, 243, 232, 116, 58, 4, 131, 65, 177, 135, 33, 43, 178, 41, 64, 82, 53, 48, 216, 220, 189, 60, 246, 111, 81, 4, 96, 198, 111, 143, 185, 185, 31, 78, 59, 209, 92, 213, 134, 53, 181, 235, 69, 190, 124, 226, 49, 228, 60, 141, 113, 247, 57, 98, 252, 113, 24, 140, 134, 148, 77, 44, 9, 185, 145, 92, 128, 100, 80, 27, 19, 222, 23, 69, 0, 6, 102, 250, 99, 115, 92, 62, 196, 99, 220, 125, 30, 78, 191, 3, 203, 140, 43, 208, 162, 107, 19, 249, 146, 149, 248, 90, 56, 78, 192, 234, 33, 41, 190, 201, 80, 148, 2, 52, 77, 102, 168, 197, 166, 228, 169, 192, 135, 44, 251, 83, 70, 183, 221, 1, 23, 78, 205, 156, 192, 9, 123, 159, 216, 215, 164, 100, 12, 56, 78, 192, 60, 55, 86, 144, 241, 75, 177, 251, 44, 65, 62, 148, 84, 98, 15, 219, 14, 44, 185, 174, 205, 135, 120, 88, 60, 19, 112, 176, 51, 104, 215, 118, 96, 121, 109, 143, 216, 215, 167, 104, 156, 176, 247, 193, 230, 181, 32, 20, 46, 172, 191, 127, 166, 228, 143, 82, 183, 157, 34, 136, 207, 82, 5, 72, 20, 40, 88, 92, 67, 177, 215, 75, 230, 1, 252, 201, 118, 8, 78, 54, 187, 116, 86, 235, 148, 21, 125, 230, 15, 208, 55, 117, 164, 84, 195, 43, 41, 125, 83, 71, 96, 241, 76, 20, 108, 252, 128, 124, 186, 207, 18, 164, 129, 46, 204, 193, 225, 181, 196, 254, 148, 68, 0, 250, 166, 142, 192, 193, 218, 179, 62, 222, 235, 245, 66, 87, 85, 141, 41, 223, 36, 222, 25, 127, 11, 3, 142, 19, 162, 93, 48, 161, 57, 106, 61, 140, 41, 223, 100, 225, 39, 74, 131, 84, 55, 230, 32, 72, 147, 162, 11, 192, 9, 123, 95, 65, 55, 60, 203, 251, 113, 116, 232, 79, 56, 100, 217, 143, 25, 127, 246, 34, 34, 69, 14, 219, 14, 8, 94, 209, 71, 54, 230, 32, 20, 66, 81, 5, 96, 192, 113, 2, 22, 207, 68, 206, 159, 75, 190, 169, 25, 53, 3, 203, 204, 4, 14, 141, 238, 151, 173, 55, 112, 200, 178, 63, 235, 41, 80, 46, 200, 165, 251, 44, 65, 154, 20, 45, 8, 24, 141, 112, 231, 67, 186, 140, 38, 141, 73, 139, 113, 247, 121, 204, 248, 166, 177, 162, 166, 71, 22, 75, 134, 197, 46, 234, 145, 75, 247, 89, 130, 52, 41, 138, 0, 12, 57, 79, 195, 60, 55, 150, 119, 144, 43, 155, 155, 122, 204, 54, 138, 245, 157, 189, 88, 111, 218, 88, 250, 171, 150, 37, 98, 21, 245, 136, 221, 125, 182, 156, 200, 84, 99, 223, 216, 216, 32, 250, 238, 62, 133, 32, 248, 20, 224, 172, 107, 4, 227, 238, 115, 130, 68, 184, 227, 137, 223, 247, 190, 181, 173, 5, 38, 147, 9, 22, 207, 4, 222, 51, 191, 131, 113, 247, 104, 129, 103, 23, 30, 139, 119, 2, 39, 103, 62, 172, 232, 138, 190, 114, 64, 46, 155, 124, 230, 139, 160, 2, 48, 238, 30, 45, 121, 62, 187, 151, 247, 224, 244, 236, 73, 73, 45, 25, 142, 187, 71, 49, 232, 24, 168, 248, 138, 190, 114, 160, 220, 151, 89, 5, 19, 128, 41, 159, 13, 35, 174, 65, 81, 82, 90, 67, 225, 16, 166, 124, 147, 216, 111, 222, 131, 179, 174, 145, 146, 255, 254, 120, 206, 186, 70, 48, 228, 28, 40, 121, 69, 159, 148, 186, 207, 150, 51, 229, 182, 204, 42, 136, 0, 204, 248, 237, 56, 229, 56, 41, 248, 77, 159, 235, 77, 237, 231, 125, 56, 227, 28, 196, 159, 108, 135, 4, 190, 76, 217, 49, 228, 60, 77, 42, 250, 202, 140, 114, 95, 102, 21, 68, 0, 226, 139, 123, 196, 38, 20, 14, 193, 193, 218, 177, 111, 226, 45, 12, 205, 158, 42, 217, 239, 29, 112, 156, 192, 168, 235, 12, 49, 254, 50, 163, 220, 151, 89, 11, 14, 95, 166, 43, 238, 17, 27, 46, 200, 226, 156, 107, 4, 124, 152, 47, 122, 169, 113, 223, 212, 17, 156, 60, 123, 18, 140, 154, 41, 187, 40, 113, 165, 83, 238, 203, 172, 5, 121, 0, 153, 138, 123, 42, 129, 163, 214, 195, 56, 59, 57, 130, 250, 198, 122, 0, 229, 23, 37, 38, 44, 38, 121, 69, 74, 206, 177, 150, 188, 5, 32, 151, 226, 30, 49, 169, 215, 52, 20, 237, 220, 125, 83, 71, 112, 242, 252, 9, 232, 170, 170, 203, 54, 74, 76, 40, 111, 242, 18, 128, 92, 139, 123, 196, 130, 166, 104, 52, 104, 155, 138, 114, 238, 104, 81, 79, 40, 148, 152, 239, 80, 110, 81, 98, 66, 121, 147, 151, 0, 84, 49, 6, 168, 40, 233, 119, 101, 165, 20, 202, 162, 156, 55, 190, 168, 167, 220, 163, 196, 132, 8, 197, 94, 102, 21, 171, 177, 75, 94, 2, 208, 99, 92, 141, 222, 250, 77, 208, 209, 85, 146, 238, 58, 67, 43, 132, 15, 190, 37, 23, 245, 148, 123, 148, 56, 27, 146, 111, 210, 116, 55, 51, 33, 61, 98, 101, 28, 230, 29, 3, 168, 211, 152, 240, 241, 214, 171, 177, 97, 121, 47, 66, 156, 8, 87, 44, 11, 84, 74, 70, 176, 115, 185, 56, 103, 202, 109, 186, 162, 81, 98, 0, 48, 153, 76, 160, 40, 69, 44, 74, 108, 158, 176, 96, 114, 114, 10, 124, 80, 154, 2, 41, 20, 201, 55, 105, 186, 155, 153, 144, 30, 177, 50, 14, 11, 206, 3, 184, 176, 249, 18, 172, 109, 91, 15, 29, 93, 85, 218, 43, 150, 5, 12, 165, 18, 228, 60, 51, 126, 59, 250, 167, 143, 101, 189, 226, 81, 78, 81, 226, 108, 72, 190, 73, 211, 221, 204, 132, 236, 41, 85, 44, 73, 144, 68, 160, 229, 134, 149, 248, 120, 235, 213, 104, 208, 54, 130, 82, 148, 188, 207, 104, 90, 170, 4, 232, 133, 71, 138, 122, 150, 38, 155, 155, 148, 162, 164, 115, 79, 72, 21, 177, 98, 73, 130, 254, 207, 108, 108, 216, 140, 78, 195, 10, 201, 4, 8, 77, 2, 44, 1, 218, 125, 83, 146, 201, 114, 148, 34, 169, 110, 210, 84, 55, 51, 97, 105, 196, 138, 37, 9, 46, 205, 209, 0, 161, 94, 101, 40, 254, 85, 91, 2, 154, 162, 5, 217, 113, 200, 207, 103, 119, 209, 43, 181, 24, 39, 213, 77, 154, 234, 102, 38, 44, 141, 88, 177, 164, 162, 228, 168, 214, 105, 76, 216, 210, 114, 197, 124, 63, 64, 91, 73, 243, 227, 163, 13, 28, 148, 20, 13, 180, 23, 126, 62, 62, 20, 40, 217, 216, 229, 72, 170, 180, 216, 84, 233, 179, 132, 220, 41, 69, 99, 151, 162, 38, 169, 175, 55, 109, 196, 184, 123, 20, 231, 221, 103, 74, 54, 135, 142, 70, 160, 91, 27, 90, 5, 57, 95, 32, 76, 4, 96, 41, 72, 247, 33, 121, 83, 244, 232, 76, 187, 190, 179, 164, 1, 194, 104, 4, 90, 168, 37, 192, 32, 89, 195, 38, 148, 49, 37, 11, 207, 150, 58, 64, 168, 163, 117, 130, 156, 39, 72, 60, 0, 66, 9, 41, 117, 44, 169, 164, 235, 51, 165, 8, 16, 70, 35, 208, 106, 101, 225, 145, 103, 139, 87, 152, 221, 123, 42, 1, 177, 130, 160, 28, 199, 145, 61, 17, 11, 160, 228, 11, 180, 209, 0, 97, 75, 85, 27, 104, 74, 248, 16, 68, 52, 2, 45, 196, 18, 224, 172, 12, 170, 29, 43, 29, 134, 137, 36, 123, 145, 50, 236, 252, 16, 45, 67, 99, 189, 105, 35, 122, 140, 107, 4, 207, 32, 164, 40, 5, 154, 26, 154, 4, 89, 2, 100, 201, 250, 191, 12, 32, 101, 216, 133, 32, 106, 138, 86, 177, 2, 132, 180, 64, 41, 192, 28, 89, 2, 148, 13, 164, 12, 59, 63, 36, 145, 163, 41, 116, 128, 80, 165, 16, 70, 0, 66, 161, 160, 152, 151, 37, 143, 241, 138, 83, 82, 42, 46, 164, 12, 187, 16, 36, 33, 0, 128, 176, 1, 66, 161, 60, 0, 185, 229, 0, 148, 251, 38, 22, 169, 224, 184, 64, 197, 150, 97, 11, 129, 100, 4, 0, 16, 46, 64, 168, 161, 133, 201, 61, 151, 91, 22, 160, 216, 155, 88, 240, 1, 30, 167, 250, 134, 75, 254, 189, 133, 74, 157, 61, 117, 108, 168, 76, 189, 164, 244, 72, 74, 0, 162, 20, 26, 32, 84, 211, 90, 65, 198, 33, 247, 22, 223, 165, 156, 23, 7, 184, 0, 190, 115, 223, 15, 241, 248, 157, 223, 195, 177, 247, 250, 69, 249, 190, 133, 148, 97, 31, 123, 175, 31, 143, 111, 255, 62, 190, 115, 223, 15, 43, 74, 4, 36, 41, 0, 64, 97, 1, 194, 38, 109, 115, 193, 191, 95, 236, 29, 134, 242, 65, 172, 146, 210, 0, 23, 192, 83, 247, 62, 139, 15, 15, 158, 4, 31, 224, 241, 204, 95, 63, 39, 154, 8, 228, 195, 177, 247, 250, 241, 204, 95, 63, 7, 62, 192, 227, 195, 131, 39, 241, 157, 251, 126, 136, 0, 39, 47, 239, 47, 95, 36, 43, 0, 81, 114, 13, 16, 210, 20, 13, 3, 99, 44, 248, 247, 122, 2, 242, 235, 98, 35, 70, 73, 41, 235, 231, 240, 212, 189, 207, 226, 228, 145, 83, 88, 182, 108, 25, 190, 254, 245, 175, 203, 74, 4, 226, 141, 223, 100, 50, 129, 97, 24, 124, 120, 240, 36, 158, 186, 247, 217, 138, 16, 1, 201, 11, 0, 144, 91, 128, 80, 168, 0, 160, 220, 230, 255, 64, 233, 75, 74, 89, 63, 135, 39, 239, 222, 133, 147, 71, 78, 161, 189, 189, 29, 239, 190, 251, 46, 118, 237, 218, 133, 103, 159, 125, 54, 38, 2, 253, 135, 7, 138, 250, 157, 25, 102, 113, 205, 71, 182, 174, 127, 188, 241, 223, 124, 243, 205, 176, 217, 108, 216, 183, 111, 31, 170, 170, 170, 112, 242, 200, 41, 60, 117, 239, 179, 96, 253, 18, 237, 119, 39, 16, 178, 16, 0, 32, 251, 0, 161, 80, 75, 128, 229, 146, 3, 80, 172, 246, 100, 62, 175, 31, 79, 222, 189, 11, 167, 251, 134, 209, 222, 222, 142, 253, 251, 247, 163, 189, 61, 82, 127, 253, 192, 3, 15, 196, 68, 224, 233, 175, 254, 160, 232, 34, 144, 15, 241, 198, 127, 235, 173, 183, 226, 213, 87, 95, 133, 82, 169, 196, 150, 45, 91, 176, 111, 223, 62, 24, 12, 6, 156, 60, 114, 10, 79, 222, 189, 171, 172, 69, 64, 54, 2, 16, 37, 83, 128, 80, 176, 37, 192, 96, 249, 254, 167, 23, 138, 207, 235, 199, 19, 219, 191, 159, 96, 252, 93, 93, 93, 9, 199, 68, 69, 32, 192, 5, 36, 39, 2, 253, 135, 7, 18, 140, 255, 165, 151, 94, 130, 82, 185, 208, 66, 254, 162, 139, 46, 194, 254, 253, 251, 81, 83, 83, 131, 211, 125, 195, 120, 242, 238, 93, 240, 121, 203, 115, 73, 81, 118, 2, 0, 44, 29, 32, 20, 108, 9, 48, 92, 57, 145, 224, 92, 57, 221, 55, 140, 225, 19, 103, 1, 0, 59, 118, 236, 88, 100, 252, 81, 164, 40, 2, 253, 135, 7, 240, 244, 87, 127, 144, 96, 252, 10, 133, 98, 209, 113, 189, 189, 189, 120, 244, 209, 71, 99, 223, 119, 232, 248, 25, 177, 135, 94, 20, 100, 41, 0, 81, 162, 1, 66, 70, 185, 16, 32, 20, 162, 17, 40, 0, 132, 194, 242, 202, 2, 140, 167, 216, 149, 121, 27, 47, 91, 143, 135, 118, 221, 11, 74, 73, 225, 129, 7, 30, 192, 27, 111, 188, 145, 246, 88, 41, 137, 64, 212, 248, 3, 92, 96, 73, 227, 7, 128, 215, 94, 123, 13, 143, 62, 250, 40, 40, 37, 133, 135, 118, 221, 139, 222, 45, 107, 83, 30, 39, 247, 61, 17, 100, 45, 0, 64, 36, 64, 184, 193, 20, 9, 16, 82, 10, 10, 203, 13, 43, 11, 62, 167, 139, 115, 22, 156, 3, 32, 247, 27, 35, 19, 91, 175, 187, 24, 127, 243, 221, 191, 66, 40, 28, 194, 77, 55, 221, 36, 121, 17, 200, 213, 248, 111, 185, 229, 22, 132, 194, 33, 252, 205, 119, 255, 10, 91, 175, 187, 56, 237, 121, 229, 190, 39, 130, 236, 5, 0, 88, 8, 16, 118, 234, 151, 11, 114, 62, 155, 207, 154, 242, 117, 69, 128, 135, 225, 131, 97, 180, 253, 234, 109, 172, 248, 238, 43, 184, 224, 161, 95, 96, 245, 55, 127, 137, 21, 207, 188, 130, 182, 95, 189, 13, 195, 7, 195, 80, 204, 27, 188, 220, 111, 140, 108, 136, 138, 64, 48, 24, 196, 77, 55, 221, 132, 215, 94, 123, 45, 237, 177, 15, 60, 240, 0, 118, 238, 220, 41, 138, 8, 196, 27, 255, 237, 183, 223, 158, 149, 241, 135, 17, 206, 104, 252, 128, 252, 247, 68, 40, 11, 1, 136, 210, 83, 115, 129, 32, 231, 73, 85, 6, 92, 253, 209, 40, 86, 62, 253, 47, 104, 255, 167, 63, 192, 120, 100, 16, 154, 9, 59, 40, 142, 135, 210, 23, 128, 198, 108, 135, 241, 200, 32, 218, 255, 233, 15, 88, 249, 244, 191, 160, 250, 163, 81, 217, 223, 24, 217, 178, 245, 186, 139, 113, 255, 211, 119, 34, 24, 12, 226, 150, 91, 110, 89, 82, 4, 118, 236, 216, 145, 32, 2, 3, 199, 6, 139, 62, 190, 100, 227, 127, 225, 133, 23, 4, 51, 254, 114, 216, 19, 65, 218, 163, 19, 137, 228, 86, 224, 245, 111, 126, 128, 206, 159, 190, 1, 102, 102, 46, 227, 103, 153, 153, 57, 116, 254, 244, 13, 44, 63, 120, 74, 214, 55, 70, 46, 92, 121, 227, 86, 220, 255, 244, 157, 8, 133, 66, 57, 137, 192, 83, 247, 60, 91, 84, 17, 200, 197, 248, 95, 126, 249, 229, 156, 140, 31, 40, 143, 61, 17, 202, 231, 46, 20, 144, 248, 36, 160, 250, 55, 63, 64, 227, 27, 135, 115, 62, 199, 178, 189, 39, 80, 255, 230, 7, 178, 189, 49, 114, 37, 31, 17, 96, 125, 108, 209, 68, 96, 224, 216, 96, 78, 198, 127, 219, 109, 183, 229, 100, 252, 64, 121, 236, 137, 64, 191, 49, 240, 107, 180, 212, 182, 160, 86, 103, 18, 36, 128, 38, 22, 238, 57, 15, 94, 248, 231, 215, 240, 238, 193, 163, 176, 216, 166, 10, 63, 33, 128, 13, 138, 0, 30, 81, 185, 129, 52, 55, 78, 38, 26, 223, 56, 140, 185, 118, 19, 70, 77, 85, 168, 111, 172, 135, 195, 49, 3, 171, 197, 10, 181, 134, 65, 109, 109, 157, 216, 151, 76, 112, 174, 188, 113, 43, 0, 224, 71, 143, 253, 18, 183, 220, 114, 11, 94, 124, 241, 69, 220, 122, 235, 173, 41, 143, 221, 177, 99, 7, 0, 224, 145, 71, 30, 193, 83, 247, 60, 139, 191, 253, 201, 131, 88, 179, 105, 149, 32, 227, 24, 56, 54, 136, 167, 238, 137, 4, 29, 183, 111, 223, 142, 159, 253, 236, 103, 25, 141, 95, 65, 41, 114, 50, 126, 64, 62, 123, 34, 68, 247, 202, 96, 253, 28, 140, 29, 106, 84, 211, 11, 217, 147, 148, 219, 239, 196, 121, 251, 25, 12, 207, 158, 194, 222, 241, 55, 241, 158, 249, 29, 28, 181, 30, 198, 144, 243, 180, 216, 227, 206, 137, 127, 124, 241, 63, 240, 234, 235, 111, 46, 105, 252, 52, 157, 125, 137, 49, 131, 48, 238, 86, 121, 210, 222, 56, 217, 210, 250, 210, 59, 8, 249, 217, 148, 105, 186, 229, 72, 188, 39, 112, 219, 109, 183, 225, 229, 151, 95, 78, 123, 108, 49, 60, 129, 168, 241, 179, 62, 22, 219, 183, 111, 199, 207, 127, 254, 243, 140, 198, 15, 32, 103, 227, 7, 228, 179, 9, 108, 114, 0, 58, 30, 218, 104, 48, 98, 198, 238, 64, 181, 94, 15, 62, 196, 131, 15, 241, 240, 194, 3, 59, 55, 133, 113, 247, 57, 208, 148, 10, 90, 165, 14, 85, 76, 53, 214, 212, 174, 23, 251, 187, 164, 229, 205, 183, 222, 77, 255, 166, 174, 26, 45, 219, 191, 129, 150, 143, 93, 136, 41, 127, 16, 142, 131, 123, 225, 126, 233, 121, 40, 150, 200, 139, 191, 68, 193, 193, 164, 8, 167, 125, 127, 142, 15, 194, 50, 95, 44, 210, 194, 168, 80, 77, 43, 83, 30, 167, 113, 251, 176, 122, 210, 11, 103, 151, 216, 87, 168, 116, 196, 123, 2, 81, 3, 91, 202, 19, 96, 89, 22, 79, 60, 241, 68, 193, 158, 64, 42, 227, 79, 199, 238, 221, 187, 113, 199, 29, 119, 0, 0, 238, 127, 250, 206, 156, 141, 95, 78, 120, 189, 94, 52, 183, 52, 131, 162, 20, 9, 79, 127, 0, 160, 227, 3, 85, 180, 42, 241, 9, 25, 21, 4, 63, 239, 131, 131, 181, 195, 60, 55, 6, 70, 169, 134, 90, 169, 129, 158, 49, 160, 73, 219, 34, 72, 243, 77, 33, 112, 123, 210, 68, 213, 181, 85, 104, 186, 243, 27, 168, 89, 191, 9, 238, 64, 16, 246, 183, 126, 13, 239, 235, 47, 64, 145, 97, 29, 126, 147, 50, 125, 45, 192, 144, 215, 143, 253, 206, 196, 157, 142, 174, 48, 86, 161, 71, 151, 122, 94, 95, 125, 242, 28, 156, 151, 8, 227, 222, 202, 133, 92, 68, 224, 241, 199, 31, 135, 223, 239, 199, 51, 207, 60, 147, 183, 8, 20, 98, 252, 209, 177, 86, 34, 52, 176, 16, 193, 172, 206, 144, 69, 23, 10, 135, 224, 231, 125, 240, 243, 62, 56, 89, 7, 198, 221, 231, 35, 130, 64, 169, 81, 205, 232, 97, 210, 54, 160, 69, 215, 38, 246, 119, 138, 161, 168, 50, 96, 217, 87, 31, 5, 213, 189, 1, 254, 96, 16, 83, 7, 246, 194, 243, 250, 238, 140, 198, 15, 0, 203, 21, 169, 51, 1, 231, 248, 224, 34, 227, 7, 128, 253, 78, 79, 90, 79, 64, 59, 110, 23, 251, 82, 136, 66, 178, 8, 112, 28, 135, 219, 111, 191, 61, 229, 177, 59, 119, 238, 4, 128, 152, 8, 60, 241, 139, 135, 179, 254, 61, 241, 198, 255, 181, 175, 125, 13, 207, 61, 247, 92, 218, 99, 139, 97, 252, 82, 223, 24, 54, 26, 128, 54, 24, 13, 152, 227, 185, 196, 24, 0, 80, 88, 157, 56, 23, 100, 225, 14, 184, 96, 241, 76, 224, 196, 116, 31, 222, 25, 127, 11, 135, 44, 251, 209, 55, 117, 4, 227, 238, 81, 209, 190, 180, 66, 163, 67, 199, 87, 30, 132, 113, 213, 90, 240, 60, 15, 219, 127, 189, 14, 247, 139, 207, 67, 145, 101, 134, 95, 157, 34, 181, 72, 156, 91, 162, 50, 204, 146, 166, 126, 156, 158, 93, 72, 250, 145, 210, 141, 33, 20, 75, 101, 61, 246, 94, 182, 22, 247, 125, 251, 43, 0, 128, 59, 238, 184, 3, 187, 119, 239, 78, 123, 158, 157, 59, 119, 70, 166, 4, 62, 22, 223, 186, 107, 23, 134, 250, 51, 231, 223, 15, 30, 31, 17, 213, 248, 229, 64, 252, 202, 68, 50, 148, 208, 117, 226, 129, 80, 68, 16, 166, 124, 147, 24, 112, 244, 199, 2, 139, 125, 83, 71, 74, 215, 101, 167, 74, 143, 186, 175, 125, 11, 117, 31, 219, 12, 132, 21, 152, 57, 184, 23, 222, 223, 254, 10, 10, 74, 1, 208, 42, 132, 149, 153, 43, 6, 133, 44, 5, 10, 169, 138, 186, 7, 171, 232, 100, 202, 122, 252, 216, 199, 215, 225, 158, 199, 239, 0, 144, 189, 8, 228, 82, 125, 151, 141, 241, 255, 226, 23, 191, 168, 72, 227, 7, 18, 251, 68, 44, 138, 1, 0, 197, 125, 42, 197, 2, 139, 188, 7, 83, 190, 73, 156, 115, 13, 131, 161, 34, 113, 4, 163, 186, 70, 176, 236, 189, 120, 212, 23, 94, 14, 69, 215, 42, 216, 217, 0, 102, 222, 250, 29, 60, 191, 255, 87, 104, 175, 255, 28, 148, 109, 93, 64, 56, 4, 238, 232, 1, 176, 71, 247, 67, 177, 196, 182, 95, 147, 97, 10, 203, 83, 120, 1, 93, 26, 6, 135, 221, 139, 227, 13, 12, 20, 104, 97, 82, 11, 11, 215, 80, 120, 135, 34, 41, 227, 245, 248, 81, 103, 170, 141, 5, 147, 227, 131, 78, 6, 163, 1, 86, 139, 21, 215, 222, 124, 5, 0, 224, 39, 79, 190, 16, 51, 196, 108, 166, 3, 217, 144, 141, 241, 223, 117, 215, 93, 80, 40, 20, 21, 103, 252, 153, 40, 249, 163, 41, 94, 16, 28, 172, 29, 163, 238, 179, 9, 129, 197, 182, 170, 142, 130, 91, 122, 113, 71, 246, 33, 112, 225, 229, 240, 217, 38, 48, 247, 155, 221, 80, 223, 248, 37, 168, 55, 109, 5, 20, 0, 123, 244, 61, 176, 199, 150, 54, 126, 0, 24, 11, 41, 177, 156, 90, 124, 76, 53, 173, 196, 21, 198, 170, 69, 113, 128, 75, 141, 186, 180, 43, 1, 190, 174, 194, 123, 20, 74, 149, 92, 210, 97, 139, 33, 2, 196, 248, 11, 67, 116, 223, 52, 57, 176, 104, 158, 27, 139, 44, 61, 210, 58, 232, 104, 29, 154, 116, 45, 104, 208, 54, 229, 116, 206, 176, 223, 11, 231, 79, 191, 5, 168, 212, 96, 62, 243, 151, 160, 47, 222, 6, 62, 200, 130, 63, 250, 14, 216, 255, 122, 37, 171, 32, 224, 31, 130, 106, 92, 73, 167, 158, 211, 247, 232, 52, 104, 97, 84, 89, 45, 3, 2, 128, 227, 202, 117, 98, 95, 230, 162, 177, 84, 58, 172, 193, 104, 88, 148, 245, 152, 44, 2, 44, 27, 137, 218, 167, 34, 42, 2, 233, 120, 232, 161, 135, 240, 253, 239, 127, 63, 237, 251, 196, 248, 51, 67, 75, 45, 40, 21, 10, 135, 192, 5, 89, 112, 65, 22, 78, 214, 1, 139, 103, 34, 97, 165, 161, 134, 169, 67, 187, 190, 51, 243, 137, 88, 22, 225, 48, 64, 85, 233, 17, 6, 48, 113, 98, 20, 179, 239, 157, 70, 135, 207, 151, 85, 254, 243, 48, 84, 24, 8, 41, 177, 134, 74, 189, 26, 80, 77, 43, 209, 179, 132, 209, 71, 241, 244, 180, 130, 107, 168, 17, 251, 178, 22, 141, 168, 251, 15, 68, 130, 201, 51, 118, 71, 198, 172, 199, 120, 17, 184, 235, 174, 187, 0, 96, 73, 17, 240, 249, 22, 23, 103, 109, 218, 180, 9, 91, 183, 166, 55, 232, 231, 159, 127, 30, 247, 221, 119, 31, 49, 254, 56, 82, 217, 186, 232, 30, 64, 54, 68, 5, 33, 186, 218, 48, 236, 28, 132, 70, 169, 134, 134, 214, 192, 168, 174, 75, 155, 194, 172, 224, 88, 176, 175, 60, 15, 254, 234, 219, 48, 49, 228, 65, 64, 213, 12, 218, 176, 1, 45, 174, 126, 80, 8, 103, 252, 189, 154, 155, 84, 192, 27, 65, 32, 207, 222, 32, 97, 37, 5, 243, 231, 175, 18, 251, 242, 21, 149, 124, 211, 97, 115, 17, 1, 173, 118, 241, 62, 15, 75, 213, 82, 16, 227, 207, 30, 89, 8, 64, 50, 129, 16, 155, 176, 218, 112, 206, 181, 196, 110, 52, 156, 31, 138, 63, 236, 134, 174, 230, 34, 184, 116, 29, 176, 87, 119, 131, 87, 170, 209, 225, 248, 0, 20, 210, 79, 5, 158, 252, 10, 135, 79, 93, 14, 216, 41, 26, 83, 175, 231, 183, 38, 48, 245, 169, 139, 17, 104, 172, 17, 251, 114, 21, 149, 232, 83, 197, 60, 97, 201, 57, 152, 156, 139, 8, 100, 75, 188, 241, 255, 229, 35, 183, 226, 178, 235, 55, 139, 125, 137, 36, 141, 44, 5, 32, 153, 76, 221, 123, 148, 97, 30, 93, 142, 247, 1, 199, 251, 89, 157, 239, 201, 175, 112, 248, 179, 203, 35, 143, 125, 211, 13, 74, 120, 6, 130, 240, 158, 206, 236, 49, 196, 227, 233, 105, 197, 244, 117, 23, 138, 125, 105, 36, 79, 178, 8, 176, 108, 100, 73, 47, 31, 226, 141, 255, 158, 199, 239, 192, 165, 159, 216, 152, 85, 130, 91, 37, 67, 202, 129, 147, 136, 55, 126, 0, 128, 66, 129, 182, 237, 12, 168, 26, 38, 235, 115, 132, 141, 70, 140, 127, 249, 134, 188, 171, 8, 43, 141, 107, 111, 190, 34, 150, 39, 112, 223, 125, 247, 225, 249, 231, 159, 207, 249, 28, 187, 118, 237, 138, 25, 255, 221, 255, 235, 118, 92, 123, 243, 21, 100, 131, 208, 44, 32, 2, 16, 199, 34, 227, 159, 199, 161, 217, 12, 207, 23, 238, 201, 250, 60, 190, 47, 126, 25, 193, 234, 242, 170, 247, 207, 68, 161, 233, 176, 215, 222, 124, 5, 238, 122, 236, 75, 0, 114, 23, 129, 93, 187, 118, 225, 225, 135, 31, 134, 66, 161, 192, 23, 254, 250, 102, 172, 219, 186, 170, 40, 27, 161, 148, 35, 68, 0, 230, 73, 103, 252, 211, 252, 197, 56, 199, 222, 138, 96, 119, 15, 184, 109, 215, 102, 60, 15, 119, 245, 53, 8, 173, 236, 46, 201, 152, 211, 53, 26, 77, 78, 205, 149, 11, 55, 124, 110, 91, 206, 34, 16, 111, 252, 247, 60, 126, 7, 62, 251, 229, 79, 203, 162, 68, 87, 42, 16, 1, 64, 102, 227, 143, 194, 221, 240, 105, 132, 140, 53, 105, 207, 19, 170, 51, 129, 251, 228, 103, 74, 54, 238, 116, 141, 70, 147, 83, 115, 229, 68, 178, 8, 236, 218, 181, 43, 237, 177, 201, 198, 31, 141, 39, 16, 178, 167, 226, 5, 32, 157, 241, 115, 218, 107, 224, 53, 62, 144, 248, 162, 74, 5, 246, 150, 91, 211, 158, 139, 253, 252, 109, 128, 74, 152, 157, 137, 178, 33, 93, 163, 209, 228, 134, 164, 114, 35, 94, 4, 30, 126, 248, 225, 148, 34, 240, 228, 147, 79, 38, 184, 253, 196, 248, 243, 163, 162, 5, 32, 147, 241, 27, 244, 6, 52, 54, 38, 166, 241, 6, 215, 172, 71, 112, 213, 234, 69, 159, 9, 174, 90, 141, 96, 247, 106, 136, 9, 69, 81, 89, 165, 230, 202, 129, 100, 17, 248, 193, 15, 126, 16, 123, 239, 153, 103, 158, 193, 19, 79, 60, 145, 224, 246, 39, 67, 92, 255, 236, 40, 139, 101, 192, 124, 200, 246, 201, 111, 208, 71, 154, 58, 78, 78, 46, 148, 82, 114, 87, 108, 131, 118, 48, 177, 101, 26, 119, 229, 182, 146, 127, 135, 84, 41, 183, 169, 82, 115, 229, 186, 12, 118, 195, 231, 34, 215, 244, 23, 223, 121, 17, 15, 62, 248, 32, 212, 106, 53, 88, 150, 197, 35, 143, 60, 146, 16, 237, 39, 100, 134, 166, 104, 52, 209, 139, 19, 170, 42, 82, 0, 210, 25, 255, 107, 254, 110, 120, 212, 159, 69, 242, 243, 36, 89, 4, 130, 107, 214, 33, 100, 170, 7, 101, 159, 6, 16, 153, 251, 7, 47, 40, 125, 190, 191, 193, 104, 88, 148, 114, 59, 61, 57, 189, 40, 53, 183, 90, 47, 79, 1, 0, 34, 34, 160, 173, 210, 224, 71, 143, 253, 18, 247, 222, 123, 47, 128, 72, 166, 225, 125, 223, 38, 25, 126, 217, 98, 164, 212, 139, 202, 128, 163, 84, 156, 0, 44, 101, 252, 223, 116, 95, 5, 184, 143, 2, 0, 62, 93, 219, 145, 240, 126, 130, 8, 40, 20, 8, 108, 190, 20, 234, 223, 71, 182, 195, 10, 92, 178, 69, 148, 53, 255, 84, 41, 183, 169, 82, 115, 229, 206, 149, 55, 110, 5, 173, 162, 241, 119, 15, 255, 4, 0, 240, 240, 223, 127, 13, 151, 108, 219, 36, 246, 176, 100, 65, 27, 179, 180, 248, 87, 148, 0, 100, 52, 254, 121, 190, 53, 158, 89, 4, 66, 221, 61, 177, 215, 67, 43, 165, 211, 239, 175, 144, 212, 92, 41, 115, 217, 245, 155, 161, 98, 104, 168, 181, 106, 244, 94, 186, 182, 240, 19, 150, 57, 26, 138, 134, 41, 133, 203, 159, 76, 197, 8, 64, 182, 198, 31, 37, 163, 8, 4, 131, 192, 124, 155, 241, 224, 178, 46, 177, 191, 94, 69, 176, 249, 106, 242, 212, 207, 134, 165, 92, 254, 100, 42, 66, 0, 114, 53, 254, 40, 153, 68, 192, 181, 124, 190, 10, 81, 153, 185, 44, 152, 64, 200, 23, 62, 16, 153, 214, 53, 54, 54, 128, 86, 209, 9, 27, 125, 232, 116, 58, 24, 141, 122, 40, 230, 155, 174, 100, 114, 249, 147, 41, 251, 101, 192, 124, 141, 63, 202, 183, 198, 143, 226, 119, 142, 177, 69, 175, 27, 244, 6, 232, 46, 189, 12, 225, 246, 44, 122, 19, 20, 25, 169, 119, 165, 37, 20, 70, 54, 59, 77, 27, 41, 117, 206, 198, 15, 228, 233, 1, 164, 83, 160, 100, 165, 18, 155, 66, 141, 63, 74, 58, 79, 160, 238, 47, 190, 0, 218, 237, 74, 88, 34, 44, 71, 254, 245, 39, 191, 22, 123, 8, 146, 228, 243, 247, 252, 247, 146, 252, 158, 76, 61, 23, 89, 199, 92, 214, 46, 127, 50, 121, 89, 233, 130, 2, 213, 193, 229, 116, 193, 233, 116, 163, 166, 214, 152, 160, 84, 98, 175, 61, 11, 101, 252, 81, 178, 9, 12, 150, 43, 175, 252, 148, 8, 64, 42, 62, 123, 231, 141, 69, 127, 224, 101, 74, 236, 234, 208, 24, 128, 150, 252, 55, 32, 205, 107, 212, 169, 186, 190, 214, 212, 26, 23, 41, 149, 88, 8, 109, 252, 81, 42, 89, 4, 128, 200, 14, 62, 132, 72, 26, 50, 128, 146, 60, 240, 210, 245, 92, 12, 186, 253, 232, 104, 104, 44, 248, 252, 130, 200, 86, 186, 20, 84, 49, 166, 1, 197, 50, 254, 40, 149, 44, 2, 79, 60, 241, 132, 216, 67, 144, 4, 81, 1, 40, 197, 3, 47, 85, 207, 197, 158, 214, 246, 188, 93, 254, 100, 242, 178, 80, 169, 166, 160, 22, 219, 248, 163, 84, 178, 8, 16, 22, 40, 197, 3, 47, 62, 177, 139, 86, 40, 209, 94, 215, 32, 152, 241, 3, 121, 174, 2, 196, 111, 53, 20, 12, 241, 145, 74, 52, 143, 31, 26, 77, 36, 241, 64, 140, 78, 44, 165, 50, 254, 40, 75, 173, 14, 24, 141, 181, 37, 253, 238, 4, 113, 72, 126, 224, 21, 131, 104, 79, 3, 131, 90, 139, 77, 93, 43, 209, 96, 200, 127, 190, 159, 138, 188, 36, 75, 138, 41, 168, 215, 94, 180, 184, 193, 103, 177, 140, 63, 74, 58, 79, 32, 28, 38, 173, 192, 42, 129, 248, 7, 94, 49, 167, 1, 109, 140, 30, 104, 45, 206, 185, 5, 243, 89, 196, 78, 65, 221, 127, 174, 25, 159, 92, 99, 137, 253, 187, 216, 198, 31, 37, 149, 8, 124, 56, 212, 135, 112, 147, 56, 49, 16, 66, 233, 40, 246, 3, 47, 219, 116, 222, 66, 40, 155, 59, 212, 187, 242, 147, 216, 51, 240, 91, 172, 107, 158, 193, 1, 170, 19, 223, 228, 74, 215, 143, 255, 91, 227, 71, 209, 74, 169, 209, 30, 160, 240, 209, 217, 147, 240, 25, 205, 128, 31, 162, 47, 133, 10, 141, 130, 52, 57, 77, 160, 181, 173, 165, 104, 15, 188, 92, 210, 121, 11, 161, 108, 4, 0, 0, 102, 87, 126, 6, 239, 205, 255, 252, 140, 54, 243, 246, 95, 249, 18, 159, 8, 21, 45, 195, 29, 60, 253, 58, 166, 77, 181, 160, 155, 104, 104, 2, 242, 47, 195, 37, 136, 71, 62, 25, 125, 249, 82, 144, 0, 84, 106, 10, 170, 20, 99, 32, 197, 228, 185, 255, 247, 116, 44, 239, 35, 20, 10, 99, 210, 54, 137, 250, 122, 19, 156, 46, 39, 76, 38, 19, 236, 118, 59, 140, 6, 99, 217, 79, 121, 138, 61, 189, 213, 64, 13, 19, 83, 252, 167, 126, 60, 229, 253, 63, 86, 66, 196, 142, 129, 20, 19, 169, 46, 251, 150, 154, 98, 62, 240, 244, 42, 3, 12, 138, 220, 54, 159, 17, 130, 178, 47, 6, 34, 20, 142, 20, 151, 125, 203, 137, 54, 70, 47, 138, 241, 3, 50, 244, 0, 114, 41, 141, 36, 8, 67, 165, 77, 121, 74, 69, 169, 2, 125, 75, 33, 59, 75, 201, 166, 52, 178, 84, 84, 106, 12, 36, 250, 61, 201, 6, 28, 249, 19, 53, 254, 169, 201, 105, 81, 199, 33, 59, 1, 72, 238, 121, 159, 174, 55, 62, 129, 32, 69, 52, 20, 13, 35, 165, 198, 220, 212, 12, 92, 46, 23, 212, 26, 226, 1, 100, 77, 54, 61, 239, 41, 226, 254, 151, 13, 229, 182, 245, 153, 145, 82, 195, 68, 107, 225, 155, 113, 1, 0, 88, 63, 7, 131, 192, 169, 189, 185, 34, 43, 107, 73, 87, 26, 233, 114, 186, 16, 10, 133, 99, 17, 106, 66, 113, 40, 245, 148, 167, 156, 182, 62, 107, 99, 244, 240, 205, 184, 34, 129, 212, 249, 13, 75, 131, 193, 72, 96, 213, 106, 17, 174, 120, 44, 215, 115, 201, 42, 8, 152, 170, 52, 178, 190, 177, 126, 81, 111, 124, 66, 121, 32, 245, 190, 19, 217, 16, 159, 206, 219, 208, 88, 47, 246, 112, 22, 33, 43, 15, 32, 26, 121, 142, 223, 250, 57, 26, 161, 6, 0, 147, 201, 4, 138, 34, 233, 170, 229, 138, 220, 182, 62, 139, 186, 252, 165, 32, 57, 152, 152, 109, 112, 81, 86, 30, 64, 57, 39, 219, 16, 22, 35, 231, 4, 164, 82, 166, 243, 2, 128, 90, 195, 192, 229, 138, 196, 22, 114, 9, 46, 202, 202, 3, 32, 84, 22, 114, 76, 64, 202, 183, 59, 111, 193, 215, 202, 96, 0, 235, 231, 0, 228, 22, 92, 148, 149, 7, 64, 168, 44, 228, 150, 128, 36, 86, 98, 79, 114, 224, 47, 26, 92, 4, 16, 11, 160, 166, 67, 150, 2, 80, 201, 9, 56, 149, 142, 20, 167, 129, 233, 118, 222, 45, 21, 153, 140, 124, 201, 177, 139, 54, 106, 2, 161, 12, 144, 66, 58, 111, 33, 16, 1, 32, 16, 242, 68, 140, 185, 190, 208, 16, 1, 32, 72, 30, 169, 77, 249, 74, 209, 170, 171, 84, 144, 85, 0, 153, 144, 156, 254, 154, 46, 77, 150, 80, 92, 74, 185, 182, 95, 10, 136, 0, 200, 4, 41, 85, 65, 86, 42, 26, 138, 150, 245, 124, 63, 21, 68, 0, 100, 2, 169, 130, 20, 31, 127, 136, 199, 4, 231, 134, 157, 151, 79, 13, 66, 38, 72, 12, 64, 6, 144, 42, 72, 105, 17, 21, 2, 64, 254, 241, 0, 34, 0, 50, 96, 169, 42, 200, 248, 52, 89, 66, 233, 137, 23, 3, 154, 162, 81, 5, 165, 172, 166, 9, 228, 177, 33, 3, 82, 165, 191, 166, 74, 147, 37, 136, 11, 31, 226, 225, 12, 177, 152, 224, 220, 152, 224, 220, 152, 227, 57, 184, 61, 156, 216, 195, 90, 18, 193, 61, 128, 116, 61, 250, 146, 123, 249, 17, 178, 39, 85, 250, 107, 170, 52, 89, 130, 180, 112, 134, 88, 152, 39, 45, 104, 110, 105, 70, 45, 173, 65, 53, 205, 96, 142, 231, 36, 229, 33, 8, 238, 1, 148, 83, 19, 7, 169, 64, 250, 239, 201, 31, 103, 136, 197, 152, 223, 133, 211, 19, 163, 176, 115, 28, 166, 230, 43, 247, 196, 70, 112, 1, 72, 23, 157, 78, 142, 98, 151, 3, 229, 214, 178, 138, 32, 60, 169, 58, 86, 77, 187, 167, 49, 19, 152, 195, 4, 231, 198, 148, 203, 37, 234, 52, 161, 232, 49, 0, 185, 53, 113, 200, 5, 226, 237, 16, 50, 145, 169, 164, 217, 171, 12, 98, 216, 62, 30, 139, 25, 148, 26, 193, 5, 32, 149, 226, 165, 138, 98, 151, 3, 165, 246, 118, 164, 150, 18, 75, 200, 76, 170, 142, 85, 169, 58, 91, 1, 88, 20, 64, 44, 201, 248, 132, 62, 161, 28, 155, 56, 8, 69, 57, 123, 59, 4, 225, 200, 38, 166, 147, 73, 12, 132, 106, 36, 42, 120, 56, 94, 110, 77, 28, 10, 65, 206, 45, 171, 42, 25, 185, 237, 46, 229, 12, 177, 112, 114, 44, 128, 133, 196, 35, 93, 149, 78, 144, 115, 151, 228, 91, 150, 107, 20, 187, 146, 189, 29, 57, 35, 231, 186, 138, 104, 226, 145, 91, 163, 128, 157, 247, 21, 60, 85, 144, 142, 204, 201, 144, 92, 230, 119, 149, 140, 212, 42, 25, 197, 168, 171, 40, 70, 252, 198, 31, 151, 120, 100, 203, 83, 12, 72, 70, 142, 192, 72, 177, 101, 149, 216, 196, 63, 113, 171, 85, 250, 184, 39, 110, 29, 92, 78, 23, 156, 78, 55, 106, 106, 141, 37, 25, 75, 185, 214, 85, 240, 33, 30, 78, 240, 177, 169, 66, 192, 233, 67, 87, 67, 99, 198, 207, 21, 237, 155, 146, 136, 53, 33, 138, 148, 42, 25, 43, 101, 119, 169, 169, 185, 89, 28, 57, 59, 136, 35, 103, 7, 49, 108, 49, 167, 77, 60, 146, 159, 212, 17, 100, 133, 212, 158, 184, 149, 82, 87, 17, 31, 107, 211, 154, 244, 224, 52, 10, 216, 82, 148, 49, 147, 41, 128, 0, 16, 111, 39, 61, 82, 171, 100, 172, 228, 186, 10, 62, 196, 199, 226, 4, 209, 122, 4, 226, 1, 16, 138, 138, 212, 158, 184, 229, 186, 34, 149, 45, 206, 16, 11, 235, 212, 194, 182, 97, 196, 3, 32, 20, 149, 74, 126, 226, 74, 129, 84, 2, 167, 53, 45, 228, 165, 16, 1, 32, 20, 21, 178, 42, 34, 77, 38, 56, 55, 218, 24, 61, 254, 63, 91, 213, 67, 145, 233, 13, 169, 197, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130]; diff --git a/tile_server/static/generated/land.dart b/tile_server/static/generated/land.dart index 1029ac5f..48881eab 100644 --- a/tile_server/static/generated/land.dart +++ b/tile_server/static/generated/land.dart @@ -1,2 +1 @@ -// Will be replaced automatically by GitHub Actions -final landTileBytes = []; +final landTileBytes = [137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 1, 0, 0, 0, 1, 0, 8, 3, 0, 0, 0, 107, 172, 88, 84, 0, 0, 2, 253, 80, 76, 84, 69, 51, 52, 49, 72, 52, 55, 60, 65, 58, 76, 85, 24, 157, 17, 17, 83, 81, 48, 162, 28, 28, 73, 74, 72, 84, 75, 67, 78, 83, 73, 92, 100, 43, 84, 85, 71, 85, 87, 84, 169, 46, 46, 110, 80, 76, 125, 90, 55, 104, 112, 57, 89, 110, 87, 129, 96, 61, 102, 109, 86, 113, 94, 97, 130, 88, 87, 134, 105, 70, 104, 105, 102, 94, 131, 93, 104, 115, 103, 121, 128, 77, 139, 112, 78, 140, 101, 88, 141, 89, 99, 113, 117, 107, 182, 78, 78, 142, 116, 84, 117, 117, 116, 149, 107, 95, 146, 121, 90, 151, 150, 59, 145, 110, 113, 117, 141, 112, 128, 134, 111, 138, 144, 96, 148, 150, 83, 172, 121, 91, 130, 132, 125, 154, 135, 104, 176, 113, 111, 129, 154, 119, 177, 153, 75, 136, 137, 133, 147, 153, 107, 150, 171, 86, 141, 169, 102, 157, 140, 115, 212, 117, 89, 153, 166, 102, 157, 149, 115, 165, 165, 91, 134, 164, 124, 200, 110, 114, 125, 185, 115, 151, 138, 138, 141, 150, 137, 134, 167, 129, 218, 89, 126, 224, 83, 126, 151, 136, 147, 206, 145, 85, 149, 150, 138, 142, 189, 107, 166, 171, 101, 172, 150, 116, 153, 171, 115, 156, 181, 103, 132, 189, 120, 221, 92, 129, 182, 122, 144, 224, 93, 131, 210, 110, 130, 162, 166, 123, 168, 179, 105, 180, 172, 102, 140, 184, 131, 148, 170, 137, 207, 140, 108, 206, 164, 86, 152, 187, 120, 155, 155, 151, 142, 195, 125, 147, 182, 133, 142, 194, 129, 149, 178, 138, 149, 184, 135, 167, 155, 147, 182, 183, 104, 157, 182, 131, 172, 165, 133, 183, 166, 121, 168, 184, 120, 151, 185, 137, 154, 179, 141, 157, 164, 153, 147, 197, 133, 157, 187, 134, 228, 110, 142, 148, 205, 128, 154, 189, 139, 150, 197, 137, 155, 196, 133, 172, 146, 168, 155, 185, 147, 180, 168, 139, 170, 188, 130, 184, 186, 118, 204, 173, 112, 165, 169, 156, 207, 142, 141, 228, 143, 119, 157, 192, 142, 153, 206, 133, 186, 195, 115, 155, 201, 141, 156, 209, 135, 158, 198, 145, 168, 168, 165, 161, 196, 145, 182, 172, 148, 169, 196, 138, 183, 186, 135, 168, 184, 157, 171, 198, 146, 163, 204, 149, 167, 193, 156, 182, 183, 151, 163, 213, 141, 171, 182, 166, 195, 202, 122, 188, 197, 135, 180, 171, 171, 231, 141, 150, 172, 203, 148, 203, 154, 166, 167, 205, 152, 171, 196, 157, 204, 179, 141, 184, 167, 178, 171, 205, 156, 195, 201, 136, 180, 199, 154, 228, 186, 120, 185, 183, 167, 172, 202, 163, 200, 174, 164, 173, 209, 158, 232, 146, 162, 175, 210, 161, 201, 210, 136, 181, 214, 155, 197, 186, 167, 212, 199, 139, 182, 202, 168, 219, 167, 166, 178, 212, 163, 198, 201, 155, 205, 169, 180, 187, 186, 183, 213, 185, 162, 182, 216, 164, 197, 197, 168, 182, 213, 169, 187, 212, 165, 196, 216, 153, 210, 215, 140, 186, 199, 181, 184, 225, 158, 214, 201, 153, 194, 189, 187, 186, 219, 166, 187, 214, 171, 209, 172, 191, 188, 219, 171, 248, 177, 155, 190, 225, 166, 189, 216, 178, 197, 201, 185, 215, 220, 148, 215, 201, 168, 199, 215, 171, 217, 186, 184, 191, 206, 193, 241, 203, 146, 198, 197, 196, 234, 180, 178, 216, 199, 183, 216, 212, 170, 226, 202, 170, 198, 228, 173, 199, 218, 183, 221, 226, 155, 224, 229, 159, 210, 204, 200, 247, 194, 173, 205, 234, 176, 213, 217, 185, 204, 216, 197, 233, 201, 184, 237, 188, 198, 204, 226, 196, 210, 230, 187, 229, 233, 166, 246, 198, 184, 232, 200, 198, 252, 214, 164, 214, 217, 200, 228, 218, 187, 215, 228, 201, 244, 199, 203, 232, 231, 184, 217, 217, 215, 232, 219, 199, 245, 220, 185, 217, 240, 195, 237, 241, 178, 243, 203, 210, 249, 230, 178, 226, 221, 219, 229, 233, 204, 228, 227, 212, 220, 234, 214, 252, 215, 204, 228, 226, 220, 229, 234, 211, 232, 227, 215, 245, 230, 200, 244, 216, 218, 227, 236, 219, 235, 238, 211, 237, 228, 219, 243, 231, 214, 247, 250, 191, 234, 234, 222, 238, 240, 213, 232, 231, 230, 247, 220, 226, 247, 250, 196, 241, 242, 214, 234, 243, 229, 243, 238, 233, 243, 243, 242, 246, 247, 235, 252, 238, 241, 244, 249, 241, 251, 243, 244, 251, 251, 245, 253, 246, 248, 254, 254, 254, 34, 181, 220, 167, 0, 0, 69, 129, 73, 68, 65, 84, 120, 156, 205, 125, 15, 124, 19, 215, 157, 167, 46, 221, 75, 66, 22, 54, 9, 189, 66, 73, 110, 113, 56, 183, 185, 101, 33, 9, 203, 166, 65, 33, 205, 101, 91, 214, 148, 166, 80, 177, 161, 136, 73, 90, 47, 56, 81, 2, 52, 9, 38, 50, 156, 147, 51, 24, 185, 118, 194, 31, 23, 145, 137, 11, 25, 11, 136, 173, 172, 107, 11, 66, 99, 251, 140, 229, 198, 161, 38, 169, 38, 85, 101, 23, 145, 176, 98, 178, 202, 110, 56, 217, 194, 161, 216, 92, 192, 28, 150, 13, 24, 125, 238, 247, 123, 111, 102, 52, 163, 153, 145, 70, 134, 238, 221, 239, 3, 99, 253, 153, 25, 205, 251, 190, 223, 255, 247, 123, 239, 89, 184, 255, 95, 201, 243, 238, 91, 23, 252, 60, 147, 246, 105, 107, 103, 69, 167, 68, 129, 80, 35, 199, 5, 66, 89, 168, 179, 147, 207, 244, 181, 229, 255, 69, 219, 76, 145, 119, 240, 165, 193, 11, 199, 153, 180, 79, 15, 0, 0, 53, 18, 2, 33, 175, 167, 77, 209, 22, 93, 44, 248, 206, 204, 16, 253, 255, 11, 0, 215, 251, 225, 135, 23, 248, 227, 124, 218, 167, 53, 21, 53, 53, 41, 30, 224, 36, 0, 218, 218, 2, 141, 92, 99, 22, 102, 8, 232, 48, 195, 159, 22, 128, 45, 91, 174, 227, 226, 227, 131, 47, 93, 184, 112, 161, 55, 237, 83, 0, 128, 54, 126, 253, 219, 192, 2, 82, 239, 6, 232, 151, 89, 154, 143, 76, 243, 239, 11, 192, 143, 126, 164, 247, 233, 78, 102, 39, 199, 174, 101, 54, 178, 89, 174, 62, 254, 214, 103, 128, 128, 154, 5, 188, 85, 21, 64, 50, 0, 18, 181, 227, 119, 45, 134, 204, 14, 122, 32, 16, 234, 252, 247, 7, 96, 203, 119, 191, 171, 199, 2, 155, 214, 110, 226, 54, 110, 100, 55, 110, 204, 118, 125, 231, 46, 0, 64, 80, 127, 68, 9, 94, 109, 219, 66, 164, 187, 157, 182, 58, 224, 81, 169, 131, 180, 230, 75, 164, 163, 14, 115, 7, 192, 108, 255, 113, 28, 251, 93, 32, 157, 211, 158, 222, 249, 52, 199, 176, 28, 203, 100, 189, 3, 202, 128, 79, 245, 9, 105, 199, 1, 242, 114, 41, 109, 129, 200, 247, 1, 3, 21, 40, 178, 190, 104, 54, 110, 4, 0, 166, 251, 143, 123, 9, 1, 88, 165, 249, 120, 231, 90, 110, 237, 78, 4, 224, 199, 89, 239, 240, 198, 201, 11, 131, 234, 79, 14, 64, 227, 189, 244, 101, 33, 109, 65, 139, 162, 53, 109, 222, 52, 54, 64, 166, 231, 101, 0, 16, 39, 95, 0, 193, 106, 191, 14, 0, 76, 247, 95, 205, 119, 9, 213, 164, 127, 190, 105, 19, 252, 51, 7, 97, 247, 73, 141, 18, 76, 209, 54, 218, 161, 156, 87, 108, 10, 188, 109, 75, 87, 3, 164, 209, 138, 246, 183, 113, 158, 0, 81, 153, 129, 113, 3, 96, 190, 255, 94, 162, 0, 188, 148, 254, 249, 211, 59, 57, 192, 112, 45, 179, 54, 187, 16, 85, 108, 17, 90, 13, 191, 20, 219, 32, 189, 8, 116, 106, 165, 128, 182, 154, 39, 127, 240, 75, 210, 114, 138, 192, 184, 1, 200, 161, 255, 12, 136, 65, 130, 63, 38, 78, 173, 176, 101, 254, 190, 69, 234, 205, 0, 85, 242, 105, 8, 212, 117, 18, 37, 16, 74, 233, 126, 196, 168, 5, 46, 51, 0, 192, 140, 126, 203, 161, 255, 12, 137, 145, 15, 217, 200, 154, 241, 219, 98, 250, 12, 141, 33, 165, 148, 43, 25, 32, 245, 82, 1, 13, 241, 25, 244, 69, 192, 140, 126, 203, 161, 255, 140, 239, 33, 31, 178, 145, 53, 227, 183, 197, 34, 131, 164, 236, 156, 214, 206, 203, 141, 86, 182, 95, 225, 50, 170, 0, 48, 167, 223, 204, 63, 254, 245, 223, 193, 154, 153, 207, 10, 172, 214, 130, 114, 21, 0, 250, 8, 96, 163, 201, 159, 198, 22, 122, 161, 62, 0, 38, 245, 155, 249, 199, 191, 126, 42, 200, 42, 104, 213, 240, 255, 237, 165, 153, 76, 125, 128, 52, 58, 16, 106, 135, 191, 237, 1, 160, 70, 165, 207, 104, 81, 252, 130, 73, 253, 198, 200, 135, 63, 61, 89, 205, 156, 4, 110, 241, 27, 70, 12, 208, 142, 102, 2, 41, 128, 92, 160, 3, 143, 165, 200, 90, 45, 221, 232, 70, 232, 183, 27, 76, 214, 236, 167, 180, 66, 179, 183, 109, 241, 28, 192, 238, 15, 144, 22, 43, 136, 229, 218, 219, 83, 167, 102, 244, 4, 109, 197, 55, 68, 191, 113, 173, 60, 15, 166, 219, 227, 247, 131, 199, 198, 243, 126, 207, 245, 220, 171, 216, 90, 148, 253, 36, 108, 249, 250, 26, 206, 67, 187, 95, 221, 202, 198, 167, 149, 103, 182, 235, 180, 63, 5, 0, 91, 204, 221, 8, 238, 14, 247, 172, 92, 217, 195, 251, 226, 85, 29, 126, 95, 124, 243, 188, 170, 248, 184, 17, 96, 109, 214, 34, 29, 86, 164, 102, 26, 13, 182, 68, 128, 192, 82, 15, 231, 37, 28, 64, 186, 59, 213, 208, 181, 140, 242, 82, 221, 104, 65, 237, 8, 49, 28, 203, 142, 31, 0, 120, 42, 175, 48, 121, 247, 238, 201, 3, 241, 217, 147, 55, 251, 195, 15, 46, 236, 200, 175, 226, 57, 79, 56, 46, 8, 225, 176, 16, 246, 101, 191, 133, 130, 138, 202, 117, 63, 166, 58, 10, 13, 182, 76, 32, 2, 30, 194, 8, 36, 74, 36, 238, 17, 168, 190, 54, 226, 31, 41, 47, 213, 107, 191, 6, 128, 10, 107, 49, 147, 211, 115, 42, 8, 158, 202, 215, 51, 37, 46, 76, 142, 11, 85, 11, 1, 128, 252, 166, 248, 202, 117, 60, 231, 111, 218, 60, 59, 191, 105, 221, 61, 243, 198, 193, 13, 197, 154, 79, 168, 153, 70, 131, 45, 81, 43, 9, 16, 83, 97, 34, 198, 6, 129, 181, 32, 134, 161, 144, 164, 1, 41, 233, 3, 160, 200, 55, 8, 131, 131, 189, 194, 23, 233, 57, 40, 243, 132, 79, 37, 172, 188, 231, 158, 205, 130, 63, 14, 0, 248, 249, 41, 179, 243, 227, 62, 206, 95, 117, 187, 208, 115, 123, 85, 60, 127, 191, 223, 236, 157, 100, 235, 103, 213, 124, 69, 204, 52, 49, 216, 244, 253, 129, 148, 11, 128, 97, 131, 183, 157, 178, 250, 38, 15, 253, 171, 184, 82, 63, 97, 102, 105, 232, 168, 19, 127, 77, 128, 224, 251, 194, 96, 239, 113, 146, 129, 168, 230, 114, 38, 124, 42, 46, 146, 191, 114, 246, 236, 184, 135, 71, 14, 168, 202, 95, 57, 133, 247, 3, 0, 179, 195, 241, 219, 227, 248, 145, 241, 197, 42, 209, 182, 201, 29, 95, 80, 145, 126, 34, 17, 1, 98, 176, 85, 237, 167, 41, 2, 42, 254, 68, 214, 3, 212, 214, 43, 174, 212, 109, 127, 200, 146, 28, 233, 15, 118, 53, 188, 230, 116, 213, 117, 247, 18, 4, 224, 159, 208, 219, 187, 30, 37, 80, 248, 162, 247, 184, 121, 0, 240, 169, 124, 155, 31, 20, 132, 252, 166, 86, 108, 109, 124, 114, 60, 220, 148, 31, 6, 0, 30, 164, 0, 172, 211, 3, 160, 133, 250, 41, 58, 162, 141, 84, 93, 160, 185, 0, 205, 52, 53, 216, 72, 168, 251, 58, 183, 73, 33, 119, 64, 12, 128, 67, 41, 231, 223, 203, 165, 190, 211, 82, 128, 179, 36, 69, 26, 25, 138, 118, 53, 55, 52, 247, 124, 65, 81, 248, 227, 111, 187, 59, 143, 227, 43, 243, 0, 224, 83, 185, 155, 238, 137, 68, 166, 244, 80, 0, 166, 116, 196, 215, 61, 152, 17, 128, 70, 154, 209, 106, 211, 136, 118, 133, 196, 190, 58, 190, 48, 35, 7, 36, 132, 14, 116, 30, 0, 125, 217, 70, 148, 94, 40, 149, 248, 224, 196, 68, 73, 91, 38, 0, 64, 94, 100, 0, 68, 26, 10, 54, 55, 116, 8, 255, 74, 232, 143, 36, 35, 199, 11, 38, 181, 55, 62, 212, 143, 133, 117, 247, 228, 239, 134, 139, 170, 154, 252, 124, 207, 236, 252, 121, 16, 207, 251, 59, 54, 135, 227, 15, 198, 241, 35, 205, 53, 50, 167, 170, 69, 59, 180, 109, 105, 59, 245, 224, 203, 181, 44, 192, 200, 7, 137, 138, 139, 41, 127, 75, 201, 47, 244, 135, 72, 144, 12, 250, 176, 81, 58, 201, 171, 69, 0, 141, 166, 165, 161, 161, 185, 43, 218, 63, 162, 4, 97, 36, 218, 213, 176, 255, 3, 64, 96, 16, 84, 2, 128, 96, 90, 12, 224, 169, 60, 60, 31, 230, 193, 42, 135, 195, 30, 206, 79, 77, 159, 39, 28, 246, 249, 195, 126, 242, 81, 58, 97, 122, 130, 36, 40, 100, 209, 102, 193, 105, 7, 117, 21, 40, 124, 131, 178, 111, 145, 198, 23, 210, 2, 80, 94, 196, 41, 219, 143, 44, 128, 40, 194, 109, 3, 33, 133, 33, 12, 133, 246, 150, 22, 2, 137, 9, 148, 125, 69, 168, 106, 44, 200, 241, 189, 145, 142, 134, 134, 134, 174, 96, 76, 129, 67, 127, 87, 67, 7, 81, 10, 231, 211, 242, 178, 153, 1, 200, 209, 143, 242, 134, 2, 91, 169, 160, 138, 162, 141, 253, 4, 207, 8, 159, 189, 177, 180, 240, 32, 158, 82, 144, 238, 15, 104, 127, 165, 186, 128, 40, 63, 146, 252, 59, 126, 60, 5, 0, 116, 113, 72, 233, 9, 7, 14, 238, 219, 199, 181, 96, 64, 116, 176, 192, 90, 96, 43, 174, 150, 0, 16, 233, 11, 192, 161, 57, 56, 148, 226, 132, 224, 110, 80, 137, 253, 253, 125, 127, 58, 0, 160, 135, 218, 69, 141, 213, 24, 104, 223, 4, 130, 141, 13, 225, 15, 22, 46, 45, 44, 124, 32, 244, 6, 120, 50, 1, 86, 235, 11, 104, 200, 138, 130, 196, 119, 66, 219, 143, 35, 12, 60, 0, 208, 142, 247, 109, 76, 157, 82, 110, 37, 127, 52, 142, 164, 18, 0, 17, 133, 230, 134, 174, 168, 200, 10, 35, 13, 145, 254, 88, 44, 150, 75, 147, 114, 34, 73, 24, 121, 49, 97, 31, 96, 20, 198, 106, 239, 86, 250, 215, 196, 125, 88, 188, 21, 54, 254, 184, 148, 7, 12, 144, 192, 40, 117, 70, 133, 168, 77, 53, 214, 70, 3, 0, 33, 1, 88, 33, 74, 32, 136, 54, 68, 99, 177, 158, 158, 158, 27, 221, 116, 74, 26, 171, 180, 79, 124, 65, 26, 241, 6, 125, 109, 42, 54, 109, 36, 151, 28, 15, 73, 0, 64, 231, 123, 149, 223, 23, 136, 142, 141, 198, 145, 212, 7, 128, 128, 176, 59, 74, 229, 160, 171, 33, 24, 139, 141, 223, 63, 204, 68, 129, 144, 222, 152, 46, 178, 51, 52, 68, 228, 138, 118, 171, 153, 59, 177, 235, 81, 252, 241, 226, 3, 7, 90, 85, 170, 143, 82, 145, 40, 72, 233, 142, 100, 6, 0, 64, 55, 54, 196, 168, 36, 4, 1, 129, 136, 113, 126, 122, 220, 68, 218, 126, 176, 240, 160, 170, 249, 141, 34, 99, 16, 177, 88, 31, 8, 88, 205, 57, 165, 197, 214, 183, 65, 6, 148, 92, 79, 168, 220, 74, 175, 151, 0, 72, 115, 36, 1, 128, 15, 79, 246, 26, 67, 208, 211, 64, 149, 65, 3, 40, 130, 232, 13, 106, 181, 130, 8, 0, 45, 7, 81, 93, 173, 47, 220, 10, 56, 128, 138, 110, 84, 121, 44, 7, 151, 154, 108, 63, 208, 122, 48, 27, 129, 182, 180, 15, 139, 217, 106, 107, 5, 8, 17, 43, 9, 146, 218, 145, 68, 0, 222, 221, 245, 150, 49, 2, 131, 29, 13, 93, 96, 22, 134, 250, 135, 64, 10, 188, 40, 85, 94, 31, 24, 116, 62, 12, 1, 174, 233, 208, 198, 144, 218, 69, 111, 132, 114, 194, 214, 165, 235, 169, 214, 150, 51, 118, 45, 109, 251, 178, 100, 69, 213, 132, 151, 87, 107, 162, 232, 10, 117, 64, 193, 168, 29, 73, 73, 4, 222, 59, 137, 199, 75, 163, 151, 210, 33, 232, 143, 53, 36, 147, 99, 93, 35, 253, 177, 248, 232, 112, 152, 227, 227, 64, 71, 226, 66, 207, 240, 145, 120, 110, 241, 189, 222, 3, 99, 43, 177, 207, 90, 218, 209, 37, 14, 28, 92, 42, 250, 125, 162, 159, 214, 216, 146, 83, 251, 9, 177, 54, 171, 108, 55, 53, 145, 20, 18, 35, 31, 8, 89, 46, 93, 186, 112, 9, 254, 247, 190, 123, 18, 254, 140, 38, 8, 2, 151, 144, 232, 95, 0, 0, 88, 32, 122, 203, 200, 80, 255, 104, 67, 89, 220, 19, 127, 109, 193, 240, 192, 140, 89, 19, 34, 61, 183, 196, 253, 62, 31, 231, 105, 109, 5, 190, 240, 121, 91, 91, 61, 222, 214, 92, 17, 9, 132, 210, 180, 149, 151, 180, 183, 200, 38, 13, 120, 6, 172, 57, 222, 17, 137, 45, 2, 8, 138, 172, 64, 186, 30, 4, 35, 31, 8, 89, 70, 175, 92, 25, 5, 26, 134, 255, 87, 198, 34, 95, 29, 29, 77, 92, 24, 165, 116, 137, 28, 207, 15, 141, 0, 0, 19, 16, 0, 251, 172, 97, 79, 220, 62, 45, 62, 16, 159, 115, 104, 24, 0, 144, 40, 44, 200, 47, 115, 211, 148, 45, 90, 109, 77, 168, 220, 186, 158, 74, 193, 62, 109, 36, 96, 138, 138, 170, 139, 13, 89, 135, 145, 15, 132, 44, 19, 163, 67, 51, 38, 206, 74, 140, 53, 79, 156, 56, 35, 49, 209, 50, 113, 243, 103, 189, 163, 101, 219, 167, 78, 180, 143, 94, 25, 158, 51, 113, 106, 100, 100, 164, 97, 234, 84, 199, 196, 4, 1, 32, 142, 0, 12, 15, 60, 58, 97, 98, 67, 240, 150, 184, 48, 173, 231, 202, 107, 19, 39, 44, 24, 24, 126, 244, 181, 59, 39, 190, 230, 152, 120, 231, 145, 120, 110, 15, 218, 238, 53, 250, 166, 96, 41, 232, 196, 64, 69, 238, 249, 233, 114, 155, 153, 76, 106, 138, 44, 177, 177, 137, 101, 163, 115, 102, 141, 89, 250, 71, 131, 99, 13, 83, 135, 71, 19, 131, 163, 115, 110, 9, 118, 223, 178, 123, 108, 250, 156, 225, 134, 155, 18, 253, 150, 232, 208, 212, 137, 35, 67, 177, 17, 0, 96, 19, 2, 96, 159, 208, 211, 19, 7, 14, 152, 232, 24, 237, 184, 233, 72, 207, 68, 251, 149, 169, 19, 123, 14, 89, 102, 9, 143, 78, 189, 126, 189, 32, 17, 107, 43, 88, 159, 115, 86, 6, 20, 128, 77, 63, 149, 104, 72, 150, 43, 67, 150, 237, 219, 23, 220, 50, 54, 117, 70, 100, 20, 0, 24, 29, 189, 112, 97, 116, 206, 156, 209, 81, 231, 140, 107, 150, 196, 232, 149, 137, 13, 101, 211, 199, 18, 13, 19, 65, 9, 2, 0, 163, 155, 4, 251, 180, 145, 137, 206, 120, 120, 160, 99, 194, 157, 143, 198, 135, 103, 205, 137, 199, 95, 155, 120, 101, 170, 61, 30, 183, 244, 196, 143, 128, 94, 200, 245, 153, 51, 80, 121, 78, 93, 137, 196, 230, 216, 122, 14, 117, 64, 212, 98, 183, 219, 203, 198, 174, 148, 77, 152, 142, 0, 92, 66, 0, 22, 140, 142, 150, 77, 29, 177, 36, 18, 35, 83, 183, 151, 205, 74, 140, 118, 77, 28, 139, 17, 0, 188, 225, 5, 211, 175, 76, 120, 77, 240, 9, 135, 44, 19, 167, 14, 12, 79, 95, 16, 15, 31, 154, 112, 101, 170, 19, 0, 16, 180, 0, 104, 171, 196, 180, 89, 109, 99, 170, 182, 226, 81, 27, 15, 235, 157, 90, 92, 84, 96, 34, 104, 210, 33, 203, 232, 168, 37, 54, 58, 58, 54, 6, 170, 240, 150, 104, 51, 112, 192, 40, 0, 0, 127, 102, 205, 25, 187, 169, 249, 179, 161, 155, 162, 93, 19, 0, 141, 137, 35, 111, 245, 143, 118, 221, 50, 58, 32, 76, 125, 244, 202, 52, 232, 118, 84, 130, 192, 251, 11, 166, 197, 227, 143, 78, 55, 4, 64, 91, 37, 102, 144, 250, 210, 39, 104, 123, 53, 74, 116, 150, 42, 1, 56, 169, 160, 168, 120, 28, 89, 76, 36, 203, 240, 104, 217, 77, 179, 102, 60, 58, 54, 113, 206, 172, 9, 163, 67, 55, 205, 234, 64, 0, 38, 76, 159, 113, 83, 255, 88, 195, 45, 179, 38, 206, 73, 142, 76, 157, 58, 107, 234, 196, 196, 201, 183, 134, 71, 103, 220, 50, 109, 194, 68, 97, 224, 200, 77, 211, 166, 149, 129, 18, 236, 185, 233, 200, 240, 196, 169, 211, 111, 233, 25, 53, 0, 64, 167, 74, 76, 155, 213, 206, 68, 5, 212, 145, 213, 166, 70, 111, 28, 89, 46, 13, 143, 158, 111, 14, 142, 142, 245, 55, 119, 129, 69, 60, 223, 124, 30, 1, 176, 199, 58, 18, 35, 67, 137, 33, 112, 1, 134, 18, 87, 130, 93, 137, 216, 200, 165, 119, 223, 29, 16, 58, 94, 59, 36, 8, 173, 241, 200, 107, 187, 7, 192, 17, 138, 247, 244, 140, 14, 28, 58, 20, 31, 24, 232, 1, 67, 216, 65, 60, 36, 165, 18, 212, 171, 18, 211, 4, 35, 25, 72, 174, 215, 40, 207, 198, 2, 215, 65, 22, 116, 255, 192, 248, 147, 3, 121, 121, 9, 1, 24, 1, 7, 32, 22, 75, 38, 250, 199, 198, 162, 49, 64, 34, 57, 244, 197, 133, 119, 7, 253, 126, 33, 188, 231, 199, 204, 218, 58, 28, 233, 9, 251, 91, 225, 63, 15, 47, 189, 254, 112, 216, 199, 135, 121, 47, 175, 118, 143, 245, 170, 196, 52, 193, 72, 6, 74, 213, 107, 88, 179, 156, 89, 161, 81, 126, 237, 161, 128, 151, 75, 15, 12, 244, 72, 47, 26, 28, 181, 151, 141, 68, 35, 209, 88, 114, 36, 26, 11, 98, 52, 4, 0, 196, 6, 49, 66, 102, 240, 18, 198, 196, 109, 145, 244, 171, 196, 210, 131, 145, 12, 148, 170, 215, 80, 122, 196, 45, 152, 234, 72, 107, 26, 107, 77, 19, 18, 18, 103, 55, 30, 64, 125, 187, 49, 32, 70, 24, 250, 169, 149, 52, 0, 6, 251, 34, 145, 232, 208, 8, 16, 201, 16, 67, 16, 24, 196, 236, 208, 72, 127, 132, 124, 203, 224, 37, 76, 246, 71, 39, 100, 80, 37, 198, 168, 131, 17, 99, 82, 212, 107, 20, 43, 100, 128, 134, 144, 105, 231, 166, 141, 31, 180, 137, 121, 133, 77, 27, 1, 175, 125, 212, 219, 50, 200, 45, 41, 0, 192, 182, 203, 233, 225, 145, 88, 180, 43, 24, 133, 246, 67, 44, 24, 13, 10, 95, 208, 83, 102, 178, 156, 33, 0, 102, 43, 72, 25, 249, 160, 71, 224, 202, 88, 173, 180, 185, 202, 122, 141, 20, 11, 144, 17, 63, 173, 7, 109, 85, 190, 17, 163, 201, 128, 215, 203, 120, 3, 161, 125, 36, 73, 160, 101, 27, 74, 41, 0, 190, 32, 217, 143, 145, 254, 104, 48, 8, 124, 223, 21, 140, 141, 37, 163, 93, 93, 35, 201, 161, 200, 160, 204, 31, 229, 25, 162, 51, 179, 21, 164, 140, 124, 208, 35, 28, 19, 246, 82, 169, 81, 214, 107, 40, 134, 138, 3, 217, 1, 144, 83, 75, 236, 143, 189, 52, 231, 26, 8, 24, 48, 0, 103, 25, 28, 28, 236, 237, 21, 142, 251, 143, 15, 6, 163, 208, 246, 40, 166, 62, 250, 41, 19, 16, 110, 136, 246, 41, 4, 132, 203, 128, 128, 217, 10, 82, 70, 62, 24, 145, 167, 49, 16, 240, 84, 168, 234, 53, 108, 210, 207, 146, 129, 164, 128, 118, 128, 65, 37, 2, 18, 7, 132, 90, 48, 172, 228, 54, 137, 3, 230, 186, 63, 38, 15, 143, 247, 146, 216, 159, 80, 116, 100, 44, 58, 20, 13, 70, 131, 152, 13, 27, 188, 160, 4, 128, 171, 176, 26, 120, 155, 146, 196, 250, 48, 52, 12, 103, 106, 159, 49, 129, 38, 19, 37, 160, 177, 209, 102, 211, 197, 170, 49, 100, 192, 202, 74, 106, 33, 249, 240, 198, 181, 107, 215, 178, 108, 11, 178, 81, 27, 230, 25, 245, 207, 149, 1, 56, 222, 43, 3, 16, 139, 38, 163, 132, 19, 128, 255, 21, 253, 127, 129, 140, 17, 25, 165, 233, 37, 137, 221, 211, 145, 191, 110, 158, 56, 148, 226, 51, 12, 246, 116, 169, 88, 236, 198, 98, 162, 210, 177, 102, 7, 255, 50, 234, 147, 196, 174, 12, 120, 165, 131, 30, 209, 1, 87, 83, 250, 86, 6, 96, 211, 166, 227, 66, 127, 159, 32, 28, 63, 30, 18, 130, 99, 9, 4, 32, 56, 150, 12, 166, 25, 9, 122, 46, 171, 227, 153, 201, 18, 27, 111, 90, 24, 167, 57, 100, 62, 34, 228, 130, 128, 56, 116, 33, 147, 88, 170, 193, 128, 167, 171, 248, 61, 194, 212, 59, 55, 98, 186, 156, 13, 168, 228, 81, 27, 7, 50, 242, 193, 152, 100, 0, 176, 1, 21, 84, 118, 132, 232, 208, 88, 144, 48, 66, 255, 23, 105, 46, 2, 29, 38, 100, 11, 180, 174, 153, 36, 177, 124, 71, 254, 148, 217, 116, 60, 53, 60, 187, 41, 135, 4, 73, 133, 53, 253, 19, 41, 188, 145, 218, 79, 236, 121, 35, 152, 56, 239, 166, 181, 192, 210, 7, 2, 94, 149, 194, 101, 53, 55, 96, 228, 131, 49, 201, 0, 144, 177, 93, 17, 0, 104, 250, 80, 127, 44, 58, 54, 18, 73, 247, 145, 6, 197, 241, 129, 34, 157, 100, 45, 253, 185, 248, 237, 29, 224, 21, 163, 38, 8, 3, 0, 112, 108, 5, 157, 48, 48, 0, 135, 204, 163, 204, 213, 198, 234, 85, 108, 88, 128, 72, 113, 0, 147, 254, 79, 147, 190, 10, 164, 143, 16, 234, 63, 81, 166, 95, 85, 0, 32, 158, 25, 32, 0, 4, 135, 198, 200, 120, 128, 214, 75, 148, 16, 168, 176, 106, 152, 128, 254, 92, 252, 246, 72, 216, 239, 139, 207, 190, 123, 74, 83, 124, 118, 126, 254, 228, 221, 241, 149, 43, 167, 172, 220, 63, 27, 249, 34, 211, 131, 84, 27, 182, 95, 236, 217, 0, 117, 127, 2, 33, 15, 81, 184, 88, 247, 47, 149, 180, 210, 75, 205, 39, 208, 149, 148, 14, 64, 69, 11, 2, 16, 67, 103, 32, 184, 107, 80, 11, 64, 106, 2, 131, 230, 231, 200, 245, 76, 120, 247, 237, 243, 34, 66, 213, 236, 184, 48, 121, 120, 246, 194, 184, 112, 251, 240, 194, 187, 227, 241, 42, 248, 63, 37, 146, 155, 78, 36, 100, 147, 37, 32, 68, 45, 57, 56, 249, 84, 225, 178, 7, 151, 206, 156, 105, 45, 175, 46, 22, 155, 46, 87, 18, 148, 231, 4, 132, 197, 37, 35, 143, 13, 104, 220, 4, 50, 38, 136, 198, 96, 151, 78, 251, 213, 37, 35, 58, 249, 39, 79, 56, 190, 238, 118, 97, 225, 148, 252, 252, 219, 135, 103, 239, 246, 11, 147, 251, 86, 174, 228, 253, 155, 31, 12, 135, 239, 233, 200, 40, 3, 250, 121, 143, 130, 10, 177, 96, 136, 20, 189, 5, 72, 213, 119, 128, 40, 220, 34, 235, 143, 193, 202, 21, 96, 227, 139, 241, 218, 114, 150, 45, 199, 199, 177, 229, 150, 20, 179, 216, 237, 46, 183, 252, 142, 186, 15, 8, 64, 52, 18, 17, 116, 1, 80, 149, 75, 84, 131, 217, 182, 169, 77, 2, 207, 251, 133, 121, 85, 43, 87, 162, 228, 207, 174, 226, 227, 183, 15, 172, 92, 7, 0, 204, 227, 179, 1, 160, 175, 3, 138, 173, 98, 199, 66, 219, 219, 209, 163, 67, 43, 80, 141, 10, 203, 170, 144, 110, 188, 182, 218, 106, 53, 40, 45, 204, 72, 22, 206, 229, 176, 75, 63, 237, 161, 74, 240, 120, 172, 63, 12, 13, 57, 174, 15, 64, 122, 213, 144, 58, 115, 231, 17, 22, 86, 85, 77, 238, 137, 76, 222, 188, 127, 243, 192, 236, 187, 155, 86, 230, 15, 44, 204, 14, 64, 113, 65, 145, 126, 18, 159, 227, 30, 16, 27, 5, 166, 29, 213, 30, 169, 4, 10, 181, 51, 56, 216, 199, 164, 158, 193, 90, 93, 174, 204, 154, 228, 144, 76, 70, 29, 0, 28, 224, 34, 111, 164, 41, 87, 125, 208, 205, 188, 112, 65, 31, 128, 11, 122, 3, 197, 69, 214, 2, 49, 19, 31, 223, 188, 112, 101, 79, 152, 239, 89, 185, 178, 41, 222, 20, 89, 183, 46, 30, 238, 232, 240, 251, 123, 154, 248, 240, 110, 93, 175, 128, 58, 215, 198, 121, 124, 182, 160, 72, 52, 0, 196, 171, 15, 144, 33, 240, 16, 131, 166, 33, 5, 0, 176, 162, 234, 34, 43, 253, 227, 53, 161, 116, 168, 18, 100, 237, 174, 20, 0, 255, 6, 186, 207, 15, 188, 124, 193, 96, 212, 84, 127, 164, 156, 45, 178, 146, 166, 120, 253, 60, 15, 61, 237, 3, 81, 240, 250, 225, 181, 151, 212, 77, 251, 253, 30, 47, 175, 117, 10, 224, 34, 155, 153, 222, 146, 74, 94, 100, 98, 80, 236, 25, 195, 243, 69, 53, 96, 224, 254, 171, 72, 180, 2, 110, 242, 15, 188, 140, 192, 241, 65, 177, 145, 126, 253, 230, 95, 248, 194, 196, 3, 155, 164, 98, 35, 190, 79, 39, 16, 254, 55, 10, 183, 97, 203, 247, 110, 45, 44, 44, 180, 22, 88, 51, 207, 164, 96, 169, 73, 108, 52, 138, 128, 20, 148, 170, 22, 167, 76, 128, 5, 179, 4, 0, 159, 158, 13, 76, 87, 130, 90, 42, 150, 187, 148, 205, 125, 84, 199, 144, 14, 22, 22, 110, 107, 59, 136, 134, 224, 13, 60, 22, 102, 59, 191, 45, 176, 148, 252, 5, 196, 178, 213, 39, 167, 252, 0, 151, 172, 11, 1, 130, 94, 174, 245, 130, 129, 8, 100, 169, 25, 243, 135, 79, 190, 7, 188, 206, 119, 111, 43, 216, 242, 225, 135, 130, 112, 67, 70, 74, 80, 245, 43, 74, 125, 151, 102, 195, 54, 20, 58, 72, 186, 190, 45, 7, 14, 160, 50, 32, 210, 241, 11, 199, 249, 11, 6, 74, 48, 51, 0, 173, 145, 252, 252, 252, 15, 252, 194, 254, 123, 226, 92, 252, 158, 41, 224, 254, 129, 33, 43, 174, 0, 109, 228, 81, 245, 69, 121, 65, 78, 142, 155, 56, 5, 64, 44, 166, 202, 90, 53, 32, 135, 255, 109, 57, 112, 0, 144, 211, 73, 255, 82, 61, 96, 0, 64, 230, 106, 161, 240, 131, 85, 241, 205, 15, 198, 215, 205, 190, 29, 0, 152, 12, 206, 0, 156, 206, 11, 66, 55, 196, 6, 66, 167, 205, 106, 35, 161, 46, 14, 225, 229, 232, 183, 42, 203, 70, 2, 129, 242, 204, 3, 160, 98, 121, 5, 121, 153, 141, 5, 84, 0, 176, 78, 84, 223, 199, 37, 214, 223, 165, 39, 3, 10, 55, 160, 145, 78, 136, 241, 146, 40, 141, 22, 153, 115, 252, 202, 121, 241, 121, 235, 132, 72, 252, 246, 184, 71, 152, 2, 177, 16, 244, 189, 112, 247, 131, 249, 147, 87, 230, 79, 89, 24, 102, 43, 138, 208, 94, 149, 143, 195, 97, 73, 33, 64, 154, 84, 148, 137, 9, 196, 124, 16, 125, 200, 44, 247, 213, 76, 157, 85, 100, 64, 122, 95, 210, 65, 32, 5, 64, 235, 224, 133, 65, 15, 138, 11, 9, 144, 224, 13, 66, 224, 139, 223, 125, 123, 126, 220, 231, 3, 0, 188, 194, 148, 123, 38, 175, 132, 15, 133, 41, 85, 194, 186, 124, 33, 114, 123, 142, 131, 231, 42, 162, 89, 173, 84, 253, 151, 54, 122, 78, 17, 130, 117, 176, 212, 76, 129, 161, 22, 0, 231, 235, 89, 16, 72, 1, 64, 202, 136, 125, 100, 154, 1, 207, 209, 19, 7, 123, 135, 87, 206, 139, 204, 94, 25, 230, 0, 0, 100, 253, 248, 228, 30, 47, 0, 208, 225, 175, 154, 231, 143, 95, 23, 0, 56, 30, 0, 189, 153, 170, 2, 51, 230, 34, 226, 208, 238, 125, 35, 27, 0, 52, 179, 166, 225, 128, 134, 215, 187, 84, 105, 192, 193, 116, 115, 40, 159, 201, 247, 246, 10, 131, 130, 7, 186, 254, 184, 71, 212, 26, 23, 122, 113, 146, 128, 48, 69, 64, 0, 60, 97, 8, 11, 238, 238, 1, 136, 166, 244, 212, 85, 205, 131, 176, 192, 72, 127, 230, 50, 100, 108, 130, 2, 98, 70, 52, 227, 73, 222, 0, 213, 143, 218, 217, 227, 205, 175, 55, 71, 101, 141, 15, 62, 221, 247, 150, 190, 117, 82, 9, 64, 42, 37, 201, 99, 171, 27, 249, 227, 66, 10, 35, 33, 252, 224, 131, 251, 103, 47, 36, 28, 224, 239, 88, 88, 53, 15, 140, 1, 2, 224, 203, 8, 64, 78, 67, 198, 217, 137, 170, 139, 204, 194, 223, 40, 105, 73, 157, 233, 243, 108, 243, 235, 255, 20, 19, 219, 195, 97, 174, 172, 188, 96, 102, 97, 39, 47, 32, 47, 12, 14, 42, 22, 52, 160, 115, 108, 252, 190, 148, 200, 0, 176, 241, 166, 117, 77, 113, 31, 23, 7, 207, 63, 190, 123, 221, 126, 172, 26, 226, 247, 11, 117, 61, 29, 62, 161, 202, 40, 89, 156, 219, 144, 177, 68, 134, 245, 67, 1, 227, 36, 184, 76, 109, 18, 68, 186, 235, 7, 116, 116, 93, 184, 16, 5, 165, 70, 4, 94, 78, 207, 87, 20, 96, 202, 90, 161, 124, 143, 83, 33, 241, 202, 137, 67, 68, 199, 227, 231, 209, 247, 241, 250, 189, 228, 37, 178, 217, 218, 86, 223, 206, 202, 214, 181, 107, 247, 24, 217, 228, 92, 134, 140, 83, 100, 53, 250, 194, 43, 27, 193, 12, 212, 38, 78, 169, 51, 88, 64, 65, 232, 123, 189, 11, 186, 142, 186, 189, 140, 124, 0, 213, 163, 240, 222, 121, 177, 223, 143, 167, 68, 64, 215, 79, 206, 58, 35, 53, 151, 33, 227, 20, 21, 233, 4, 18, 226, 248, 28, 27, 48, 17, 6, 81, 50, 92, 65, 162, 193, 233, 150, 50, 37, 140, 124, 160, 4, 17, 42, 141, 189, 5, 218, 106, 159, 191, 55, 29, 0, 159, 32, 8, 189, 189, 95, 244, 246, 242, 186, 119, 72, 167, 28, 134, 140, 101, 10, 4, 116, 66, 41, 243, 43, 188, 72, 100, 188, 132, 134, 203, 110, 23, 227, 35, 70, 62, 136, 196, 22, 219, 200, 248, 141, 31, 92, 128, 65, 210, 106, 17, 10, 201, 77, 228, 133, 54, 95, 43, 88, 193, 222, 11, 90, 30, 210, 37, 198, 236, 144, 113, 138, 192, 37, 88, 47, 15, 164, 74, 148, 113, 124, 14, 7, 94, 53, 238, 83, 166, 53, 68, 92, 246, 74, 206, 229, 202, 228, 118, 159, 20, 88, 239, 113, 210, 203, 148, 9, 36, 39, 65, 230, 8, 81, 101, 50, 242, 193, 128, 178, 158, 160, 67, 242, 112, 103, 234, 9, 51, 175, 128, 96, 43, 214, 153, 131, 149, 113, 17, 149, 74, 187, 211, 9, 49, 162, 34, 105, 40, 147, 203, 225, 112, 186, 185, 109, 75, 11, 196, 62, 224, 121, 190, 55, 21, 40, 137, 0, 12, 230, 50, 221, 202, 24, 0, 253, 129, 119, 175, 12, 64, 129, 28, 24, 100, 95, 1, 193, 150, 30, 68, 100, 4, 128, 117, 216, 237, 228, 159, 83, 253, 219, 24, 234, 187, 92, 240, 49, 97, 15, 116, 201, 192, 45, 190, 144, 90, 241, 166, 77, 52, 162, 230, 211, 224, 140, 124, 208, 33, 125, 193, 110, 73, 13, 120, 203, 243, 170, 50, 172, 128, 32, 105, 111, 101, 53, 29, 152, 170, 214, 204, 28, 224, 178, 59, 42, 57, 183, 203, 254, 29, 187, 83, 228, 2, 55, 70, 140, 118, 59, 125, 13, 106, 66, 252, 165, 162, 15, 85, 61, 222, 171, 208, 7, 148, 52, 133, 252, 57, 144, 66, 176, 149, 61, 145, 2, 128, 149, 68, 59, 131, 189, 145, 165, 223, 138, 135, 234, 226, 114, 120, 47, 4, 227, 81, 75, 134, 220, 18, 91, 233, 182, 83, 21, 0, 189, 141, 92, 224, 118, 83, 0, 228, 75, 88, 208, 146, 78, 23, 126, 96, 91, 223, 185, 145, 239, 245, 3, 248, 24, 240, 130, 98, 80, 79, 186, 109, 9, 181, 141, 99, 84, 132, 146, 82, 176, 93, 82, 222, 138, 35, 0, 72, 247, 76, 137, 54, 35, 31, 212, 148, 42, 183, 196, 135, 47, 40, 40, 2, 133, 88, 16, 127, 225, 133, 102, 75, 121, 134, 188, 28, 203, 166, 84, 160, 203, 238, 116, 124, 199, 46, 126, 14, 128, 200, 39, 33, 0, 128, 78, 249, 67, 51, 31, 2, 70, 181, 205, 252, 94, 129, 182, 132, 192, 67, 166, 242, 55, 142, 143, 11, 84, 130, 237, 118, 218, 157, 34, 6, 33, 69, 209, 135, 60, 221, 154, 145, 15, 42, 162, 69, 167, 34, 137, 77, 246, 133, 71, 95, 120, 33, 110, 161, 115, 244, 179, 231, 239, 92, 174, 74, 55, 138, 129, 195, 85, 233, 4, 3, 233, 80, 126, 199, 186, 128, 45, 22, 216, 37, 70, 101, 53, 217, 10, 82, 214, 210, 34, 251, 167, 222, 156, 22, 18, 72, 19, 108, 214, 101, 151, 111, 154, 242, 247, 165, 252, 18, 35, 31, 84, 164, 170, 34, 171, 102, 113, 62, 176, 63, 250, 122, 215, 240, 235, 175, 139, 58, 160, 200, 116, 141, 185, 219, 233, 112, 161, 13, 168, 84, 125, 234, 176, 131, 85, 120, 166, 242, 219, 208, 61, 162, 5, 178, 89, 139, 228, 164, 79, 139, 152, 161, 144, 122, 204, 212, 92, 64, 153, 100, 193, 198, 62, 128, 247, 149, 34, 248, 158, 128, 34, 222, 81, 117, 177, 134, 104, 165, 165, 28, 116, 122, 133, 96, 115, 56, 254, 66, 48, 46, 244, 189, 32, 43, 193, 34, 235, 67, 38, 98, 82, 41, 227, 155, 202, 160, 74, 244, 226, 139, 168, 40, 236, 246, 239, 124, 199, 69, 205, 38, 91, 76, 147, 215, 96, 50, 82, 217, 28, 236, 122, 47, 166, 53, 114, 226, 1, 134, 30, 92, 78, 7, 128, 207, 85, 234, 158, 147, 177, 154, 150, 106, 64, 57, 232, 12, 119, 125, 167, 89, 8, 190, 16, 108, 126, 61, 18, 73, 89, 129, 141, 203, 11, 204, 199, 164, 218, 156, 55, 48, 234, 139, 118, 199, 51, 79, 61, 227, 64, 7, 178, 18, 254, 195, 57, 196, 74, 90, 173, 197, 138, 165, 63, 73, 185, 78, 91, 246, 116, 181, 146, 24, 249, 224, 6, 223, 44, 247, 116, 187, 168, 33, 228, 160, 179, 53, 30, 253, 78, 100, 160, 225, 245, 230, 174, 23, 4, 2, 0, 239, 85, 126, 157, 243, 253, 233, 221, 25, 230, 9, 208, 133, 12, 209, 83, 196, 123, 112, 74, 58, 19, 7, 206, 170, 27, 113, 100, 83, 177, 222, 225, 56, 1, 112, 216, 53, 188, 39, 145, 241, 44, 25, 197, 172, 81, 49, 232, 228, 187, 94, 136, 135, 227, 241, 224, 235, 130, 101, 73, 29, 186, 113, 32, 81, 244, 235, 153, 21, 185, 197, 164, 202, 199, 100, 29, 110, 250, 152, 132, 63, 48, 150, 176, 139, 131, 142, 92, 69, 53, 142, 223, 21, 203, 11, 220, 100, 11, 86, 13, 8, 180, 45, 0, 107, 240, 101, 69, 182, 217, 34, 202, 160, 83, 120, 253, 245, 248, 192, 11, 175, 199, 125, 150, 218, 197, 111, 250, 133, 4, 31, 15, 251, 194, 62, 248, 122, 99, 129, 245, 31, 199, 247, 112, 233, 10, 184, 18, 4, 150, 117, 43, 236, 54, 87, 94, 4, 182, 119, 91, 168, 165, 197, 212, 28, 8, 61, 114, 179, 174, 74, 206, 161, 175, 4, 56, 156, 33, 168, 87, 86, 94, 46, 115, 134, 34, 232, 244, 198, 95, 104, 142, 14, 8, 62, 206, 82, 91, 187, 122, 117, 19, 159, 136, 183, 250, 54, 210, 175, 43, 30, 210, 137, 153, 76, 80, 107, 15, 186, 126, 12, 190, 186, 50, 6, 47, 235, 98, 61, 110, 3, 251, 202, 22, 93, 79, 253, 187, 203, 225, 52, 132, 64, 179, 216, 128, 186, 118, 138, 73, 5, 157, 225, 230, 23, 200, 228, 6, 0, 160, 182, 118, 241, 146, 58, 175, 242, 235, 114, 163, 106, 200, 76, 52, 92, 114, 85, 116, 204, 132, 146, 18, 136, 138, 134, 239, 59, 75, 134, 197, 116, 33, 208, 121, 208, 28, 8, 32, 72, 221, 85, 29, 174, 106, 199, 155, 148, 63, 197, 200, 7, 46, 28, 161, 115, 252, 8, 0, 0, 65, 9, 171, 252, 154, 213, 71, 32, 211, 120, 187, 39, 121, 115, 18, 180, 201, 240, 240, 216, 192, 112, 222, 137, 100, 220, 155, 60, 113, 109, 56, 121, 101, 152, 119, 234, 201, 44, 107, 118, 92, 216, 128, 92, 14, 41, 66, 5, 173, 160, 144, 50, 173, 63, 80, 161, 12, 128, 25, 249, 0, 1, 60, 81, 195, 46, 17, 128, 218, 29, 139, 151, 108, 86, 124, 205, 166, 164, 128, 31, 144, 114, 153, 252, 213, 171, 56, 208, 117, 69, 55, 189, 239, 63, 149, 55, 204, 241, 151, 243, 242, 142, 214, 39, 39, 213, 231, 149, 12, 95, 158, 159, 44, 169, 207, 203, 75, 180, 218, 117, 181, 214, 245, 1, 64, 114, 21, 34, 185, 29, 74, 4, 210, 74, 164, 88, 149, 56, 51, 242, 65, 188, 212, 105, 151, 0, 0, 90, 253, 88, 73, 141, 142, 27, 57, 54, 63, 41, 190, 138, 207, 175, 7, 44, 174, 228, 37, 244, 74, 93, 194, 245, 37, 113, 248, 174, 254, 218, 164, 250, 203, 127, 86, 127, 237, 230, 107, 239, 207, 31, 203, 203, 187, 156, 87, 31, 134, 144, 210, 32, 171, 98, 203, 232, 189, 100, 166, 202, 20, 2, 172, 177, 90, 228, 50, 10, 179, 27, 92, 22, 203, 142, 20, 2, 192, 6, 143, 151, 84, 201, 95, 211, 146, 35, 95, 98, 210, 21, 16, 235, 100, 50, 121, 229, 202, 164, 139, 201, 97, 95, 242, 98, 18, 139, 8, 211, 83, 220, 3, 243, 223, 15, 3, 23, 192, 73, 103, 223, 207, 187, 50, 118, 243, 181, 146, 250, 228, 159, 93, 76, 230, 189, 207, 115, 123, 48, 117, 224, 209, 161, 234, 2, 107, 133, 222, 231, 166, 168, 210, 33, 189, 218, 227, 176, 87, 42, 239, 202, 166, 94, 23, 20, 43, 190, 224, 210, 124, 15, 183, 29, 4, 201, 178, 100, 113, 173, 138, 86, 63, 190, 100, 29, 173, 215, 167, 234, 51, 124, 116, 254, 0, 39, 156, 154, 148, 7, 13, 154, 52, 127, 82, 201, 240, 169, 249, 99, 243, 75, 38, 77, 74, 164, 10, 94, 240, 222, 220, 149, 73, 151, 7, 134, 235, 231, 15, 95, 188, 57, 89, 82, 50, 144, 152, 148, 204, 59, 117, 118, 210, 149, 228, 205, 151, 137, 222, 128, 56, 138, 117, 234, 176, 1, 171, 19, 58, 154, 37, 201, 33, 192, 216, 84, 169, 7, 20, 53, 167, 122, 137, 99, 197, 143, 99, 146, 195, 194, 85, 45, 89, 173, 134, 160, 118, 7, 128, 80, 178, 153, 165, 213, 219, 200, 247, 222, 107, 147, 206, 38, 111, 62, 113, 234, 207, 78, 93, 195, 230, 37, 111, 94, 2, 61, 27, 230, 195, 97, 65, 136, 39, 40, 9, 87, 111, 62, 123, 54, 81, 159, 119, 53, 47, 239, 90, 222, 124, 224, 124, 208, 137, 8, 7, 178, 15, 37, 183, 93, 39, 179, 54, 174, 201, 158, 98, 179, 193, 217, 36, 81, 7, 188, 114, 56, 149, 122, 160, 194, 92, 225, 17, 71, 242, 43, 232, 10, 107, 33, 64, 16, 22, 63, 190, 228, 190, 191, 222, 92, 53, 156, 119, 214, 31, 134, 166, 36, 111, 190, 246, 243, 249, 3, 208, 172, 249, 239, 39, 254, 236, 90, 114, 210, 217, 163, 171, 255, 110, 245, 142, 29, 171, 69, 170, 189, 152, 151, 151, 247, 254, 181, 249, 121, 39, 234, 129, 249, 243, 74, 46, 95, 94, 114, 185, 254, 216, 192, 169, 146, 129, 212, 35, 143, 175, 165, 6, 132, 78, 177, 3, 250, 222, 69, 34, 117, 104, 139, 50, 64, 47, 199, 49, 156, 236, 51, 207, 49, 189, 66, 131, 161, 170, 116, 65, 144, 96, 88, 178, 26, 154, 92, 123, 172, 164, 228, 242, 177, 73, 215, 230, 215, 159, 61, 145, 151, 156, 116, 249, 253, 188, 139, 87, 225, 83, 53, 157, 186, 120, 246, 236, 169, 163, 103, 47, 94, 62, 123, 236, 226, 197, 139, 167, 106, 63, 61, 123, 236, 236, 167, 171, 143, 157, 104, 82, 254, 160, 75, 255, 65, 198, 227, 25, 130, 163, 9, 189, 190, 243, 41, 135, 152, 161, 98, 237, 89, 47, 73, 39, 22, 217, 70, 138, 6, 107, 74, 30, 215, 178, 1, 208, 209, 179, 147, 234, 235, 47, 214, 231, 157, 200, 155, 15, 42, 224, 196, 164, 19, 87, 39, 93, 91, 82, 114, 241, 68, 222, 69, 93, 196, 180, 180, 250, 241, 18, 121, 226, 156, 107, 193, 19, 250, 17, 183, 45, 119, 207, 112, 227, 90, 231, 51, 107, 49, 120, 149, 186, 222, 48, 72, 50, 110, 191, 19, 254, 43, 146, 162, 235, 150, 44, 222, 161, 121, 254, 159, 159, 45, 41, 41, 57, 123, 13, 254, 159, 72, 214, 31, 155, 127, 226, 98, 162, 254, 114, 61, 244, 245, 251, 159, 150, 148, 212, 214, 126, 211, 12, 4, 224, 98, 136, 166, 101, 227, 51, 236, 198, 181, 172, 94, 196, 157, 177, 224, 67, 151, 52, 193, 171, 211, 145, 91, 93, 26, 75, 178, 155, 78, 85, 86, 184, 166, 100, 201, 226, 52, 62, 184, 47, 250, 217, 216, 246, 99, 181, 167, 78, 157, 58, 118, 244, 83, 56, 212, 30, 251, 244, 232, 167, 120, 172, 253, 230, 15, 106, 107, 255, 66, 159, 105, 128, 78, 169, 62, 90, 188, 164, 70, 122, 232, 5, 118, 221, 136, 59, 211, 124, 52, 67, 0, 212, 3, 170, 46, 162, 21, 76, 223, 192, 73, 148, 50, 155, 158, 22, 103, 55, 151, 60, 174, 2, 97, 226, 185, 117, 37, 186, 29, 123, 223, 125, 6, 0, 92, 155, 15, 116, 236, 152, 250, 67, 234, 107, 103, 24, 5, 54, 158, 47, 160, 79, 250, 3, 170, 149, 134, 193, 114, 58, 185, 68, 163, 164, 55, 46, 80, 179, 185, 100, 9, 160, 176, 154, 8, 196, 15, 244, 155, 95, 91, 91, 2, 28, 112, 159, 226, 61, 90, 132, 197, 143, 63, 190, 100, 201, 229, 19, 39, 78, 76, 58, 11, 65, 166, 234, 244, 29, 143, 173, 147, 30, 218, 105, 191, 17, 149, 16, 250, 3, 170, 14, 209, 54, 102, 37, 105, 160, 195, 120, 96, 4, 96, 40, 89, 178, 228, 241, 199, 23, 3, 20, 96, 239, 106, 107, 63, 189, 136, 164, 70, 1, 49, 90, 189, 24, 44, 230, 146, 146, 146, 205, 85, 84, 219, 121, 255, 13, 28, 162, 209, 64, 205, 58, 64, 81, 169, 84, 86, 47, 113, 139, 15, 237, 92, 160, 219, 221, 24, 184, 7, 52, 107, 65, 25, 18, 163, 51, 160, 74, 108, 131, 60, 138, 147, 133, 200, 242, 14, 38, 54, 88, 168, 169, 218, 188, 110, 29, 168, 66, 240, 113, 242, 242, 78, 212, 62, 254, 248, 99, 139, 101, 33, 129, 191, 59, 74, 84, 21, 41, 237, 161, 208, 241, 171, 147, 46, 31, 39, 137, 223, 52, 219, 242, 216, 102, 241, 161, 23, 232, 50, 106, 185, 149, 20, 110, 28, 216, 100, 174, 94, 136, 145, 15, 50, 129, 83, 136, 90, 192, 149, 77, 14, 88, 7, 246, 0, 46, 239, 160, 209, 1, 198, 228, 25, 187, 122, 53, 49, 233, 42, 169, 86, 15, 253, 178, 68, 238, 92, 17, 0, 113, 17, 119, 4, 96, 120, 126, 201, 112, 168, 189, 133, 118, 165, 202, 197, 88, 125, 31, 125, 104, 183, 190, 168, 150, 91, 201, 20, 215, 118, 115, 185, 89, 70, 62, 40, 155, 134, 113, 23, 171, 239, 116, 166, 200, 141, 63, 79, 150, 119, 112, 231, 176, 197, 70, 75, 104, 116, 126, 125, 183, 148, 214, 11, 200, 16, 236, 88, 34, 87, 112, 6, 112, 105, 211, 127, 59, 59, 41, 25, 10, 144, 210, 94, 178, 50, 88, 167, 2, 130, 29, 75, 104, 222, 129, 77, 31, 111, 165, 84, 108, 13, 208, 132, 169, 153, 220, 44, 35, 31, 52, 228, 178, 103, 183, 6, 116, 121, 135, 92, 0, 224, 120, 104, 217, 65, 107, 106, 237, 183, 93, 127, 183, 67, 82, 241, 202, 101, 237, 199, 242, 142, 125, 17, 106, 75, 141, 5, 52, 42, 125, 237, 29, 143, 213, 144, 135, 118, 59, 244, 213, 128, 181, 58, 16, 106, 99, 217, 92, 235, 133, 210, 169, 210, 110, 224, 114, 33, 81, 135, 148, 46, 239, 224, 204, 5, 128, 43, 121, 39, 132, 80, 123, 185, 60, 53, 25, 36, 65, 20, 113, 240, 117, 222, 147, 218, 251, 111, 137, 146, 177, 80, 232, 233, 64, 91, 75, 75, 27, 206, 114, 36, 227, 97, 235, 30, 79, 41, 2, 234, 21, 177, 6, 193, 193, 198, 141, 129, 208, 206, 156, 235, 133, 52, 119, 121, 209, 254, 162, 145, 32, 209, 68, 173, 184, 188, 195, 79, 114, 0, 64, 128, 56, 31, 84, 129, 183, 113, 223, 65, 9, 0, 128, 224, 49, 145, 11, 86, 175, 254, 165, 136, 192, 23, 23, 142, 135, 26, 89, 17, 165, 182, 70, 58, 12, 86, 147, 98, 130, 199, 69, 191, 208, 165, 47, 169, 107, 25, 235, 250, 239, 85, 231, 86, 47, 164, 33, 6, 212, 161, 225, 32, 71, 138, 1, 190, 251, 221, 151, 44, 47, 167, 125, 107, 88, 181, 233, 187, 54, 233, 34, 102, 57, 219, 66, 1, 91, 1, 66, 208, 40, 66, 32, 121, 208, 171, 23, 75, 16, 132, 66, 21, 41, 137, 144, 234, 181, 214, 45, 214, 32, 96, 244, 240, 229, 15, 205, 156, 57, 147, 25, 71, 187, 21, 247, 224, 156, 79, 232, 186, 92, 106, 127, 217, 205, 89, 190, 49, 87, 29, 144, 27, 86, 109, 134, 47, 190, 79, 67, 91, 156, 184, 81, 100, 45, 88, 106, 45, 22, 87, 254, 147, 245, 225, 234, 197, 187, 196, 86, 115, 84, 39, 182, 181, 41, 38, 187, 87, 61, 150, 142, 0, 169, 47, 208, 62, 188, 124, 40, 182, 142, 119, 129, 32, 104, 197, 51, 246, 23, 245, 4, 201, 169, 84, 62, 160, 139, 45, 111, 189, 245, 163, 7, 10, 20, 191, 97, 60, 66, 198, 147, 44, 152, 199, 227, 1, 217, 198, 228, 18, 139, 249, 150, 162, 130, 245, 104, 186, 118, 137, 158, 193, 142, 199, 137, 46, 8, 52, 74, 166, 17, 57, 160, 93, 28, 7, 170, 145, 17, 120, 76, 12, 16, 117, 181, 117, 10, 0, 142, 44, 17, 53, 174, 25, 177, 107, 159, 128, 192, 64, 175, 240, 206, 173, 148, 59, 183, 203, 101, 217, 250, 214, 91, 191, 249, 205, 143, 30, 144, 115, 40, 90, 127, 189, 149, 238, 140, 224, 229, 121, 222, 227, 209, 14, 233, 85, 88, 109, 68, 31, 138, 66, 142, 114, 176, 148, 85, 24, 133, 246, 22, 121, 36, 172, 70, 82, 24, 181, 143, 137, 63, 151, 153, 3, 20, 191, 81, 80, 94, 93, 156, 75, 238, 136, 197, 225, 73, 109, 225, 157, 86, 235, 88, 214, 35, 2, 255, 252, 207, 191, 248, 209, 3, 52, 85, 175, 13, 50, 124, 137, 184, 23, 32, 104, 77, 24, 45, 162, 185, 141, 174, 127, 251, 30, 133, 96, 113, 103, 104, 169, 212, 120, 52, 134, 100, 133, 99, 113, 40, 159, 93, 34, 251, 14, 242, 213, 230, 226, 183, 234, 162, 2, 100, 5, 189, 149, 11, 244, 233, 169, 74, 183, 211, 197, 164, 125, 232, 86, 115, 28, 190, 181, 172, 23, 17, 248, 231, 127, 254, 13, 128, 240, 192, 3, 86, 208, 63, 214, 153, 214, 2, 235, 204, 130, 2, 91, 81, 81, 113, 121, 5, 23, 78, 36, 18, 126, 31, 175, 72, 131, 170, 200, 182, 84, 178, 249, 20, 130, 37, 235, 247, 42, 24, 64, 222, 1, 141, 122, 138, 50, 2, 37, 210, 67, 228, 16, 193, 234, 175, 92, 160, 79, 12, 58, 188, 105, 206, 6, 155, 38, 113, 36, 37, 246, 95, 231, 110, 123, 67, 68, 128, 208, 111, 148, 244, 139, 95, 252, 226, 165, 31, 33, 189, 244, 214, 111, 63, 51, 0, 0, 52, 65, 202, 233, 41, 129, 246, 45, 94, 26, 82, 2, 160, 120, 221, 162, 64, 96, 117, 149, 226, 65, 100, 206, 196, 250, 187, 204, 161, 76, 177, 201, 201, 86, 12, 30, 158, 80, 141, 24, 96, 10, 196, 67, 146, 212, 244, 200, 97, 17, 164, 101, 251, 244, 123, 191, 215, 249, 174, 2, 1, 125, 250, 13, 34, 241, 0, 114, 136, 173, 168, 92, 235, 193, 200, 221, 12, 14, 207, 142, 101, 33, 35, 194, 167, 144, 52, 97, 74, 8, 56, 57, 126, 115, 99, 89, 166, 211, 5, 214, 129, 36, 43, 88, 183, 78, 146, 167, 58, 151, 68, 122, 165, 50, 85, 10, 6, 128, 143, 71, 226, 97, 111, 24, 142, 126, 206, 23, 143, 52, 227, 138, 146, 101, 211, 190, 254, 176, 91, 232, 254, 215, 44, 8, 164, 56, 68, 134, 162, 192, 6, 50, 82, 84, 12, 82, 82, 205, 226, 212, 110, 148, 246, 191, 67, 13, 103, 8, 0, 198, 71, 146, 45, 216, 161, 104, 27, 169, 253, 130, 70, 75, 85, 105, 110, 0, 196, 109, 39, 164, 144, 16, 86, 100, 141, 92, 70, 213, 20, 169, 82, 112, 188, 252, 29, 83, 102, 79, 105, 138, 111, 190, 123, 246, 20, 129, 23, 242, 243, 123, 0, 128, 89, 51, 166, 79, 187, 243, 235, 47, 30, 234, 25, 29, 251, 223, 127, 52, 139, 130, 36, 44, 191, 32, 244, 18, 21, 19, 4, 229, 129, 249, 208, 180, 249, 7, 117, 183, 52, 18, 89, 192, 87, 133, 30, 209, 225, 195, 181, 37, 202, 7, 117, 187, 156, 105, 202, 128, 117, 3, 177, 232, 41, 0, 35, 112, 88, 186, 110, 48, 178, 144, 145, 228, 65, 84, 114, 53, 78, 234, 107, 154, 61, 60, 89, 136, 207, 171, 26, 200, 95, 71, 0, 24, 218, 78, 16, 120, 120, 237, 139, 135, 34, 209, 254, 243, 163, 99, 72, 9, 252, 255, 191, 79, 254, 246, 221, 93, 47, 1, 253, 34, 155, 124, 168, 233, 127, 125, 252, 209, 59, 123, 183, 110, 45, 45, 44, 213, 89, 54, 156, 243, 38, 18, 181, 59, 160, 253, 135, 15, 255, 165, 217, 36, 152, 211, 142, 255, 41, 60, 162, 186, 48, 157, 63, 115, 42, 24, 8, 231, 174, 220, 195, 231, 239, 142, 63, 248, 96, 211, 221, 17, 94, 232, 32, 0, 212, 5, 251, 23, 0, 2, 119, 222, 251, 15, 63, 101, 185, 61, 117, 117, 117, 29, 193, 161, 100, 236, 208, 33, 120, 37, 206, 240, 168, 46, 182, 89, 161, 111, 65, 19, 254, 226, 55, 166, 160, 248, 95, 95, 138, 180, 117, 155, 14, 11, 8, 137, 196, 98, 2, 192, 142, 7, 205, 182, 66, 17, 56, 137, 234, 194, 180, 30, 144, 148, 0, 245, 57, 125, 241, 123, 38, 223, 35, 240, 252, 237, 183, 47, 140, 111, 170, 108, 34, 0, 112, 92, 93, 52, 56, 235, 206, 59, 239, 159, 118, 255, 79, 201, 175, 120, 19, 177, 161, 228, 80, 157, 206, 99, 148, 23, 17, 36, 8, 20, 153, 184, 66, 6, 224, 203, 207, 239, 210, 2, 224, 227, 195, 77, 171, 17, 128, 195, 181, 198, 131, 186, 154, 223, 150, 35, 20, 55, 229, 5, 211, 19, 111, 69, 25, 160, 85, 10, 225, 217, 85, 241, 170, 252, 225, 41, 61, 241, 217, 155, 235, 188, 18, 0, 156, 231, 80, 172, 236, 206, 167, 124, 143, 78, 251, 111, 4, 1, 33, 113, 168, 95, 23, 129, 212, 227, 212, 108, 43, 42, 32, 88, 0, 125, 247, 71, 34, 201, 12, 242, 101, 138, 222, 181, 165, 139, 0, 161, 175, 149, 16, 4, 76, 179, 128, 42, 66, 193, 241, 48, 243, 139, 76, 186, 148, 75, 132, 8, 83, 34, 225, 248, 228, 129, 201, 241, 112, 213, 131, 126, 31, 238, 1, 36, 230, 4, 247, 28, 9, 222, 123, 239, 158, 67, 247, 222, 139, 125, 18, 78, 240, 61, 209, 100, 226, 144, 225, 61, 189, 32, 54, 66, 171, 15, 11, 94, 113, 204, 217, 159, 72, 128, 147, 196, 130, 215, 110, 67, 84, 126, 84, 88, 90, 186, 117, 235, 222, 119, 62, 166, 60, 96, 211, 105, 63, 199, 222, 74, 0, 56, 108, 218, 7, 82, 71, 40, 88, 44, 107, 53, 117, 29, 43, 150, 252, 187, 40, 179, 241, 85, 119, 207, 187, 123, 243, 192, 236, 252, 7, 167, 116, 248, 124, 205, 41, 0, 16, 130, 167, 190, 254, 112, 248, 181, 233, 143, 178, 60, 14, 246, 70, 1, 1, 35, 30, 240, 29, 234, 234, 106, 110, 238, 138, 14, 37, 112, 108, 216, 239, 5, 196, 6, 52, 181, 51, 108, 197, 242, 130, 15, 17, 129, 223, 45, 106, 9, 4, 210, 218, 207, 113, 15, 255, 53, 182, 255, 247, 111, 230, 2, 128, 34, 66, 1, 78, 157, 105, 210, 34, 84, 58, 112, 190, 131, 20, 117, 133, 193, 250, 133, 125, 225, 158, 142, 120, 221, 78, 79, 36, 200, 43, 179, 194, 123, 94, 188, 255, 235, 175, 133, 103, 204, 120, 109, 24, 218, 227, 241, 68, 13, 164, 192, 227, 111, 238, 34, 212, 140, 32, 32, 88, 62, 136, 18, 116, 127, 251, 142, 207, 17, 129, 143, 138, 7, 18, 3, 97, 127, 155, 106, 219, 7, 207, 159, 19, 14, 56, 173, 217, 149, 217, 128, 180, 17, 138, 211, 101, 110, 137, 134, 74, 187, 3, 43, 140, 193, 45, 38, 162, 224, 241, 145, 137, 124, 62, 15, 46, 156, 133, 252, 164, 200, 8, 177, 255, 240, 223, 190, 126, 239, 180, 105, 211, 23, 12, 97, 123, 234, 98, 201, 168, 78, 53, 167, 151, 23, 219, 47, 129, 16, 236, 231, 189, 58, 28, 64, 104, 46, 69, 224, 109, 82, 65, 192, 171, 238, 230, 36, 44, 240, 201, 251, 38, 154, 64, 72, 59, 12, 194, 162, 69, 200, 126, 161, 251, 59, 162, 115, 85, 73, 10, 171, 232, 82, 207, 12, 57, 144, 220, 160, 50, 37, 198, 174, 216, 120, 255, 180, 59, 167, 77, 159, 177, 189, 206, 195, 133, 99, 137, 164, 86, 13, 248, 142, 40, 219, 79, 168, 33, 193, 199, 19, 250, 245, 99, 62, 155, 10, 1, 250, 161, 136, 3, 213, 2, 151, 205, 178, 128, 222, 48, 72, 5, 182, 32, 115, 228, 224, 86, 70, 192, 136, 0, 93, 234, 153, 220, 133, 113, 160, 137, 84, 111, 188, 252, 253, 71, 94, 188, 119, 218, 244, 233, 51, 202, 162, 29, 231, 207, 31, 209, 10, 1, 31, 211, 180, 191, 171, 57, 8, 141, 211, 221, 74, 176, 39, 22, 179, 17, 77, 248, 241, 250, 243, 68, 86, 144, 3, 253, 3, 62, 63, 26, 180, 175, 19, 67, 112, 250, 132, 116, 178, 39, 203, 226, 131, 140, 124, 144, 201, 202, 97, 128, 227, 146, 93, 100, 13, 177, 78, 117, 209, 128, 203, 177, 138, 46, 245, 76, 111, 70, 68, 72, 157, 20, 253, 217, 35, 223, 127, 246, 225, 25, 211, 167, 79, 127, 180, 63, 26, 246, 196, 146, 65, 233, 78, 212, 12, 111, 74, 104, 154, 143, 8, 36, 18, 3, 58, 129, 162, 135, 172, 201, 104, 21, 93, 162, 245, 239, 157, 79, 224, 54, 164, 132, 23, 188, 96, 208, 158, 254, 26, 81, 131, 71, 59, 252, 180, 225, 158, 176, 224, 207, 180, 210, 148, 22, 0, 185, 172, 194, 165, 90, 13, 75, 209, 94, 205, 24, 153, 75, 187, 212, 115, 90, 86, 248, 251, 143, 60, 249, 236, 166, 89, 211, 203, 182, 207, 232, 226, 185, 67, 99, 146, 37, 16, 205, 48, 159, 72, 99, 128, 104, 20, 143, 231, 195, 94, 157, 76, 17, 135, 0, 196, 86, 21, 74, 62, 209, 222, 210, 45, 229, 8, 192, 64, 36, 220, 195, 111, 220, 249, 52, 85, 131, 181, 71, 19, 116, 153, 61, 47, 120, 136, 153, 22, 92, 209, 0, 160, 152, 2, 200, 146, 25, 108, 105, 141, 117, 59, 181, 249, 182, 85, 170, 165, 158, 49, 26, 86, 3, 112, 224, 192, 147, 143, 172, 120, 246, 103, 220, 130, 89, 253, 209, 89, 219, 235, 184, 126, 73, 11, 136, 102, 56, 158, 38, 1, 177, 161, 161, 32, 162, 16, 231, 117, 246, 81, 227, 124, 100, 65, 182, 254, 245, 31, 203, 110, 209, 71, 91, 75, 75, 215, 151, 174, 47, 180, 109, 59, 224, 89, 123, 47, 117, 134, 106, 207, 38, 112, 122, 157, 15, 218, 159, 219, 246, 20, 234, 130, 2, 172, 153, 82, 125, 173, 55, 68, 90, 161, 94, 234, 153, 84, 8, 168, 1, 168, 121, 18, 56, 96, 39, 119, 192, 53, 163, 121, 104, 86, 89, 115, 84, 146, 1, 209, 12, 243, 93, 105, 237, 143, 225, 159, 96, 208, 160, 235, 120, 4, 96, 40, 241, 198, 59, 95, 106, 232, 227, 119, 230, 254, 237, 95, 80, 0, 142, 129, 76, 12, 8, 3, 67, 67, 130, 222, 42, 141, 134, 164, 221, 124, 192, 141, 147, 183, 228, 119, 122, 202, 49, 109, 169, 103, 151, 6, 0, 174, 243, 201, 71, 158, 125, 22, 144, 237, 172, 155, 181, 125, 100, 142, 35, 153, 20, 55, 23, 18, 205, 176, 247, 144, 138, 255, 135, 98, 253, 244, 149, 96, 48, 251, 33, 74, 0, 72, 68, 108, 123, 63, 215, 98, 240, 249, 127, 36, 0, 36, 146, 167, 136, 94, 232, 143, 13, 213, 84, 20, 120, 175, 111, 135, 6, 201, 44, 152, 43, 17, 32, 222, 145, 26, 128, 157, 43, 86, 60, 251, 83, 174, 21, 247, 113, 157, 227, 24, 177, 63, 58, 38, 217, 1, 209, 12, 239, 81, 49, 64, 127, 87, 16, 68, 32, 216, 223, 229, 195, 237, 135, 116, 126, 1, 132, 96, 23, 105, 221, 153, 183, 215, 151, 254, 46, 29, 129, 149, 203, 136, 29, 72, 94, 62, 9, 103, 12, 197, 98, 31, 36, 60, 75, 195, 137, 204, 171, 244, 100, 39, 55, 106, 126, 199, 120, 1, 120, 249, 251, 207, 62, 91, 45, 110, 90, 248, 232, 130, 232, 140, 57, 35, 65, 159, 207, 135, 218, 153, 33, 102, 120, 163, 74, 4, 64, 254, 1, 129, 161, 46, 172, 160, 214, 127, 240, 104, 100, 127, 66, 162, 238, 109, 133, 91, 127, 253, 241, 231, 41, 94, 248, 156, 200, 192, 239, 147, 201, 83, 165, 219, 222, 139, 197, 130, 171, 122, 184, 165, 137, 156, 214, 161, 85, 82, 42, 94, 52, 157, 43, 112, 75, 0, 164, 24, 248, 167, 143, 60, 255, 51, 143, 180, 109, 227, 2, 176, 135, 179, 162, 177, 4, 209, 78, 12, 126, 205, 120, 52, 54, 48, 58, 20, 237, 138, 67, 60, 196, 235, 74, 1, 120, 2, 212, 7, 226, 19, 113, 76, 170, 151, 47, 47, 42, 42, 90, 52, 215, 86, 72, 20, 227, 87, 169, 47, 148, 76, 254, 235, 151, 123, 31, 88, 186, 75, 240, 122, 121, 147, 237, 215, 201, 140, 42, 226, 69, 179, 41, 51, 146, 36, 182, 212, 213, 237, 241, 138, 59, 54, 115, 220, 147, 143, 60, 95, 157, 218, 200, 215, 1, 14, 193, 140, 134, 126, 98, 158, 68, 0, 210, 253, 160, 32, 232, 193, 243, 9, 31, 154, 118, 61, 242, 199, 250, 177, 253, 97, 31, 215, 202, 135, 201, 41, 7, 106, 58, 59, 55, 117, 118, 238, 91, 100, 219, 250, 229, 58, 81, 6, 146, 103, 81, 45, 22, 70, 192, 159, 50, 183, 73, 77, 250, 10, 196, 228, 217, 82, 241, 162, 217, 100, 9, 229, 128, 177, 177, 68, 16, 100, 158, 118, 224, 247, 31, 217, 164, 0, 32, 24, 4, 30, 152, 177, 189, 63, 5, 128, 79, 211, 254, 40, 56, 130, 130, 215, 80, 114, 99, 49, 1, 1, 144, 223, 123, 17, 0, 32, 92, 91, 172, 200, 70, 100, 224, 147, 171, 127, 164, 34, 241, 206, 27, 102, 55, 233, 41, 208, 177, 22, 227, 88, 135, 132, 234, 128, 161, 161, 177, 100, 76, 106, 242, 147, 43, 42, 170, 229, 246, 119, 118, 36, 187, 102, 1, 19, 148, 197, 68, 43, 239, 225, 59, 52, 237, 71, 63, 144, 247, 27, 114, 110, 52, 22, 197, 240, 90, 86, 237, 98, 251, 1, 1, 79, 103, 162, 155, 4, 68, 135, 255, 248, 127, 68, 165, 240, 59, 179, 21, 179, 86, 157, 207, 198, 177, 14, 9, 73, 178, 90, 60, 158, 67, 67, 201, 126, 113, 251, 230, 21, 10, 0, 186, 35, 184, 194, 246, 172, 233, 51, 230, 148, 69, 105, 7, 183, 38, 162, 205, 154, 246, 67, 68, 44, 24, 75, 46, 223, 195, 251, 19, 66, 152, 79, 157, 80, 83, 33, 221, 255, 124, 231, 50, 154, 22, 73, 142, 137, 16, 236, 53, 201, 188, 186, 217, 32, 69, 188, 104, 114, 0, 141, 250, 1, 208, 185, 117, 253, 201, 40, 121, 166, 125, 43, 158, 175, 150, 1, 136, 196, 130, 201, 145, 40, 63, 203, 1, 14, 65, 148, 56, 105, 224, 199, 170, 21, 96, 148, 4, 2, 188, 42, 140, 81, 215, 23, 192, 221, 89, 175, 106, 243, 203, 206, 206, 214, 3, 7, 184, 198, 206, 78, 65, 144, 0, 248, 36, 153, 28, 123, 151, 32, 176, 200, 28, 0, 250, 196, 200, 241, 162, 118, 145, 109, 93, 114, 99, 157, 146, 69, 192, 25, 110, 35, 99, 29, 216, 230, 106, 0, 96, 159, 4, 64, 52, 58, 148, 196, 189, 54, 231, 148, 141, 60, 250, 104, 23, 185, 34, 158, 72, 15, 6, 154, 99, 233, 94, 160, 186, 190, 192, 163, 225, 215, 214, 3, 28, 189, 255, 0, 223, 249, 173, 195, 34, 11, 36, 63, 173, 93, 143, 0, 148, 94, 39, 0, 82, 180, 144, 121, 193, 77, 66, 210, 144, 145, 133, 52, 0, 34, 95, 68, 128, 251, 233, 203, 41, 29, 40, 45, 178, 207, 207, 177, 143, 216, 231, 28, 225, 60, 254, 214, 1, 112, 88, 212, 8, 52, 159, 79, 215, 255, 234, 236, 157, 22, 0, 128, 128, 222, 255, 139, 78, 137, 3, 14, 127, 90, 91, 91, 123, 116, 47, 6, 11, 215, 51, 155, 142, 145, 15, 102, 200, 206, 186, 201, 152, 129, 133, 136, 183, 39, 154, 28, 27, 138, 117, 236, 169, 60, 116, 36, 34, 68, 212, 0, 196, 248, 71, 31, 29, 41, 155, 211, 213, 74, 44, 122, 90, 56, 212, 220, 159, 72, 115, 0, 212, 218, 216, 195, 85, 232, 9, 108, 39, 192, 220, 219, 217, 185, 75, 4, 224, 40, 14, 149, 109, 53, 47, 3, 218, 5, 10, 228, 182, 51, 38, 219, 207, 58, 196, 177, 114, 11, 72, 176, 223, 207, 181, 70, 113, 191, 229, 61, 47, 214, 29, 249, 32, 26, 149, 0, 24, 162, 8, 244, 243, 79, 204, 73, 52, 204, 138, 6, 251, 81, 233, 165, 137, 64, 34, 29, 0, 181, 54, 246, 112, 213, 186, 10, 9, 0, 16, 224, 55, 68, 25, 216, 81, 15, 0, 252, 193, 52, 0, 6, 179, 26, 13, 79, 215, 171, 122, 114, 209, 226, 250, 74, 167, 197, 159, 8, 131, 167, 7, 108, 217, 209, 21, 243, 251, 195, 117, 235, 196, 246, 119, 131, 115, 154, 160, 59, 237, 68, 59, 157, 179, 202, 166, 207, 24, 26, 218, 189, 121, 221, 126, 185, 237, 13, 20, 128, 129, 244, 223, 83, 101, 239, 12, 231, 136, 119, 30, 64, 0, 34, 20, 129, 13, 103, 107, 79, 37, 175, 33, 0, 54, 51, 187, 213, 152, 216, 121, 75, 201, 34, 233, 85, 79, 36, 78, 192, 177, 5, 214, 141, 0, 196, 7, 60, 97, 31, 109, 115, 205, 63, 84, 134, 7, 146, 67, 93, 162, 17, 0, 4, 250, 147, 100, 167, 5, 190, 211, 133, 46, 81, 176, 106, 247, 238, 85, 155, 165, 190, 95, 69, 254, 36, 180, 75, 228, 49, 138, 236, 157, 241, 36, 121, 15, 2, 208, 93, 79, 1, 56, 119, 10, 248, 15, 109, 225, 86, 214, 196, 106, 88, 214, 172, 237, 87, 157, 146, 86, 245, 4, 238, 79, 101, 37, 153, 222, 15, 193, 179, 203, 105, 1, 255, 179, 213, 67, 182, 45, 71, 35, 80, 9, 78, 65, 50, 17, 4, 141, 72, 196, 63, 218, 65, 16, 0, 173, 80, 133, 8, 52, 40, 152, 127, 243, 238, 46, 146, 15, 212, 250, 128, 140, 124, 200, 4, 0, 215, 141, 63, 121, 142, 234, 193, 111, 157, 131, 136, 224, 18, 6, 72, 115, 179, 239, 86, 99, 198, 200, 165, 3, 160, 244, 17, 89, 92, 136, 194, 97, 119, 218, 237, 110, 223, 145, 30, 75, 92, 72, 180, 122, 5, 14, 226, 129, 206, 242, 21, 24, 9, 116, 196, 198, 146, 35, 65, 10, 64, 108, 85, 48, 57, 4, 175, 144, 61, 102, 76, 119, 136, 10, 96, 55, 182, 125, 85, 51, 213, 129, 90, 239, 53, 13, 0, 35, 126, 37, 160, 11, 162, 12, 28, 5, 22, 24, 35, 222, 224, 115, 25, 87, 195, 34, 100, 66, 3, 40, 1, 208, 245, 17, 113, 37, 24, 167, 47, 62, 47, 223, 194, 129, 4, 8, 224, 172, 183, 182, 118, 62, 191, 162, 156, 88, 193, 142, 232, 24, 8, 63, 34, 16, 217, 21, 141, 37, 163, 40, 3, 157, 157, 117, 79, 240, 61, 34, 2, 11, 197, 255, 232, 6, 104, 157, 192, 52, 0, 244, 28, 119, 36, 31, 178, 64, 183, 104, 9, 191, 117, 254, 34, 149, 129, 47, 109, 89, 87, 195, 50, 67, 42, 53, 161, 55, 181, 194, 141, 203, 99, 245, 204, 94, 25, 177, 248, 195, 100, 222, 163, 183, 85, 25, 9, 116, 244, 211, 205, 182, 86, 225, 86, 51, 67, 251, 99, 221, 146, 251, 74, 17, 216, 45, 169, 128, 230, 104, 150, 4, 142, 71, 220, 253, 64, 143, 252, 66, 183, 240, 197, 31, 14, 203, 44, 112, 145, 248, 66, 95, 206, 53, 92, 13, 43, 7, 170, 38, 33, 177, 180, 235, 8, 163, 25, 83, 112, 145, 185, 166, 194, 228, 224, 107, 22, 49, 93, 113, 30, 58, 89, 25, 9, 116, 196, 112, 159, 169, 88, 16, 141, 225, 174, 88, 68, 14, 16, 210, 236, 96, 71, 150, 7, 241, 232, 237, 125, 34, 145, 215, 15, 63, 188, 129, 34, 176, 44, 113, 106, 44, 65, 92, 129, 223, 253, 173, 209, 106, 88, 50, 153, 198, 70, 100, 4, 70, 62, 136, 4, 237, 71, 91, 208, 117, 207, 238, 168, 4, 64, 130, 135, 72, 224, 217, 106, 101, 44, 136, 149, 18, 201, 88, 52, 56, 118, 53, 152, 66, 0, 152, 64, 1, 65, 179, 241, 16, 50, 37, 84, 130, 198, 70, 11, 1, 16, 93, 129, 13, 39, 206, 94, 75, 246, 254, 154, 140, 33, 148, 115, 89, 0, 176, 102, 249, 85, 36, 50, 106, 32, 206, 29, 102, 228, 3, 33, 151, 180, 58, 92, 93, 48, 63, 95, 6, 64, 80, 71, 2, 148, 186, 18, 160, 1, 48, 38, 138, 69, 37, 41, 232, 228, 21, 92, 208, 156, 45, 128, 207, 188, 86, 12, 74, 223, 9, 145, 5, 54, 36, 64, 15, 190, 77, 18, 102, 54, 150, 243, 50, 153, 174, 179, 102, 249, 85, 36, 162, 121, 172, 116, 13, 107, 70, 62, 16, 74, 21, 232, 242, 241, 184, 12, 64, 162, 123, 185, 22, 128, 206, 32, 0, 64, 54, 28, 236, 82, 50, 129, 16, 36, 99, 195, 93, 205, 217, 36, 32, 51, 0, 30, 50, 243, 90, 84, 131, 203, 206, 163, 41, 164, 195, 40, 115, 189, 137, 112, 171, 241, 165, 166, 130, 61, 60, 167, 220, 102, 211, 170, 96, 87, 74, 132, 124, 66, 79, 85, 10, 128, 196, 198, 21, 202, 124, 152, 232, 15, 39, 163, 253, 65, 220, 119, 110, 36, 24, 12, 166, 62, 230, 187, 19, 231, 163, 193, 174, 96, 214, 12, 78, 102, 14, 104, 21, 20, 44, 112, 52, 113, 234, 218, 177, 29, 165, 100, 212, 96, 17, 124, 49, 192, 235, 100, 25, 196, 45, 72, 77, 76, 169, 178, 234, 126, 10, 206, 159, 61, 85, 107, 230, 239, 152, 189, 82, 1, 128, 239, 249, 229, 229, 105, 237, 239, 24, 75, 4, 163, 137, 100, 48, 26, 77, 142, 140, 13, 241, 10, 4, 128, 123, 195, 188, 63, 107, 246, 5, 1, 200, 96, 182, 125, 137, 148, 22, 248, 86, 34, 1, 65, 81, 9, 25, 69, 249, 117, 49, 10, 165, 206, 5, 104, 31, 159, 179, 62, 103, 98, 74, 149, 190, 241, 133, 0, 80, 57, 102, 192, 135, 249, 20, 0, 231, 189, 222, 231, 170, 211, 0, 8, 146, 141, 7, 49, 36, 130, 87, 81, 41, 157, 213, 217, 205, 175, 245, 122, 127, 108, 102, 17, 84, 4, 192, 200, 17, 64, 82, 176, 192, 134, 207, 18, 103, 33, 38, 90, 72, 18, 198, 91, 107, 48, 149, 232, 215, 48, 16, 113, 17, 171, 205, 76, 169, 178, 89, 213, 203, 135, 16, 210, 169, 40, 72, 1, 16, 247, 250, 170, 211, 1, 136, 129, 31, 24, 13, 10, 208, 245, 53, 29, 29, 171, 36, 0, 60, 94, 31, 121, 0, 38, 235, 83, 80, 14, 200, 80, 207, 68, 98, 108, 17, 128, 101, 126, 239, 155, 181, 71, 47, 83, 111, 96, 107, 185, 162, 164, 32, 69, 0, 64, 181, 201, 212, 103, 185, 118, 51, 48, 183, 93, 59, 85, 43, 5, 64, 79, 101, 101, 221, 145, 104, 164, 67, 41, 1, 201, 161, 126, 63, 231, 241, 122, 224, 117, 77, 133, 216, 254, 3, 228, 65, 228, 67, 102, 34, 93, 104, 77, 251, 80, 25, 159, 122, 253, 16, 142, 19, 33, 248, 214, 39, 171, 56, 238, 40, 120, 196, 84, 17, 190, 67, 170, 42, 210, 252, 44, 246, 161, 153, 214, 239, 129, 91, 203, 102, 77, 125, 234, 149, 207, 176, 14, 157, 249, 25, 20, 0, 232, 231, 232, 17, 127, 56, 62, 76, 246, 154, 149, 49, 8, 38, 131, 173, 100, 240, 186, 243, 128, 167, 174, 2, 8, 184, 159, 114, 37, 35, 31, 244, 136, 143, 156, 233, 139, 68, 120, 9, 128, 244, 103, 209, 204, 202, 217, 2, 8, 108, 56, 124, 250, 36, 199, 53, 93, 77, 38, 71, 75, 105, 142, 120, 61, 65, 64, 148, 130, 138, 7, 170, 177, 83, 203, 215, 50, 255, 104, 157, 105, 181, 150, 103, 147, 1, 189, 213, 67, 116, 198, 203, 1, 0, 127, 103, 247, 121, 178, 201, 104, 120, 207, 158, 61, 135, 58, 130, 49, 176, 121, 67, 168, 225, 35, 193, 14, 0, 64, 220, 26, 196, 203, 121, 168, 244, 155, 24, 187, 20, 206, 156, 19, 41, 204, 241, 242, 249, 138, 209, 106, 237, 172, 156, 85, 27, 118, 156, 6, 250, 229, 22, 238, 3, 120, 146, 51, 20, 129, 207, 11, 187, 81, 50, 121, 98, 106, 138, 37, 119, 138, 65, 183, 246, 123, 15, 49, 89, 30, 66, 171, 120, 244, 234, 5, 16, 0, 14, 56, 125, 4, 68, 61, 248, 227, 127, 144, 66, 161, 68, 82, 218, 123, 55, 25, 83, 168, 33, 31, 15, 148, 117, 12, 63, 114, 238, 156, 4, 64, 31, 128, 113, 46, 218, 71, 181, 185, 98, 126, 148, 206, 24, 198, 178, 79, 17, 128, 211, 27, 182, 112, 152, 25, 56, 81, 64, 211, 228, 191, 222, 114, 254, 252, 182, 210, 82, 149, 44, 147, 150, 51, 89, 199, 209, 53, 28, 192, 218, 245, 75, 170, 0, 128, 142, 177, 36, 154, 120, 69, 36, 208, 209, 21, 3, 153, 128, 64, 48, 57, 100, 114, 172, 70, 34, 63, 182, 63, 122, 46, 122, 230, 92, 4, 219, 207, 133, 17, 140, 51, 61, 234, 147, 116, 226, 211, 234, 183, 9, 0, 167, 151, 85, 215, 36, 78, 30, 171, 173, 181, 210, 49, 212, 207, 75, 145, 25, 62, 87, 38, 202, 24, 249, 144, 145, 216, 244, 8, 68, 191, 255, 9, 0, 67, 164, 253, 7, 86, 60, 169, 52, 2, 88, 177, 146, 0, 11, 192, 251, 114, 89, 249, 48, 34, 246, 253, 25, 145, 1, 56, 207, 25, 124, 25, 225, 84, 14, 152, 110, 124, 250, 30, 69, 160, 138, 91, 133, 11, 22, 212, 47, 250, 181, 98, 32, 253, 215, 229, 169, 139, 25, 249, 144, 153, 210, 0, 48, 154, 77, 204, 90, 246, 116, 209, 113, 33, 140, 4, 106, 82, 0, 28, 240, 144, 48, 185, 231, 208, 17, 243, 60, 224, 239, 59, 23, 147, 229, 255, 220, 57, 84, 130, 194, 153, 230, 186, 14, 4, 0, 87, 108, 177, 187, 20, 173, 72, 175, 121, 91, 181, 131, 34, 176, 165, 224, 77, 204, 17, 191, 95, 92, 168, 24, 72, 95, 158, 94, 17, 146, 107, 250, 156, 53, 154, 155, 85, 105, 129, 136, 143, 104, 253, 138, 21, 207, 111, 98, 21, 38, 176, 21, 66, 53, 33, 220, 115, 228, 136, 233, 202, 157, 72, 20, 27, 221, 23, 197, 46, 223, 222, 16, 62, 132, 53, 59, 117, 118, 187, 195, 33, 62, 2, 86, 173, 74, 43, 64, 203, 135, 20, 173, 58, 76, 0, 216, 245, 215, 92, 61, 120, 3, 201, 15, 216, 20, 19, 160, 12, 168, 3, 128, 204, 75, 68, 105, 200, 229, 48, 250, 198, 105, 25, 195, 33, 17, 80, 126, 47, 63, 242, 228, 179, 63, 83, 134, 2, 94, 193, 239, 241, 241, 71, 122, 76, 214, 172, 180, 246, 33, 227, 115, 100, 153, 245, 86, 182, 210, 225, 194, 150, 179, 78, 123, 144, 85, 172, 111, 35, 173, 1, 199, 200, 7, 5, 45, 251, 4, 218, 255, 201, 239, 55, 172, 242, 28, 189, 150, 76, 94, 107, 226, 158, 183, 237, 21, 235, 41, 62, 90, 196, 170, 57, 192, 56, 201, 160, 71, 172, 211, 112, 174, 9, 107, 233, 32, 174, 77, 231, 129, 239, 211, 242, 40, 232, 122, 9, 128, 156, 244, 159, 40, 253, 125, 135, 28, 78, 178, 234, 52, 46, 255, 90, 233, 178, 151, 53, 171, 24, 200, 69, 22, 69, 118, 56, 159, 192, 55, 76, 250, 61, 14, 64, 251, 193, 35, 186, 239, 222, 154, 95, 190, 151, 76, 94, 109, 130, 118, 46, 95, 62, 151, 50, 65, 193, 76, 117, 81, 152, 53, 203, 243, 40, 57, 196, 173, 55, 97, 82, 34, 75, 39, 125, 66, 246, 145, 71, 158, 125, 86, 124, 14, 17, 129, 44, 63, 161, 164, 214, 62, 0, 160, 15, 0, 104, 112, 116, 69, 35, 132, 103, 80, 230, 88, 71, 115, 36, 130, 85, 70, 78, 186, 16, 115, 37, 93, 146, 27, 254, 232, 79, 149, 216, 117, 154, 56, 197, 255, 249, 222, 42, 136, 140, 147, 151, 201, 34, 84, 207, 125, 68, 17, 248, 184, 112, 145, 18, 2, 171, 209, 163, 208, 97, 133, 135, 190, 151, 58, 217, 237, 200, 52, 215, 72, 170, 17, 194, 26, 209, 159, 138, 175, 69, 14, 200, 220, 102, 37, 17, 219, 127, 38, 118, 46, 184, 221, 190, 253, 92, 95, 68, 112, 224, 66, 95, 206, 237, 107, 153, 200, 153, 136, 189, 172, 161, 199, 229, 112, 145, 249, 95, 164, 217, 248, 100, 78, 29, 153, 132, 223, 171, 22, 115, 3, 95, 187, 31, 60, 194, 177, 99, 181, 187, 241, 243, 69, 82, 149, 221, 231, 133, 138, 110, 45, 54, 146, 1, 58, 172, 96, 83, 228, 84, 141, 12, 32, 37, 9, 128, 151, 1, 128, 151, 197, 215, 7, 48, 25, 97, 126, 243, 163, 86, 236, 250, 115, 193, 134, 237, 142, 50, 135, 195, 1, 29, 30, 105, 112, 108, 47, 43, 179, 55, 56, 252, 168, 23, 206, 52, 160, 19, 232, 112, 176, 149, 84, 21, 145, 137, 112, 96, 15, 100, 197, 212, 218, 215, 231, 231, 248, 62, 172, 77, 119, 46, 164, 8, 252, 197, 195, 77, 215, 142, 45, 91, 182, 140, 204, 230, 45, 159, 43, 21, 152, 109, 53, 161, 252, 53, 195, 10, 238, 204, 53, 229, 18, 0, 207, 2, 0, 255, 67, 249, 133, 233, 205, 143, 122, 160, 141, 205, 208, 228, 221, 117, 145, 51, 29, 101, 14, 103, 228, 76, 20, 154, 220, 16, 117, 148, 117, 81, 104, 136, 67, 224, 74, 49, 189, 219, 233, 116, 57, 237, 169, 79, 124, 224, 43, 156, 57, 215, 135, 30, 163, 243, 254, 175, 137, 8, 60, 177, 159, 76, 46, 36, 197, 237, 220, 242, 82, 209, 36, 170, 227, 59, 189, 128, 71, 179, 201, 70, 6, 5, 168, 2, 96, 5, 26, 1, 229, 23, 102, 23, 154, 70, 237, 215, 188, 189, 57, 122, 238, 76, 95, 95, 15, 91, 233, 239, 139, 110, 183, 59, 42, 215, 70, 250, 182, 67, 203, 225, 203, 24, 89, 182, 153, 85, 61, 44, 46, 178, 73, 38, 133, 186, 113, 50, 187, 0, 119, 232, 106, 112, 190, 214, 5, 74, 211, 125, 255, 127, 166, 8, 220, 250, 148, 19, 218, 127, 153, 234, 1, 142, 93, 100, 35, 170, 96, 175, 202, 5, 174, 208, 25, 36, 5, 239, 242, 57, 171, 114, 88, 193, 145, 101, 82, 129, 4, 0, 53, 2, 105, 0, 152, 136, 188, 209, 248, 7, 237, 209, 115, 61, 2, 196, 61, 108, 165, 243, 80, 7, 206, 80, 217, 184, 137, 139, 136, 118, 33, 170, 119, 21, 107, 39, 171, 111, 131, 165, 116, 56, 92, 125, 219, 203, 236, 101, 219, 241, 29, 230, 106, 239, 253, 47, 34, 2, 143, 218, 107, 65, 21, 94, 251, 64, 188, 228, 185, 173, 162, 71, 164, 188, 141, 118, 109, 90, 112, 48, 173, 140, 114, 88, 193, 46, 110, 4, 98, 8, 0, 21, 114, 133, 17, 16, 201, 76, 209, 145, 31, 2, 191, 232, 185, 134, 178, 32, 225, 114, 14, 55, 31, 112, 56, 220, 149, 78, 215, 83, 224, 231, 118, 40, 28, 194, 116, 194, 137, 60, 116, 233, 117, 28, 163, 114, 108, 7, 253, 129, 11, 244, 147, 185, 163, 211, 36, 4, 54, 98, 108, 156, 76, 74, 8, 216, 244, 134, 207, 53, 245, 130, 16, 41, 226, 226, 27, 140, 252, 129, 203, 105, 16, 5, 137, 74, 206, 178, 118, 83, 15, 248, 58, 74, 35, 32, 82, 150, 205, 143, 90, 59, 34, 125, 49, 108, 96, 179, 35, 122, 46, 162, 106, 165, 219, 69, 252, 92, 136, 4, 163, 125, 18, 52, 106, 114, 59, 20, 51, 58, 201, 94, 37, 14, 167, 179, 18, 193, 112, 185, 28, 83, 239, 19, 17, 216, 212, 132, 214, 48, 121, 148, 86, 119, 47, 210, 42, 1, 221, 109, 5, 136, 195, 192, 40, 63, 209, 103, 1, 81, 201, 89, 58, 234, 48, 120, 105, 85, 24, 1, 137, 24, 157, 181, 186, 100, 226, 203, 28, 246, 237, 196, 242, 219, 29, 13, 231, 116, 199, 200, 57, 254, 181, 142, 142, 115, 61, 154, 175, 56, 140, 141, 85, 43, 89, 224, 104, 181, 211, 141, 79, 233, 118, 58, 42, 43, 239, 76, 33, 112, 49, 153, 60, 85, 91, 75, 16, 208, 5, 64, 39, 247, 57, 83, 250, 249, 44, 36, 42, 57, 203, 185, 142, 62, 8, 94, 127, 150, 110, 4, 184, 204, 113, 151, 191, 204, 222, 28, 41, 3, 159, 191, 172, 204, 105, 119, 234, 12, 15, 224, 117, 236, 19, 206, 237, 93, 0, 64, 214, 169, 77, 44, 56, 10, 226, 73, 61, 188, 175, 149, 187, 83, 244, 7, 110, 221, 89, 119, 150, 172, 222, 182, 31, 190, 121, 254, 99, 115, 85, 84, 218, 199, 118, 233, 46, 110, 45, 42, 57, 203, 185, 158, 158, 51, 2, 175, 49, 2, 186, 119, 146, 201, 191, 187, 161, 185, 140, 181, 59, 65, 153, 243, 29, 187, 245, 236, 12, 94, 199, 58, 236, 101, 193, 8, 167, 251, 243, 106, 114, 87, 98, 247, 123, 234, 234, 26, 26, 182, 219, 65, 156, 238, 164, 147, 41, 118, 220, 202, 122, 176, 122, 166, 246, 242, 9, 15, 87, 141, 14, 209, 214, 172, 183, 210, 187, 187, 174, 33, 20, 149, 156, 5, 133, 220, 163, 53, 2, 92, 70, 0, 58, 202, 206, 0, 111, 87, 210, 53, 169, 140, 167, 239, 1, 103, 55, 235, 195, 175, 71, 158, 237, 101, 13, 205, 13, 46, 59, 250, 178, 19, 40, 2, 37, 203, 88, 14, 162, 99, 16, 132, 179, 77, 28, 154, 129, 95, 107, 7, 26, 171, 179, 77, 162, 101, 13, 6, 83, 169, 146, 179, 128, 144, 243, 125, 94, 141, 17, 200, 72, 188, 35, 72, 179, 28, 198, 63, 202, 162, 203, 135, 118, 206, 120, 41, 252, 116, 242, 159, 59, 35, 244, 113, 236, 51, 118, 240, 101, 215, 254, 121, 137, 56, 92, 82, 205, 237, 63, 134, 170, 240, 234, 7, 152, 28, 250, 152, 218, 65, 207, 219, 89, 72, 113, 91, 227, 133, 213, 24, 84, 114, 22, 6, 247, 70, 255, 25, 0, 240, 83, 131, 211, 180, 196, 55, 52, 159, 203, 12, 128, 3, 181, 26, 110, 198, 101, 24, 135, 235, 81, 43, 137, 63, 159, 118, 61, 221, 211, 225, 103, 111, 221, 65, 71, 11, 190, 85, 193, 213, 157, 34, 25, 202, 165, 41, 59, 248, 118, 247, 133, 75, 25, 232, 66, 119, 10, 1, 214, 56, 20, 98, 240, 96, 97, 34, 62, 117, 36, 144, 157, 58, 202, 208, 252, 101, 203, 19, 184, 156, 196, 213, 113, 186, 42, 115, 42, 117, 0, 223, 171, 174, 239, 76, 31, 235, 16, 231, 87, 31, 254, 214, 74, 142, 251, 0, 60, 130, 83, 4, 128, 185, 139, 158, 135, 219, 189, 157, 177, 253, 128, 64, 10, 128, 12, 43, 235, 49, 120, 176, 144, 163, 38, 18, 200, 68, 126, 112, 124, 98, 193, 51, 244, 141, 164, 0, 244, 27, 201, 186, 221, 24, 2, 131, 28, 144, 181, 143, 43, 77, 204, 100, 1, 199, 203, 221, 115, 46, 118, 38, 82, 247, 147, 122, 113, 220, 120, 21, 199, 53, 157, 189, 92, 187, 80, 140, 139, 183, 46, 98, 223, 206, 220, 254, 75, 151, 222, 174, 144, 30, 200, 97, 188, 222, 48, 105, 58, 117, 133, 159, 212, 49, 2, 74, 234, 233, 233, 59, 87, 86, 86, 22, 36, 189, 222, 188, 29, 195, 95, 158, 104, 23, 220, 80, 198, 137, 17, 103, 70, 102, 119, 211, 93, 16, 92, 70, 203, 236, 43, 9, 125, 47, 247, 153, 190, 136, 155, 173, 92, 32, 149, 14, 128, 42, 4, 93, 88, 34, 207, 58, 90, 148, 29, 128, 98, 209, 73, 206, 186, 180, 34, 5, 64, 207, 8, 40, 72, 32, 78, 125, 164, 185, 172, 25, 172, 186, 223, 14, 126, 3, 58, 120, 96, 4, 64, 195, 147, 121, 171, 208, 176, 44, 186, 14, 60, 28, 167, 221, 196, 58, 143, 212, 247, 226, 207, 96, 233, 137, 235, 190, 13, 146, 24, 128, 242, 175, 169, 95, 42, 1, 240, 142, 8, 64, 119, 119, 247, 201, 75, 151, 122, 155, 122, 241, 205, 96, 231, 165, 94, 248, 160, 187, 151, 114, 64, 193, 34, 91, 233, 214, 194, 69, 11, 244, 23, 110, 76, 3, 64, 27, 9, 168, 136, 151, 242, 188, 125, 142, 230, 62, 161, 161, 225, 12, 25, 244, 65, 35, 151, 203, 178, 46, 232, 238, 153, 178, 8, 140, 124, 112, 45, 147, 120, 96, 3, 78, 247, 124, 179, 80, 202, 14, 81, 0, 6, 191, 114, 215, 93, 63, 188, 212, 125, 219, 95, 221, 214, 13, 239, 254, 230, 43, 151, 106, 238, 186, 235, 174, 175, 84, 17, 0, 108, 123, 201, 153, 239, 175, 203, 182, 155, 9, 1, 32, 179, 17, 192, 176, 14, 58, 61, 22, 59, 215, 215, 12, 109, 46, 235, 19, 67, 252, 172, 145, 102, 58, 85, 58, 76, 77, 103, 99, 228, 3, 142, 153, 81, 0, 14, 147, 128, 192, 179, 255, 237, 111, 144, 28, 41, 5, 224, 228, 29, 120, 252, 171, 170, 75, 111, 223, 117, 233, 82, 211, 93, 183, 225, 187, 222, 219, 6, 9, 0, 20, 169, 238, 55, 247, 127, 249, 78, 230, 250, 99, 2, 64, 102, 35, 128, 13, 38, 97, 15, 230, 61, 236, 219, 207, 192, 123, 129, 196, 82, 110, 123, 101, 150, 132, 81, 250, 160, 138, 203, 97, 98, 201, 95, 70, 62, 112, 116, 220, 20, 0, 120, 243, 244, 233, 38, 100, 130, 58, 58, 116, 76, 1, 232, 188, 163, 24, 204, 1, 116, 255, 224, 109, 151, 122, 239, 232, 253, 10, 126, 246, 195, 121, 151, 82, 0, 156, 124, 243, 205, 127, 193, 234, 211, 140, 225, 48, 30, 50, 27, 1, 225, 92, 80, 12, 237, 207, 52, 59, 28, 93, 208, 253, 81, 116, 36, 129, 165, 157, 217, 18, 70, 218, 81, 37, 205, 62, 117, 25, 136, 60, 119, 245, 183, 54, 108, 216, 80, 242, 254, 233, 211, 167, 137, 57, 176, 41, 0, 232, 253, 225, 202, 187, 238, 184, 116, 27, 8, 253, 87, 46, 221, 213, 116, 9, 1, 184, 112, 91, 111, 10, 128, 223, 109, 126, 243, 36, 57, 221, 154, 225, 71, 8, 0, 89, 140, 0, 116, 249, 153, 62, 204, 238, 156, 11, 118, 149, 69, 34, 231, 130, 61, 24, 74, 64, 247, 103, 77, 24, 105, 1, 96, 215, 226, 142, 100, 36, 117, 235, 206, 166, 56, 69, 90, 184, 108, 179, 125, 59, 25, 52, 57, 122, 128, 91, 244, 121, 10, 0, 164, 175, 244, 222, 209, 13, 124, 255, 246, 109, 63, 252, 225, 87, 86, 94, 186, 180, 242, 175, 68, 43, 240, 229, 231, 165, 139, 158, 47, 127, 168, 84, 30, 88, 201, 8, 64, 22, 35, 64, 9, 60, 85, 224, 132, 51, 103, 98, 68, 1, 60, 5, 65, 118, 246, 132, 145, 22, 0, 12, 65, 158, 177, 59, 158, 217, 84, 233, 116, 178, 166, 116, 72, 37, 186, 149, 39, 232, 184, 217, 233, 63, 44, 35, 203, 114, 136, 74, 16, 152, 224, 43, 23, 254, 230, 135, 151, 170, 238, 234, 94, 183, 110, 29, 170, 63, 162, 14, 137, 18, 92, 206, 226, 234, 201, 236, 34, 98, 59, 11, 53, 119, 197, 181, 153, 128, 123, 31, 126, 130, 0, 240, 8, 157, 50, 157, 133, 34, 56, 208, 201, 247, 69, 207, 181, 226, 146, 36, 14, 51, 179, 212, 180, 0, 144, 40, 156, 125, 2, 130, 4, 187, 9, 167, 128, 60, 42, 154, 207, 147, 167, 37, 58, 252, 128, 173, 180, 144, 2, 80, 117, 219, 29, 183, 85, 129, 244, 223, 65, 155, 13, 34, 80, 117, 151, 228, 7, 112, 210, 250, 249, 132, 101, 52, 81, 244, 253, 95, 173, 169, 217, 127, 203, 137, 19, 223, 254, 54, 2, 96, 58, 18, 8, 99, 0, 176, 187, 3, 58, 197, 101, 110, 183, 108, 125, 0, 128, 115, 88, 135, 41, 167, 128, 80, 37, 180, 100, 11, 25, 55, 163, 244, 89, 231, 50, 81, 4, 46, 244, 74, 156, 160, 113, 132, 240, 66, 162, 111, 73, 30, 165, 148, 251, 153, 136, 118, 77, 83, 211, 7, 39, 78, 156, 45, 249, 11, 136, 49, 111, 73, 38, 187, 102, 89, 184, 64, 224, 229, 71, 254, 251, 190, 125, 166, 118, 43, 7, 39, 61, 226, 119, 80, 61, 198, 100, 74, 24, 25, 2, 32, 167, 26, 95, 52, 161, 13, 165, 245, 95, 192, 203, 100, 87, 41, 32, 56, 157, 221, 19, 196, 139, 201, 118, 143, 239, 124, 249, 229, 191, 252, 185, 197, 114, 235, 254, 15, 78, 156, 58, 90, 82, 242, 181, 178, 228, 208, 212, 169, 142, 163, 255, 161, 182, 246, 44, 0, 16, 155, 106, 9, 133, 66, 251, 214, 188, 186, 239, 87, 242, 186, 159, 153, 8, 93, 194, 136, 35, 195, 0, 103, 86, 0, 20, 156, 3, 194, 237, 204, 28, 44, 139, 38, 83, 76, 59, 108, 233, 148, 33, 200, 30, 12, 157, 61, 246, 194, 15, 234, 143, 157, 61, 75, 204, 230, 222, 251, 190, 153, 220, 30, 76, 206, 250, 249, 215, 254, 195, 15, 110, 25, 154, 213, 21, 180, 124, 10, 0, 156, 154, 48, 150, 28, 154, 136, 0, 188, 10, 0, 224, 10, 128, 217, 119, 65, 140, 40, 194, 224, 113, 2, 160, 228, 28, 136, 148, 204, 56, 135, 114, 2, 117, 213, 6, 17, 130, 15, 179, 133, 195, 221, 201, 19, 63, 248, 121, 237, 169, 100, 55, 169, 58, 92, 248, 131, 255, 148, 116, 148, 37, 23, 252, 228, 47, 255, 178, 246, 155, 13, 211, 167, 151, 53, 3, 0, 245, 239, 207, 154, 184, 96, 194, 189, 8, 192, 43, 0, 128, 185, 141, 13, 90, 207, 164, 234, 93, 174, 3, 0, 197, 117, 149, 68, 21, 24, 36, 149, 196, 134, 167, 226, 57, 118, 225, 178, 63, 80, 4, 50, 231, 67, 186, 199, 146, 255, 19, 0, 248, 52, 73, 43, 111, 151, 254, 245, 173, 99, 13, 115, 146, 101, 223, 190, 239, 107, 181, 223, 156, 245, 147, 91, 127, 50, 189, 230, 38, 188, 231, 253, 95, 125, 152, 67, 0, 214, 172, 217, 183, 207, 228, 78, 184, 125, 173, 17, 221, 52, 175, 1, 101, 7, 128, 174, 42, 91, 169, 171, 15, 196, 65, 173, 180, 145, 212, 3, 59, 78, 43, 233, 147, 195, 245, 157, 187, 150, 45, 92, 181, 5, 44, 91, 77, 211, 254, 55, 235, 235, 143, 93, 188, 124, 249, 234, 213, 159, 255, 160, 182, 246, 211, 107, 165, 226, 184, 234, 127, 220, 59, 243, 166, 205, 183, 126, 245, 153, 9, 220, 19, 247, 114, 15, 127, 245, 254, 84, 248, 14, 0, 4, 214, 128, 14, 84, 46, 244, 197, 53, 182, 27, 73, 67, 142, 43, 124, 152, 0, 0, 139, 103, 92, 14, 93, 73, 192, 205, 50, 200, 110, 64, 106, 170, 88, 182, 225, 247, 167, 211, 233, 147, 63, 156, 252, 229, 134, 101, 203, 182, 120, 86, 173, 90, 181, 165, 198, 83, 83, 245, 147, 205, 53, 64, 54, 49, 126, 252, 234, 151, 95, 222, 241, 231, 204, 83, 236, 211, 154, 233, 120, 0, 192, 175, 214, 188, 178, 239, 87, 82, 139, 27, 165, 197, 243, 233, 170, 225, 1, 67, 40, 76, 145, 169, 2, 43, 178, 224, 157, 174, 42, 192, 96, 222, 169, 99, 45, 217, 45, 7, 126, 171, 129, 64, 134, 226, 232, 225, 223, 118, 239, 90, 118, 223, 178, 42, 0, 163, 32, 181, 130, 205, 59, 140, 238, 116, 60, 139, 100, 4, 136, 10, 104, 108, 9, 4, 90, 26, 197, 157, 177, 189, 141, 109, 109, 129, 212, 178, 160, 184, 127, 132, 118, 93, 188, 27, 0, 0, 137, 148, 221, 110, 157, 40, 9, 139, 108, 12, 18, 58, 21, 203, 150, 29, 54, 196, 224, 244, 233, 79, 127, 254, 79, 20, 140, 151, 10, 183, 138, 121, 148, 143, 172, 11, 171, 117, 166, 227, 89, 36, 35, 208, 40, 238, 144, 205, 109, 82, 237, 12, 238, 85, 47, 141, 44, 66, 97, 118, 39, 24, 211, 37, 118, 46, 169, 150, 74, 69, 118, 123, 101, 134, 253, 82, 182, 108, 233, 252, 195, 39, 6, 0, 188, 255, 243, 247, 229, 215, 191, 221, 69, 108, 97, 233, 39, 191, 255, 195, 7, 245, 16, 89, 117, 119, 195, 97, 195, 135, 226, 209, 34, 26, 129, 128, 184, 65, 56, 18, 89, 16, 223, 219, 6, 127, 60, 45, 235, 3, 102, 123, 91, 143, 114, 169, 49, 212, 225, 117, 220, 51, 38, 163, 153, 172, 94, 181, 108, 89, 73, 247, 201, 223, 107, 112, 248, 167, 159, 159, 82, 188, 35, 170, 112, 239, 250, 127, 213, 131, 202, 66, 141, 64, 64, 177, 60, 182, 204, 236, 129, 150, 64, 104, 95, 233, 191, 23, 0, 227, 216, 46, 78, 186, 114, 213, 194, 101, 203, 90, 171, 54, 236, 56, 44, 41, 199, 19, 10, 6, 0, 18, 215, 51, 219, 186, 94, 23, 0, 98, 4, 168, 238, 11, 52, 182, 164, 51, 123, 142, 143, 146, 86, 87, 147, 17, 128, 198, 64, 91, 118, 223, 211, 60, 49, 108, 245, 150, 31, 238, 252, 199, 101, 64, 157, 251, 127, 80, 127, 88, 97, 41, 118, 73, 203, 153, 149, 106, 33, 176, 16, 35, 176, 147, 112, 62, 110, 239, 162, 106, 191, 66, 212, 205, 173, 202, 145, 94, 87, 147, 17, 0, 175, 98, 247, 137, 27, 64, 170, 130, 134, 234, 45, 91, 86, 113, 53, 4, 140, 247, 54, 28, 62, 188, 116, 175, 84, 104, 85, 186, 75, 3, 192, 190, 53, 175, 112, 191, 2, 77, 23, 240, 166, 107, 187, 84, 251, 43, 76, 110, 19, 156, 94, 87, 147, 89, 4, 2, 57, 237, 192, 158, 149, 82, 81, 134, 122, 129, 122, 182, 98, 203, 150, 10, 171, 92, 107, 86, 186, 158, 168, 191, 110, 241, 104, 1, 35, 240, 202, 190, 246, 0, 23, 106, 107, 76, 147, 129, 113, 60, 67, 122, 93, 77, 54, 0, 188, 28, 215, 22, 16, 235, 241, 204, 151, 165, 25, 254, 186, 20, 101, 232, 213, 69, 149, 203, 149, 183, 159, 151, 46, 34, 37, 75, 44, 57, 90, 66, 175, 172, 0, 43, 40, 234, 192, 64, 203, 38, 89, 252, 205, 239, 122, 150, 162, 244, 186, 154, 236, 34, 80, 46, 47, 54, 107, 186, 44, 205, 144, 24, 249, 160, 191, 137, 147, 77, 134, 160, 48, 149, 35, 179, 16, 35, 192, 114, 162, 18, 132, 235, 169, 62, 108, 131, 174, 25, 199, 51, 164, 165, 73, 50, 2, 128, 91, 212, 216, 222, 144, 89, 205, 108, 89, 154, 49, 49, 226, 193, 229, 54, 200, 53, 61, 111, 251, 72, 134, 64, 42, 183, 178, 4, 0, 128, 87, 65, 4, 218, 229, 39, 9, 72, 182, 112, 156, 15, 161, 72, 147, 100, 145, 241, 64, 160, 218, 35, 27, 155, 113, 172, 133, 165, 253, 109, 114, 200, 48, 28, 152, 130, 224, 119, 115, 233, 239, 88, 246, 97, 36, 112, 156, 184, 123, 180, 197, 237, 215, 161, 3, 210, 67, 157, 44, 0, 224, 98, 188, 30, 178, 43, 89, 227, 184, 214, 194, 210, 167, 204, 165, 161, 207, 217, 164, 33, 198, 66, 50, 102, 100, 121, 121, 205, 43, 175, 254, 10, 52, 126, 74, 235, 7, 254, 221, 0, 16, 9, 126, 11, 21, 161, 169, 44, 163, 9, 202, 82, 26, 154, 130, 160, 20, 17, 176, 128, 17, 120, 181, 5, 122, 32, 197, 241, 84, 29, 180, 175, 63, 168, 185, 52, 251, 2, 47, 227, 2, 160, 69, 228, 61, 198, 68, 150, 49, 59, 153, 72, 182, 47, 167, 11, 62, 227, 52, 117, 206, 242, 202, 154, 159, 66, 147, 21, 249, 64, 209, 16, 182, 180, 104, 1, 200, 190, 192, 75, 26, 153, 180, 243, 45, 212, 18, 50, 242, 225, 122, 200, 204, 94, 131, 220, 162, 189, 210, 128, 137, 101, 205, 154, 103, 95, 85, 5, 184, 200, 0, 123, 245, 21, 160, 92, 137, 109, 182, 230, 35, 35, 0, 154, 13, 194, 25, 249, 112, 61, 148, 65, 5, 74, 244, 188, 173, 116, 235, 151, 226, 128, 9, 2, 176, 15, 85, 160, 152, 248, 160, 221, 95, 168, 235, 3, 236, 92, 203, 206, 44, 32, 202, 186, 194, 106, 110, 233, 191, 76, 0, 136, 19, 219, 216, 20, 14, 140, 124, 208, 57, 219, 172, 155, 100, 130, 1, 82, 147, 145, 182, 202, 0, 112, 180, 253, 141, 162, 9, 56, 168, 224, 0, 182, 162, 192, 106, 125, 0, 159, 118, 83, 206, 11, 188, 152, 16, 129, 34, 115, 19, 192, 76, 187, 73, 58, 243, 163, 211, 104, 121, 106, 165, 103, 194, 1, 43, 20, 35, 227, 41, 71, 152, 178, 0, 139, 155, 184, 23, 20, 225, 42, 166, 44, 173, 94, 201, 109, 129, 151, 27, 231, 235, 155, 117, 147, 12, 118, 113, 84, 82, 169, 220, 126, 44, 186, 179, 172, 81, 141, 140, 183, 75, 94, 128, 94, 42, 80, 84, 209, 140, 249, 199, 54, 6, 192, 6, 200, 154, 191, 143, 105, 55, 41, 203, 244, 16, 164, 114, 226, 10, 189, 243, 206, 71, 191, 46, 69, 135, 24, 57, 64, 53, 50, 30, 200, 228, 4, 48, 242, 193, 28, 221, 56, 14, 48, 235, 38, 185, 179, 106, 128, 229, 104, 2, 11, 171, 171, 233, 250, 42, 0, 64, 218, 200, 184, 2, 0, 114, 74, 185, 92, 143, 93, 94, 164, 6, 192, 196, 66, 224, 55, 48, 220, 53, 229, 38, 233, 101, 86, 185, 180, 217, 95, 68, 5, 164, 198, 203, 17, 0, 61, 14, 104, 199, 36, 64, 117, 81, 129, 85, 177, 58, 165, 205, 198, 224, 31, 134, 190, 51, 51, 119, 81, 23, 0, 157, 26, 127, 51, 196, 152, 112, 147, 244, 156, 192, 198, 52, 113, 174, 64, 27, 176, 87, 238, 244, 52, 29, 32, 110, 149, 25, 90, 127, 112, 169, 181, 160, 184, 32, 109, 191, 207, 34, 101, 155, 43, 76, 88, 66, 93, 0, 114, 89, 67, 94, 65, 140, 124, 48, 36, 221, 25, 194, 141, 233, 121, 39, 50, 33, 93, 158, 123, 131, 0, 188, 172, 60, 91, 180, 131, 223, 216, 167, 247, 11, 229, 218, 117, 73, 50, 146, 30, 0, 134, 107, 139, 101, 33, 70, 62, 24, 146, 91, 47, 17, 224, 73, 31, 245, 86, 207, 189, 177, 172, 81, 151, 8, 138, 237, 95, 106, 144, 15, 41, 55, 189, 229, 37, 253, 113, 157, 207, 198, 187, 72, 26, 35, 31, 140, 200, 148, 19, 44, 151, 76, 136, 100, 121, 229, 251, 42, 45, 136, 219, 196, 182, 238, 45, 204, 57, 31, 172, 79, 55, 52, 231, 151, 149, 76, 56, 193, 72, 213, 123, 149, 50, 96, 121, 117, 197, 10, 85, 137, 28, 246, 127, 97, 166, 177, 47, 253, 85, 82, 245, 73, 5, 128, 137, 88, 242, 186, 200, 36, 3, 136, 50, 32, 217, 1, 203, 190, 53, 170, 249, 98, 168, 4, 182, 22, 30, 12, 25, 103, 236, 193, 51, 30, 159, 39, 136, 126, 236, 63, 126, 239, 58, 87, 74, 52, 38, 214, 110, 142, 1, 68, 67, 40, 149, 206, 89, 218, 1, 0, 165, 12, 4, 140, 61, 65, 137, 204, 171, 66, 21, 0, 232, 199, 62, 84, 156, 97, 177, 208, 235, 35, 211, 12, 32, 178, 0, 89, 200, 220, 195, 91, 56, 148, 1, 133, 39, 112, 240, 96, 218, 144, 136, 14, 145, 69, 125, 204, 144, 18, 0, 226, 199, 206, 44, 42, 202, 176, 88, 232, 245, 144, 51, 135, 194, 229, 229, 180, 120, 176, 154, 45, 111, 250, 204, 194, 237, 83, 26, 194, 10, 235, 250, 128, 137, 116, 152, 21, 245, 64, 65, 81, 86, 109, 160, 4, 128, 248, 177, 223, 120, 104, 39, 136, 64, 69, 110, 11, 96, 152, 33, 77, 251, 51, 198, 207, 116, 85, 134, 130, 242, 109, 239, 109, 177, 112, 45, 16, 13, 72, 101, 146, 16, 244, 101, 151, 0, 174, 136, 78, 90, 102, 203, 179, 70, 52, 74, 0, 136, 31, 107, 37, 177, 36, 91, 36, 94, 151, 211, 22, 186, 153, 200, 37, 174, 17, 153, 162, 140, 241, 243, 78, 58, 92, 90, 248, 153, 31, 75, 101, 95, 145, 189, 225, 242, 165, 45, 162, 35, 176, 41, 151, 65, 154, 98, 107, 145, 129, 82, 144, 0, 40, 46, 42, 166, 126, 172, 85, 118, 164, 241, 144, 211, 198, 66, 153, 72, 103, 149, 184, 204, 241, 243, 115, 116, 164, 108, 235, 114, 4, 224, 85, 0, 224, 101, 242, 241, 193, 144, 148, 17, 8, 24, 160, 167, 191, 172, 57, 64, 160, 191, 102, 30, 5, 96, 203, 210, 45, 196, 129, 122, 200, 10, 29, 206, 40, 239, 150, 161, 77, 185, 145, 182, 236, 54, 75, 252, 188, 156, 102, 133, 62, 90, 4, 0, 52, 174, 89, 67, 237, 0, 202, 190, 152, 15, 8, 24, 160, 103, 51, 98, 89, 41, 52, 210, 236, 175, 192, 181, 224, 86, 228, 226, 51, 177, 55, 32, 231, 167, 37, 221, 169, 153, 217, 226, 231, 229, 116, 192, 248, 115, 172, 21, 126, 245, 239, 215, 60, 251, 63, 90, 200, 22, 185, 156, 52, 38, 96, 128, 158, 45, 155, 222, 211, 236, 175, 208, 150, 74, 47, 48, 242, 225, 198, 146, 51, 93, 254, 9, 101, 139, 159, 159, 167, 122, 0, 0, 8, 88, 191, 49, 247, 27, 115, 151, 238, 37, 131, 131, 141, 232, 8, 112, 134, 232, 21, 103, 83, 90, 105, 251, 43, 84, 84, 80, 64, 255, 132, 62, 177, 203, 72, 148, 152, 76, 241, 51, 240, 104, 53, 89, 176, 204, 2, 158, 239, 3, 208, 126, 82, 43, 27, 8, 136, 234, 127, 124, 131, 52, 24, 45, 171, 69, 175, 194, 202, 82, 14, 24, 239, 254, 81, 217, 201, 216, 254, 51, 242, 65, 135, 200, 16, 7, 174, 206, 2, 0, 188, 81, 248, 234, 220, 53, 88, 45, 173, 40, 137, 97, 198, 53, 72, 83, 108, 85, 139, 30, 113, 25, 3, 127, 66, 14, 216, 201, 64, 251, 141, 194, 11, 70, 62, 232, 16, 29, 226, 88, 190, 21, 1, 104, 95, 31, 152, 139, 117, 82, 164, 62, 194, 204, 197, 198, 4, 45, 86, 48, 143, 184, 33, 148, 167, 165, 237, 186, 170, 45, 51, 208, 166, 103, 22, 56, 13, 243, 244, 140, 124, 208, 146, 184, 216, 76, 203, 65, 27, 0, 176, 180, 61, 180, 232, 239, 73, 189, 184, 185, 139, 51, 81, 185, 53, 197, 60, 172, 149, 213, 233, 120, 15, 231, 53, 87, 26, 101, 38, 120, 124, 186, 242, 219, 142, 140, 123, 17, 24, 17, 29, 226, 216, 20, 106, 217, 104, 9, 60, 0, 206, 255, 190, 191, 127, 5, 100, 32, 144, 146, 84, 70, 62, 168, 169, 58, 91, 28, 84, 173, 186, 52, 29, 0, 111, 91, 75, 40, 144, 33, 210, 84, 61, 99, 246, 129, 72, 232, 199, 103, 236, 11, 50, 238, 69, 96, 64, 132, 71, 61, 237, 109, 155, 88, 139, 21, 107, 52, 2, 180, 86, 208, 4, 85, 88, 179, 157, 193, 200, 7, 21, 0, 94, 172, 8, 18, 139, 79, 76, 62, 99, 150, 157, 38, 104, 63, 62, 179, 192, 62, 142, 8, 155, 240, 40, 184, 123, 88, 33, 66, 180, 52, 86, 139, 154, 171, 9, 202, 110, 8, 171, 173, 224, 247, 210, 133, 237, 37, 0, 196, 18, 108, 60, 122, 2, 230, 0, 80, 46, 9, 5, 142, 38, 254, 42, 155, 254, 211, 164, 31, 157, 11, 198, 181, 23, 1, 131, 53, 90, 12, 169, 21, 70, 194, 58, 17, 208, 2, 227, 168, 139, 210, 33, 136, 117, 196, 32, 71, 6, 32, 164, 32, 115, 38, 81, 61, 16, 73, 83, 208, 5, 105, 142, 56, 233, 71, 183, 157, 25, 207, 67, 50, 40, 138, 140, 12, 128, 100, 7, 110, 12, 2, 50, 165, 3, 224, 229, 26, 219, 76, 90, 68, 221, 129, 200, 162, 244, 224, 147, 193, 209, 176, 39, 198, 243, 104, 213, 208, 254, 138, 185, 165, 165, 34, 0, 161, 229, 127, 251, 234, 206, 95, 253, 201, 0, 144, 38, 33, 228, 112, 169, 60, 16, 153, 49, 235, 192, 224, 34, 13, 204, 56, 158, 44, 16, 218, 86, 196, 22, 80, 79, 144, 208, 175, 230, 254, 247, 87, 247, 153, 124, 66, 157, 13, 47, 13, 72, 2, 160, 197, 235, 201, 21, 0, 89, 155, 102, 180, 59, 140, 124, 200, 141, 218, 193, 229, 45, 250, 6, 186, 194, 255, 23, 173, 60, 17, 44, 251, 36, 61, 236, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130]; diff --git a/tile_server/static/generated/sea.dart b/tile_server/static/generated/sea.dart index 196bd5af..f3707c9e 100644 --- a/tile_server/static/generated/sea.dart +++ b/tile_server/static/generated/sea.dart @@ -1,2 +1 @@ -// Will be replaced automatically by GitHub Actions -final seaTileBytes = []; +final seaTileBytes = [137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 1, 0, 0, 0, 1, 0, 1, 3, 0, 0, 0, 102, 188, 58, 37, 0, 0, 0, 3, 80, 76, 84, 69, 170, 211, 223, 207, 236, 188, 245, 0, 0, 0, 31, 73, 68, 65, 84, 104, 129, 237, 193, 1, 13, 0, 0, 0, 194, 160, 247, 79, 109, 14, 55, 160, 0, 0, 0, 0, 0, 0, 0, 0, 190, 13, 33, 0, 0, 1, 154, 96, 225, 213, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130]; From b4a21e2cc15472c4117bf67a73e4e4fb5b4ab040 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sun, 25 Feb 2024 18:09:24 +0000 Subject: [PATCH 110/168] Fixed minor bug in tile server Former-commit-id: 4efb73d51bebdae5bea45d9569f459cce66ae86c [formerly 7faeab934a4a2fa0d5e4741c0ede5ff6e40c1b2e] Former-commit-id: 59707f68947f8a26c7054aa22a9ef9e558a35e05 --- lib/src/bulk_download/tile_loops/count.dart | 14 -------------- lib/src/bulk_download/tile_loops/generate.dart | 7 +++++-- tile_server/bin/tile_server.dart | 6 ++---- 3 files changed, 7 insertions(+), 20 deletions(-) diff --git a/lib/src/bulk_download/tile_loops/count.dart b/lib/src/bulk_download/tile_loops/count.dart index 2f2ec777..e87487b1 100644 --- a/lib/src/bulk_download/tile_loops/count.dart +++ b/lib/src/bulk_download/tile_loops/count.dart @@ -33,13 +33,6 @@ class TilesCounter { static int circleTiles(DownloadableRegion region) { region as DownloadableRegion; - // This took some time and is fairly complicated, so this is the overall explanation: - // 1. Given a `LatLng` for every x degrees on a circle's circumference, convert it into a tile number - // 2. Using a `Map` per zoom level, record all the X values in it without duplicates - // 3. Under the previous record, add all the Y values within the circle (ie. to opposite the X value) - // 4. Loop over these XY values and add them to the list - // Theoretically, this could have been done using the same method as `lineTiles`, but `lineTiles` was built after this algorithm and this makes more sense for a circle - final circleOutline = region.originalRegion.toOutline(); // Format: Map>> @@ -81,13 +74,6 @@ class TilesCounter { static int lineTiles(DownloadableRegion region) { region as DownloadableRegion; - // This took some time and is fairly complicated, so this is the overall explanation: - // 1. Given 4 `LatLng` points, create a 'straight' rectangle around the 'rotated' rectangle, that can be defined with just 2 `LatLng` points - // 2. Convert the straight rectangle into tile numbers, and loop through the same as `rectangleTiles` - // 3. For every generated tile number (which represents top-left of the tile), generate the rest of the tile corners - // 4. Check whether the square tile overlaps the rotated rectangle from the start, add it to the list if it does - // 5. Keep track of the number of overlaps per row: if there was one overlap and now there isn't, skip the rest of the row because we can be sure there are no more tiles - // Overlap algorithm originally in Python, available at https://stackoverflow.com/a/56962827/11846040 bool overlap(_Polygon a, _Polygon b) { for (int x = 0; x < 2; x++) { diff --git a/lib/src/bulk_download/tile_loops/generate.dart b/lib/src/bulk_download/tile_loops/generate.dart index 60cef68b..82512e15 100644 --- a/lib/src/bulk_download/tile_loops/generate.dart +++ b/lib/src/bulk_download/tile_loops/generate.dart @@ -46,8 +46,11 @@ class TilesGenerator { // 3. Under the previous record, add all the Y values within the circle (ie. to opposite the X value) // 4. Loop over these XY values and add them to the list // Theoretically, this could have been done using the same method as `lineTiles`, but `lineTiles` was built after this algorithm and this makes more sense for a circle - - // TODO: https://en.wikipedia.org/wiki/Midpoint_circle_algorithm + // Could also implement with the simpler method: + // 1. Calculate the radius in tiles using `Distance` + // 2. Iterate through y, then x + // 3. Use the circle formula x^2 + y^2 = r^2 to determine all points within the radius + // However, effectively scaling this proved to be difficult. final region = input.region as DownloadableRegion; final circleOutline = region.originalRegion.toOutline(); diff --git a/tile_server/bin/tile_server.dart b/tile_server/bin/tile_server.dart index b4d76a66..9e2beb94 100644 --- a/tile_server/bin/tile_server.dart +++ b/tile_server/bin/tile_server.dart @@ -114,12 +114,10 @@ Future main(List _) async { final x = ctx.pathParams.getInt('x', -1)!; final y = ctx.pathParams.getInt('y', -1)!; - // Check if tile request is in valid format - if (z == -1 || x == -1 || y == -1) { + // Check if tile request is inside valid range + if (x < 0 || y < 0 || z < 0) { return Response(statusCode: 400); } - - // Check if tile request is inside valid range final maxTileNum = sqrt(pow(4, z)) - 1; if (x > maxTileNum || y > maxTileNum) { return Response(statusCode: 400); From ab1d9a1d68dffeeb1bde763a6df33aae5305aef7 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sun, 25 Feb 2024 19:06:07 +0000 Subject: [PATCH 111/168] Added deprecations Added `RootStats.realSize` Renamed `RootStats.rootSize` & `rootLength` to `size` & `length` Improved error handling Minor cleanup Former-commit-id: 7f21743fb48ba68eb9c4b40cb22a0f5a86cd78cd [formerly f973da8450b9c3bb43c04e76c4c76e5d5993fb4d] Former-commit-id: f9a72878507a128824081419dd738bf1424be29c --- .../components/recovery_start_button.dart | 4 - lib/flutter_map_tile_caching.dart | 7 +- .../impls/objectbox/backend/internal.dart | 31 +++- .../objectbox/backend/internal_worker.dart | 28 ++-- .../backend/interfaces/backend/internal.dart | 22 ++- lib/src/errors/damaged_store.dart | 40 ----- lib/src/misc/deprecations.dart | 157 +++++++++++++++--- .../browsing_errors.dart} | 0 lib/src/root/statistics.dart | 7 +- lib/src/store/manage.dart | 2 - 10 files changed, 206 insertions(+), 92 deletions(-) delete mode 100644 lib/src/errors/damaged_store.dart rename lib/src/{errors/browsing.dart => providers/browsing_errors.dart} (100%) diff --git a/example/lib/screens/main/pages/recovery/components/recovery_start_button.dart b/example/lib/screens/main/pages/recovery/components/recovery_start_button.dart index 05b29932..44fcfbf2 100644 --- a/example/lib/screens/main/pages/recovery/components/recovery_start_button.dart +++ b/example/lib/screens/main/pages/recovery/components/recovery_start_button.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; import 'package:provider/provider.dart'; @@ -34,9 +33,6 @@ class RecoveryStartButton extends StatelessWidget { FMTCStore(result.region.storeName), ); - // TODO: Check - //..regionTiles = tiles.data; - await Navigator.of(context).push( MaterialPageRoute( builder: (context) => ConfigureDownloadPopup( diff --git a/lib/flutter_map_tile_caching.dart b/lib/flutter_map_tile_caching.dart index c81e65c6..c8ca09eb 100644 --- a/lib/flutter_map_tile_caching.dart +++ b/lib/flutter_map_tile_caching.dart @@ -32,14 +32,13 @@ import 'src/backend/export_internal.dart'; import 'src/bulk_download/instance.dart'; import 'src/bulk_download/rate_limited_stream.dart'; import 'src/bulk_download/tile_loops/shared.dart'; -import 'src/errors/browsing.dart'; import 'src/misc/int_extremes.dart'; import 'src/misc/obscure_query_params.dart'; +import 'src/providers/browsing_errors.dart'; import 'src/providers/image_provider.dart'; export 'src/backend/export_external.dart'; -export 'src/errors/browsing.dart'; -export 'src/errors/damaged_store.dart'; +export 'src/providers/browsing_errors.dart'; part 'src/bulk_download/download_progress.dart'; part 'src/bulk_download/manager.dart'; @@ -47,6 +46,7 @@ part 'src/bulk_download/thread.dart'; part 'src/bulk_download/tile_event.dart'; part 'src/misc/deprecations.dart'; part 'src/providers/tile_provider.dart'; +part 'src/providers/tile_provider_settings.dart'; part 'src/regions/base_region.dart'; part 'src/regions/circle.dart'; part 'src/regions/custom_polygon.dart'; @@ -58,7 +58,6 @@ part 'src/root/directory.dart'; part 'src/root/import.dart'; part 'src/root/recovery.dart'; part 'src/root/statistics.dart'; -part 'src/providers/tile_provider_settings.dart'; part 'src/store/directory.dart'; part 'src/store/download.dart'; part 'src/store/export.dart'; diff --git a/lib/src/backend/impls/objectbox/backend/internal.dart b/lib/src/backend/impls/objectbox/backend/internal.dart index f28f3887..c5011fc5 100644 --- a/lib/src/backend/impls/objectbox/backend/internal.dart +++ b/lib/src/backend/impls/objectbox/backend/internal.dart @@ -48,9 +48,11 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { // Handle errors (if missed by direct handler) final err = res?['error']; if (err != null) { - if (err is FMTCBackendError) throw err; + if (err is RootUnavailable) throw err; - debugPrint('An unexpected error in the FMTC backend occurred:'); + debugPrint( + 'An unexpected error in the FMTC ObjectBox Backend occurred, and should not have occurred at this point:', + ); Error.throwWithStackTrace( err, StackTrace.fromString( @@ -125,16 +127,29 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { // Killed forcefully by environment (eg. hot restart) if (evt == null) { _workerComplete.complete(); - _workerHandler.cancel(); + _workerHandler.cancel(); // Ensure this handler is cancelled on return return; } // Handle errors final err = evt.data?['error']; if (err != null) { - if (err is FMTCBackendError) throw err; - - debugPrint('An unexpected error in the FMTC backend occurred:'); + if (err is FMTCBackendError) { + debugPrint( + 'It looks like you may have made an incorrect assumption when using FMTC:', + ); + throw err; + } + if (err is StorageException) { + debugPrint( + 'It looks like the FMTC ObjectBox Backend failed to write/read some data:', + ); + throw err; + } + + debugPrint( + 'An unexpected error in the FMTC ObjectBox Backend occurred:', + ); Error.throwWithStackTrace( err, StackTrace.fromString( @@ -218,6 +233,10 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { FMTCBackendAccessThreadSafe.internal = null; } + @override + Future realSize() async => + (await _sendCmdOneShot(type: _WorkerCmdType.realSize))!['size']; + @override Future rootSize() async => (await _sendCmdOneShot(type: _WorkerCmdType.rootSize))!['size']; diff --git a/lib/src/backend/impls/objectbox/backend/internal_worker.dart b/lib/src/backend/impls/objectbox/backend/internal_worker.dart index e9943b42..2ae66b50 100644 --- a/lib/src/backend/impls/objectbox/backend/internal_worker.dart +++ b/lib/src/backend/impls/objectbox/backend/internal_worker.dart @@ -6,6 +6,7 @@ part of 'backend.dart'; enum _WorkerCmdType { initialise_, // Only valid as a request destroy, + realSize, rootSize, rootLength, listStores, @@ -162,6 +163,14 @@ Future _worker( sendRes(id: cmd.id); Isolate.exit(); + case _WorkerCmdType.realSize: + sendRes( + id: cmd.id, + data: { + 'size': Store.dbFileSize(input.rootDirectory.absolute.path) / + 1024, // Convert to KiB + }, + ); case _WorkerCmdType.rootSize: final query = root .box() @@ -248,7 +257,6 @@ Future _worker( sendRes(id: cmd.id); case _WorkerCmdType.resetStore: - // TODO: Consider just deleting then creating final storeName = cmd.args['storeName']! as String; final removeIds = []; @@ -290,16 +298,16 @@ Future _worker( (throw StoreNotExists(storeName: storeName)); storeQuery.close(); - assert(store.tiles.isEmpty); - // TODO: Hits & misses - stores.put( - store - ..tiles.clear() - ..length = 0 - ..size = 0 - ..hits = 0 - ..misses = 0, + ObjectBoxStore( + name: store.name, + length: 0, + size: 0, + hits: 0, + misses: 0, + metadataJson: '', + ), + mode: PutMode.update, ); }, ); diff --git a/lib/src/backend/interfaces/backend/internal.dart b/lib/src/backend/interfaces/backend/internal.dart index 1b542e31..96b38be7 100644 --- a/lib/src/backend/interfaces/backend/internal.dart +++ b/lib/src/backend/interfaces/backend/internal.dart @@ -13,8 +13,8 @@ import '../../export_internal.dart'; /// /// Should implement methods that operate in another isolate/thread to avoid /// blocking the normal thread. In this case, [FMTCBackendInternalThreadSafe] -/// must also be implemented, which should not operate in another thread, must be -/// sendable between isolates (because it will already be operated in another +/// should also be implemented, which should not operate in another thread & must +/// be sendable between isolates (because it will already be operated in another /// thread), and must be suitable for simultaneous initialisation across multiple /// threads. /// @@ -37,9 +37,20 @@ abstract interface class FMTCBackendInternal /// initialised. Directory? get rootDirectory; + /// {@template fmtc.backend.realSize} + /// Retrieve the actual total size of the database in KiBs + /// + /// Should include 'unused' space, 'calculation' space, overheads, etc. May be + /// much larger than `rootSize` in some backends. + /// {@endtemplate} + Future realSize(); + /// {@template fmtc.backend.rootSize} /// Retrieve the total number of KiBs of all tiles' bytes (not 'real total' /// size) from all stores + /// + /// Does not include any storage used by metadata or database overheads, as in + /// `realSize`. /// {@endtemplate} Future rootSize(); @@ -62,6 +73,8 @@ abstract interface class FMTCBackendInternal /// {@template fmtc.backend.createStore} /// Create a new store with the specified name + /// + /// Throws [StoreAlreadyExists] if the specified store already exists. /// {@endtemplate} Future createStore({ required String storeName, @@ -295,8 +308,9 @@ abstract interface class FMTCBackendInternal /// Whenever this has an event, it is likely the other statistics will have /// changed. /// - /// Emits an event every time a change is made to a store (every time a - /// statistic changes, which should include every time a tile is changed). + /// Emits an event every time a change is made to a store: + /// * a statistic change, which should include every time a tile is changed + /// * a metadata change /// {@endtemplate} Stream watchStores({ required List storeNames, diff --git a/lib/src/errors/damaged_store.dart b/lib/src/errors/damaged_store.dart deleted file mode 100644 index ad92ba06..00000000 --- a/lib/src/errors/damaged_store.dart +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright © Luka S (JaffaKetchup) under GPL-v3 -// A full license can be found at .\LICENSE - -import 'package:meta/meta.dart'; - -/// An [Exception] indicating that an operation was attempted on a damaged store -/// -/// Can be thrown for multiple reasons. See [type] and -/// [FMTCDamagedStoreExceptionType] for more information. -class FMTCDamagedStoreException implements Exception { - /// An [Exception] indicating that an operation was attempted on a damaged store - /// - /// Can be thrown for multiple reasons. See [type] and - /// [FMTCDamagedStoreExceptionType] for more information. - @internal - FMTCDamagedStoreException(this.message, this.type); - - /// Friendly message - final String message; - - /// Programmatic error descriptor - final FMTCDamagedStoreExceptionType type; - - @override - String toString() => 'FMTCDamagedStoreException: $message'; -} - -/// Pragmatic error descriptor for a [FMTCDamagedStoreException.message] -/// -/// See documentation on that object for more information. -enum FMTCDamagedStoreExceptionType { - /// Paired with friendly message: - /// "Failed to perform an operation on a store due to the core descriptor - /// being missing." - missingStoreDescriptor, - - /// Paired with friendly message: - /// "Something went wrong." - unknown, -} diff --git a/lib/src/misc/deprecations.dart b/lib/src/misc/deprecations.dart index 9bb1569e..65688891 100644 --- a/lib/src/misc/deprecations.dart +++ b/lib/src/misc/deprecations.dart @@ -3,15 +3,15 @@ part of flutter_map_tile_caching; -// TODO: Store management deprecations - const _syncRemoval = ''' Synchronous operations have been removed throughout FMTC v9, therefore the distinction between sync and async operations has been removed. This deprecated member will be removed in a future version. '''; -/// Provides deprecations where possible for previous methods in [StoreMetadata] +//! ROOT !// + +/// Provides deprecations where possible for previous methods in [RootStats] /// after the v9 release. /// /// Synchronous operations have been removed throughout FMTC v9, therefore the @@ -21,23 +21,115 @@ This deprecated member will be removed in a future version. @Deprecated( 'Migrate to the suggested replacements for each operation. $_syncRemoval', ) -extension StoreMetadataDeprecations on StoreMetadata { - /// {@macro fmtc.backend.readMetadata} - @Deprecated('Migrate to `read`. $_syncRemoval') - Future> get readAsync => read; +extension RootStatsDeprecations on RootStats { + /// {@macro fmtc.backend.listStores} + @Deprecated('Migrate to `storesAvailable`. $_syncRemoval') + Future> get storesAvailableAsync => storesAvailable; - /// {@macro fmtc.backend.setMetadata} - @Deprecated('Migrate to `set`. $_syncRemoval') - Future addAsync({required String key, required String value}) => - set(key: key, value: value); + /// {@macro fmtc.backend.rootSize} + @Deprecated('Migrate to `size`. $_syncRemoval') + Future get rootSizeAsync => size; - /// {@macro fmtc.backend.removeMetadata} - @Deprecated('Migrate to `remove`.$_syncRemoval') - Future removeAsync({required String key}) => remove(key: key); + /// {@macro fmtc.backend.rootLength} + @Deprecated('Migrate to `length`. $_syncRemoval') + Future get rootLengthAsync => length; +} - /// {@macro fmtc.backend.resetMetadata} +/// Provides deprecations where possible for previous methods in [RootRecovery] +/// after the v9 release. +/// +/// Synchronous operations have been removed throughout FMTC v9, therefore the +/// distinction between sync and async operations has been removed. +/// +/// Provided in an extension method for easy differentiation and quick removal. +@Deprecated( + 'Migrate to the suggested replacements for each operation. $_syncRemoval', +) +extension RootRecoveryDeprecations on RootRecovery { + /// List all failed failed downloads + /// + /// {@macro fmtc.rootRecovery.failedDefinition} + @Deprecated('Migrate to `recoverableRegions.failedOnly`. $_syncRemoval') + Future> get failedRegions => + recoverableRegions.then((e) => e.failedOnly.toList()); +} + +//! STORE !// + +/// Provides deprecations where possible for previous methods in +/// [StoreManagement] after the v9 release. +/// +/// Synchronous operations have been removed throughout FMTC v9, therefore the +/// distinction between sync and async operations has been removed. +/// +/// Provided in an extension method for easy differentiation and quick removal. +@Deprecated( + 'Migrate to the suggested replacements for each operation. $_syncRemoval', +) +extension StoreManagementDeprecations on StoreManagement { + /// {@macro fmtc.backend.createStore} + @Deprecated('Migrate to `create`. $_syncRemoval') + Future createAsync() => create(); + + /// {@macro fmtc.backend.resetStore} @Deprecated('Migrate to `reset`. $_syncRemoval') Future resetAsync() => reset(); + + /// {@macro fmtc.backend.deleteStore} + @Deprecated('Migrate to `delete`. $_syncRemoval') + Future deleteAsync() => delete(); + + /// {@macro fmtc.backend.renameStore} + @Deprecated('Migrate to `rename`. $_syncRemoval') + Future renameAsync(String newStoreName) => rename(newStoreName); + + /// {@macro fmtc.backend.tileImage} + /// , then render the bytes to an [Image] + @Deprecated('Migrate to `tileImage`. $_syncRemoval') + Future tileImageAsync({ + double? size, + Key? key, + double scale = 1.0, + ImageFrameBuilder? frameBuilder, + ImageErrorWidgetBuilder? errorBuilder, + String? semanticLabel, + bool excludeFromSemantics = false, + Color? color, + Animation? opacity, + BlendMode? colorBlendMode, + BoxFit? fit, + AlignmentGeometry alignment = Alignment.center, + ImageRepeat repeat = ImageRepeat.noRepeat, + Rect? centerSlice, + bool matchTextDirection = false, + bool gaplessPlayback = false, + bool isAntiAlias = false, + FilterQuality filterQuality = FilterQuality.low, + int? cacheWidth, + int? cacheHeight, + }) => + tileImage( + size: size, + key: key, + scale: scale, + frameBuilder: frameBuilder, + errorBuilder: errorBuilder, + semanticLabel: semanticLabel, + excludeFromSemantics: excludeFromSemantics, + color: color, + opacity: opacity, + colorBlendMode: colorBlendMode, + fit: fit, + alignment: alignment, + repeat: repeat, + centerSlice: centerSlice, + matchTextDirection: matchTextDirection, + gaplessPlayback: gaplessPlayback, + isAntiAlias: isAntiAlias, + filterQuality: filterQuality, + cacheWidth: cacheWidth, + cacheHeight: cacheHeight, + ); } /// Provides deprecations where possible for previous methods in [StoreStats] @@ -48,11 +140,7 @@ extension StoreMetadataDeprecations on StoreMetadata { /// /// Provided in an extension method for easy differentiation and quick removal. @Deprecated( - ''' -Migrate to the suggested replacements for each operation. -Synchronous operations have been removed throughout FMTC v9, therefore the distinction between sync and async operations has been removed. -This deprecated member will be removed in a future version. -''', + 'Migrate to the suggested replacements for each operation. $_syncRemoval', ) extension StoreStatsDeprecations on StoreStats { /// Retrieve the total number of KiBs of all tiles' bytes (not 'real total' @@ -80,3 +168,32 @@ extension StoreStatsDeprecations on StoreStats { @Deprecated('Migrate to `misses`. $_syncRemoval') Future get cacheMissesAsync => misses; } + +/// Provides deprecations where possible for previous methods in [StoreMetadata] +/// after the v9 release. +/// +/// Synchronous operations have been removed throughout FMTC v9, therefore the +/// distinction between sync and async operations has been removed. +/// +/// Provided in an extension method for easy differentiation and quick removal. +@Deprecated( + 'Migrate to the suggested replacements for each operation. $_syncRemoval', +) +extension StoreMetadataDeprecations on StoreMetadata { + /// {@macro fmtc.backend.readMetadata} + @Deprecated('Migrate to `read`. $_syncRemoval') + Future> get readAsync => read; + + /// {@macro fmtc.backend.setMetadata} + @Deprecated('Migrate to `set`. $_syncRemoval') + Future addAsync({required String key, required String value}) => + set(key: key, value: value); + + /// {@macro fmtc.backend.removeMetadata} + @Deprecated('Migrate to `remove`.$_syncRemoval') + Future removeAsync({required String key}) => remove(key: key); + + /// {@macro fmtc.backend.resetMetadata} + @Deprecated('Migrate to `reset`. $_syncRemoval') + Future resetAsync() => reset(); +} diff --git a/lib/src/errors/browsing.dart b/lib/src/providers/browsing_errors.dart similarity index 100% rename from lib/src/errors/browsing.dart rename to lib/src/providers/browsing_errors.dart diff --git a/lib/src/root/statistics.dart b/lib/src/root/statistics.dart index 739f2fa9..0ac60d12 100644 --- a/lib/src/root/statistics.dart +++ b/lib/src/root/statistics.dart @@ -13,11 +13,14 @@ class RootStats { .listStores() .then((s) => s.map(FMTCStore.new).toList()); + /// {@macro fmtc.backend.realSize} + Future get realSize async => FMTCBackendAccess.internal.realSize(); + /// {@macro fmtc.backend.rootSize} - Future get rootSize async => FMTCBackendAccess.internal.rootSize(); + Future get size async => FMTCBackendAccess.internal.rootSize(); /// {@macro fmtc.backend.rootLength} - Future get rootLength async => FMTCBackendAccess.internal.rootLength(); + Future get length async => FMTCBackendAccess.internal.rootLength(); /// {@macro fmtc.backend.watchRecovery} Stream watchRecovery({ diff --git a/lib/src/store/manage.dart b/lib/src/store/manage.dart index 3b82d15b..55de4c25 100644 --- a/lib/src/store/manage.dart +++ b/lib/src/store/manage.dart @@ -49,8 +49,6 @@ class StoreManagement { FMTCBackendAccess.internal .removeTilesOlderThan(storeName: _storeName, expiry: expiry); - // TODO: Define deprecation for `tileImageAsync` - /// {@macro fmtc.backend.readLatestTile} /// , then render the bytes to an [Image] Future tileImage({ From 67eb20c7cbcb99f73697d0f12914fa405d4c0722 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sun, 25 Feb 2024 22:40:36 +0000 Subject: [PATCH 112/168] Added `FMTCTileProviderSettings.fallbackToAlternativeStore` Former-commit-id: 11271ec95b226c7ffe60ed78b7418146fdf6f2db [formerly feaa71209a6846de57bbca6fb00f8892a4bbacd6] Former-commit-id: 478aa310d365226c03c2a257f6e7ae7af025f0e0 --- lib/src/bulk_download/thread.dart | 2 - lib/src/bulk_download/tile_event.dart | 3 +- lib/src/providers/image_provider.dart | 30 +++++++- lib/src/providers/tile_provider_settings.dart | 73 ++++++++++++++----- 4 files changed, 83 insertions(+), 25 deletions(-) diff --git a/lib/src/bulk_download/thread.dart b/lib/src/bulk_download/thread.dart index 2f6b0d7b..def9cdd4 100644 --- a/lib/src/bulk_download/thread.dart +++ b/lib/src/bulk_download/thread.dart @@ -71,7 +71,6 @@ Future _singleDownloadThread( obscuredQueryParams: input.obscuredQueryParams, ); - // TODO: Work across stores in the event of an error final existingTile = await input.backend.readTile( url: matcherUrl, storeName: input.storeName, @@ -136,7 +135,6 @@ Future _singleDownloadThread( } // Write tile directly to database or place in buffer queue - //final tile = DbTile(url: matcherUrl, bytes: response.bodyBytes); if (input.maxBufferLength == 0) { await input.backend.htWriteTile( storeName: input.storeName, diff --git a/lib/src/bulk_download/tile_event.dart b/lib/src/bulk_download/tile_event.dart index 20bb3fca..2a54ec6e 100644 --- a/lib/src/bulk_download/tile_event.dart +++ b/lib/src/bulk_download/tile_event.dart @@ -11,7 +11,6 @@ enum TileEventResultCategory { cached, /// The associated tile may have been downloaded, but was not cached - /// intentionally /// /// This may be because it: /// - already existed & `skipExistingTiles` was `true`: @@ -20,7 +19,7 @@ enum TileEventResultCategory { skipped, /// The associated tile was not successfully downloaded, potentially for a - /// variety of reasons. + /// variety of reasons /// /// Category for [TileEventResult.negativeFetchResponse], /// [TileEventResult.noConnectionDuringFetch], and diff --git a/lib/src/providers/image_provider.dart b/lib/src/providers/image_provider.dart index 87a44572..5d815949 100644 --- a/lib/src/providers/image_provider.dart +++ b/lib/src/providers/image_provider.dart @@ -85,13 +85,26 @@ class FMTCImageProvider extends ImageProvider { return decode(await ImmutableBuffer.fromUint8List(bytes)); } + // TODO: Test + Future attemptFinishViaAltStore(String matcherUrl) async { + if (provider.settings.fallbackToAlternativeStore) { + final existingTileAltStore = + await FMTCBackendAccess.internal.readTile(url: matcherUrl); + if (existingTileAltStore == null) return null; + return finishSuccessfully( + bytes: existingTileAltStore.bytes, + cacheHit: false, + ); + } + return null; + } + final networkUrl = provider.getTileUrl(coords, options); final matcherUrl = obscureQueryParams( url: networkUrl, obscuredQueryParams: provider.settings.obscuredQueryParams, ); - // TODO: Work across stores in event of error final existingTile = await FMTCBackendAccess.internal.readTile( url: matcherUrl, storeName: storeName, @@ -119,6 +132,9 @@ class FMTCImageProvider extends ImageProvider { // before attempting a network call if (provider.settings.behavior == CacheBehavior.cacheOnly && needsCreating) { + final codec = await attemptFinishViaAltStore(matcherUrl); + if (codec != null) return codec; + return finishWithError( FMTCBrowsingError( type: FMTCBrowsingErrorType.missingInCacheOnlyMode, @@ -138,6 +154,10 @@ class FMTCImageProvider extends ImageProvider { if (!needsCreating) { return finishSuccessfully(bytes: bytes!, cacheHit: false); } + + final codec = await attemptFinishViaAltStore(matcherUrl); + if (codec != null) return codec; + return finishWithError( FMTCBrowsingError( type: e is SocketException @@ -156,6 +176,10 @@ class FMTCImageProvider extends ImageProvider { if (!needsCreating) { return finishSuccessfully(bytes: bytes!, cacheHit: false); } + + final codec = await attemptFinishViaAltStore(matcherUrl); + if (codec != null) return codec; + return finishWithError( FMTCBrowsingError( type: FMTCBrowsingErrorType.negativeFetchResponse, @@ -200,6 +224,10 @@ class FMTCImageProvider extends ImageProvider { if (!needsCreating) { return finishSuccessfully(bytes: bytes!, cacheHit: false); } + + final codec = await attemptFinishViaAltStore(matcherUrl); + if (codec != null) return codec; + return finishWithError( FMTCBrowsingError( type: FMTCBrowsingErrorType.invalidImageData, diff --git a/lib/src/providers/tile_provider_settings.dart b/lib/src/providers/tile_provider_settings.dart index 05ab0b43..aa95c63c 100644 --- a/lib/src/providers/tile_provider_settings.dart +++ b/lib/src/providers/tile_provider_settings.dart @@ -6,48 +6,71 @@ part of flutter_map_tile_caching; /// Callback type that takes an [FMTCBrowsingError] exception typedef FMTCBrowsingErrorHandler = void Function(FMTCBrowsingError exception); -/// Behaviours dictating how and when browse caching should be carried out +/// Behaviours dictating how and when browse caching should occur +/// +/// An online only behaviour is not available: use a default [TileProvider] to +/// achieve this. enum CacheBehavior { /// Only get tiles from the local cache /// - /// Throws [FMTCBrowsingErrorType.missingInCacheOnlyMode] if a tile is not - /// available. + /// Throws [FMTCBrowsingErrorType.missingInCacheOnlyMode] if a tile is + /// unavailable. + /// + /// If [FMTCTileProviderSettings.fallbackToAlternativeStore] is enabled, cached + /// tiles may also be taken from other stores. cacheOnly, - /// Get tiles from the local cache, only using the network to update the cached - /// tile if it has expired ([FMTCTileProviderSettings.cachedValidDuration] has - /// passed) + /// Retrieve tiles from the cache, only using the network to update the cached + /// tile if it has expired + /// + /// Falls back to using cached tiles if the network is not available. + /// + /// If [FMTCTileProviderSettings.fallbackToAlternativeStore] is enabled, and + /// the network is unavailable, cached tiles may also be taken from other + /// stores. cacheFirst, /// Get tiles from the network where possible, and update the cached tiles /// - /// Safely falls back to using cached tiles if the network is not available. + /// Falls back to using cached tiles if the network is unavailable. + /// + /// If [FMTCTileProviderSettings.fallbackToAlternativeStore] is enabled, cached + /// tiles may also be taken from other stores. onlineFirst, } /// Settings for an [FMTCTileProvider] class FMTCTileProviderSettings { - /// Create new settings for an [FMTCTileProvider], and set the [instance] + /// Create new settings for an [FMTCTileProvider], and set the [instance] (if + /// [setInstance] is `true`, as default) /// /// To access the existing settings, if any, get [instance]. factory FMTCTileProviderSettings({ CacheBehavior behavior = CacheBehavior.cacheFirst, + bool fallbackToAlternativeStore = true, Duration cachedValidDuration = const Duration(days: 16), int maxStoreLength = 0, List obscuredQueryParams = const [], FMTCBrowsingErrorHandler? errorHandler, - }) => - _instance = FMTCTileProviderSettings._( - behavior: behavior, - cachedValidDuration: cachedValidDuration, - maxStoreLength: maxStoreLength, - obscuredQueryParams: obscuredQueryParams.map((e) => RegExp('$e=[^&]*')), - errorHandler: errorHandler, - ); + bool setInstance = true, + }) { + final settings = FMTCTileProviderSettings._( + behavior: behavior, + fallbackToAlternativeStore: fallbackToAlternativeStore, + cachedValidDuration: cachedValidDuration, + maxStoreLength: maxStoreLength, + obscuredQueryParams: obscuredQueryParams.map((e) => RegExp('$e=[^&]*')), + errorHandler: errorHandler, + ); + + if (setInstance) _instance = settings; + return settings; + } FMTCTileProviderSettings._({ required this.behavior, required this.cachedValidDuration, + required this.fallbackToAlternativeStore, required this.maxStoreLength, required this.obscuredQueryParams, required this.errorHandler, @@ -58,13 +81,23 @@ class FMTCTileProviderSettings { static FMTCTileProviderSettings get instance => _instance; static var _instance = FMTCTileProviderSettings(); - /// The behavior method to get and cache a tile + /// The behaviour to use when retrieving and writing tiles when browsing /// - /// Defaults to [CacheBehavior.cacheFirst] - get tiles from the local cache, - /// going on the Internet to update the cached tile if it has expired - /// ([cachedValidDuration] has passed). + /// Defaults to [CacheBehavior.cacheFirst]. final CacheBehavior behavior; + /// Whether to retrieve a tile from another store if it exists, as a fallback, + /// instead of throwing an error + /// + /// Does not add tiles taken from other stores to the specified store. + /// + /// When tiles are retrieved from other stores, it is counted as a miss for the + /// specified store. + /// + /// See details on [CacheBehavior] for information. Fallback to an alternative + /// store is always the last-resort option before throwing an error. + final bool fallbackToAlternativeStore; + /// The duration until a tile expires and needs to be fetched again when /// browsing. Also called `validDuration`. /// From 77a1a4b89816b87d5e5af71746e0ac9b61c5d6a6 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Mon, 26 Feb 2024 22:41:07 +0000 Subject: [PATCH 113/168] Improved error/exception handling Fixed worker startup on Android (`BackgroundIsolateBinaryMessenger`) Improved example application Former-commit-id: bfcad268bd0efee030c6d7bbf784059f202ab451 [formerly 61d4014bae80ce960a447d386b2d8fe0a36b903f] Former-commit-id: a1b841e3d33e071bbf1360678b4de43a46d79893 --- example/lib/main.dart | 8 +- example/lib/screens/main/main.dart | 3 - .../main/pages/downloading/downloading.dart | 2 +- .../pages/stores/components/stat_display.dart | 2 +- .../pages/stores/components/store_tile.dart | 504 +++++++++--------- .../lib/screens/main/pages/stores/stores.dart | 133 ++--- example/pubspec.yaml | 4 +- .../impls/objectbox/backend/backend.dart | 14 +- .../impls/objectbox/backend/internal.dart | 74 ++- .../objectbox/backend/internal_worker.dart | 33 +- .../backend/interfaces/backend/backend.dart | 3 + .../backend/interfaces/backend/internal.dart | 3 + lib/src/backend/utils/errors.dart | 25 + 13 files changed, 430 insertions(+), 378 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 7e5f9944..78b53063 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; @@ -20,7 +21,12 @@ void main() async { ), ); - await FMTCObjectBoxBackend().initialise(); + await FMTCObjectBoxBackend().initialise( + exceptionHandler: (_, __) { + if (kDebugMode) print('Caught internal exception externally'); + return false; + }, + ); runApp(const _AppContainer()); } diff --git a/example/lib/screens/main/main.dart b/example/lib/screens/main/main.dart index 9abedda4..4b02b067 100644 --- a/example/lib/screens/main/main.dart +++ b/example/lib/screens/main/main.dart @@ -140,9 +140,6 @@ class _MainScreenState extends State { left: MediaQuery.sizeOf(context).width > 950 ? BorderSide(color: Theme.of(context).dividerColor) : BorderSide.none, - bottom: MediaQuery.sizeOf(context).width <= 950 - ? BorderSide(color: Theme.of(context).dividerColor) - : BorderSide.none, ), ), position: DecorationPosition.foreground, diff --git a/example/lib/screens/main/pages/downloading/downloading.dart b/example/lib/screens/main/pages/downloading/downloading.dart index cf73acc6..f113d800 100644 --- a/example/lib/screens/main/pages/downloading/downloading.dart +++ b/example/lib/screens/main/pages/downloading/downloading.dart @@ -94,7 +94,7 @@ class _DownloadingPageState extends State child: Column( mainAxisSize: MainAxisSize.min, children: [ - CircularProgressIndicator(), + CircularProgressIndicator.adaptive(), SizedBox(height: 16), Text( 'Taking a while?', diff --git a/example/lib/screens/main/pages/stores/components/stat_display.dart b/example/lib/screens/main/pages/stores/components/stat_display.dart index eeae3369..3a6b5941 100644 --- a/example/lib/screens/main/pages/stores/components/stat_display.dart +++ b/example/lib/screens/main/pages/stores/components/stat_display.dart @@ -14,7 +14,7 @@ class StatDisplay extends StatelessWidget { Widget build(BuildContext context) => Column( children: [ if (statistic == null) - const CircularProgressIndicator() + const CircularProgressIndicator.adaptive() else Text( statistic!, diff --git a/example/lib/screens/main/pages/stores/components/store_tile.dart b/example/lib/screens/main/pages/stores/components/store_tile.dart index cd1c7c44..4430f69c 100644 --- a/example/lib/screens/main/pages/stores/components/store_tile.dart +++ b/example/lib/screens/main/pages/stores/components/store_tile.dart @@ -8,13 +8,10 @@ import '../../../../store_editor/store_editor.dart'; import 'stat_display.dart'; class StoreTile extends StatefulWidget { - const StoreTile({ - super.key, - required this.context, + StoreTile({ required this.storeName, - }); + }) : super(key: ValueKey(storeName)); - final BuildContext context; final String storeName; @override @@ -22,114 +19,16 @@ class StoreTile extends StatefulWidget { } class _StoreTileState extends State { - Future? _length; - Future? _size; - Future? _hits; - Future? _misses; - Future? _image; - bool _deletingProgress = false; bool _emptyingProgress = false; bool _exportingProgress = false; - late final _store = FMTCStore(widget.storeName); - - void _loadStatistics() { - final stats = _store.stats.all; - - _length = stats.then((s) => s.length.toString()); - _size = stats.then((s) => (s.size * 1024).asReadableSize); - _hits = stats.then((s) => s.hits.toString()); - _misses = stats.then((s) => s.misses.toString()); - - _image = _store.manage.tileImage(size: 125); - - setState(() {}); - } - - List> get stats => [ - FutureBuilder( - future: _length, - builder: (context, snapshot) => StatDisplay( - statistic: snapshot.connectionState != ConnectionState.done - ? null - : snapshot.data, - description: 'Total Tiles', - ), - ), - FutureBuilder( - future: _size, - builder: (context, snapshot) => StatDisplay( - statistic: snapshot.connectionState != ConnectionState.done - ? null - : snapshot.data, - description: 'Total Size', - ), - ), - FutureBuilder( - future: _hits, - builder: (context, snapshot) => StatDisplay( - statistic: snapshot.connectionState != ConnectionState.done - ? null - : snapshot.data, - description: 'Cache Hits', - ), - ), - FutureBuilder( - future: _misses, - builder: (context, snapshot) => StatDisplay( - statistic: snapshot.connectionState != ConnectionState.done - ? null - : snapshot.data, - description: 'Cache Misses', - ), - ), - ]; - - IconButton deleteStoreButton({required bool isCurrentStore}) => IconButton( - icon: _deletingProgress - ? const CircularProgressIndicator( - strokeWidth: 3, - ) - : Icon( - Icons.delete_forever, - color: isCurrentStore ? null : Colors.red, - ), - tooltip: 'Delete Store', - onPressed: isCurrentStore || _deletingProgress - ? null - : () async { - setState(() { - _deletingProgress = true; - _emptyingProgress = true; - }); - await _store.manage.delete(); - }, - ); - @override - Widget build(BuildContext context) => Consumer( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - FutureBuilder( - future: _image, - builder: (context, snapshot) => snapshot.data == null - ? const SizedBox( - height: 125, - width: 125, - child: Icon(Icons.help_outline, size: 36), - ) - : snapshot.data!, - ), - if (MediaQuery.sizeOf(context).width > 675) - ...stats - else - Column(children: stats), - ], - ), - builder: (context, provider, statistics) { - final bool isCurrentStore = provider.currentStore == widget.storeName; + Widget build(BuildContext context) => Selector( + selector: (context, provider) => provider.currentStore, + builder: (context, currentStore, child) { + final store = FMTCStore(widget.storeName); + final isCurrentStore = currentStore == widget.storeName; return ExpansionTile( title: Text( @@ -137,173 +36,258 @@ class _StoreTileState extends State { style: TextStyle( fontWeight: isCurrentStore ? FontWeight.bold : FontWeight.normal, - // color: _store.manage.ready == false ? Colors.red : null, ), ), subtitle: _deletingProgress ? const Text('Deleting...') : null, - // leading: _store.manage.ready == false - // ? const Icon( - // Icons.error, - // color: Colors.red, - // ) - // : null, - onExpansionChanged: (e) { - if (e) _loadStatistics(); - }, children: [ - FutureBuilder( - future: _store.manage.ready, - builder: (context, snapshot) { - if (snapshot.data == null) { - return const CircularProgressIndicator.adaptive(); - } + SizedBox( + width: double.infinity, + child: Padding( + padding: const EdgeInsets.all(8), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(8), + child: SizedBox( + width: double.infinity, + child: FutureBuilder( + future: store.manage.ready, + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const UnconstrainedBox( + child: CircularProgressIndicator.adaptive(), + ); + } - return SizedBox( - width: double.infinity, - child: Padding( - padding: const EdgeInsets.only(left: 18, bottom: 10), - child: snapshot.data! - ? Column( - children: [ - statistics!, - const SizedBox(height: 15), - Row( - mainAxisAlignment: - MainAxisAlignment.spaceEvenly, + if (!snapshot.data!) { + return const Wrap( + alignment: WrapAlignment.center, + runAlignment: WrapAlignment.center, + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 24, + runSpacing: 12, children: [ - deleteStoreButton( - isCurrentStore: isCurrentStore, - ), - IconButton( - icon: _emptyingProgress - ? const CircularProgressIndicator( - strokeWidth: 3, - ) - : const Icon(Icons.delete), - tooltip: 'Empty Store', - onPressed: _emptyingProgress - ? null - : () async { - setState( - () => _emptyingProgress = true, - ); - await _store.manage.reset(); - setState( - () => _emptyingProgress = false, - ); - - _loadStatistics(); - }, + Icon( + Icons.broken_image_rounded, + size: 38, ), - IconButton( - icon: _exportingProgress - ? const CircularProgressIndicator( - strokeWidth: 3, - ) - : const Icon( - Icons.upload_file_rounded, - ), - tooltip: 'Export Store', - onPressed: _exportingProgress - ? null - : () async { - // TODO: Implement - /* setState( - () => _exportingProgress = true, - ); - final bool result = await _store - .export - .withGUI(context: context); - setState( - () => - _exportingProgress = false, - ); - if (mounted) { - ScaffoldMessenger.of(context) - .showSnackBar( - SnackBar( - content: Text( - result - ? 'Exported Sucessfully' - : 'Export Cancelled', - ), - ), - ); - }*/ - }, + Text( + 'Invalid/missing store', + style: TextStyle( + fontSize: 16, + ), + textAlign: TextAlign.center, ), - IconButton( - icon: const Icon(Icons.edit), - tooltip: 'Edit Store', - onPressed: () => - Navigator.of(context).push( - MaterialPageRoute( - builder: (BuildContext context) => - StoreEditorPopup( - existingStoreName: widget.storeName, - isStoreInUse: isCurrentStore, - ), - fullscreenDialog: true, + ], + ); + } + + return FutureBuilder( + future: store.stats.all, + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const UnconstrainedBox( + child: + CircularProgressIndicator.adaptive(), + ); + } + + return Wrap( + alignment: WrapAlignment.spaceEvenly, + runAlignment: WrapAlignment.spaceEvenly, + crossAxisAlignment: + WrapCrossAlignment.center, + spacing: 32, + runSpacing: 16, + children: [ + FutureBuilder( + future: store.manage.tileImage( + size: 256 * 3 / 5, + gaplessPlayback: true, ), + builder: (context, snapshot) { + if (snapshot.connectionState != + ConnectionState.done) { + return const UnconstrainedBox( + child: CircularProgressIndicator + .adaptive(), + ); + } + + if (snapshot.data == null) { + return const Icon( + Icons.grid_view_rounded, + size: 38, + ); + } + + return snapshot.data!; + }, ), - ), - IconButton( - icon: const Icon(Icons.refresh), - tooltip: 'Force Refresh Statistics', - onPressed: _loadStatistics, - ), - IconButton( - icon: Icon( - Icons.done, - color: isCurrentStore - ? Colors.green - : null, + Wrap( + alignment: WrapAlignment.spaceEvenly, + runAlignment: WrapAlignment.spaceEvenly, + crossAxisAlignment: + WrapCrossAlignment.center, + spacing: 42, + children: [ + StatDisplay( + statistic: snapshot.data!.length + .toString(), + description: 'tiles', + ), + StatDisplay( + statistic: + (snapshot.data!.size * 1024) + .asReadableSize, + description: 'size', + ), + StatDisplay( + statistic: + snapshot.data!.hits.toString(), + description: 'hits', + ), + StatDisplay( + statistic: snapshot.data!.misses + .toString(), + description: 'misses', + ), + ], ), - tooltip: 'Use Store', - onPressed: isCurrentStore - ? null - : () { - provider - ..currentStore = - widget.storeName - ..resetMap(); - }, + ], + ); + }, + ); + }, + ), + ), + ), + const SizedBox.square(dimension: 8), + Padding( + padding: const EdgeInsets.all(8), + child: SizedBox( + width: double.infinity, + child: Wrap( + alignment: WrapAlignment.spaceEvenly, + runAlignment: WrapAlignment.spaceEvenly, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + IconButton( + icon: _deletingProgress + ? const CircularProgressIndicator( + strokeWidth: 3, + ) + : Icon( + Icons.delete_forever, + color: + isCurrentStore ? null : Colors.red, + ), + tooltip: 'Delete Store', + onPressed: isCurrentStore || _deletingProgress + ? null + : () async { + setState(() { + _deletingProgress = true; + _emptyingProgress = true; + }); + await FMTCStore(widget.storeName) + .manage + .delete(); + }, + ), + IconButton( + icon: _emptyingProgress + ? const CircularProgressIndicator( + strokeWidth: 3, + ) + : const Icon(Icons.delete), + tooltip: 'Empty Store', + onPressed: _emptyingProgress + ? null + : () async { + setState( + () => _emptyingProgress = true, + ); + await FMTCStore(widget.storeName) + .manage + .reset(); + setState( + () => _emptyingProgress = false, + ); + }, + ), + IconButton( + icon: _exportingProgress + ? const CircularProgressIndicator( + strokeWidth: 3, + ) + : const Icon( + Icons.send_time_extension_rounded, + ), + tooltip: 'Export Store', + onPressed: _exportingProgress + ? null + : () async { + // TODO: Implement + /* setState( + () => _exportingProgress = true, + ); + final bool result = await _store + .export + .withGUI(context: context); + setState( + () => + _exportingProgress = false, + ); + if (mounted) { + ScaffoldMessenger.of(context) + .showSnackBar( + SnackBar( + content: Text( + result + ? 'Exported Sucessfully' + : 'Export Cancelled', + ), + ), + ); + }*/ + }, + ), + IconButton( + icon: const Icon(Icons.edit), + tooltip: 'Edit Store', + onPressed: () => Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) => + StoreEditorPopup( + existingStoreName: widget.storeName, + isStoreInUse: isCurrentStore, ), - ], - ), - ], - ) - : Column( - children: [ - const SizedBox(height: 10), - const Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.broken_image, size: 34), - Icon(Icons.error, size: 34), - ], - ), - const SizedBox(height: 8), - const Text( - 'Invalid Store', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, + fullscreenDialog: true, ), ), - const Text( - "This store's directory structure appears to have been corrupted. You must delete the store to resolve the issue.", - textAlign: TextAlign.center, - ), - const SizedBox(height: 5), - deleteStoreButton( - isCurrentStore: isCurrentStore, + ), + IconButton( + icon: Icon( + Icons.done, + color: isCurrentStore ? Colors.green : null, ), - ], - ), - ), - ); - }, + tooltip: 'Use Store', + onPressed: isCurrentStore + ? null + : () { + context.read() + ..currentStore = widget.storeName + ..resetMap(); + }, + ), + ], + ), + ), + ), + ], + ), + ), ), ], ); diff --git a/example/lib/screens/main/pages/stores/stores.dart b/example/lib/screens/main/pages/stores/stores.dart index b4aa3c7c..d073624a 100644 --- a/example/lib/screens/main/pages/stores/stores.dart +++ b/example/lib/screens/main/pages/stores/stores.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; -import 'package:flutter_speed_dial/flutter_speed_dial.dart'; import '../../../../shared/components/loading_indicator.dart'; import '../../../import_store/import_store.dart'; @@ -17,89 +16,93 @@ class StoresPage extends StatefulWidget { } class _StoresPageState extends State { - late Future> _stores; - - @override - void initState() { - super.initState(); - - void listStores() => _stores = FMTCRoot.stats.storesAvailable; - - listStores(); - FMTCRoot.stats.watchStores().listen((_) { - if (mounted) { - listStores(); - setState(() {}); - } - }); - } + late final watchStream = FMTCRoot.stats.watchStores( + triggerImmediately: true, + ); @override Widget build(BuildContext context) => Scaffold( body: SafeArea( child: Padding( padding: const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Header(), - const SizedBox(height: 12), - Expanded( - child: FutureBuilder( - future: _stores, - builder: (context, snapshot) => snapshot.hasError - // ignore: only_throw_errors - ? throw snapshot.error! - : snapshot.hasData - ? snapshot.data!.isEmpty - ? const EmptyIndicator() - : ListView.builder( - itemCount: snapshot.data!.length, - itemBuilder: (context, index) => StoreTile( - context: context, - storeName: snapshot.data! - .elementAt(index) - .storeName, - key: ValueKey( - snapshot.data! - .elementAt(index) - .storeName, - ), - ), - ) - : const LoadingIndicator('Retrieving Stores'), - ), - ), - ], + child: StreamBuilder( + stream: watchStream, + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const LoadingIndicator('Retrieving Stores'); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Header(), + const Divider(), + const Placeholder(fallbackHeight: 100), + const Divider(), + Expanded( + child: FutureBuilder( + future: FMTCRoot.stats.storesAvailable, + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const Padding( + padding: EdgeInsets.all(12), + child: LoadingIndicator('Retrieving Stores'), + ); + } + + if (snapshot.data!.isEmpty) { + return const EmptyIndicator(); + } + + return ListView.builder( + itemCount: snapshot.data!.length + 1, + itemBuilder: (context, index) { + final addSpace = index >= snapshot.data!.length; + if (addSpace) return const SizedBox(height: 124); + + return StoreTile( + storeName: + snapshot.data!.elementAt(index).storeName, + ); + }, + ); + }, + ), + ), + ], + ); + }, ), ), ), - floatingActionButton: SpeedDial( - icon: Icons.add, - activeIcon: Icons.close, + floatingActionButton: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, children: [ - SpeedDialChild( - onTap: () => Navigator.of(context).push( + FloatingActionButton.small( + heroTag: null, + tooltip: 'Import Store', + child: const Icon(Icons.file_open_rounded), + onPressed: () => Navigator.of(context).push( MaterialPageRoute( - builder: (BuildContext context) => const StoreEditorPopup( - existingStoreName: null, - isStoreInUse: false, - ), + builder: (BuildContext context) => const ImportStorePopup(), fullscreenDialog: true, ), ), - child: const Icon(Icons.add_box), - label: 'Create New Store', ), - SpeedDialChild( - onTap: () => Navigator.of(context).push( + const SizedBox.square(dimension: 12), + FloatingActionButton.extended( + label: const Text('Create Store'), + icon: const Icon(Icons.create_new_folder_rounded), + onPressed: () => Navigator.of(context).push( MaterialPageRoute( - builder: (BuildContext context) => const ImportStorePopup(), + builder: (BuildContext context) => const StoreEditorPopup( + existingStoreName: null, + isStoreInUse: false, + ), fullscreenDialog: true, ), ), - child: const Icon(Icons.file_open), - label: 'Import Stores', ), ], ), diff --git a/example/pubspec.yaml b/example/pubspec.yaml index b9b21a12..d93f9111 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -21,9 +21,7 @@ dependencies: flutter_foreground_task: ^6.1.2 flutter_map: ^6.1.0 flutter_map_animations: ^0.5.3 - flutter_map_tile_caching: ^9.0.0-dev.5 - flutter_speed_dial: ^7.0.0 - #fmtc_plus_sharing: ^8.0.0 + flutter_map_tile_caching: google_fonts: ^6.1.0 gpx: ^2.2.1 http: ^1.1.2 diff --git a/lib/src/backend/impls/objectbox/backend/backend.dart b/lib/src/backend/impls/objectbox/backend/backend.dart index 1695de76..0d201ea4 100644 --- a/lib/src/backend/impls/objectbox/backend/backend.dart +++ b/lib/src/backend/impls/objectbox/backend/backend.dart @@ -8,6 +8,7 @@ import 'dart:isolate'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; @@ -18,6 +19,8 @@ import '../models/src/recovery.dart'; import '../models/src/store.dart'; import '../models/src/tile.dart'; +export 'package:objectbox/objectbox.dart' show StorageException; + part 'internal_thread_safe.dart'; part 'internal.dart'; part 'internal_worker.dart'; @@ -26,6 +29,12 @@ part 'internal_worker.dart'; final class FMTCObjectBoxBackend implements FMTCBackend { /// {@macro fmtc.backend.initialise} /// + /// Consider handling [StorageException], which is an exception thrown by + /// ObjectBox which indicates an issue when reading/writing data to the + /// database - for example, due to exceeding the [maxDatabaseSize]. + /// + /// --- + /// /// [maxDatabaseSize] is the maximum size the database file can grow /// to, in KB. Exceeding it throws [DbFullException]. Defaults to 10 GB. /// @@ -38,11 +47,13 @@ final class FMTCObjectBoxBackend implements FMTCBackend { String? rootDirectory, int maxDatabaseSize = 10000000, String? macosApplicationGroup, + FMTCExceptionHandler? exceptionHandler, }) => FMTCObjectBoxBackendInternal._instance.initialise( rootDirectory: rootDirectory, maxDatabaseSize: maxDatabaseSize, macosApplicationGroup: macosApplicationGroup, + exceptionHandler: exceptionHandler, ); /// {@macro fmtc.backend.uninitialise} @@ -51,8 +62,7 @@ final class FMTCObjectBoxBackend implements FMTCBackend { /// as the worker will be killed as quickly as possible (not necessarily /// instantly). /// If `false`, all operations currently underway will be allowed to complete, - /// but any operations started after this method call will be lost. A lost - /// operation may throw [RootUnavailable]. + /// but any operations started after this method call will be lost. @override Future uninitialise({ bool deleteRoot = false, diff --git a/lib/src/backend/impls/objectbox/backend/internal.dart b/lib/src/backend/impls/objectbox/backend/internal.dart index c5011fc5..ef7a5f97 100644 --- a/lib/src/backend/impls/objectbox/backend/internal.dart +++ b/lib/src/backend/impls/objectbox/backend/internal.dart @@ -48,7 +48,12 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { // Handle errors (if missed by direct handler) final err = res?['error']; if (err != null) { - if (err is RootUnavailable) throw err; + // Thrown by immediate shutdown + if (err is RootUnavailable && kDebugMode) { + debugPrint( + '[FMTC] Immediate uninitialisation caused an operation to be lost.', + ); + } debugPrint( 'An unexpected error in the FMTC ObjectBox Backend occurred, and should not have occurred at this point:', @@ -97,6 +102,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { required String? rootDirectory, required int maxDatabaseSize, required String? macosApplicationGroup, + FMTCExceptionHandler? exceptionHandler, }) async { if (_sendPort != null) throw RootAlreadyInitialised(); @@ -109,10 +115,21 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { ).create(recursive: true); // Prepare to recieve `SendPort` from worker - late final ByteData storeReference; + ByteData? storeReference; _workerResOneShot[0] = Completer(); final workerInitialRes = _workerResOneShot[0]!.future.then((res) { - storeReference = res!['storeReference']; + if (res!['error'] != null) { + _workerHandler.cancel(); + _workerComplete.complete(); + + _workerId = 0; + _workerResOneShot.clear(); + _workerResStreamed.clear(); + + return; + } + + storeReference = res['storeReference']; _sendPort = res['sendPort']; _workerResOneShot.remove(0); }); @@ -126,37 +143,31 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { // Killed forcefully by environment (eg. hot restart) if (evt == null) { - _workerComplete.complete(); _workerHandler.cancel(); // Ensure this handler is cancelled on return + _workerComplete.complete(); + // Doesn't require full cleanup, because hot restart has done that return; } // Handle errors final err = evt.data?['error']; if (err != null) { - if (err is FMTCBackendError) { - debugPrint( - 'It looks like you may have made an incorrect assumption when using FMTC:', - ); - throw err; - } - if (err is StorageException) { - debugPrint( - 'It looks like the FMTC ObjectBox Backend failed to write/read some data:', - ); - throw err; - } + if (err is FMTCBackendError) throw err; - debugPrint( - 'An unexpected error in the FMTC ObjectBox Backend occurred:', - ); - Error.throwWithStackTrace( - err, - StackTrace.fromString( - (evt.data?['stackTrace']! as StackTrace).toString() + - StackTrace.current.toString(), - ), + final stackTrace = StackTrace.fromString( + (evt.data?['stackTrace']! as StackTrace).toString() + + StackTrace.current.toString(), ); + + if (exceptionHandler != null) { + // TODO: Was exception at startup? + final handled = exceptionHandler(err, stackTrace); + if (handled == null || !handled) { + Error.throwWithStackTrace(err, stackTrace); + } + } else { + Error.throwWithStackTrace(err, stackTrace); + } } if (evt.data?['expectStream'] == true) { @@ -176,17 +187,22 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { rootDirectory: this.rootDirectory!, maxDatabaseSize: maxDatabaseSize, macosApplicationGroup: macosApplicationGroup, + rootIsolateToken: ServicesBinding.rootIsolateToken!, ), onExit: receivePort.sendPort, debugName: '[FMTC] ObjectBox Backend Worker', ); + // Wait for initial response from isolate await workerInitialRes; - FMTCBackendAccess.internal = this; - FMTCBackendAccessThreadSafe.internal = _ObjectBoxBackendThreadSafeImpl._( - storeReference: storeReference, - ); + // Check whether initialisation was successful + if (storeReference != null) { + FMTCBackendAccess.internal = this; + FMTCBackendAccessThreadSafe.internal = _ObjectBoxBackendThreadSafeImpl._( + storeReference: storeReference!, + ); + } } Future uninitialise({ diff --git a/lib/src/backend/impls/objectbox/backend/internal_worker.dart b/lib/src/backend/impls/objectbox/backend/internal_worker.dart index 2ae66b50..c63b1f1b 100644 --- a/lib/src/backend/impls/objectbox/backend/internal_worker.dart +++ b/lib/src/backend/impls/objectbox/backend/internal_worker.dart @@ -49,6 +49,7 @@ Future _worker( Directory rootDirectory, int maxDatabaseSize, String? macosApplicationGroup, + RootIsolateToken rootIsolateToken, }) input, ) async { //! SETUP !// @@ -61,12 +62,21 @@ Future _worker( }) => input.sendPort.send((id: id, data: data)); + /// Enable ObjectBox usage from this background isolate + BackgroundIsolateBinaryMessenger.ensureInitialized(input.rootIsolateToken); + // Initialise database - final root = await openStore( - directory: input.rootDirectory.absolute.path, - maxDBSizeInKB: input.maxDatabaseSize, // Defaults to 10 GB - macosApplicationGroup: input.macosApplicationGroup, - ); + late final Store root; + try { + root = await openStore( + directory: input.rootDirectory.absolute.path, + maxDBSizeInKB: input.maxDatabaseSize, // Defaults to 10 GB + macosApplicationGroup: input.macosApplicationGroup, + ); + } catch (e, s) { + sendRes(id: 0, data: {'error': e, 'stackTrace': s}); + Isolate.exit(); + } // Respond with comms channel for future cmds sendRes( @@ -299,14 +309,11 @@ Future _worker( storeQuery.close(); stores.put( - ObjectBoxStore( - name: store.name, - length: 0, - size: 0, - hits: 0, - misses: 0, - metadataJson: '', - ), + store + ..length = 0 + ..size = 0 + ..hits = 0 + ..misses = 0, mode: PutMode.update, ); }, diff --git a/lib/src/backend/interfaces/backend/backend.dart b/lib/src/backend/interfaces/backend/backend.dart index 813dad78..96ba1cc1 100644 --- a/lib/src/backend/interfaces/backend/backend.dart +++ b/lib/src/backend/interfaces/backend/backend.dart @@ -30,9 +30,12 @@ abstract interface class FMTCBackend { /// `getApplicationDocumentsDirectory()`. Alternatively, pass a custom /// directory - it is recommended to not use a typical cache directory, as the /// OS can clear these without notice at any time. + /// + /// For information on [exceptionHandler], see [FMTCExceptionHandler]'s docs. /// {@endtemplate} Future initialise({ String? rootDirectory, + FMTCExceptionHandler? exceptionHandler, }); /// {@template fmtc.backend.uninitialise} diff --git a/lib/src/backend/interfaces/backend/internal.dart b/lib/src/backend/interfaces/backend/internal.dart index 96b38be7..f7baeebf 100644 --- a/lib/src/backend/interfaces/backend/internal.dart +++ b/lib/src/backend/interfaces/backend/internal.dart @@ -93,6 +93,9 @@ abstract interface class FMTCBackendInternal /// {@template fmtc.backend.resetStore} /// Remove all the tiles from within the specified store /// + /// Also resets the hits & misses stats. Does not reset any associated + /// metadata. + /// /// This operation cannot be undone! Ensure you confirm with the user that /// this action is expected. /// {@endtemplate} diff --git a/lib/src/backend/utils/errors.dart b/lib/src/backend/utils/errors.dart index 19bcee56..a2e20539 100644 --- a/lib/src/backend/utils/errors.dart +++ b/lib/src/backend/utils/errors.dart @@ -1,6 +1,31 @@ // Copyright © Luka S (JaffaKetchup) under GPL-v3 // A full license can be found at .\LICENSE +import '../export_external.dart'; + +/// A method called by FMTC internals when an exception occurs in a backend +/// +/// A thrown [FMTCBackendError] will NOT result in this method being invoked, as +/// that error should be fixed in code, and should not occur at runtime. It will +/// always be thrown. +/// +/// Other [Exception]s/[Error]s will result in this method being invoked, with a +/// modified [StackTrace] that gives an indication as to where the issue occured +/// internally, useful should a bug need to be reported. The trace will not +/// include where the original method that caused the error was invoked. This +/// exception/error may or may not be an issue directly in FMTC, it may be an +/// issue in the usage of a dependency (such as an exception caused by +/// exceeding a database's storage limit). +/// +/// If the callback returns `true`, then FMTC will not continue to handle the +/// error. Otherwise (`false` or `null`), then FMTC will also throw the +/// exception/error. If the callback is not defined (in +/// [FMTCBackend.initialise]), then FMTC will throw the exception/error. +typedef FMTCExceptionHandler = bool? Function( + Object? exception, + StackTrace stackTrace, +); + /// An error to be thrown by backend implementations in known events only /// /// A backend can create custom errors of this type, which is useful to show From 0f062ca2bb280ffcb0cb9e27b5eb8c868b528158 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Tue, 27 Feb 2024 21:21:07 +0000 Subject: [PATCH 114/168] Fixed bugs in `deleteTiles` Improved error handling Improved example application Added debouncing to `removeOldestTilesAboveLimit` Added deprecation for `checkTileCachedAsync` Former-commit-id: bcdeb5d6abfdaae6334d8b7f687c3853629b6b1d [formerly 57d71fa456cdb9f2a4d192d377875ae39297df4e] Former-commit-id: 903b30600cea5a5ebc24e9bff34baa33f37fefbe --- example/lib/main.dart | 12 +- .../pages/stores/components/store_tile.dart | 53 ++-- .../lib/screens/main/pages/stores/stores.dart | 235 ++++++++++++------ .../screens/store_editor/store_editor.dart | 2 +- .../impls/objectbox/backend/internal.dart | 69 +++-- .../objectbox/backend/internal_worker.dart | 52 ++-- .../impls/objectbox/models/src/recovery.dart | 8 +- .../backend/interfaces/backend/internal.dart | 9 +- lib/src/backend/utils/errors.dart | 19 +- lib/src/providers/image_provider.dart | 1 - lib/src/providers/tile_provider.dart | 13 +- 11 files changed, 312 insertions(+), 161 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 78b53063..a996816d 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -22,8 +22,16 @@ void main() async { ); await FMTCObjectBoxBackend().initialise( - exceptionHandler: (_, __) { - if (kDebugMode) print('Caught internal exception externally'); + exceptionHandler: ({ + required exception, + required initialisationFailure, + required stackTrace, + }) { + if (kDebugMode) { + print( + 'Caught error externally (was init error: $initialisationFailure)', + ); + } return false; }, ); diff --git a/example/lib/screens/main/pages/stores/components/store_tile.dart b/example/lib/screens/main/pages/stores/components/store_tile.dart index 4430f69c..0302464a 100644 --- a/example/lib/screens/main/pages/stores/components/store_tile.dart +++ b/example/lib/screens/main/pages/stores/components/store_tile.dart @@ -39,6 +39,7 @@ class _StoreTileState extends State { ), ), subtitle: _deletingProgress ? const Text('Deleting...') : null, + initiallyExpanded: isCurrentStore, children: [ SizedBox( width: double.infinity, @@ -100,36 +101,43 @@ class _StoreTileState extends State { spacing: 32, runSpacing: 16, children: [ - FutureBuilder( - future: store.manage.tileImage( - size: 256 * 3 / 5, - gaplessPlayback: true, - ), - builder: (context, snapshot) { - if (snapshot.connectionState != - ConnectionState.done) { - return const UnconstrainedBox( - child: CircularProgressIndicator - .adaptive(), - ); - } + SizedBox.square( + dimension: 160, + child: ClipRRect( + borderRadius: + BorderRadius.circular(16), + child: FutureBuilder( + future: store.manage.tileImage( + gaplessPlayback: true, + ), + builder: (context, snapshot) { + if (snapshot.connectionState != + ConnectionState.done) { + return const UnconstrainedBox( + child: + CircularProgressIndicator + .adaptive(), + ); + } - if (snapshot.data == null) { - return const Icon( - Icons.grid_view_rounded, - size: 38, - ); - } + if (snapshot.data == null) { + return const Icon( + Icons.grid_view_rounded, + size: 38, + ); + } - return snapshot.data!; - }, + return snapshot.data!; + }, + ), + ), ), Wrap( alignment: WrapAlignment.spaceEvenly, runAlignment: WrapAlignment.spaceEvenly, crossAxisAlignment: WrapCrossAlignment.center, - spacing: 42, + spacing: 64, children: [ StatDisplay( statistic: snapshot.data!.length @@ -171,6 +179,7 @@ class _StoreTileState extends State { alignment: WrapAlignment.spaceEvenly, runAlignment: WrapAlignment.spaceEvenly, crossAxisAlignment: WrapCrossAlignment.center, + spacing: 16, children: [ IconButton( icon: _deletingProgress diff --git a/example/lib/screens/main/pages/stores/stores.dart b/example/lib/screens/main/pages/stores/stores.dart index d073624a..c60bec30 100644 --- a/example/lib/screens/main/pages/stores/stores.dart +++ b/example/lib/screens/main/pages/stores/stores.dart @@ -2,10 +2,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; import '../../../../shared/components/loading_indicator.dart'; +import '../../../../shared/misc/exts/size_formatter.dart'; import '../../../import_store/import_store.dart'; import '../../../store_editor/store_editor.dart'; import 'components/empty_indicator.dart'; import 'components/header.dart'; +import 'components/stat_display.dart'; import 'components/store_tile.dart'; class StoresPage extends StatefulWidget { @@ -21,90 +23,179 @@ class _StoresPageState extends State { ); @override - Widget build(BuildContext context) => Scaffold( - body: SafeArea( - child: Padding( - padding: const EdgeInsets.all(12), - child: StreamBuilder( - stream: watchStream, - builder: (context, snapshot) { - if (!snapshot.hasData) { - return const LoadingIndicator('Retrieving Stores'); - } + Widget build(BuildContext context) { + const loadingIndicator = LoadingIndicator('Retrieving Stores'); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Header(), - const Divider(), - const Placeholder(fallbackHeight: 100), - const Divider(), - Expanded( - child: FutureBuilder( - future: FMTCRoot.stats.storesAvailable, - builder: (context, snapshot) { - if (!snapshot.hasData) { - return const Padding( - padding: EdgeInsets.all(12), - child: LoadingIndicator('Retrieving Stores'), - ); - } + return Scaffold( + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(12), + child: StreamBuilder( + stream: watchStream, + builder: (context, snapshot) { + if (!snapshot.hasData) return loadingIndicator; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Header(), + const SizedBox(height: 16), + Expanded( + child: FutureBuilder( + future: FMTCRoot.stats.storesAvailable, + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const Padding( + padding: EdgeInsets.all(12), + child: loadingIndicator, + ); + } - if (snapshot.data!.isEmpty) { - return const EmptyIndicator(); - } + if (snapshot.data!.isEmpty) { + return const EmptyIndicator(); + } - return ListView.builder( - itemCount: snapshot.data!.length + 1, - itemBuilder: (context, index) { - final addSpace = index >= snapshot.data!.length; - if (addSpace) return const SizedBox(height: 124); + return ListView.builder( + itemCount: snapshot.data!.length + 2, + itemBuilder: (context, index) { + final addRootStats = index == 0; - return StoreTile( - storeName: - snapshot.data!.elementAt(index).storeName, + if (addRootStats) { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + margin: + const EdgeInsets.symmetric(vertical: 16), + decoration: BoxDecoration( + color: + Theme.of(context).colorScheme.onSecondary, + borderRadius: BorderRadius.circular(16), + ), + child: Wrap( + alignment: WrapAlignment.spaceEvenly, + runAlignment: WrapAlignment.spaceEvenly, + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 20, + children: [ + FutureBuilder( + future: FMTCRoot.stats.length, + builder: (context, snapshot) => + StatDisplay( + statistic: snapshot.data?.toString(), + description: 'total tiles', + ), + ), + FutureBuilder( + future: FMTCRoot.stats.size, + builder: (context, snapshot) => + StatDisplay( + statistic: snapshot.data == null + ? null + : ((snapshot.data! * 1024) + .asReadableSize), + description: 'total tiles size', + ), + ), + FutureBuilder( + future: FMTCRoot.stats.realSize, + builder: (context, snapshot) => Row( + mainAxisSize: MainAxisSize.min, + children: [ + StatDisplay( + statistic: snapshot.data == null + ? null + : ((snapshot.data! * 1024) + .asReadableSize), + description: 'database size', + ), + const SizedBox.square(dimension: 6), + IconButton( + icon: + const Icon(Icons.help_outline), + onPressed: () => + showDatabaseSizeInfoDialog( + context, + ), + ), + ], + ), + ), + ], + ), ); - }, - ); - }, - ), + } + + final addSpace = index >= snapshot.data!.length + 1; + if (addSpace) return const SizedBox(height: 124); + + return StoreTile( + storeName: + snapshot.data!.elementAt(index - 1).storeName, + ); + }, + ); + }, ), - ], - ); - }, - ), + ), + ], + ); + }, ), ), - floatingActionButton: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - FloatingActionButton.small( - heroTag: null, - tooltip: 'Import Store', - child: const Icon(Icons.file_open_rounded), - onPressed: () => Navigator.of(context).push( - MaterialPageRoute( - builder: (BuildContext context) => const ImportStorePopup(), - fullscreenDialog: true, - ), + ), + floatingActionButton: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + FloatingActionButton.small( + heroTag: null, + tooltip: 'Import Store', + shape: const CircleBorder(), + child: const Icon(Icons.file_open_rounded), + onPressed: () => Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) => const ImportStorePopup(), + fullscreenDialog: true, ), ), - const SizedBox.square(dimension: 12), - FloatingActionButton.extended( - label: const Text('Create Store'), - icon: const Icon(Icons.create_new_folder_rounded), - onPressed: () => Navigator.of(context).push( - MaterialPageRoute( - builder: (BuildContext context) => const StoreEditorPopup( - existingStoreName: null, - isStoreInUse: false, - ), - fullscreenDialog: true, + ), + const SizedBox.square(dimension: 12), + FloatingActionButton.extended( + label: const Text('Create Store'), + icon: const Icon(Icons.create_new_folder_rounded), + onPressed: () => Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) => const StoreEditorPopup( + existingStoreName: null, + isStoreInUse: false, ), + fullscreenDialog: true, ), ), - ], + ), + ], + ), + ); + } + + void showDatabaseSizeInfoDialog(BuildContext context) { + showAdaptiveDialog( + context: context, + builder: (context) => AlertDialog.adaptive( + title: const Text('Database Size'), + content: const Text( + ''' +This measurement refers to the actual size of the database root (which may be a +flat/file or another structure). Includes database overheads, and may not follow +the total tiles size in a linear relationship, or any relationship at all.''', ), - ); + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Close'), + ), + ], + ), + ); + } } diff --git a/example/lib/screens/store_editor/store_editor.dart b/example/lib/screens/store_editor/store_editor.dart index 1605dffe..fe404d9b 100644 --- a/example/lib/screens/store_editor/store_editor.dart +++ b/example/lib/screens/store_editor/store_editor.dart @@ -212,7 +212,7 @@ class _StoreEditorPopupState extends State { FilteringTextInputFormatter.digitsOnly, ], initialValue: metadata.data!.isEmpty - ? '20000' + ? '100000' : metadata.data!['maxLength'], textInputAction: TextInputAction.done, ), diff --git a/lib/src/backend/impls/objectbox/backend/internal.dart b/lib/src/backend/impls/objectbox/backend/internal.dart index ef7a5f97..b3051f72 100644 --- a/lib/src/backend/impls/objectbox/backend/internal.dart +++ b/lib/src/backend/impls/objectbox/backend/internal.dart @@ -19,19 +19,19 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { @override Directory? rootDirectory; - SendPort? _sendPort; void get expectInitialised => _sendPort ?? (throw RootUnavailable()); + + // Worker communication protocol storage + SendPort? _sendPort; final _workerResOneShot = ?>>{}; final _workerResStreamed = ?>>{}; int _workerId = 0; late Completer _workerComplete; late StreamSubscription _workerHandler; - // TODO: Verify if necessary and remove if not - //`removeOldestTilesAboveLimit` tracking & debouncing - //late int _rotalLength; - //late Timer _rotalDebouncer; - //late String? _rotalStore; + // `removeOldestTilesAboveLimit` tracking & debouncing + Timer? _rotalDebouncer; + String? _rotalStore; Future?> _sendCmdOneShot({ required _WorkerCmdType type, @@ -160,11 +160,12 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { ); if (exceptionHandler != null) { - // TODO: Was exception at startup? - final handled = exceptionHandler(err, stackTrace); - if (handled == null || !handled) { - Error.throwWithStackTrace(err, stackTrace); - } + final handled = exceptionHandler( + exception: err, + stackTrace: stackTrace, + initialisationFailure: evt.id == 0, + ); + if (!handled) Error.throwWithStackTrace(err, stackTrace); } else { Error.throwWithStackTrace(err, stackTrace); } @@ -241,9 +242,9 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { _workerId = 0; _workerResOneShot.clear(); _workerResStreamed.clear(); - //_rotalStore = null; - //_rotalLength = 0; - //_rotalDebouncer.cancel(); + _rotalDebouncer?.cancel(); + _rotalDebouncer = null; + _rotalStore = null; FMTCBackendAccess.internal = null; FMTCBackendAccessThreadSafe.internal = null; @@ -387,11 +388,41 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { Future removeOldestTilesAboveLimit({ required String storeName, required int tilesLimit, - }) async => - (await _sendCmdOneShot( - type: _WorkerCmdType.removeOldestTilesAboveLimit, - args: {'storeName': storeName, 'tilesLimit': tilesLimit}, - ))!['numOrphans']; + }) async { + const type = _WorkerCmdType.removeOldestTilesAboveLimit; + final args = {'storeName': storeName, 'tilesLimit': tilesLimit}; + + // Attempts to avoid flooding worker with requests to delete oldest tile, + // and 'batches' them instead + + if (_rotalStore != storeName) { + // If the store has changed, failing to reset the batch/queue will mean + // tiles are removed from the wrong store + _rotalStore = storeName; + if (_rotalDebouncer?.isActive ?? false) { + _rotalDebouncer!.cancel(); + return (await _sendCmdOneShot(type: type, args: args))!['numOrphans']; + } + } + + if (_rotalDebouncer?.isActive ?? false) { + _rotalDebouncer!.cancel(); + _rotalDebouncer = Timer( + const Duration(milliseconds: 500), + () async => + (await _sendCmdOneShot(type: type, args: args))!['numOrphans'], + ); + return -1; + } + + _rotalDebouncer = Timer( + const Duration(seconds: 1), + () async => + (await _sendCmdOneShot(type: type, args: args))!['numOrphans'], + ); + + return -1; + } /* FOR ABOVE METHOD // Attempts to avoid flooding worker with requests to delete oldest tile, diff --git a/lib/src/backend/impls/objectbox/backend/internal_worker.dart b/lib/src/backend/impls/objectbox/backend/internal_worker.dart index c63b1f1b..31120c49 100644 --- a/lib/src/backend/impls/objectbox/backend/internal_worker.dart +++ b/lib/src/backend/impls/objectbox/backend/internal_worker.dart @@ -119,14 +119,10 @@ Future _worker( (i) { final tile = queriedTiles[i]; - // Linear search through related stores, and modify when located - for (final store in tile.stores) { - if (store.name != storeName) continue; - store - ..length -= 1 - ..size -= tile.bytes.lengthInBytes; - break; - } + // Modify current store + store + ..length -= 1 + ..size -= tile.bytes.lengthInBytes; // Remove the store relation from the tile tile.stores.removeWhere((store) => store.name == storeName); @@ -136,14 +132,14 @@ Future _worker( // Otherwise register the tile to be updated (the new relation applied) tilesToUpdate.add(tile); - return -1; + return null; }, growable: false, ); stores.put(store, mode: PutMode.update); return (tiles..putMany(tilesToUpdate, mode: PutMode.update)) - .removeMany(tilesToDelete); + .removeMany(tilesToDelete.whereNotNull().toList(growable: false)); }, ); @@ -154,11 +150,13 @@ Future _worker( //! MAIN LOOP !// - await for (final ({ - int id, - _WorkerCmdType type, - Map args, - }) cmd in receivePort) { + await receivePort.listen((cmd) { + cmd as ({ + int id, + _WorkerCmdType type, + Map args, + }); + try { switch (cmd.type) { case _WorkerCmdType.initialise_: @@ -208,7 +206,7 @@ Future _worker( .box() .getAll() .map((e) => e.name) - .toList(), + .toList(growable: false), }, ); case _WorkerCmdType.storeExists: @@ -297,7 +295,7 @@ Future _worker( return null; }) .whereNotNull() - .toList(), + .toList(growable: false), mode: PutMode.update, ); tilesQuery.close(); @@ -475,7 +473,7 @@ Future _worker( ..size += (bytes.lengthInBytes - existingTile.bytes.lengthInBytes), ) - .toList(), + .toList(growable: false), ); case (true, true): // FMTC internal error throw StateError( @@ -553,15 +551,15 @@ Future _worker( final numToRemove = store.length - tilesLimit; + if (numToRemove <= 0) sendRes(id: cmd.id, data: {'numOrphans': 0}); + + tilesQuery.limit = numToRemove; + sendRes( id: cmd.id, data: { - 'numOrphans': numToRemove <= 0 - ? 0 - : deleteTiles( - storeName: storeName, - tilesQuery: tilesQuery..limit = numToRemove, - ), + 'numOrphans': + deleteTiles(storeName: storeName, tilesQuery: tilesQuery), }, ); @@ -736,7 +734,7 @@ Future _worker( .box() .getAll() .map((r) => r.toRegion()) - .toList(), + .toList(growable: false), }, ); case _WorkerCmdType.getRecoverableRegion: @@ -803,7 +801,7 @@ Future _worker( case _WorkerCmdType.cancelWatch: final id = cmd.args['id']! as int; - await streamedOutputSubscriptions[id]?.cancel(); + streamedOutputSubscriptions[id]?.cancel(); streamedOutputSubscriptions.remove(id); sendRes(id: cmd.id); @@ -811,5 +809,5 @@ Future _worker( } catch (e, s) { sendRes(id: cmd.id, data: {'error': e, 'stackTrace': s}); } - } + }).asFuture(); } diff --git a/lib/src/backend/impls/objectbox/models/src/recovery.dart b/lib/src/backend/impls/objectbox/models/src/recovery.dart index 3805f1b4..fcf4f9ef 100644 --- a/lib/src/backend/impls/objectbox/models/src/recovery.dart +++ b/lib/src/backend/impls/objectbox/models/src/recovery.dart @@ -85,13 +85,13 @@ base class ObjectBoxRecovery { ? (region.originalRegion as LineRegion) .line .map((c) => c.latitude) - .toList() + .toList(growable: false) : null, lineLngs = region.originalRegion is LineRegion ? (region.originalRegion as LineRegion) .line .map((c) => c.longitude) - .toList() + .toList(growable: false) : null, lineRadius = region.originalRegion is LineRegion ? (region.originalRegion as LineRegion).radius @@ -100,13 +100,13 @@ base class ObjectBoxRecovery { ? (region.originalRegion as CustomPolygonRegion) .outline .map((c) => c.latitude) - .toList() + .toList(growable: false) : null, customPolygonLngs = region.originalRegion is CustomPolygonRegion ? (region.originalRegion as CustomPolygonRegion) .outline .map((c) => c.longitude) - .toList() + .toList(growable: false) : null; @Id() diff --git a/lib/src/backend/interfaces/backend/internal.dart b/lib/src/backend/interfaces/backend/internal.dart index f7baeebf..82b72f09 100644 --- a/lib/src/backend/interfaces/backend/internal.dart +++ b/lib/src/backend/interfaces/backend/internal.dart @@ -183,14 +183,13 @@ abstract interface class FMTCBackendInternal required bool hit, }); - // TODO: Verify below and add to belower doc string - // - // It is recommended to invoke this operation as few times as possible, for - // example by debouncing, as this operation may be expensive. - /// Remove tiles in excess of the specified limit from the specified store, /// oldest first /// + /// May internally debounce, as this is a potentially expensive operation that + /// is likely to have no effect. When an invocation has been debounced, this + /// method will return `-1`. + /// /// Returns the number of tiles that were actually deleted (they were /// orphaned). See [deleteTile] for more information about orphan tiles. Future removeOldestTilesAboveLimit({ diff --git a/lib/src/backend/utils/errors.dart b/lib/src/backend/utils/errors.dart index a2e20539..d3dd5595 100644 --- a/lib/src/backend/utils/errors.dart +++ b/lib/src/backend/utils/errors.dart @@ -9,6 +9,11 @@ import '../export_external.dart'; /// that error should be fixed in code, and should not occur at runtime. It will /// always be thrown. /// +/// [initialisationFailure] indicates whether the error occured during +/// intialisation. If it is `true`, then the error was fatal and will have +/// killed the backend. Otherwise, the backend should still recieve and respond +/// to future operations. +/// /// Other [Exception]s/[Error]s will result in this method being invoked, with a /// modified [StackTrace] that gives an indication as to where the issue occured /// internally, useful should a bug need to be reported. The trace will not @@ -18,13 +23,13 @@ import '../export_external.dart'; /// exceeding a database's storage limit). /// /// If the callback returns `true`, then FMTC will not continue to handle the -/// error. Otherwise (`false` or `null`), then FMTC will also throw the -/// exception/error. If the callback is not defined (in -/// [FMTCBackend.initialise]), then FMTC will throw the exception/error. -typedef FMTCExceptionHandler = bool? Function( - Object? exception, - StackTrace stackTrace, -); +/// error. Otherwise, FMTC will also throw the exception/error. If the callback +/// is not defined (in [FMTCBackend.initialise]), then FMTC will throw the exception/error. +typedef FMTCExceptionHandler = bool Function({ + required Object? exception, + required StackTrace stackTrace, + required bool initialisationFailure, +}); /// An error to be thrown by backend implementations in known events only /// diff --git a/lib/src/providers/image_provider.dart b/lib/src/providers/image_provider.dart index 5d815949..3ad38733 100644 --- a/lib/src/providers/image_provider.dart +++ b/lib/src/providers/image_provider.dart @@ -250,7 +250,6 @@ class FMTCImageProvider extends ImageProvider { // Clear out old tiles if the maximum store length has been exceeded if (needsCreating && provider.settings.maxStoreLength != 0) { - // TODO: Check if performance is acceptable without checking limit excess first unawaited( FMTCBackendAccess.internal.removeOldestTilesAboveLimit( storeName: storeName, diff --git a/lib/src/providers/tile_provider.dart b/lib/src/providers/tile_provider.dart index a48d1a14..f334af61 100644 --- a/lib/src/providers/tile_provider.dart +++ b/lib/src/providers/tile_provider.dart @@ -53,7 +53,18 @@ class FMTCTileProvider extends TileProvider { coords: coords, ); - // TODO: Define deprecation for `Async` + /// Check whether a specified tile is cached in the current store + @Deprecated(''' +Migrate to `checkTileCached`. + +Synchronous operations have been removed throughout FMTC v9, therefore the +distinction between sync and async operations has been removed. This deprecated +member will be removed in a future version.''') + Future checkTileCachedAsync({ + required TileCoordinates coords, + required TileLayer options, + }) => + checkTileCached(coords: coords, options: options); /// Check whether a specified tile is cached in the current store Future checkTileCached({ From 0f11718a549f41ec7f713db006f29de132d36d2c Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Tue, 27 Feb 2024 21:36:23 +0000 Subject: [PATCH 115/168] Updated minimum SDK requirements Updated GitHub workflow Former-commit-id: 70784fb91b3a386815a13183d221c4606b8a508e [formerly 5cac37e800aea84fb8394c4e1b0a45a139580b6f] Former-commit-id: e9d16918063e5b15722aa4a96564fcd757d6a631 --- .github/workflows/main.yml | 8 ++++++-- .vscode/settings.json | 6 ------ pubspec.yaml | 12 ++++++------ tile_server/static/generated/favicon.dart | 3 ++- tile_server/static/generated/land.dart | 3 ++- tile_server/static/generated/sea.dart | 3 ++- 6 files changed, 18 insertions(+), 17 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 148cf714..2721f15d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -47,7 +47,7 @@ jobs: - name: Check Formatting run: dart format --output=none --set-exit-if-changed . - name: Check Lints - run: dart analyze --fatal-infos --fatal-warnings + run: dart analyze --fatal-warnings run-tests: name: "Run Tests" @@ -81,6 +81,8 @@ jobs: uses: subosito/flutter-action@v2 with: channel: "stable" + - name: Generate Code + run: dart run build_runner build - name: Build run: flutter build apk --obfuscate --split-debug-info=./symbols - name: Upload Artifact @@ -104,6 +106,8 @@ jobs: uses: subosito/flutter-action@v2 with: channel: "stable" + - name: Generate Code + run: dart run build_runner build - name: Build run: flutter build windows --obfuscate --split-debug-info=./symbols - name: Create Installer @@ -130,7 +134,7 @@ jobs: uses: dart-lang/setup-dart@v1.5.0 - name: Get Dependencies run: dart pub get - - name: Run Pre-Compile Generator + - name: Generate Tile Images run: dart run bin/generate_dart_images.dart - name: Compile run: dart compile exe bin/tile_server.dart diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 8699d5bc..00000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "cSpell.words": [ - "isar", - "lukas" - ] -} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 17a8589a..a3e540a6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_map_tile_caching description: Plugin for 'flutter_map' providing advanced caching functionality, with ability to download map regions for offline use. -version: 9.0.0-dev.5 +version: 9.0.0-dev.6 repository: https://github.com/JaffaKetchup/flutter_map_tile_caching issue_tracker: https://github.com/JaffaKetchup/flutter_map_tile_caching/issues @@ -23,8 +23,8 @@ platforms: windows: environment: - sdk: ">=3.0.0 <4.0.0" - flutter: ">=3.10.0" + sdk: ">=3.3.0 <4.0.0" + flutter: ">=3.19.0" dependencies: async: ^2.11.0 @@ -42,9 +42,9 @@ dependencies: path_provider: ^2.1.2 dev_dependencies: - build_runner: ^2.4.7 - objectbox_generator: ^2.4.0 - test: ^1.25.0 + build_runner: ^2.4.8 + objectbox_generator: ^2.5.0 + test: ^1.25.2 flutter: null diff --git a/tile_server/static/generated/favicon.dart b/tile_server/static/generated/favicon.dart index 259e34b4..7dd43c1b 100644 --- a/tile_server/static/generated/favicon.dart +++ b/tile_server/static/generated/favicon.dart @@ -1 +1,2 @@ -final faviconTileBytes = [0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 32, 0, 201, 45, 0, 0, 22, 0, 0, 0, 137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 1, 0, 0, 0, 1, 0, 8, 6, 0, 0, 0, 92, 114, 168, 102, 0, 0, 45, 144, 73, 68, 65, 84, 120, 218, 237, 157, 123, 124, 27, 229, 153, 239, 127, 26, 141, 70, 23, 91, 146, 109, 249, 126, 139, 147, 216, 9, 185, 185, 9, 16, 72, 40, 183, 64, 129, 246, 208, 238, 129, 237, 210, 82, 122, 202, 161, 91, 2, 11, 45, 44, 108, 161, 13, 112, 246, 0, 165, 52, 180, 205, 46, 109, 225, 244, 190, 103, 217, 148, 101, 89, 216, 93, 232, 133, 179, 101, 11, 9, 105, 32, 9, 73, 73, 76, 156, 224, 196, 151, 92, 124, 209, 197, 182, 44, 75, 178, 46, 51, 26, 73, 231, 15, 89, 178, 36, 75, 214, 109, 164, 153, 145, 222, 239, 231, 147, 79, 28, 105, 52, 126, 53, 153, 231, 55, 207, 251, 188, 207, 243, 188, 10, 254, 163, 23, 195, 40, 0, 191, 215, 9, 243, 88, 63, 204, 163, 199, 97, 29, 59, 1, 149, 190, 25, 205, 61, 215, 160, 121, 213, 39, 80, 219, 177, 9, 52, 83, 93, 200, 233, 9, 4, 66, 17, 81, 20, 42, 0, 201, 204, 76, 157, 195, 196, 249, 62, 88, 198, 250, 225, 152, 49, 163, 174, 227, 34, 52, 245, 92, 131, 166, 238, 109, 208, 55, 174, 18, 251, 251, 18, 8, 132, 56, 4, 23, 128, 120, 120, 158, 133, 101, 52, 226, 29, 152, 199, 250, 17, 166, 52, 104, 90, 181, 13, 77, 221, 219, 208, 216, 125, 37, 241, 14, 8, 4, 145, 201, 73, 0, 38, 236, 62, 184, 61, 172, 216, 99, 134, 182, 190, 153, 76, 47, 8, 4, 1, 160, 179, 61, 112, 194, 238, 131, 66, 93, 139, 21, 203, 87, 138, 61, 102, 76, 155, 71, 224, 24, 59, 134, 134, 149, 87, 136, 61, 20, 2, 65, 214, 80, 217, 30, 232, 246, 176, 168, 111, 21, 223, 248, 1, 160, 190, 117, 37, 124, 211, 86, 177, 135, 65, 32, 200, 158, 172, 5, 128, 64, 32, 148, 31, 178, 22, 0, 235, 224, 31, 192, 115, 115, 98, 15, 131, 64, 144, 45, 89, 199, 0, 164, 200, 200, 219, 79, 227, 200, 244, 121, 212, 117, 94, 130, 166, 158, 107, 208, 176, 242, 10, 24, 155, 214, 138, 61, 44, 2, 65, 54, 228, 45, 0, 111, 255, 231, 191, 225, 143, 111, 255, 22, 0, 160, 209, 106, 209, 220, 186, 12, 87, 95, 127, 19, 150, 175, 92, 19, 59, 230, 223, 94, 252, 49, 250, 251, 222, 143, 253, 123, 109, 239, 197, 248, 252, 237, 247, 1, 0, 206, 142, 12, 224, 159, 255, 225, 239, 97, 53, 143, 97, 237, 134, 139, 241, 63, 255, 234, 155, 168, 170, 210, 3, 0, 142, 31, 61, 128, 87, 95, 252, 9, 156, 179, 51, 184, 120, 203, 85, 184, 253, 174, 111, 164, 28, 195, 117, 55, 61, 6, 158, 103, 97, 29, 63, 9, 203, 232, 126, 28, 57, 240, 99, 4, 194, 10, 52, 245, 92, 131, 198, 149, 87, 162, 177, 231, 106, 48, 154, 26, 177, 175, 49, 129, 32, 89, 242, 22, 0, 135, 125, 18, 77, 45, 29, 248, 243, 47, 109, 199, 156, 195, 137, 211, 39, 251, 240, 204, 223, 222, 139, 47, 221, 245, 16, 46, 191, 250, 70, 0, 192, 208, 233, 227, 184, 230, 147, 159, 197, 138, 158, 200, 83, 89, 171, 173, 2, 0, 112, 156, 31, 127, 247, 212, 131, 248, 226, 95, 62, 136, 222, 139, 46, 195, 203, 255, 247, 7, 120, 101, 247, 243, 248, 242, 61, 143, 192, 238, 180, 227, 255, 236, 122, 12, 247, 239, 248, 30, 58, 186, 186, 241, 179, 103, 255, 55, 222, 248, 143, 221, 184, 241, 207, 111, 79, 253, 5, 104, 53, 218, 187, 46, 68, 123, 215, 133, 0, 0, 183, 203, 6, 243, 249, 15, 49, 241, 254, 79, 113, 236, 245, 7, 161, 111, 90, 131, 166, 149, 87, 161, 185, 231, 19, 168, 237, 188, 72, 236, 235, 77, 32, 72, 138, 130, 166, 0, 90, 93, 21, 154, 27, 151, 1, 141, 64, 247, 234, 94, 116, 116, 117, 227, 185, 239, 237, 192, 37, 151, 93, 11, 134, 209, 192, 53, 235, 192, 138, 158, 181, 232, 88, 214, 157, 240, 185, 161, 83, 253, 48, 24, 106, 176, 245, 202, 27, 0, 0, 55, 221, 126, 47, 118, 220, 125, 51, 190, 248, 149, 7, 209, 119, 96, 47, 214, 125, 108, 51, 214, 245, 110, 6, 0, 252, 217, 95, 124, 25, 191, 124, 254, 219, 105, 5, 32, 25, 189, 161, 9, 171, 55, 92, 143, 213, 27, 174, 7, 207, 179, 176, 219, 206, 96, 226, 124, 31, 250, 254, 253, 85, 120, 125, 30, 52, 173, 186, 38, 226, 33, 116, 95, 5, 117, 85, 189, 216, 215, 159, 64, 16, 21, 65, 99, 0, 189, 23, 94, 6, 138, 82, 98, 232, 84, 63, 214, 245, 110, 134, 219, 237, 68, 223, 159, 246, 227, 248, 7, 7, 208, 209, 213, 141, 222, 11, 47, 3, 0, 88, 39, 206, 163, 61, 78, 20, 76, 70, 19, 0, 192, 53, 235, 128, 213, 60, 138, 214, 182, 174, 216, 123, 237, 93, 61, 152, 180, 78, 228, 247, 229, 104, 53, 154, 218, 214, 160, 169, 109, 13, 112, 217, 23, 224, 247, 58, 49, 113, 190, 15, 230, 99, 187, 113, 252, 119, 223, 132, 198, 216, 129, 166, 85, 215, 162, 105, 229, 85, 36, 177, 136, 80, 145, 8, 30, 4, 172, 51, 53, 194, 237, 114, 0, 0, 62, 245, 103, 183, 197, 94, 255, 247, 151, 126, 134, 55, 127, 251, 47, 120, 248, 241, 231, 224, 247, 121, 161, 102, 212, 9, 159, 211, 234, 244, 112, 187, 103, 193, 113, 44, 106, 106, 23, 158, 204, 85, 85, 122, 4, 2, 28, 60, 30, 119, 44, 70, 144, 47, 26, 157, 17, 43, 215, 92, 133, 149, 107, 174, 2, 0, 76, 217, 134, 97, 29, 59, 129, 83, 255, 185, 3, 179, 179, 54, 152, 150, 109, 65, 211, 170, 107, 208, 188, 234, 90, 84, 213, 46, 43, 222, 85, 39, 16, 36, 130, 224, 2, 48, 61, 101, 129, 222, 80, 11, 0, 9, 110, 251, 117, 159, 254, 28, 190, 122, 251, 245, 24, 59, 63, 140, 106, 67, 13, 216, 179, 131, 9, 159, 243, 121, 221, 208, 84, 49, 208, 235, 141, 240, 121, 23, 150, 246, 60, 30, 55, 0, 20, 108, 252, 169, 104, 104, 234, 70, 67, 83, 55, 54, 92, 124, 19, 252, 94, 39, 108, 230, 83, 176, 12, 255, 30, 251, 247, 124, 15, 148, 218, 128, 166, 85, 159, 64, 211, 170, 109, 168, 239, 218, 74, 188, 3, 66, 89, 66, 191, 251, 135, 31, 163, 181, 179, 23, 173, 29, 27, 160, 209, 25, 11, 58, 217, 161, 119, 255, 11, 20, 165, 68, 207, 5, 27, 22, 189, 199, 48, 26, 104, 117, 122, 240, 124, 0, 109, 29, 93, 120, 243, 55, 47, 197, 222, 179, 59, 237, 224, 88, 22, 166, 186, 54, 52, 183, 47, 195, 209, 247, 247, 197, 222, 27, 63, 55, 132, 150, 214, 206, 162, 95, 8, 141, 206, 136, 101, 221, 151, 98, 89, 247, 165, 0, 0, 215, 172, 5, 227, 231, 142, 225, 204, 158, 157, 56, 50, 117, 22, 198, 214, 141, 104, 238, 185, 6, 77, 171, 175, 37, 75, 141, 132, 178, 129, 174, 93, 115, 51, 206, 13, 239, 197, 7, 7, 94, 65, 85, 149, 30, 45, 29, 189, 104, 237, 236, 141, 204, 155, 51, 224, 243, 122, 96, 157, 60, 15, 231, 148, 29, 253, 199, 14, 225, 205, 223, 189, 140, 237, 247, 63, 30, 9, 0, 186, 28, 24, 57, 221, 143, 85, 107, 55, 1, 0, 222, 121, 243, 53, 168, 213, 106, 180, 117, 44, 7, 195, 104, 16, 8, 112, 120, 247, 157, 55, 176, 105, 243, 149, 120, 125, 247, 143, 113, 233, 229, 215, 129, 97, 52, 216, 180, 249, 74, 188, 244, 15, 207, 226, 228, 241, 35, 232, 232, 234, 198, 111, 254, 237, 31, 99, 193, 194, 82, 98, 168, 105, 193, 218, 141, 45, 88, 187, 241, 191, 197, 150, 26, 173, 227, 135, 113, 228, 240, 47, 17, 8, 43, 208, 184, 242, 42, 52, 245, 108, 67, 195, 138, 203, 73, 48, 145, 32, 91, 20, 46, 135, 45, 86, 13, 232, 24, 253, 0, 214, 161, 183, 96, 27, 217, 7, 183, 109, 0, 77, 173, 23, 160, 181, 243, 99, 104, 237, 236, 197, 248, 172, 10, 43, 214, 127, 60, 246, 193, 228, 60, 128, 182, 142, 21, 216, 118, 195, 159, 199, 34, 254, 30, 143, 27, 255, 240, 252, 83, 56, 63, 114, 26, 148, 82, 137, 229, 221, 107, 241, 185, 47, 125, 21, 245, 141, 45, 0, 128, 177, 243, 195, 248, 167, 159, 125, 23, 147, 86, 51, 46, 88, 183, 41, 33, 15, 224, 228, 241, 35, 120, 249, 133, 31, 97, 110, 206, 137, 141, 23, 95, 142, 47, 220, 113, 63, 24, 70, 147, 48, 240, 51, 39, 222, 195, 5, 157, 53, 162, 92, 180, 57, 215, 84, 164, 196, 121, 244, 67, 216, 204, 167, 200, 82, 35, 65, 182, 36, 8, 64, 60, 172, 103, 26, 83, 103, 222, 133, 109, 104, 47, 172, 131, 111, 99, 195, 182, 199, 19, 4, 64, 108, 196, 20, 128, 100, 108, 19, 3, 176, 140, 245, 195, 60, 250, 33, 60, 30, 55, 76, 203, 183, 160, 169, 123, 27, 154, 87, 93, 11, 173, 177, 77, 236, 225, 17, 8, 105, 73, 43, 0, 201, 140, 190, 255, 42, 17, 128, 44, 32, 45, 210, 8, 114, 66, 214, 181, 0, 82, 68, 163, 51, 98, 197, 234, 203, 177, 98, 245, 229, 0, 22, 90, 164, 157, 126, 243, 49, 56, 102, 204, 48, 45, 219, 130, 198, 149, 87, 144, 22, 105, 4, 73, 64, 4, 160, 200, 212, 53, 116, 161, 174, 161, 11, 27, 46, 190, 105, 161, 69, 218, 249, 119, 112, 224, 221, 231, 72, 139, 52, 130, 232, 100, 61, 5, 152, 26, 217, 15, 85, 8, 146, 104, 10, 50, 109, 30, 65, 152, 117, 160, 205, 164, 21, 123, 40, 5, 17, 93, 106, 180, 140, 245, 99, 218, 54, 2, 99, 75, 47, 154, 87, 125, 2, 77, 221, 87, 195, 216, 186, 161, 240, 95, 64, 32, 100, 32, 107, 1, 224, 185, 57, 56, 198, 142, 73, 162, 19, 143, 190, 74, 141, 38, 35, 5, 154, 86, 23, 126, 50, 137, 192, 243, 44, 166, 204, 167, 99, 241, 3, 63, 199, 161, 169, 251, 42, 82, 183, 64, 40, 42, 89, 11, 128, 80, 36, 47, 53, 54, 182, 172, 70, 107, 231, 6, 180, 116, 108, 128, 161, 166, 69, 236, 235, 33, 25, 230, 92, 83, 176, 77, 124, 20, 105, 177, 62, 126, 18, 213, 166, 110, 52, 175, 254, 4, 26, 86, 92, 137, 250, 229, 91, 197, 30, 30, 161, 76, 40, 185, 0, 196, 195, 249, 103, 49, 125, 230, 0, 172, 131, 111, 193, 54, 180, 7, 84, 152, 71, 107, 199, 6, 180, 117, 109, 68, 115, 251, 186, 178, 122, 194, 23, 202, 148, 109, 24, 230, 115, 125, 48, 143, 126, 136, 57, 247, 52, 76, 43, 34, 129, 196, 166, 158, 109, 208, 213, 116, 136, 61, 60, 130, 76, 17, 85, 0, 146, 113, 218, 62, 194, 212, 200, 126, 216, 134, 246, 96, 102, 244, 48, 234, 234, 151, 161, 165, 115, 3, 90, 59, 122, 81, 215, 208, 37, 246, 240, 36, 67, 116, 169, 209, 58, 118, 2, 214, 241, 147, 80, 86, 53, 160, 113, 197, 21, 164, 110, 129, 144, 51, 146, 18, 128, 120, 120, 110, 14, 211, 231, 14, 98, 106, 228, 93, 88, 135, 246, 32, 224, 182, 162, 181, 179, 23, 205, 29, 235, 5, 169, 91, 40, 39, 102, 166, 206, 193, 60, 118, 28, 150, 209, 126, 204, 144, 22, 105, 132, 28, 144, 172, 0, 36, 227, 157, 29, 131, 109, 104, 47, 108, 195, 123, 97, 63, 179, 31, 213, 250, 250, 72, 154, 114, 215, 70, 52, 52, 117, 23, 254, 11, 202, 132, 133, 22, 105, 253, 176, 140, 245, 147, 22, 105, 132, 37, 145, 141, 0, 36, 51, 125, 246, 32, 166, 206, 252, 17, 214, 211, 111, 97, 206, 62, 140, 150, 246, 117, 104, 110, 95, 143, 214, 206, 94, 84, 27, 26, 196, 30, 158, 100, 136, 182, 72, 51, 143, 246, 99, 210, 114, 154, 212, 45, 16, 18, 144, 173, 0, 196, 195, 122, 166, 49, 57, 188, 15, 182, 161, 61, 176, 13, 239, 131, 134, 97, 98, 37, 206, 13, 173, 171, 73, 48, 113, 158, 248, 22, 105, 150, 209, 227, 164, 69, 26, 161, 60, 4, 32, 25, 167, 185, 31, 182, 225, 119, 96, 29, 124, 11, 78, 203, 113, 212, 55, 173, 68, 75, 199, 6, 180, 119, 109, 34, 75, 141, 113, 196, 90, 164, 141, 246, 195, 58, 113, 146, 180, 72, 171, 64, 202, 82, 0, 226, 225, 185, 57, 76, 14, 255, 17, 182, 225, 189, 176, 13, 238, 133, 34, 228, 71, 107, 199, 6, 180, 118, 246, 162, 165, 115, 3, 241, 14, 226, 136, 182, 72, 51, 159, 239, 35, 45, 210, 42, 132, 178, 23, 128, 100, 220, 147, 131, 17, 49, 152, 95, 106, 172, 53, 117, 160, 165, 99, 3, 218, 150, 109, 36, 75, 141, 113, 68, 91, 164, 153, 199, 142, 195, 114, 254, 120, 66, 139, 52, 125, 195, 42, 208, 76, 21, 153, 50, 148, 1, 21, 39, 0, 241, 68, 211, 155, 173, 131, 111, 193, 58, 180, 7, 156, 219, 130, 150, 121, 239, 128, 44, 53, 38, 50, 51, 117, 14, 214, 137, 143, 96, 62, 255, 33, 188, 115, 118, 176, 172, 7, 1, 206, 15, 5, 165, 4, 173, 214, 67, 165, 174, 134, 74, 173, 135, 82, 93, 13, 70, 87, 3, 90, 165, 131, 74, 99, 4, 163, 53, 66, 169, 210, 65, 165, 53, 130, 102, 170, 160, 210, 26, 64, 171, 170, 161, 82, 87, 131, 214, 26, 136, 144, 136, 76, 69, 11, 64, 50, 62, 231, 4, 172, 131, 111, 71, 150, 26, 207, 30, 202, 185, 69, 90, 37, 194, 243, 44, 120, 206, 143, 0, 239, 71, 128, 245, 33, 192, 249, 16, 8, 248, 192, 177, 94, 4, 56, 47, 2, 129, 200, 235, 28, 235, 137, 252, 204, 249, 16, 224, 188, 224, 3, 126, 176, 108, 228, 239, 69, 66, 162, 49, 66, 165, 49, 128, 214, 84, 167, 20, 18, 149, 198, 0, 90, 93, 5, 149, 218, 0, 90, 163, 135, 74, 173, 7, 173, 209, 147, 37, 206, 60, 32, 2, 176, 4, 75, 181, 72, 35, 75, 141, 194, 146, 74, 72, 22, 68, 195, 11, 142, 245, 130, 15, 176, 139, 132, 36, 192, 122, 193, 5, 252, 224, 3, 126, 112, 172, 7, 180, 74, 3, 74, 85, 181, 72, 72, 84, 234, 136, 183, 65, 132, 36, 17, 34, 0, 89, 18, 223, 34, 109, 114, 100, 31, 84, 138, 48, 90, 58, 54, 160, 165, 115, 3, 169, 91, 144, 16, 81, 33, 225, 184, 136, 96, 68, 133, 132, 99, 61, 224, 121, 118, 222, 51, 137, 122, 42, 139, 133, 132, 99, 61, 224, 3, 126, 208, 42, 13, 104, 141, 17, 180, 90, 63, 239, 133, 44, 8, 9, 163, 173, 1, 205, 232, 202, 66, 72, 136, 0, 228, 137, 211, 246, 17, 108, 167, 223, 134, 117, 104, 15, 156, 230, 62, 152, 26, 150, 163, 117, 217, 199, 208, 220, 182, 150, 4, 19, 203, 128, 120, 33, 97, 89, 15, 120, 214, 11, 158, 231, 82, 10, 9, 199, 121, 192, 7, 184, 140, 66, 162, 154, 23, 7, 37, 163, 75, 16, 18, 149, 182, 6, 74, 149, 54, 38, 36, 106, 77, 29, 40, 181, 22, 140, 198, 56, 31, 59, 41, 222, 114, 44, 17, 0, 1, 136, 214, 45, 216, 6, 247, 194, 54, 248, 22, 66, 172, 11, 45, 203, 122, 209, 218, 209, 139, 166, 214, 11, 72, 48, 177, 130, 89, 74, 72, 88, 214, 131, 32, 207, 45, 8, 9, 235, 141, 136, 9, 231, 3, 199, 249, 22, 4, 39, 224, 7, 205, 84, 129, 86, 87, 47, 18, 18, 70, 91, 27, 241, 56, 210, 8, 9, 173, 209, 71, 188, 147, 52, 66, 66, 4, 160, 8, 120, 28, 231, 35, 193, 196, 193, 61, 176, 159, 63, 132, 154, 154, 38, 180, 46, 219, 136, 230, 142, 245, 164, 110, 129, 144, 23, 169, 132, 132, 227, 124, 243, 193, 212, 136, 144, 112, 172, 7, 1, 214, 139, 0, 239, 143, 196, 76, 230, 133, 36, 242, 26, 155, 82, 72, 136, 0, 20, 153, 232, 82, 163, 109, 100, 31, 108, 131, 111, 195, 239, 28, 67, 115, 219, 58, 180, 118, 70, 114, 15, 136, 119, 64, 40, 37, 60, 207, 194, 239, 117, 33, 20, 226, 193, 178, 30, 34, 0, 165, 198, 231, 156, 192, 244, 185, 67, 145, 186, 133, 193, 61, 208, 105, 171, 208, 210, 217, 139, 150, 246, 117, 164, 110, 129, 80, 114, 136, 0, 136, 12, 105, 145, 70, 16, 19, 34, 0, 18, 130, 243, 207, 98, 114, 232, 29, 76, 142, 252, 17, 182, 161, 61, 100, 169, 145, 80, 116, 136, 0, 72, 24, 210, 34, 141, 80, 108, 136, 0, 200, 132, 132, 165, 198, 161, 61, 8, 249, 103, 209, 220, 190, 142, 180, 72, 35, 20, 4, 17, 0, 153, 226, 113, 156, 143, 52, 65, 33, 45, 210, 8, 5, 64, 4, 160, 76, 152, 62, 123, 16, 214, 161, 183, 49, 53, 188, 47, 214, 34, 173, 109, 217, 70, 52, 181, 173, 37, 117, 11, 132, 180, 16, 1, 40, 67, 72, 139, 52, 66, 182, 16, 1, 168, 0, 72, 139, 52, 66, 58, 136, 0, 84, 24, 164, 69, 26, 33, 30, 34, 0, 21, 78, 66, 139, 180, 177, 15, 80, 91, 215, 74, 90, 164, 85, 16, 68, 0, 8, 49, 146, 91, 164, 5, 220, 86, 52, 119, 172, 71, 93, 67, 23, 12, 53, 205, 208, 85, 213, 129, 86, 169, 193, 48, 58, 208, 140, 134, 120, 11, 101, 0, 17, 0, 66, 90, 162, 117, 11, 179, 230, 227, 112, 79, 143, 128, 157, 155, 66, 192, 239, 68, 128, 157, 67, 40, 176, 80, 239, 206, 168, 171, 192, 168, 52, 80, 169, 117, 80, 49, 58, 168, 24, 45, 84, 42, 77, 228, 111, 70, 11, 38, 250, 250, 252, 177, 42, 70, 75, 132, 68, 34, 16, 1, 32, 20, 4, 231, 159, 5, 239, 119, 35, 192, 186, 231, 255, 118, 129, 103, 61, 8, 248, 93, 8, 248, 156, 8, 6, 188, 224, 124, 78, 240, 156, 39, 242, 158, 127, 46, 242, 222, 188, 144, 240, 172, 27, 225, 80, 16, 42, 70, 19, 17, 147, 20, 66, 194, 168, 171, 34, 130, 145, 66, 72, 84, 106, 45, 84, 180, 134, 8, 73, 158, 16, 1, 32, 72, 2, 214, 51, 13, 158, 243, 68, 254, 204, 11, 73, 192, 231, 138, 8, 71, 156, 144, 4, 252, 78, 240, 1, 47, 56, 239, 44, 130, 156, 55, 173, 144, 168, 213, 58, 208, 42, 77, 76, 72, 212, 234, 170, 200, 191, 213, 81, 239, 68, 23, 17, 20, 149, 182, 162, 133, 132, 8, 0, 161, 172, 136, 9, 137, 207, 21, 17, 134, 192, 92, 130, 144, 112, 126, 103, 76, 56, 162, 158, 73, 144, 157, 139, 8, 9, 231, 77, 16, 18, 245, 188, 231, 17, 21, 17, 38, 234, 149, 48, 90, 208, 140, 166, 44, 132, 132, 8, 0, 129, 144, 4, 207, 205, 33, 24, 240, 39, 8, 73, 128, 117, 130, 103, 61, 17, 1, 137, 254, 157, 78, 72, 230, 167, 68, 0, 192, 196, 98, 32, 218, 121, 143, 100, 177, 144, 48, 76, 220, 20, 71, 21, 141, 155, 84, 129, 166, 153, 162, 215, 120, 16, 1, 32, 16, 138, 68, 182, 66, 18, 152, 23, 19, 206, 231, 4, 207, 186, 17, 240, 187, 16, 228, 60, 105, 133, 68, 197, 232, 98, 65, 212, 168, 88, 40, 85, 76, 94, 66, 66, 4, 128, 64, 144, 56, 201, 66, 194, 249, 102, 17, 96, 221, 8, 6, 124, 25, 133, 132, 103, 61, 145, 159, 211, 8, 9, 17, 0, 2, 161, 66, 136, 10, 9, 231, 153, 65, 136, 103, 193, 249, 102, 137, 0, 16, 8, 149, 12, 37, 246, 0, 8, 4, 130, 120, 16, 1, 32, 16, 42, 24, 34, 0, 4, 66, 5, 67, 4, 128, 64, 168, 96, 136, 0, 16, 8, 21, 12, 17, 0, 2, 161, 130, 161, 197, 30, 0, 129, 16, 101, 192, 113, 2, 78, 191, 3, 0, 64, 81, 74, 208, 97, 26, 148, 82, 1, 53, 173, 5, 173, 160, 161, 85, 234, 208, 174, 239, 20, 123, 152, 101, 5, 201, 3, 32, 136, 206, 128, 227, 4, 166, 125, 147, 240, 243, 190, 172, 142, 167, 20, 20, 40, 5, 5, 5, 148, 80, 82, 20, 40, 80, 80, 42, 148, 160, 40, 37, 40, 80, 160, 41, 37, 104, 74, 5, 37, 69, 67, 163, 212, 194, 200, 212, 160, 78, 99, 18, 251, 107, 74, 18, 226, 1, 16, 68, 35, 87, 195, 143, 18, 10, 135, 16, 10, 135, 0, 240, 8, 132, 178, 255, 28, 77, 69, 110, 119, 74, 161, 140, 136, 72, 156, 112, 68, 189, 13, 154, 82, 65, 173, 212, 84, 140, 183, 65, 60, 0, 66, 201, 201, 215, 240, 197, 32, 87, 111, 163, 90, 85, 141, 6, 109, 147, 216, 195, 206, 26, 226, 1, 16, 74, 134, 156, 12, 63, 74, 185, 123, 27, 196, 3, 32, 20, 29, 57, 26, 190, 24, 228, 228, 109, 64, 141, 106, 141, 161, 96, 111, 131, 120, 0, 132, 162, 81, 206, 134, 207, 7, 120, 76, 78, 78, 161, 177, 177, 1, 180, 138, 70, 40, 20, 134, 195, 49, 3, 214, 207, 65, 167, 211, 193, 104, 212, 67, 65, 229, 182, 202, 158, 179, 183, 17, 169, 240, 205, 217, 219, 48, 168, 141, 48, 48, 145, 254, 0, 196, 3, 32, 8, 78, 57, 27, 126, 148, 57, 183, 27, 44, 199, 65, 205, 48, 168, 214, 235, 49, 235, 112, 2, 0, 12, 70, 3, 92, 78, 23, 0, 160, 166, 86, 186, 59, 54, 71, 189, 13, 146, 8, 68, 16, 140, 1, 199, 9, 236, 55, 239, 193, 184, 251, 124, 89, 27, 63, 0, 120, 61, 126, 24, 13, 70, 120, 61, 254, 200, 191, 189, 94, 24, 140, 6, 80, 148, 2, 6, 163, 1, 94, 175, 87, 236, 33, 46, 73, 40, 28, 2, 31, 226, 201, 20, 128, 80, 56, 149, 240, 196, 143, 135, 15, 240, 80, 170, 40, 208, 42, 26, 74, 21, 5, 62, 192, 47, 58, 134, 202, 209, 253, 23, 11, 34, 0, 132, 188, 17, 218, 240, 139, 49, 175, 46, 6, 126, 191, 15, 106, 134, 1, 0, 168, 25, 6, 126, 191, 15, 58, 157, 14, 46, 167, 43, 54, 5, 208, 104, 52, 98, 15, 51, 43, 196, 191, 154, 4, 217, 81, 44, 87, 223, 239, 247, 65, 173, 137, 24, 20, 0, 184, 156, 46, 40, 41, 26, 205, 45, 205, 0, 0, 167, 211, 45, 246, 87, 7, 16, 113, 255, 53, 26, 45, 0, 64, 163, 209, 194, 235, 241, 195, 96, 52, 32, 24, 226, 97, 181, 88, 17, 12, 241, 48, 24, 13, 98, 15, 51, 43, 136, 7, 64, 200, 154, 98, 187, 250, 94, 143, 31, 117, 166, 90, 204, 216, 29, 168, 214, 235, 225, 245, 122, 209, 220, 210, 28, 155, 87, 91, 45, 86, 73, 4, 214, 248, 96, 196, 83, 137, 135, 162, 20, 48, 153, 76, 48, 79, 88, 96, 50, 201, 39, 237, 152, 8, 0, 33, 35, 165, 152, 227, 203, 105, 94, 221, 218, 214, 2, 0, 48, 79, 88, 98, 63, 203, 21, 34, 0, 132, 180, 148, 50, 184, 87, 78, 243, 106, 57, 65, 4, 128, 176, 8, 49, 162, 250, 81, 247, 31, 136, 204, 171, 103, 236, 14, 212, 55, 214, 195, 225, 152, 129, 213, 98, 133, 90, 195, 160, 182, 182, 78, 236, 75, 83, 118, 144, 68, 32, 66, 12, 49, 151, 243, 204, 19, 150, 69, 175, 149, 147, 171, 45, 4, 233, 86, 69, 146, 87, 79, 114, 129, 120, 0, 4, 73, 172, 227, 19, 99, 207, 204, 194, 170, 72, 29, 92, 78, 23, 156, 78, 55, 106, 106, 141, 9, 171, 39, 213, 42, 125, 78, 231, 148, 70, 84, 133, 32, 10, 149, 148, 185, 87, 14, 164, 203, 54, 76, 206, 74, 204, 5, 226, 1, 84, 32, 82, 120, 226, 19, 10, 135, 154, 119, 255, 147, 87, 79, 114, 153, 6, 144, 24, 64, 5, 65, 12, 95, 222, 164, 42, 56, 162, 233, 136, 19, 95, 173, 215, 99, 206, 237, 142, 253, 156, 45, 68, 0, 42, 0, 98, 248, 229, 65, 124, 16, 48, 186, 42, 50, 61, 57, 141, 58, 83, 45, 104, 21, 13, 62, 192, 99, 198, 238, 64, 99, 115, 67, 194, 231, 194, 161, 16, 154, 221, 141, 9, 175, 241, 170, 32, 0, 50, 5, 40, 107, 136, 225, 151, 23, 169, 178, 13, 83, 101, 37, 46, 194, 27, 68, 107, 221, 10, 212, 183, 45, 8, 3, 207, 205, 97, 206, 21, 34, 2, 80, 142, 16, 195, 175, 28, 178, 90, 61, 209, 41, 17, 240, 37, 222, 11, 52, 83, 141, 154, 122, 226, 1, 148, 21, 196, 240, 9, 169, 80, 80, 20, 248, 32, 159, 242, 61, 34, 0, 101, 0, 49, 124, 66, 190, 16, 1, 144, 57, 135, 44, 251, 225, 14, 184, 196, 30, 6, 161, 132, 164, 114, 245, 51, 37, 79, 133, 66, 169, 61, 0, 146, 8, 36, 115, 124, 65, 105, 183, 158, 34, 72, 27, 34, 0, 50, 102, 220, 61, 10, 62, 141, 178, 19, 8, 217, 64, 4, 64, 198, 204, 114, 51, 98, 15, 129, 32, 115, 136, 0, 200, 24, 63, 159, 123, 238, 55, 161, 50, 225, 130, 108, 202, 215, 137, 0, 200, 24, 54, 72, 4, 32, 19, 161, 80, 24, 118, 187, 29, 230, 9, 11, 102, 29, 78, 132, 67, 145, 29, 55, 248, 0, 15, 243, 132, 37, 101, 231, 161, 74, 130, 8, 128, 140, 153, 117, 146, 27, 58, 19, 233, 26, 139, 38, 55, 32, 173, 84, 136, 0, 200, 24, 46, 228, 35, 55, 116, 6, 138, 81, 66, 43, 71, 248, 112, 32, 229, 235, 68, 0, 100, 202, 89, 215, 8, 116, 85, 213, 21, 123, 67, 231, 75, 186, 18, 218, 114, 39, 0, 34, 0, 101, 133, 39, 16, 121, 242, 87, 234, 13, 157, 45, 209, 198, 162, 161, 80, 56, 214, 88, 52, 85, 3, 210, 74, 133, 8, 128, 76, 241, 242, 94, 114, 67, 103, 65, 170, 13, 59, 82, 109, 236, 81, 238, 240, 170, 32, 102, 167, 23, 103, 140, 18, 1, 144, 41, 211, 147, 230, 138, 190, 161, 179, 37, 90, 66, 11, 0, 38, 147, 9, 20, 165, 136, 149, 208, 154, 39, 44, 152, 156, 156, 74, 91, 40, 83, 78, 76, 135, 38, 83, 190, 78, 106, 1, 100, 138, 39, 196, 229, 86, 19, 94, 36, 138, 209, 169, 182, 216, 84, 100, 3, 82, 154, 2, 207, 46, 206, 5, 32, 30, 128, 76, 241, 251, 23, 158, 242, 173, 109, 45, 177, 27, 57, 254, 231, 82, 64, 150, 217, 100, 2, 163, 128, 223, 51, 183, 232, 101, 34, 0, 50, 100, 200, 121, 26, 161, 249, 245, 127, 177, 33, 203, 108, 242, 64, 65, 81, 8, 135, 23, 119, 255, 35, 2, 32, 67, 60, 156, 116, 203, 127, 165, 186, 42, 145, 79, 9, 109, 33, 72, 49, 3, 49, 85, 73, 48, 17, 0, 25, 194, 133, 2, 37, 191, 161, 211, 65, 150, 217, 82, 35, 151, 169, 17, 17, 0, 25, 34, 165, 26, 0, 178, 204, 150, 26, 185, 76, 141, 164, 21, 158, 37, 100, 5, 31, 10, 20, 126, 18, 129, 200, 187, 83, 109, 133, 33, 196, 38, 30, 197, 128, 8, 128, 204, 152, 241, 219, 37, 223, 4, 164, 34, 151, 217, 146, 72, 181, 181, 121, 170, 169, 81, 174, 123, 249, 21, 66, 170, 146, 96, 50, 5, 144, 25, 54, 159, 165, 240, 147, 16, 138, 142, 92, 166, 70, 196, 3, 144, 25, 44, 233, 252, 43, 11, 228, 50, 53, 34, 2, 32, 51, 164, 218, 5, 72, 42, 171, 18, 82, 70, 236, 169, 81, 170, 146, 96, 50, 5, 144, 25, 129, 176, 116, 2, 128, 4, 121, 145, 170, 36, 152, 8, 128, 204, 144, 210, 10, 0, 65, 254, 16, 1, 144, 17, 22, 239, 132, 228, 87, 0, 8, 137, 72, 105, 106, 196, 171, 130, 224, 185, 196, 122, 0, 34, 0, 50, 194, 230, 49, 139, 61, 4, 130, 140, 9, 168, 188, 152, 115, 37, 214, 144, 144, 32, 160, 76, 56, 106, 61, 12, 59, 39, 173, 8, 50, 65, 94, 56, 217, 185, 69, 37, 193, 68, 0, 100, 192, 97, 219, 1, 56, 57, 135, 216, 195, 32, 72, 156, 76, 189, 25, 26, 76, 245, 139, 183, 9, 23, 123, 208, 132, 165, 169, 228, 205, 63, 127, 254, 244, 175, 48, 126, 102, 233, 105, 79, 199, 202, 54, 108, 127, 244, 127, 136, 61, 84, 73, 176, 80, 128, 84, 7, 151, 211, 5, 167, 211, 141, 154, 90, 99, 172, 0, 137, 13, 248, 193, 211, 137, 49, 36, 34, 0, 18, 197, 197, 57, 241, 145, 253, 120, 197, 26, 127, 40, 20, 198, 96, 255, 8, 206, 14, 140, 46, 121, 92, 128, 35, 65, 209, 40, 94, 175, 23, 205, 45, 205, 177, 2, 36, 171, 197, 138, 154, 218, 72, 225, 81, 157, 169, 22, 51, 118, 7, 66, 122, 34, 0, 146, 103, 198, 111, 199, 192, 76, 63, 188, 188, 71, 236, 161, 136, 134, 203, 233, 130, 66, 161, 0, 0, 236, 220, 185, 19, 91, 182, 108, 73, 120, 255, 224, 193, 131, 120, 244, 209, 71, 197, 30, 166, 164, 73, 85, 128, 148, 12, 17, 0, 137, 97, 241, 78, 96, 120, 246, 52, 252, 21, 158, 242, 235, 245, 122, 161, 84, 42, 1, 0, 87, 95, 125, 245, 34, 1, 80, 171, 213, 98, 15, 81, 114, 100, 83, 128, 148, 92, 16, 68, 4, 64, 66, 140, 187, 71, 49, 226, 26, 76, 187, 145, 35, 129, 176, 20, 6, 163, 1, 14, 199, 12, 172, 22, 43, 212, 26, 6, 181, 181, 117, 152, 158, 156, 70, 157, 169, 22, 64, 164, 0, 105, 118, 102, 22, 227, 131, 103, 65, 205, 183, 7, 19, 77, 0, 44, 222, 9, 76, 184, 199, 176, 194, 216, 131, 58, 141, 73, 236, 107, 39, 58, 103, 93, 35, 56, 231, 26, 38, 137, 62, 243, 232, 116, 58, 4, 131, 65, 177, 135, 33, 43, 178, 41, 64, 82, 53, 48, 216, 220, 189, 60, 246, 111, 81, 4, 96, 198, 111, 143, 185, 185, 31, 78, 59, 209, 92, 213, 134, 53, 181, 235, 69, 190, 124, 226, 49, 228, 60, 141, 113, 247, 57, 98, 252, 113, 24, 140, 134, 148, 77, 44, 9, 185, 145, 92, 128, 100, 80, 27, 19, 222, 23, 69, 0, 6, 102, 250, 99, 115, 92, 62, 196, 99, 220, 125, 30, 78, 191, 3, 203, 140, 43, 208, 162, 107, 19, 249, 146, 149, 248, 90, 56, 78, 192, 234, 33, 41, 190, 201, 80, 148, 2, 52, 77, 102, 168, 197, 166, 228, 169, 192, 135, 44, 251, 83, 70, 183, 221, 1, 23, 78, 205, 156, 192, 9, 123, 159, 216, 215, 164, 100, 12, 56, 78, 192, 60, 55, 86, 144, 241, 75, 177, 251, 44, 65, 62, 148, 84, 98, 15, 219, 14, 44, 185, 174, 205, 135, 120, 88, 60, 19, 112, 176, 51, 104, 215, 118, 96, 121, 109, 143, 216, 215, 167, 104, 156, 176, 247, 193, 230, 181, 32, 20, 46, 172, 191, 127, 166, 228, 143, 82, 183, 157, 34, 136, 207, 82, 5, 72, 20, 40, 88, 92, 67, 177, 215, 75, 230, 1, 252, 201, 118, 8, 78, 54, 187, 116, 86, 235, 148, 21, 125, 230, 15, 208, 55, 117, 164, 84, 195, 43, 41, 125, 83, 71, 96, 241, 76, 20, 108, 252, 128, 124, 186, 207, 18, 164, 129, 46, 204, 193, 225, 181, 196, 254, 148, 68, 0, 250, 166, 142, 192, 193, 218, 179, 62, 222, 235, 245, 66, 87, 85, 141, 41, 223, 36, 222, 25, 127, 11, 3, 142, 19, 162, 93, 48, 161, 57, 106, 61, 140, 41, 223, 100, 225, 39, 74, 131, 84, 55, 230, 32, 72, 147, 162, 11, 192, 9, 123, 95, 65, 55, 60, 203, 251, 113, 116, 232, 79, 56, 100, 217, 143, 25, 127, 246, 34, 34, 69, 14, 219, 14, 8, 94, 209, 71, 54, 230, 32, 20, 66, 81, 5, 96, 192, 113, 2, 22, 207, 68, 206, 159, 75, 190, 169, 25, 53, 3, 203, 204, 4, 14, 141, 238, 151, 173, 55, 112, 200, 178, 63, 235, 41, 80, 46, 200, 165, 251, 44, 65, 154, 20, 45, 8, 24, 141, 112, 231, 67, 186, 140, 38, 141, 73, 139, 113, 247, 121, 204, 248, 166, 177, 162, 166, 71, 22, 75, 134, 197, 46, 234, 145, 75, 247, 89, 130, 52, 41, 138, 0, 12, 57, 79, 195, 60, 55, 150, 119, 144, 43, 155, 155, 122, 204, 54, 138, 245, 157, 189, 88, 111, 218, 88, 250, 171, 150, 37, 98, 21, 245, 136, 221, 125, 182, 156, 200, 84, 99, 223, 216, 216, 32, 250, 238, 62, 133, 32, 248, 20, 224, 172, 107, 4, 227, 238, 115, 130, 68, 184, 227, 137, 223, 247, 190, 181, 173, 5, 38, 147, 9, 22, 207, 4, 222, 51, 191, 131, 113, 247, 104, 129, 103, 23, 30, 139, 119, 2, 39, 103, 62, 172, 232, 138, 190, 114, 64, 46, 155, 124, 230, 139, 160, 2, 48, 238, 30, 45, 121, 62, 187, 151, 247, 224, 244, 236, 73, 73, 45, 25, 142, 187, 71, 49, 232, 24, 168, 248, 138, 190, 114, 160, 220, 151, 89, 5, 19, 128, 41, 159, 13, 35, 174, 65, 81, 82, 90, 67, 225, 16, 166, 124, 147, 216, 111, 222, 131, 179, 174, 145, 146, 255, 254, 120, 206, 186, 70, 48, 228, 28, 40, 121, 69, 159, 148, 186, 207, 150, 51, 229, 182, 204, 42, 136, 0, 204, 248, 237, 56, 229, 56, 41, 248, 77, 159, 235, 77, 237, 231, 125, 56, 227, 28, 196, 159, 108, 135, 4, 190, 76, 217, 49, 228, 60, 77, 42, 250, 202, 140, 114, 95, 102, 21, 68, 0, 226, 139, 123, 196, 38, 20, 14, 193, 193, 218, 177, 111, 226, 45, 12, 205, 158, 42, 217, 239, 29, 112, 156, 192, 168, 235, 12, 49, 254, 50, 163, 220, 151, 89, 11, 14, 95, 166, 43, 238, 17, 27, 46, 200, 226, 156, 107, 4, 124, 152, 47, 122, 169, 113, 223, 212, 17, 156, 60, 123, 18, 140, 154, 41, 187, 40, 113, 165, 83, 238, 203, 172, 5, 121, 0, 153, 138, 123, 42, 129, 163, 214, 195, 56, 59, 57, 130, 250, 198, 122, 0, 229, 23, 37, 38, 44, 38, 121, 69, 74, 206, 177, 150, 188, 5, 32, 151, 226, 30, 49, 169, 215, 52, 20, 237, 220, 125, 83, 71, 112, 242, 252, 9, 232, 170, 170, 203, 54, 74, 76, 40, 111, 242, 18, 128, 92, 139, 123, 196, 130, 166, 104, 52, 104, 155, 138, 114, 238, 104, 81, 79, 40, 148, 152, 239, 80, 110, 81, 98, 66, 121, 147, 151, 0, 84, 49, 6, 168, 40, 233, 119, 101, 165, 20, 202, 162, 156, 55, 190, 168, 167, 220, 163, 196, 132, 8, 197, 94, 102, 21, 171, 177, 75, 94, 2, 208, 99, 92, 141, 222, 250, 77, 208, 209, 85, 146, 238, 58, 67, 43, 132, 15, 190, 37, 23, 245, 148, 123, 148, 56, 27, 146, 111, 210, 116, 55, 51, 33, 61, 98, 101, 28, 230, 29, 3, 168, 211, 152, 240, 241, 214, 171, 177, 97, 121, 47, 66, 156, 8, 87, 44, 11, 84, 74, 70, 176, 115, 185, 56, 103, 202, 109, 186, 162, 81, 98, 0, 48, 153, 76, 160, 40, 69, 44, 74, 108, 158, 176, 96, 114, 114, 10, 124, 80, 154, 2, 41, 20, 201, 55, 105, 186, 155, 153, 144, 30, 177, 50, 14, 11, 206, 3, 184, 176, 249, 18, 172, 109, 91, 15, 29, 93, 85, 218, 43, 150, 5, 12, 165, 18, 228, 60, 51, 126, 59, 250, 167, 143, 101, 189, 226, 81, 78, 81, 226, 108, 72, 190, 73, 211, 221, 204, 132, 236, 41, 85, 44, 73, 144, 68, 160, 229, 134, 149, 248, 120, 235, 213, 104, 208, 54, 130, 82, 148, 188, 207, 104, 90, 170, 4, 232, 133, 71, 138, 122, 150, 38, 155, 155, 148, 162, 164, 115, 79, 72, 21, 177, 98, 73, 130, 254, 207, 108, 108, 216, 140, 78, 195, 10, 201, 4, 8, 77, 2, 44, 1, 218, 125, 83, 146, 201, 114, 148, 34, 169, 110, 210, 84, 55, 51, 97, 105, 196, 138, 37, 9, 46, 205, 209, 0, 161, 94, 101, 40, 254, 85, 91, 2, 154, 162, 5, 217, 113, 200, 207, 103, 119, 209, 43, 181, 24, 39, 213, 77, 154, 234, 102, 38, 44, 141, 88, 177, 164, 162, 228, 168, 214, 105, 76, 216, 210, 114, 197, 124, 63, 64, 91, 73, 243, 227, 163, 13, 28, 148, 20, 13, 180, 23, 126, 62, 62, 20, 40, 217, 216, 229, 72, 170, 180, 216, 84, 233, 179, 132, 220, 41, 69, 99, 151, 162, 38, 169, 175, 55, 109, 196, 184, 123, 20, 231, 221, 103, 74, 54, 135, 142, 70, 160, 91, 27, 90, 5, 57, 95, 32, 76, 4, 96, 41, 72, 247, 33, 121, 83, 244, 232, 76, 187, 190, 179, 164, 1, 194, 104, 4, 90, 168, 37, 192, 32, 89, 195, 38, 148, 49, 37, 11, 207, 150, 58, 64, 168, 163, 117, 130, 156, 39, 72, 60, 0, 66, 9, 41, 117, 44, 169, 164, 235, 51, 165, 8, 16, 70, 35, 208, 106, 101, 225, 145, 103, 139, 87, 152, 221, 123, 42, 1, 177, 130, 160, 28, 199, 145, 61, 17, 11, 160, 228, 11, 180, 209, 0, 97, 75, 85, 27, 104, 74, 248, 16, 68, 52, 2, 45, 196, 18, 224, 172, 12, 170, 29, 43, 29, 134, 137, 36, 123, 145, 50, 236, 252, 16, 45, 67, 99, 189, 105, 35, 122, 140, 107, 4, 207, 32, 164, 40, 5, 154, 26, 154, 4, 89, 2, 100, 201, 250, 191, 12, 32, 101, 216, 133, 32, 106, 138, 86, 177, 2, 132, 180, 64, 41, 192, 28, 89, 2, 148, 13, 164, 12, 59, 63, 36, 145, 163, 41, 116, 128, 80, 165, 16, 70, 0, 66, 161, 160, 152, 151, 37, 143, 241, 138, 83, 82, 42, 46, 164, 12, 187, 16, 36, 33, 0, 128, 176, 1, 66, 161, 60, 0, 185, 229, 0, 148, 251, 38, 22, 169, 224, 184, 64, 197, 150, 97, 11, 129, 100, 4, 0, 16, 46, 64, 168, 161, 133, 201, 61, 151, 91, 22, 160, 216, 155, 88, 240, 1, 30, 167, 250, 134, 75, 254, 189, 133, 74, 157, 61, 117, 108, 168, 76, 189, 164, 244, 72, 74, 0, 162, 20, 26, 32, 84, 211, 90, 65, 198, 33, 247, 22, 223, 165, 156, 23, 7, 184, 0, 190, 115, 223, 15, 241, 248, 157, 223, 195, 177, 247, 250, 69, 249, 190, 133, 148, 97, 31, 123, 175, 31, 143, 111, 255, 62, 190, 115, 223, 15, 43, 74, 4, 36, 41, 0, 64, 97, 1, 194, 38, 109, 115, 193, 191, 95, 236, 29, 134, 242, 65, 172, 146, 210, 0, 23, 192, 83, 247, 62, 139, 15, 15, 158, 4, 31, 224, 241, 204, 95, 63, 39, 154, 8, 228, 195, 177, 247, 250, 241, 204, 95, 63, 7, 62, 192, 227, 195, 131, 39, 241, 157, 251, 126, 136, 0, 39, 47, 239, 47, 95, 36, 43, 0, 81, 114, 13, 16, 210, 20, 13, 3, 99, 44, 248, 247, 122, 2, 242, 235, 98, 35, 70, 73, 41, 235, 231, 240, 212, 189, 207, 226, 228, 145, 83, 88, 182, 108, 25, 190, 254, 245, 175, 203, 74, 4, 226, 141, 223, 100, 50, 129, 97, 24, 124, 120, 240, 36, 158, 186, 247, 217, 138, 16, 1, 201, 11, 0, 144, 91, 128, 80, 168, 0, 160, 220, 230, 255, 64, 233, 75, 74, 89, 63, 135, 39, 239, 222, 133, 147, 71, 78, 161, 189, 189, 29, 239, 190, 251, 46, 118, 237, 218, 133, 103, 159, 125, 54, 38, 2, 253, 135, 7, 138, 250, 157, 25, 102, 113, 205, 71, 182, 174, 127, 188, 241, 223, 124, 243, 205, 176, 217, 108, 216, 183, 111, 31, 170, 170, 170, 112, 242, 200, 41, 60, 117, 239, 179, 96, 253, 18, 237, 119, 39, 16, 178, 16, 0, 32, 251, 0, 161, 80, 75, 128, 229, 146, 3, 80, 172, 246, 100, 62, 175, 31, 79, 222, 189, 11, 167, 251, 134, 209, 222, 222, 142, 253, 251, 247, 163, 189, 61, 82, 127, 253, 192, 3, 15, 196, 68, 224, 233, 175, 254, 160, 232, 34, 144, 15, 241, 198, 127, 235, 173, 183, 226, 213, 87, 95, 133, 82, 169, 196, 150, 45, 91, 176, 111, 223, 62, 24, 12, 6, 156, 60, 114, 10, 79, 222, 189, 171, 172, 69, 64, 54, 2, 16, 37, 83, 128, 80, 176, 37, 192, 96, 249, 254, 167, 23, 138, 207, 235, 199, 19, 219, 191, 159, 96, 252, 93, 93, 93, 9, 199, 68, 69, 32, 192, 5, 36, 39, 2, 253, 135, 7, 18, 140, 255, 165, 151, 94, 130, 82, 185, 208, 66, 254, 162, 139, 46, 194, 254, 253, 251, 81, 83, 83, 131, 211, 125, 195, 120, 242, 238, 93, 240, 121, 203, 115, 73, 81, 118, 2, 0, 44, 29, 32, 20, 108, 9, 48, 92, 57, 145, 224, 92, 57, 221, 55, 140, 225, 19, 103, 1, 0, 59, 118, 236, 88, 100, 252, 81, 164, 40, 2, 253, 135, 7, 240, 244, 87, 127, 144, 96, 252, 10, 133, 98, 209, 113, 189, 189, 189, 120, 244, 209, 71, 99, 223, 119, 232, 248, 25, 177, 135, 94, 20, 100, 41, 0, 81, 162, 1, 66, 70, 185, 16, 32, 20, 162, 17, 40, 0, 132, 194, 242, 202, 2, 140, 167, 216, 149, 121, 27, 47, 91, 143, 135, 118, 221, 11, 74, 73, 225, 129, 7, 30, 192, 27, 111, 188, 145, 246, 88, 41, 137, 64, 212, 248, 3, 92, 96, 73, 227, 7, 128, 215, 94, 123, 13, 143, 62, 250, 40, 40, 37, 133, 135, 118, 221, 139, 222, 45, 107, 83, 30, 39, 247, 61, 17, 100, 45, 0, 64, 36, 64, 184, 193, 20, 9, 16, 82, 10, 10, 203, 13, 43, 11, 62, 167, 139, 115, 22, 156, 3, 32, 247, 27, 35, 19, 91, 175, 187, 24, 127, 243, 221, 191, 66, 40, 28, 194, 77, 55, 221, 36, 121, 17, 200, 213, 248, 111, 185, 229, 22, 132, 194, 33, 252, 205, 119, 255, 10, 91, 175, 187, 56, 237, 121, 229, 190, 39, 130, 236, 5, 0, 88, 8, 16, 118, 234, 151, 11, 114, 62, 155, 207, 154, 242, 117, 69, 128, 135, 225, 131, 97, 180, 253, 234, 109, 172, 248, 238, 43, 184, 224, 161, 95, 96, 245, 55, 127, 137, 21, 207, 188, 130, 182, 95, 189, 13, 195, 7, 195, 80, 204, 27, 188, 220, 111, 140, 108, 136, 138, 64, 48, 24, 196, 77, 55, 221, 132, 215, 94, 123, 45, 237, 177, 15, 60, 240, 0, 118, 238, 220, 41, 138, 8, 196, 27, 255, 237, 183, 223, 158, 149, 241, 135, 17, 206, 104, 252, 128, 252, 247, 68, 40, 11, 1, 136, 210, 83, 115, 129, 32, 231, 73, 85, 6, 92, 253, 209, 40, 86, 62, 253, 47, 104, 255, 167, 63, 192, 120, 100, 16, 154, 9, 59, 40, 142, 135, 210, 23, 128, 198, 108, 135, 241, 200, 32, 218, 255, 233, 15, 88, 249, 244, 191, 160, 250, 163, 81, 217, 223, 24, 217, 178, 245, 186, 139, 113, 255, 211, 119, 34, 24, 12, 226, 150, 91, 110, 89, 82, 4, 118, 236, 216, 145, 32, 2, 3, 199, 6, 139, 62, 190, 100, 227, 127, 225, 133, 23, 4, 51, 254, 114, 216, 19, 65, 218, 163, 19, 137, 228, 86, 224, 245, 111, 126, 128, 206, 159, 190, 1, 102, 102, 46, 227, 103, 153, 153, 57, 116, 254, 244, 13, 44, 63, 120, 74, 214, 55, 70, 46, 92, 121, 227, 86, 220, 255, 244, 157, 8, 133, 66, 57, 137, 192, 83, 247, 60, 91, 84, 17, 200, 197, 248, 95, 126, 249, 229, 156, 140, 31, 40, 143, 61, 17, 202, 231, 46, 20, 144, 248, 36, 160, 250, 55, 63, 64, 227, 27, 135, 115, 62, 199, 178, 189, 39, 80, 255, 230, 7, 178, 189, 49, 114, 37, 31, 17, 96, 125, 108, 209, 68, 96, 224, 216, 96, 78, 198, 127, 219, 109, 183, 229, 100, 252, 64, 121, 236, 137, 64, 191, 49, 240, 107, 180, 212, 182, 160, 86, 103, 18, 36, 128, 38, 22, 238, 57, 15, 94, 248, 231, 215, 240, 238, 193, 163, 176, 216, 166, 10, 63, 33, 128, 13, 138, 0, 30, 81, 185, 129, 52, 55, 78, 38, 26, 223, 56, 140, 185, 118, 19, 70, 77, 85, 168, 111, 172, 135, 195, 49, 3, 171, 197, 10, 181, 134, 65, 109, 109, 157, 216, 151, 76, 112, 174, 188, 113, 43, 0, 224, 71, 143, 253, 18, 183, 220, 114, 11, 94, 124, 241, 69, 220, 122, 235, 173, 41, 143, 221, 177, 99, 7, 0, 224, 145, 71, 30, 193, 83, 247, 60, 139, 191, 253, 201, 131, 88, 179, 105, 149, 32, 227, 24, 56, 54, 136, 167, 238, 137, 4, 29, 183, 111, 223, 142, 159, 253, 236, 103, 25, 141, 95, 65, 41, 114, 50, 126, 64, 62, 123, 34, 68, 247, 202, 96, 253, 28, 140, 29, 106, 84, 211, 11, 217, 147, 148, 219, 239, 196, 121, 251, 25, 12, 207, 158, 194, 222, 241, 55, 241, 158, 249, 29, 28, 181, 30, 198, 144, 243, 180, 216, 227, 206, 137, 127, 124, 241, 63, 240, 234, 235, 111, 46, 105, 252, 52, 157, 125, 137, 49, 131, 48, 238, 86, 121, 210, 222, 56, 217, 210, 250, 210, 59, 8, 249, 217, 148, 105, 186, 229, 72, 188, 39, 112, 219, 109, 183, 225, 229, 151, 95, 78, 123, 108, 49, 60, 129, 168, 241, 179, 62, 22, 219, 183, 111, 199, 207, 127, 254, 243, 140, 198, 15, 32, 103, 227, 7, 228, 179, 9, 108, 114, 0, 58, 30, 218, 104, 48, 98, 198, 238, 64, 181, 94, 15, 62, 196, 131, 15, 241, 240, 194, 3, 59, 55, 133, 113, 247, 57, 208, 148, 10, 90, 165, 14, 85, 76, 53, 214, 212, 174, 23, 251, 187, 164, 229, 205, 183, 222, 77, 255, 166, 174, 26, 45, 219, 191, 129, 150, 143, 93, 136, 41, 127, 16, 142, 131, 123, 225, 126, 233, 121, 40, 150, 200, 139, 191, 68, 193, 193, 164, 8, 167, 125, 127, 142, 15, 194, 50, 95, 44, 210, 194, 168, 80, 77, 43, 83, 30, 167, 113, 251, 176, 122, 210, 11, 103, 151, 216, 87, 168, 116, 196, 123, 2, 81, 3, 91, 202, 19, 96, 89, 22, 79, 60, 241, 68, 193, 158, 64, 42, 227, 79, 199, 238, 221, 187, 113, 199, 29, 119, 0, 0, 238, 127, 250, 206, 156, 141, 95, 78, 120, 189, 94, 52, 183, 52, 131, 162, 20, 9, 79, 127, 0, 160, 227, 3, 85, 180, 42, 241, 9, 25, 21, 4, 63, 239, 131, 131, 181, 195, 60, 55, 6, 70, 169, 134, 90, 169, 129, 158, 49, 160, 73, 219, 34, 72, 243, 77, 33, 112, 123, 210, 68, 213, 181, 85, 104, 186, 243, 27, 168, 89, 191, 9, 238, 64, 16, 246, 183, 126, 13, 239, 235, 47, 64, 145, 97, 29, 126, 147, 50, 125, 45, 192, 144, 215, 143, 253, 206, 196, 157, 142, 174, 48, 86, 161, 71, 151, 122, 94, 95, 125, 242, 28, 156, 151, 8, 227, 222, 202, 133, 92, 68, 224, 241, 199, 31, 135, 223, 239, 199, 51, 207, 60, 147, 183, 8, 20, 98, 252, 209, 177, 86, 34, 52, 176, 16, 193, 172, 206, 144, 69, 23, 10, 135, 224, 231, 125, 240, 243, 62, 56, 89, 7, 198, 221, 231, 35, 130, 64, 169, 81, 205, 232, 97, 210, 54, 160, 69, 215, 38, 246, 119, 138, 161, 168, 50, 96, 217, 87, 31, 5, 213, 189, 1, 254, 96, 16, 83, 7, 246, 194, 243, 250, 238, 140, 198, 15, 0, 203, 21, 169, 51, 1, 231, 248, 224, 34, 227, 7, 128, 253, 78, 79, 90, 79, 64, 59, 110, 23, 251, 82, 136, 66, 178, 8, 112, 28, 135, 219, 111, 191, 61, 229, 177, 59, 119, 238, 4, 128, 152, 8, 60, 241, 139, 135, 179, 254, 61, 241, 198, 255, 181, 175, 125, 13, 207, 61, 247, 92, 218, 99, 139, 97, 252, 82, 223, 24, 54, 26, 128, 54, 24, 13, 152, 227, 185, 196, 24, 0, 80, 88, 157, 56, 23, 100, 225, 14, 184, 96, 241, 76, 224, 196, 116, 31, 222, 25, 127, 11, 135, 44, 251, 209, 55, 117, 4, 227, 238, 81, 209, 190, 180, 66, 163, 67, 199, 87, 30, 132, 113, 213, 90, 240, 60, 15, 219, 127, 189, 14, 247, 139, 207, 67, 145, 101, 134, 95, 157, 34, 181, 72, 156, 91, 162, 50, 204, 146, 166, 126, 156, 158, 93, 72, 250, 145, 210, 141, 33, 20, 75, 101, 61, 246, 94, 182, 22, 247, 125, 251, 43, 0, 128, 59, 238, 184, 3, 187, 119, 239, 78, 123, 158, 157, 59, 119, 70, 166, 4, 62, 22, 223, 186, 107, 23, 134, 250, 51, 231, 223, 15, 30, 31, 17, 213, 248, 229, 64, 252, 202, 68, 50, 148, 208, 117, 226, 129, 80, 68, 16, 166, 124, 147, 24, 112, 244, 199, 2, 139, 125, 83, 71, 74, 215, 101, 167, 74, 143, 186, 175, 125, 11, 117, 31, 219, 12, 132, 21, 152, 57, 184, 23, 222, 223, 254, 10, 10, 74, 1, 208, 42, 132, 149, 153, 43, 6, 133, 44, 5, 10, 169, 138, 186, 7, 171, 232, 100, 202, 122, 252, 216, 199, 215, 225, 158, 199, 239, 0, 144, 189, 8, 228, 82, 125, 151, 141, 241, 255, 226, 23, 191, 168, 72, 227, 7, 18, 251, 68, 44, 138, 1, 0, 197, 125, 42, 197, 2, 139, 188, 7, 83, 190, 73, 156, 115, 13, 131, 161, 34, 113, 4, 163, 186, 70, 176, 236, 189, 120, 212, 23, 94, 14, 69, 215, 42, 216, 217, 0, 102, 222, 250, 29, 60, 191, 255, 87, 104, 175, 255, 28, 148, 109, 93, 64, 56, 4, 238, 232, 1, 176, 71, 247, 67, 177, 196, 182, 95, 147, 97, 10, 203, 83, 120, 1, 93, 26, 6, 135, 221, 139, 227, 13, 12, 20, 104, 97, 82, 11, 11, 215, 80, 120, 135, 34, 41, 227, 245, 248, 81, 103, 170, 141, 5, 147, 227, 131, 78, 6, 163, 1, 86, 139, 21, 215, 222, 124, 5, 0, 224, 39, 79, 190, 16, 51, 196, 108, 166, 3, 217, 144, 141, 241, 223, 117, 215, 93, 80, 40, 20, 21, 103, 252, 153, 40, 249, 163, 41, 94, 16, 28, 172, 29, 163, 238, 179, 9, 129, 197, 182, 170, 142, 130, 91, 122, 113, 71, 246, 33, 112, 225, 229, 240, 217, 38, 48, 247, 155, 221, 80, 223, 248, 37, 168, 55, 109, 5, 20, 0, 123, 244, 61, 176, 199, 150, 54, 126, 0, 24, 11, 41, 177, 156, 90, 124, 76, 53, 173, 196, 21, 198, 170, 69, 113, 128, 75, 141, 186, 180, 43, 1, 190, 174, 194, 123, 20, 74, 149, 92, 210, 97, 139, 33, 2, 196, 248, 11, 67, 116, 223, 52, 57, 176, 104, 158, 27, 139, 44, 61, 210, 58, 232, 104, 29, 154, 116, 45, 104, 208, 54, 229, 116, 206, 176, 223, 11, 231, 79, 191, 5, 168, 212, 96, 62, 243, 151, 160, 47, 222, 6, 62, 200, 130, 63, 250, 14, 216, 255, 122, 37, 171, 32, 224, 31, 130, 106, 92, 73, 167, 158, 211, 247, 232, 52, 104, 97, 84, 89, 45, 3, 2, 128, 227, 202, 117, 98, 95, 230, 162, 177, 84, 58, 172, 193, 104, 88, 148, 245, 152, 44, 2, 44, 27, 137, 218, 167, 34, 42, 2, 233, 120, 232, 161, 135, 240, 253, 239, 127, 63, 237, 251, 196, 248, 51, 67, 75, 45, 40, 21, 10, 135, 192, 5, 89, 112, 65, 22, 78, 214, 1, 139, 103, 34, 97, 165, 161, 134, 169, 67, 187, 190, 51, 243, 137, 88, 22, 225, 48, 64, 85, 233, 17, 6, 48, 113, 98, 20, 179, 239, 157, 70, 135, 207, 151, 85, 254, 243, 48, 84, 24, 8, 41, 177, 134, 74, 189, 26, 80, 77, 43, 209, 179, 132, 209, 71, 241, 244, 180, 130, 107, 168, 17, 251, 178, 22, 141, 168, 251, 15, 68, 130, 201, 51, 118, 71, 198, 172, 199, 120, 17, 184, 235, 174, 187, 0, 96, 73, 17, 240, 249, 22, 23, 103, 109, 218, 180, 9, 91, 183, 166, 55, 232, 231, 159, 127, 30, 247, 221, 119, 31, 49, 254, 56, 82, 217, 186, 232, 30, 64, 54, 68, 5, 33, 186, 218, 48, 236, 28, 132, 70, 169, 134, 134, 214, 192, 168, 174, 75, 155, 194, 172, 224, 88, 176, 175, 60, 15, 254, 234, 219, 48, 49, 228, 65, 64, 213, 12, 218, 176, 1, 45, 174, 126, 80, 8, 103, 252, 189, 154, 155, 84, 192, 27, 65, 32, 207, 222, 32, 97, 37, 5, 243, 231, 175, 18, 251, 242, 21, 149, 124, 211, 97, 115, 17, 1, 173, 118, 241, 62, 15, 75, 213, 82, 16, 227, 207, 30, 89, 8, 64, 50, 129, 16, 155, 176, 218, 112, 206, 181, 196, 110, 52, 156, 31, 138, 63, 236, 134, 174, 230, 34, 184, 116, 29, 176, 87, 119, 131, 87, 170, 209, 225, 248, 0, 20, 210, 79, 5, 158, 252, 10, 135, 79, 93, 14, 216, 41, 26, 83, 175, 231, 183, 38, 48, 245, 169, 139, 17, 104, 172, 17, 251, 114, 21, 149, 232, 83, 197, 60, 97, 201, 57, 152, 156, 139, 8, 100, 75, 188, 241, 255, 229, 35, 183, 226, 178, 235, 55, 139, 125, 137, 36, 141, 44, 5, 32, 153, 76, 221, 123, 148, 97, 30, 93, 142, 247, 1, 199, 251, 89, 157, 239, 201, 175, 112, 248, 179, 203, 35, 143, 125, 211, 13, 74, 120, 6, 130, 240, 158, 206, 236, 49, 196, 227, 233, 105, 197, 244, 117, 23, 138, 125, 105, 36, 79, 178, 8, 176, 108, 100, 73, 47, 31, 226, 141, 255, 158, 199, 239, 192, 165, 159, 216, 152, 85, 130, 91, 37, 67, 202, 129, 147, 136, 55, 126, 0, 128, 66, 129, 182, 237, 12, 168, 26, 38, 235, 115, 132, 141, 70, 140, 127, 249, 134, 188, 171, 8, 43, 141, 107, 111, 190, 34, 150, 39, 112, 223, 125, 247, 225, 249, 231, 159, 207, 249, 28, 187, 118, 237, 138, 25, 255, 221, 255, 235, 118, 92, 123, 243, 21, 100, 131, 208, 44, 32, 2, 16, 199, 34, 227, 159, 199, 161, 217, 12, 207, 23, 238, 201, 250, 60, 190, 47, 126, 25, 193, 234, 242, 170, 247, 207, 68, 161, 233, 176, 215, 222, 124, 5, 238, 122, 236, 75, 0, 114, 23, 129, 93, 187, 118, 225, 225, 135, 31, 134, 66, 161, 192, 23, 254, 250, 102, 172, 219, 186, 170, 40, 27, 161, 148, 35, 68, 0, 230, 73, 103, 252, 211, 252, 197, 56, 199, 222, 138, 96, 119, 15, 184, 109, 215, 102, 60, 15, 119, 245, 53, 8, 173, 236, 46, 201, 152, 211, 53, 26, 77, 78, 205, 149, 11, 55, 124, 110, 91, 206, 34, 16, 111, 252, 247, 60, 126, 7, 62, 251, 229, 79, 203, 162, 68, 87, 42, 16, 1, 64, 102, 227, 143, 194, 221, 240, 105, 132, 140, 53, 105, 207, 19, 170, 51, 129, 251, 228, 103, 74, 54, 238, 116, 141, 70, 147, 83, 115, 229, 68, 178, 8, 236, 218, 181, 43, 237, 177, 201, 198, 31, 141, 39, 16, 178, 167, 226, 5, 32, 157, 241, 115, 218, 107, 224, 53, 62, 144, 248, 162, 74, 5, 246, 150, 91, 211, 158, 139, 253, 252, 109, 128, 74, 152, 157, 137, 178, 33, 93, 163, 209, 228, 134, 164, 114, 35, 94, 4, 30, 126, 248, 225, 148, 34, 240, 228, 147, 79, 38, 184, 253, 196, 248, 243, 163, 162, 5, 32, 147, 241, 27, 244, 6, 52, 54, 38, 166, 241, 6, 215, 172, 71, 112, 213, 234, 69, 159, 9, 174, 90, 141, 96, 247, 106, 136, 9, 69, 81, 89, 165, 230, 202, 129, 100, 17, 248, 193, 15, 126, 16, 123, 239, 153, 103, 158, 193, 19, 79, 60, 145, 224, 246, 39, 67, 92, 255, 236, 40, 139, 101, 192, 124, 200, 246, 201, 111, 208, 71, 154, 58, 78, 78, 46, 148, 82, 114, 87, 108, 131, 118, 48, 177, 101, 26, 119, 229, 182, 146, 127, 135, 84, 41, 183, 169, 82, 115, 229, 186, 12, 118, 195, 231, 34, 215, 244, 23, 223, 121, 17, 15, 62, 248, 32, 212, 106, 53, 88, 150, 197, 35, 143, 60, 146, 16, 237, 39, 100, 134, 166, 104, 52, 209, 139, 19, 170, 42, 82, 0, 210, 25, 255, 107, 254, 110, 120, 212, 159, 69, 242, 243, 36, 89, 4, 130, 107, 214, 33, 100, 170, 7, 101, 159, 6, 16, 153, 251, 7, 47, 40, 125, 190, 191, 193, 104, 88, 148, 114, 59, 61, 57, 189, 40, 53, 183, 90, 47, 79, 1, 0, 34, 34, 160, 173, 210, 224, 71, 143, 253, 18, 247, 222, 123, 47, 128, 72, 166, 225, 125, 223, 38, 25, 126, 217, 98, 164, 212, 139, 202, 128, 163, 84, 156, 0, 44, 101, 252, 223, 116, 95, 5, 184, 143, 2, 0, 62, 93, 219, 145, 240, 126, 130, 8, 40, 20, 8, 108, 190, 20, 234, 223, 71, 182, 195, 10, 92, 178, 69, 148, 53, 255, 84, 41, 183, 169, 82, 115, 229, 206, 149, 55, 110, 5, 173, 162, 241, 119, 15, 255, 4, 0, 240, 240, 223, 127, 13, 151, 108, 219, 36, 246, 176, 100, 65, 27, 179, 180, 248, 87, 148, 0, 100, 52, 254, 121, 190, 53, 158, 89, 4, 66, 221, 61, 177, 215, 67, 43, 165, 211, 239, 175, 144, 212, 92, 41, 115, 217, 245, 155, 161, 98, 104, 168, 181, 106, 244, 94, 186, 182, 240, 19, 150, 57, 26, 138, 134, 41, 133, 203, 159, 76, 197, 8, 64, 182, 198, 31, 37, 163, 8, 4, 131, 192, 124, 155, 241, 224, 178, 46, 177, 191, 94, 69, 176, 249, 106, 242, 212, 207, 134, 165, 92, 254, 100, 42, 66, 0, 114, 53, 254, 40, 153, 68, 192, 181, 124, 190, 10, 81, 153, 185, 44, 152, 64, 200, 23, 62, 16, 153, 214, 53, 54, 54, 128, 86, 209, 9, 27, 125, 232, 116, 58, 24, 141, 122, 40, 230, 155, 174, 100, 114, 249, 147, 41, 251, 101, 192, 124, 141, 63, 202, 183, 198, 143, 226, 119, 142, 177, 69, 175, 27, 244, 6, 232, 46, 189, 12, 225, 246, 44, 122, 19, 20, 25, 169, 119, 165, 37, 20, 70, 54, 59, 77, 27, 41, 117, 206, 198, 15, 228, 233, 1, 164, 83, 160, 100, 165, 18, 155, 66, 141, 63, 74, 58, 79, 160, 238, 47, 190, 0, 218, 237, 74, 88, 34, 44, 71, 254, 245, 39, 191, 22, 123, 8, 146, 228, 243, 247, 252, 247, 146, 252, 158, 76, 61, 23, 89, 199, 92, 214, 46, 127, 50, 121, 89, 233, 130, 2, 213, 193, 229, 116, 193, 233, 116, 163, 166, 214, 152, 160, 84, 98, 175, 61, 11, 101, 252, 81, 178, 9, 12, 150, 43, 175, 252, 148, 8, 64, 42, 62, 123, 231, 141, 69, 127, 224, 101, 74, 236, 234, 208, 24, 128, 150, 252, 55, 32, 205, 107, 212, 169, 186, 190, 214, 212, 26, 23, 41, 149, 88, 8, 109, 252, 81, 42, 89, 4, 128, 200, 14, 62, 132, 72, 26, 50, 128, 146, 60, 240, 210, 245, 92, 12, 186, 253, 232, 104, 104, 44, 248, 252, 130, 200, 86, 186, 20, 84, 49, 166, 1, 197, 50, 254, 40, 149, 44, 2, 79, 60, 241, 132, 216, 67, 144, 4, 81, 1, 40, 197, 3, 47, 85, 207, 197, 158, 214, 246, 188, 93, 254, 100, 242, 178, 80, 169, 166, 160, 22, 219, 248, 163, 84, 178, 8, 16, 22, 40, 197, 3, 47, 62, 177, 139, 86, 40, 209, 94, 215, 32, 152, 241, 3, 121, 174, 2, 196, 111, 53, 20, 12, 241, 145, 74, 52, 143, 31, 26, 77, 36, 241, 64, 140, 78, 44, 165, 50, 254, 40, 75, 173, 14, 24, 141, 181, 37, 253, 238, 4, 113, 72, 126, 224, 21, 131, 104, 79, 3, 131, 90, 139, 77, 93, 43, 209, 96, 200, 127, 190, 159, 138, 188, 36, 75, 138, 41, 168, 215, 94, 180, 184, 193, 103, 177, 140, 63, 74, 58, 79, 32, 28, 38, 173, 192, 42, 129, 248, 7, 94, 49, 167, 1, 109, 140, 30, 104, 45, 206, 185, 5, 243, 89, 196, 78, 65, 221, 127, 174, 25, 159, 92, 99, 137, 253, 187, 216, 198, 31, 37, 149, 8, 124, 56, 212, 135, 112, 147, 56, 49, 16, 66, 233, 40, 246, 3, 47, 219, 116, 222, 66, 40, 155, 59, 212, 187, 242, 147, 216, 51, 240, 91, 172, 107, 158, 193, 1, 170, 19, 223, 228, 74, 215, 143, 255, 91, 227, 71, 209, 74, 169, 209, 30, 160, 240, 209, 217, 147, 240, 25, 205, 128, 31, 162, 47, 133, 10, 141, 130, 52, 57, 77, 160, 181, 173, 165, 104, 15, 188, 92, 210, 121, 11, 161, 108, 4, 0, 0, 102, 87, 126, 6, 239, 205, 255, 252, 140, 54, 243, 246, 95, 249, 18, 159, 8, 21, 45, 195, 29, 60, 253, 58, 166, 77, 181, 160, 155, 104, 104, 2, 242, 47, 195, 37, 136, 71, 62, 25, 125, 249, 82, 144, 0, 84, 106, 10, 170, 20, 99, 32, 197, 228, 185, 255, 247, 116, 44, 239, 35, 20, 10, 99, 210, 54, 137, 250, 122, 19, 156, 46, 39, 76, 38, 19, 236, 118, 59, 140, 6, 99, 217, 79, 121, 138, 61, 189, 213, 64, 13, 19, 83, 252, 167, 126, 60, 229, 253, 63, 86, 66, 196, 142, 129, 20, 19, 169, 46, 251, 150, 154, 98, 62, 240, 244, 42, 3, 12, 138, 220, 54, 159, 17, 130, 178, 47, 6, 34, 20, 142, 20, 151, 125, 203, 137, 54, 70, 47, 138, 241, 3, 50, 244, 0, 114, 41, 141, 36, 8, 67, 165, 77, 121, 74, 69, 169, 2, 125, 75, 33, 59, 75, 201, 166, 52, 178, 84, 84, 106, 12, 36, 250, 61, 201, 6, 28, 249, 19, 53, 254, 169, 201, 105, 81, 199, 33, 59, 1, 72, 238, 121, 159, 174, 55, 62, 129, 32, 69, 52, 20, 13, 35, 165, 198, 220, 212, 12, 92, 46, 23, 212, 26, 226, 1, 100, 77, 54, 61, 239, 41, 226, 254, 151, 13, 229, 182, 245, 153, 145, 82, 195, 68, 107, 225, 155, 113, 1, 0, 88, 63, 7, 131, 192, 169, 189, 185, 34, 43, 107, 73, 87, 26, 233, 114, 186, 16, 10, 133, 99, 17, 106, 66, 113, 40, 245, 148, 167, 156, 182, 62, 107, 99, 244, 240, 205, 184, 34, 129, 212, 249, 13, 75, 131, 193, 72, 96, 213, 106, 17, 174, 120, 44, 215, 115, 201, 42, 8, 152, 170, 52, 178, 190, 177, 126, 81, 111, 124, 66, 121, 32, 245, 190, 19, 217, 16, 159, 206, 219, 208, 88, 47, 246, 112, 22, 33, 43, 15, 32, 26, 121, 142, 223, 250, 57, 26, 161, 6, 0, 147, 201, 4, 138, 34, 233, 170, 229, 138, 220, 182, 62, 139, 186, 252, 165, 32, 57, 152, 152, 109, 112, 81, 86, 30, 64, 57, 39, 219, 16, 22, 35, 231, 4, 164, 82, 166, 243, 2, 128, 90, 195, 192, 229, 138, 196, 22, 114, 9, 46, 202, 202, 3, 32, 84, 22, 114, 76, 64, 202, 183, 59, 111, 193, 215, 202, 96, 0, 235, 231, 0, 228, 22, 92, 148, 149, 7, 64, 168, 44, 228, 150, 128, 36, 86, 98, 79, 114, 224, 47, 26, 92, 4, 16, 11, 160, 166, 67, 150, 2, 80, 201, 9, 56, 149, 142, 20, 167, 129, 233, 118, 222, 45, 21, 153, 140, 124, 201, 177, 139, 54, 106, 2, 161, 12, 144, 66, 58, 111, 33, 16, 1, 32, 16, 242, 68, 140, 185, 190, 208, 16, 1, 32, 72, 30, 169, 77, 249, 74, 209, 170, 171, 84, 144, 85, 0, 153, 144, 156, 254, 154, 46, 77, 150, 80, 92, 74, 185, 182, 95, 10, 136, 0, 200, 4, 41, 85, 65, 86, 42, 26, 138, 150, 245, 124, 63, 21, 68, 0, 100, 2, 169, 130, 20, 31, 127, 136, 199, 4, 231, 134, 157, 151, 79, 13, 66, 38, 72, 12, 64, 6, 144, 42, 72, 105, 17, 21, 2, 64, 254, 241, 0, 34, 0, 50, 96, 169, 42, 200, 248, 52, 89, 66, 233, 137, 23, 3, 154, 162, 81, 5, 165, 172, 166, 9, 228, 177, 33, 3, 82, 165, 191, 166, 74, 147, 37, 136, 11, 31, 226, 225, 12, 177, 152, 224, 220, 152, 224, 220, 152, 227, 57, 184, 61, 156, 216, 195, 90, 18, 193, 61, 128, 116, 61, 250, 146, 123, 249, 17, 178, 39, 85, 250, 107, 170, 52, 89, 130, 180, 112, 134, 88, 152, 39, 45, 104, 110, 105, 70, 45, 173, 65, 53, 205, 96, 142, 231, 36, 229, 33, 8, 238, 1, 148, 83, 19, 7, 169, 64, 250, 239, 201, 31, 103, 136, 197, 152, 223, 133, 211, 19, 163, 176, 115, 28, 166, 230, 43, 247, 196, 70, 112, 1, 72, 23, 157, 78, 142, 98, 151, 3, 229, 214, 178, 138, 32, 60, 169, 58, 86, 77, 187, 167, 49, 19, 152, 195, 4, 231, 198, 148, 203, 37, 234, 52, 161, 232, 49, 0, 185, 53, 113, 200, 5, 226, 237, 16, 50, 145, 169, 164, 217, 171, 12, 98, 216, 62, 30, 139, 25, 148, 26, 193, 5, 32, 149, 226, 165, 138, 98, 151, 3, 165, 246, 118, 164, 150, 18, 75, 200, 76, 170, 142, 85, 169, 58, 91, 1, 88, 20, 64, 44, 201, 248, 132, 62, 161, 28, 155, 56, 8, 69, 57, 123, 59, 4, 225, 200, 38, 166, 147, 73, 12, 132, 106, 36, 42, 120, 56, 94, 110, 77, 28, 10, 65, 206, 45, 171, 42, 25, 185, 237, 46, 229, 12, 177, 112, 114, 44, 128, 133, 196, 35, 93, 149, 78, 144, 115, 151, 228, 91, 150, 107, 20, 187, 146, 189, 29, 57, 35, 231, 186, 138, 104, 226, 145, 91, 163, 128, 157, 247, 21, 60, 85, 144, 142, 204, 201, 144, 92, 230, 119, 149, 140, 212, 42, 25, 197, 168, 171, 40, 70, 252, 198, 31, 151, 120, 100, 203, 83, 12, 72, 70, 142, 192, 72, 177, 101, 149, 216, 196, 63, 113, 171, 85, 250, 184, 39, 110, 29, 92, 78, 23, 156, 78, 55, 106, 106, 141, 37, 25, 75, 185, 214, 85, 240, 33, 30, 78, 240, 177, 169, 66, 192, 233, 67, 87, 67, 99, 198, 207, 21, 237, 155, 146, 136, 53, 33, 138, 148, 42, 25, 43, 101, 119, 169, 169, 185, 89, 28, 57, 59, 136, 35, 103, 7, 49, 108, 49, 167, 77, 60, 146, 159, 212, 17, 100, 133, 212, 158, 184, 149, 82, 87, 17, 31, 107, 211, 154, 244, 224, 52, 10, 216, 82, 148, 49, 147, 41, 128, 0, 16, 111, 39, 61, 82, 171, 100, 172, 228, 186, 10, 62, 196, 199, 226, 4, 209, 122, 4, 226, 1, 16, 138, 138, 212, 158, 184, 229, 186, 34, 149, 45, 206, 16, 11, 235, 212, 194, 182, 97, 196, 3, 32, 20, 149, 74, 126, 226, 74, 129, 84, 2, 167, 53, 45, 228, 165, 16, 1, 32, 20, 21, 178, 42, 34, 77, 38, 56, 55, 218, 24, 61, 254, 63, 91, 213, 67, 145, 233, 13, 169, 197, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130]; +// Will be replaced automatically by GitHub Actions +final faviconTileBytes = []; diff --git a/tile_server/static/generated/land.dart b/tile_server/static/generated/land.dart index 48881eab..1029ac5f 100644 --- a/tile_server/static/generated/land.dart +++ b/tile_server/static/generated/land.dart @@ -1 +1,2 @@ -final landTileBytes = [137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 1, 0, 0, 0, 1, 0, 8, 3, 0, 0, 0, 107, 172, 88, 84, 0, 0, 2, 253, 80, 76, 84, 69, 51, 52, 49, 72, 52, 55, 60, 65, 58, 76, 85, 24, 157, 17, 17, 83, 81, 48, 162, 28, 28, 73, 74, 72, 84, 75, 67, 78, 83, 73, 92, 100, 43, 84, 85, 71, 85, 87, 84, 169, 46, 46, 110, 80, 76, 125, 90, 55, 104, 112, 57, 89, 110, 87, 129, 96, 61, 102, 109, 86, 113, 94, 97, 130, 88, 87, 134, 105, 70, 104, 105, 102, 94, 131, 93, 104, 115, 103, 121, 128, 77, 139, 112, 78, 140, 101, 88, 141, 89, 99, 113, 117, 107, 182, 78, 78, 142, 116, 84, 117, 117, 116, 149, 107, 95, 146, 121, 90, 151, 150, 59, 145, 110, 113, 117, 141, 112, 128, 134, 111, 138, 144, 96, 148, 150, 83, 172, 121, 91, 130, 132, 125, 154, 135, 104, 176, 113, 111, 129, 154, 119, 177, 153, 75, 136, 137, 133, 147, 153, 107, 150, 171, 86, 141, 169, 102, 157, 140, 115, 212, 117, 89, 153, 166, 102, 157, 149, 115, 165, 165, 91, 134, 164, 124, 200, 110, 114, 125, 185, 115, 151, 138, 138, 141, 150, 137, 134, 167, 129, 218, 89, 126, 224, 83, 126, 151, 136, 147, 206, 145, 85, 149, 150, 138, 142, 189, 107, 166, 171, 101, 172, 150, 116, 153, 171, 115, 156, 181, 103, 132, 189, 120, 221, 92, 129, 182, 122, 144, 224, 93, 131, 210, 110, 130, 162, 166, 123, 168, 179, 105, 180, 172, 102, 140, 184, 131, 148, 170, 137, 207, 140, 108, 206, 164, 86, 152, 187, 120, 155, 155, 151, 142, 195, 125, 147, 182, 133, 142, 194, 129, 149, 178, 138, 149, 184, 135, 167, 155, 147, 182, 183, 104, 157, 182, 131, 172, 165, 133, 183, 166, 121, 168, 184, 120, 151, 185, 137, 154, 179, 141, 157, 164, 153, 147, 197, 133, 157, 187, 134, 228, 110, 142, 148, 205, 128, 154, 189, 139, 150, 197, 137, 155, 196, 133, 172, 146, 168, 155, 185, 147, 180, 168, 139, 170, 188, 130, 184, 186, 118, 204, 173, 112, 165, 169, 156, 207, 142, 141, 228, 143, 119, 157, 192, 142, 153, 206, 133, 186, 195, 115, 155, 201, 141, 156, 209, 135, 158, 198, 145, 168, 168, 165, 161, 196, 145, 182, 172, 148, 169, 196, 138, 183, 186, 135, 168, 184, 157, 171, 198, 146, 163, 204, 149, 167, 193, 156, 182, 183, 151, 163, 213, 141, 171, 182, 166, 195, 202, 122, 188, 197, 135, 180, 171, 171, 231, 141, 150, 172, 203, 148, 203, 154, 166, 167, 205, 152, 171, 196, 157, 204, 179, 141, 184, 167, 178, 171, 205, 156, 195, 201, 136, 180, 199, 154, 228, 186, 120, 185, 183, 167, 172, 202, 163, 200, 174, 164, 173, 209, 158, 232, 146, 162, 175, 210, 161, 201, 210, 136, 181, 214, 155, 197, 186, 167, 212, 199, 139, 182, 202, 168, 219, 167, 166, 178, 212, 163, 198, 201, 155, 205, 169, 180, 187, 186, 183, 213, 185, 162, 182, 216, 164, 197, 197, 168, 182, 213, 169, 187, 212, 165, 196, 216, 153, 210, 215, 140, 186, 199, 181, 184, 225, 158, 214, 201, 153, 194, 189, 187, 186, 219, 166, 187, 214, 171, 209, 172, 191, 188, 219, 171, 248, 177, 155, 190, 225, 166, 189, 216, 178, 197, 201, 185, 215, 220, 148, 215, 201, 168, 199, 215, 171, 217, 186, 184, 191, 206, 193, 241, 203, 146, 198, 197, 196, 234, 180, 178, 216, 199, 183, 216, 212, 170, 226, 202, 170, 198, 228, 173, 199, 218, 183, 221, 226, 155, 224, 229, 159, 210, 204, 200, 247, 194, 173, 205, 234, 176, 213, 217, 185, 204, 216, 197, 233, 201, 184, 237, 188, 198, 204, 226, 196, 210, 230, 187, 229, 233, 166, 246, 198, 184, 232, 200, 198, 252, 214, 164, 214, 217, 200, 228, 218, 187, 215, 228, 201, 244, 199, 203, 232, 231, 184, 217, 217, 215, 232, 219, 199, 245, 220, 185, 217, 240, 195, 237, 241, 178, 243, 203, 210, 249, 230, 178, 226, 221, 219, 229, 233, 204, 228, 227, 212, 220, 234, 214, 252, 215, 204, 228, 226, 220, 229, 234, 211, 232, 227, 215, 245, 230, 200, 244, 216, 218, 227, 236, 219, 235, 238, 211, 237, 228, 219, 243, 231, 214, 247, 250, 191, 234, 234, 222, 238, 240, 213, 232, 231, 230, 247, 220, 226, 247, 250, 196, 241, 242, 214, 234, 243, 229, 243, 238, 233, 243, 243, 242, 246, 247, 235, 252, 238, 241, 244, 249, 241, 251, 243, 244, 251, 251, 245, 253, 246, 248, 254, 254, 254, 34, 181, 220, 167, 0, 0, 69, 129, 73, 68, 65, 84, 120, 156, 205, 125, 15, 124, 19, 215, 157, 167, 46, 221, 75, 66, 22, 54, 9, 189, 66, 73, 110, 113, 56, 183, 185, 101, 33, 9, 203, 166, 65, 33, 205, 101, 91, 214, 148, 166, 80, 177, 161, 136, 73, 90, 47, 56, 81, 2, 52, 9, 38, 50, 156, 147, 51, 24, 185, 118, 194, 31, 23, 145, 137, 11, 25, 11, 136, 173, 172, 107, 11, 66, 99, 251, 140, 229, 198, 161, 38, 169, 38, 85, 101, 23, 145, 176, 98, 178, 202, 110, 56, 217, 194, 161, 216, 92, 192, 28, 150, 13, 24, 125, 238, 247, 123, 111, 102, 52, 163, 153, 145, 70, 134, 238, 221, 239, 3, 99, 253, 153, 25, 205, 251, 190, 223, 255, 247, 123, 239, 89, 184, 255, 95, 201, 243, 238, 91, 23, 252, 60, 147, 246, 105, 107, 103, 69, 167, 68, 129, 80, 35, 199, 5, 66, 89, 168, 179, 147, 207, 244, 181, 229, 255, 69, 219, 76, 145, 119, 240, 165, 193, 11, 199, 153, 180, 79, 15, 0, 0, 53, 18, 2, 33, 175, 167, 77, 209, 22, 93, 44, 248, 206, 204, 16, 253, 255, 11, 0, 215, 251, 225, 135, 23, 248, 227, 124, 218, 167, 53, 21, 53, 53, 41, 30, 224, 36, 0, 218, 218, 2, 141, 92, 99, 22, 102, 8, 232, 48, 195, 159, 22, 128, 45, 91, 174, 227, 226, 227, 131, 47, 93, 184, 112, 161, 55, 237, 83, 0, 128, 54, 126, 253, 219, 192, 2, 82, 239, 6, 232, 151, 89, 154, 143, 76, 243, 239, 11, 192, 143, 126, 164, 247, 233, 78, 102, 39, 199, 174, 101, 54, 178, 89, 174, 62, 254, 214, 103, 128, 128, 154, 5, 188, 85, 21, 64, 50, 0, 18, 181, 227, 119, 45, 134, 204, 14, 122, 32, 16, 234, 252, 247, 7, 96, 203, 119, 191, 171, 199, 2, 155, 214, 110, 226, 54, 110, 100, 55, 110, 204, 118, 125, 231, 46, 0, 64, 80, 127, 68, 9, 94, 109, 219, 66, 164, 187, 157, 182, 58, 224, 81, 169, 131, 180, 230, 75, 164, 163, 14, 115, 7, 192, 108, 255, 113, 28, 251, 93, 32, 157, 211, 158, 222, 249, 52, 199, 176, 28, 203, 100, 189, 3, 202, 128, 79, 245, 9, 105, 199, 1, 242, 114, 41, 109, 129, 200, 247, 1, 3, 21, 40, 178, 190, 104, 54, 110, 4, 0, 166, 251, 143, 123, 9, 1, 88, 165, 249, 120, 231, 90, 110, 237, 78, 4, 224, 199, 89, 239, 240, 198, 201, 11, 131, 234, 79, 14, 64, 227, 189, 244, 101, 33, 109, 65, 139, 162, 53, 109, 222, 52, 54, 64, 166, 231, 101, 0, 16, 39, 95, 0, 193, 106, 191, 14, 0, 76, 247, 95, 205, 119, 9, 213, 164, 127, 190, 105, 19, 252, 51, 7, 97, 247, 73, 141, 18, 76, 209, 54, 218, 161, 156, 87, 108, 10, 188, 109, 75, 87, 3, 164, 209, 138, 246, 183, 113, 158, 0, 81, 153, 129, 113, 3, 96, 190, 255, 94, 162, 0, 188, 148, 254, 249, 211, 59, 57, 192, 112, 45, 179, 54, 187, 16, 85, 108, 17, 90, 13, 191, 20, 219, 32, 189, 8, 116, 106, 165, 128, 182, 154, 39, 127, 240, 75, 210, 114, 138, 192, 184, 1, 200, 161, 255, 12, 136, 65, 130, 63, 38, 78, 173, 176, 101, 254, 190, 69, 234, 205, 0, 85, 242, 105, 8, 212, 117, 18, 37, 16, 74, 233, 126, 196, 168, 5, 46, 51, 0, 192, 140, 126, 203, 161, 255, 12, 137, 145, 15, 217, 200, 154, 241, 219, 98, 250, 12, 141, 33, 165, 148, 43, 25, 32, 245, 82, 1, 13, 241, 25, 244, 69, 192, 140, 126, 203, 161, 255, 140, 239, 33, 31, 178, 145, 53, 227, 183, 197, 34, 131, 164, 236, 156, 214, 206, 203, 141, 86, 182, 95, 225, 50, 170, 0, 48, 167, 223, 204, 63, 254, 245, 223, 193, 154, 153, 207, 10, 172, 214, 130, 114, 21, 0, 250, 8, 96, 163, 201, 159, 198, 22, 122, 161, 62, 0, 38, 245, 155, 249, 199, 191, 126, 42, 200, 42, 104, 213, 240, 255, 237, 165, 153, 76, 125, 128, 52, 58, 16, 106, 135, 191, 237, 1, 160, 70, 165, 207, 104, 81, 252, 130, 73, 253, 198, 200, 135, 63, 61, 89, 205, 156, 4, 110, 241, 27, 70, 12, 208, 142, 102, 2, 41, 128, 92, 160, 3, 143, 165, 200, 90, 45, 221, 232, 70, 232, 183, 27, 76, 214, 236, 167, 180, 66, 179, 183, 109, 241, 28, 192, 238, 15, 144, 22, 43, 136, 229, 218, 219, 83, 167, 102, 244, 4, 109, 197, 55, 68, 191, 113, 173, 60, 15, 166, 219, 227, 247, 131, 199, 198, 243, 126, 207, 245, 220, 171, 216, 90, 148, 253, 36, 108, 249, 250, 26, 206, 67, 187, 95, 221, 202, 198, 167, 149, 103, 182, 235, 180, 63, 5, 0, 91, 204, 221, 8, 238, 14, 247, 172, 92, 217, 195, 251, 226, 85, 29, 126, 95, 124, 243, 188, 170, 248, 184, 17, 96, 109, 214, 34, 29, 86, 164, 102, 26, 13, 182, 68, 128, 192, 82, 15, 231, 37, 28, 64, 186, 59, 213, 208, 181, 140, 242, 82, 221, 104, 65, 237, 8, 49, 28, 203, 142, 31, 0, 120, 42, 175, 48, 121, 247, 238, 201, 3, 241, 217, 147, 55, 251, 195, 15, 46, 236, 200, 175, 226, 57, 79, 56, 46, 8, 225, 176, 16, 246, 101, 191, 133, 130, 138, 202, 117, 63, 166, 58, 10, 13, 182, 76, 32, 2, 30, 194, 8, 36, 74, 36, 238, 17, 168, 190, 54, 226, 31, 41, 47, 213, 107, 191, 6, 128, 10, 107, 49, 147, 211, 115, 42, 8, 158, 202, 215, 51, 37, 46, 76, 142, 11, 85, 11, 1, 128, 252, 166, 248, 202, 117, 60, 231, 111, 218, 60, 59, 191, 105, 221, 61, 243, 198, 193, 13, 197, 154, 79, 168, 153, 70, 131, 45, 81, 43, 9, 16, 83, 97, 34, 198, 6, 129, 181, 32, 134, 161, 144, 164, 1, 41, 233, 3, 160, 200, 55, 8, 131, 131, 189, 194, 23, 233, 57, 40, 243, 132, 79, 37, 172, 188, 231, 158, 205, 130, 63, 14, 0, 248, 249, 41, 179, 243, 227, 62, 206, 95, 117, 187, 208, 115, 123, 85, 60, 127, 191, 223, 236, 157, 100, 235, 103, 213, 124, 69, 204, 52, 49, 216, 244, 253, 129, 148, 11, 128, 97, 131, 183, 157, 178, 250, 38, 15, 253, 171, 184, 82, 63, 97, 102, 105, 232, 168, 19, 127, 77, 128, 224, 251, 194, 96, 239, 113, 146, 129, 168, 230, 114, 38, 124, 42, 46, 146, 191, 114, 246, 236, 184, 135, 71, 14, 168, 202, 95, 57, 133, 247, 3, 0, 179, 195, 241, 219, 227, 248, 145, 241, 197, 42, 209, 182, 201, 29, 95, 80, 145, 126, 34, 17, 1, 98, 176, 85, 237, 167, 41, 2, 42, 254, 68, 214, 3, 212, 214, 43, 174, 212, 109, 127, 200, 146, 28, 233, 15, 118, 53, 188, 230, 116, 213, 117, 247, 18, 4, 224, 159, 208, 219, 187, 30, 37, 80, 248, 162, 247, 184, 121, 0, 240, 169, 124, 155, 31, 20, 132, 252, 166, 86, 108, 109, 124, 114, 60, 220, 148, 31, 6, 0, 30, 164, 0, 172, 211, 3, 160, 133, 250, 41, 58, 162, 141, 84, 93, 160, 185, 0, 205, 52, 53, 216, 72, 168, 251, 58, 183, 73, 33, 119, 64, 12, 128, 67, 41, 231, 223, 203, 165, 190, 211, 82, 128, 179, 36, 69, 26, 25, 138, 118, 53, 55, 52, 247, 124, 65, 81, 248, 227, 111, 187, 59, 143, 227, 43, 243, 0, 224, 83, 185, 155, 238, 137, 68, 166, 244, 80, 0, 166, 116, 196, 215, 61, 152, 17, 128, 70, 154, 209, 106, 211, 136, 118, 133, 196, 190, 58, 190, 48, 35, 7, 36, 132, 14, 116, 30, 0, 125, 217, 70, 148, 94, 40, 149, 248, 224, 196, 68, 73, 91, 38, 0, 64, 94, 100, 0, 68, 26, 10, 54, 55, 116, 8, 255, 74, 232, 143, 36, 35, 199, 11, 38, 181, 55, 62, 212, 143, 133, 117, 247, 228, 239, 134, 139, 170, 154, 252, 124, 207, 236, 252, 121, 16, 207, 251, 59, 54, 135, 227, 15, 198, 241, 35, 205, 53, 50, 167, 170, 69, 59, 180, 109, 105, 59, 245, 224, 203, 181, 44, 192, 200, 7, 137, 138, 139, 41, 127, 75, 201, 47, 244, 135, 72, 144, 12, 250, 176, 81, 58, 201, 171, 69, 0, 141, 166, 165, 161, 161, 185, 43, 218, 63, 162, 4, 97, 36, 218, 213, 176, 255, 3, 64, 96, 16, 84, 2, 128, 96, 90, 12, 224, 169, 60, 60, 31, 230, 193, 42, 135, 195, 30, 206, 79, 77, 159, 39, 28, 246, 249, 195, 126, 242, 81, 58, 97, 122, 130, 36, 40, 100, 209, 102, 193, 105, 7, 117, 21, 40, 124, 131, 178, 111, 145, 198, 23, 210, 2, 80, 94, 196, 41, 219, 143, 44, 128, 40, 194, 109, 3, 33, 133, 33, 12, 133, 246, 150, 22, 2, 137, 9, 148, 125, 69, 168, 106, 44, 200, 241, 189, 145, 142, 134, 134, 134, 174, 96, 76, 129, 67, 127, 87, 67, 7, 81, 10, 231, 211, 242, 178, 153, 1, 200, 209, 143, 242, 134, 2, 91, 169, 160, 138, 162, 141, 253, 4, 207, 8, 159, 189, 177, 180, 240, 32, 158, 82, 144, 238, 15, 104, 127, 165, 186, 128, 40, 63, 146, 252, 59, 126, 60, 5, 0, 116, 113, 72, 233, 9, 7, 14, 238, 219, 199, 181, 96, 64, 116, 176, 192, 90, 96, 43, 174, 150, 0, 16, 233, 11, 192, 161, 57, 56, 148, 226, 132, 224, 110, 80, 137, 253, 253, 125, 127, 58, 0, 160, 135, 218, 69, 141, 213, 24, 104, 223, 4, 130, 141, 13, 225, 15, 22, 46, 45, 44, 124, 32, 244, 6, 120, 50, 1, 86, 235, 11, 104, 200, 138, 130, 196, 119, 66, 219, 143, 35, 12, 60, 0, 208, 142, 247, 109, 76, 157, 82, 110, 37, 127, 52, 142, 164, 18, 0, 17, 133, 230, 134, 174, 168, 200, 10, 35, 13, 145, 254, 88, 44, 150, 75, 147, 114, 34, 73, 24, 121, 49, 97, 31, 96, 20, 198, 106, 239, 86, 250, 215, 196, 125, 88, 188, 21, 54, 254, 184, 148, 7, 12, 144, 192, 40, 117, 70, 133, 168, 77, 53, 214, 70, 3, 0, 33, 1, 88, 33, 74, 32, 136, 54, 68, 99, 177, 158, 158, 158, 27, 221, 116, 74, 26, 171, 180, 79, 124, 65, 26, 241, 6, 125, 109, 42, 54, 109, 36, 151, 28, 15, 73, 0, 64, 231, 123, 149, 223, 23, 136, 142, 141, 198, 145, 212, 7, 128, 128, 176, 59, 74, 229, 160, 171, 33, 24, 139, 141, 223, 63, 204, 68, 129, 144, 222, 152, 46, 178, 51, 52, 68, 228, 138, 118, 171, 153, 59, 177, 235, 81, 252, 241, 226, 3, 7, 90, 85, 170, 143, 82, 145, 40, 72, 233, 142, 100, 6, 0, 64, 55, 54, 196, 168, 36, 4, 1, 129, 136, 113, 126, 122, 220, 68, 218, 126, 176, 240, 160, 170, 249, 141, 34, 99, 16, 177, 88, 31, 8, 88, 205, 57, 165, 197, 214, 183, 65, 6, 148, 92, 79, 168, 220, 74, 175, 151, 0, 72, 115, 36, 1, 128, 15, 79, 246, 26, 67, 208, 211, 64, 149, 65, 3, 40, 130, 232, 13, 106, 181, 130, 8, 0, 45, 7, 81, 93, 173, 47, 220, 10, 56, 128, 138, 110, 84, 121, 44, 7, 151, 154, 108, 63, 208, 122, 48, 27, 129, 182, 180, 15, 139, 217, 106, 107, 5, 8, 17, 43, 9, 146, 218, 145, 68, 0, 222, 221, 245, 150, 49, 2, 131, 29, 13, 93, 96, 22, 134, 250, 135, 64, 10, 188, 40, 85, 94, 31, 24, 116, 62, 12, 1, 174, 233, 208, 198, 144, 218, 69, 111, 132, 114, 194, 214, 165, 235, 169, 214, 150, 51, 118, 45, 109, 251, 178, 100, 69, 213, 132, 151, 87, 107, 162, 232, 10, 117, 64, 193, 168, 29, 73, 73, 4, 222, 59, 137, 199, 75, 163, 151, 210, 33, 232, 143, 53, 36, 147, 99, 93, 35, 253, 177, 248, 232, 112, 152, 227, 227, 64, 71, 226, 66, 207, 240, 145, 120, 110, 241, 189, 222, 3, 99, 43, 177, 207, 90, 218, 209, 37, 14, 28, 92, 42, 250, 125, 162, 159, 214, 216, 146, 83, 251, 9, 177, 54, 171, 108, 55, 53, 145, 20, 18, 35, 31, 8, 89, 46, 93, 186, 112, 9, 254, 247, 190, 123, 18, 254, 140, 38, 8, 2, 151, 144, 232, 95, 0, 0, 88, 32, 122, 203, 200, 80, 255, 104, 67, 89, 220, 19, 127, 109, 193, 240, 192, 140, 89, 19, 34, 61, 183, 196, 253, 62, 31, 231, 105, 109, 5, 190, 240, 121, 91, 91, 61, 222, 214, 92, 17, 9, 132, 210, 180, 149, 151, 180, 183, 200, 38, 13, 120, 6, 172, 57, 222, 17, 137, 45, 2, 8, 138, 172, 64, 186, 30, 4, 35, 31, 8, 89, 70, 175, 92, 25, 5, 26, 134, 255, 87, 198, 34, 95, 29, 29, 77, 92, 24, 165, 116, 137, 28, 207, 15, 141, 0, 0, 19, 16, 0, 251, 172, 97, 79, 220, 62, 45, 62, 16, 159, 115, 104, 24, 0, 144, 40, 44, 200, 47, 115, 211, 148, 45, 90, 109, 77, 168, 220, 186, 158, 74, 193, 62, 109, 36, 96, 138, 138, 170, 139, 13, 89, 135, 145, 15, 132, 44, 19, 163, 67, 51, 38, 206, 74, 140, 53, 79, 156, 56, 35, 49, 209, 50, 113, 243, 103, 189, 163, 101, 219, 167, 78, 180, 143, 94, 25, 158, 51, 113, 106, 100, 100, 164, 97, 234, 84, 199, 196, 4, 1, 32, 142, 0, 12, 15, 60, 58, 97, 98, 67, 240, 150, 184, 48, 173, 231, 202, 107, 19, 39, 44, 24, 24, 126, 244, 181, 59, 39, 190, 230, 152, 120, 231, 145, 120, 110, 15, 218, 238, 53, 250, 166, 96, 41, 232, 196, 64, 69, 238, 249, 233, 114, 155, 153, 76, 106, 138, 44, 177, 177, 137, 101, 163, 115, 102, 141, 89, 250, 71, 131, 99, 13, 83, 135, 71, 19, 131, 163, 115, 110, 9, 118, 223, 178, 123, 108, 250, 156, 225, 134, 155, 18, 253, 150, 232, 208, 212, 137, 35, 67, 177, 17, 0, 96, 19, 2, 96, 159, 208, 211, 19, 7, 14, 152, 232, 24, 237, 184, 233, 72, 207, 68, 251, 149, 169, 19, 123, 14, 89, 102, 9, 143, 78, 189, 126, 189, 32, 17, 107, 43, 88, 159, 115, 86, 6, 20, 128, 77, 63, 149, 104, 72, 150, 43, 67, 150, 237, 219, 23, 220, 50, 54, 117, 70, 100, 20, 0, 24, 29, 189, 112, 97, 116, 206, 156, 209, 81, 231, 140, 107, 150, 196, 232, 149, 137, 13, 101, 211, 199, 18, 13, 19, 65, 9, 2, 0, 163, 155, 4, 251, 180, 145, 137, 206, 120, 120, 160, 99, 194, 157, 143, 198, 135, 103, 205, 137, 199, 95, 155, 120, 101, 170, 61, 30, 183, 244, 196, 143, 128, 94, 200, 245, 153, 51, 80, 121, 78, 93, 137, 196, 230, 216, 122, 14, 117, 64, 212, 98, 183, 219, 203, 198, 174, 148, 77, 152, 142, 0, 92, 66, 0, 22, 140, 142, 150, 77, 29, 177, 36, 18, 35, 83, 183, 151, 205, 74, 140, 118, 77, 28, 139, 17, 0, 188, 225, 5, 211, 175, 76, 120, 77, 240, 9, 135, 44, 19, 167, 14, 12, 79, 95, 16, 15, 31, 154, 112, 101, 170, 19, 0, 16, 180, 0, 104, 171, 196, 180, 89, 109, 99, 170, 182, 226, 81, 27, 15, 235, 157, 90, 92, 84, 96, 34, 104, 210, 33, 203, 232, 168, 37, 54, 58, 58, 54, 6, 170, 240, 150, 104, 51, 112, 192, 40, 0, 0, 127, 102, 205, 25, 187, 169, 249, 179, 161, 155, 162, 93, 19, 0, 141, 137, 35, 111, 245, 143, 118, 221, 50, 58, 32, 76, 125, 244, 202, 52, 232, 118, 84, 130, 192, 251, 11, 166, 197, 227, 143, 78, 55, 4, 64, 91, 37, 102, 144, 250, 210, 39, 104, 123, 53, 74, 116, 150, 42, 1, 56, 169, 160, 168, 120, 28, 89, 76, 36, 203, 240, 104, 217, 77, 179, 102, 60, 58, 54, 113, 206, 172, 9, 163, 67, 55, 205, 234, 64, 0, 38, 76, 159, 113, 83, 255, 88, 195, 45, 179, 38, 206, 73, 142, 76, 157, 58, 107, 234, 196, 196, 201, 183, 134, 71, 103, 220, 50, 109, 194, 68, 97, 224, 200, 77, 211, 166, 149, 129, 18, 236, 185, 233, 200, 240, 196, 169, 211, 111, 233, 25, 53, 0, 64, 167, 74, 76, 155, 213, 206, 68, 5, 212, 145, 213, 166, 70, 111, 28, 89, 46, 13, 143, 158, 111, 14, 142, 142, 245, 55, 119, 129, 69, 60, 223, 124, 30, 1, 176, 199, 58, 18, 35, 67, 137, 33, 112, 1, 134, 18, 87, 130, 93, 137, 216, 200, 165, 119, 223, 29, 16, 58, 94, 59, 36, 8, 173, 241, 200, 107, 187, 7, 192, 17, 138, 247, 244, 140, 14, 28, 58, 20, 31, 24, 232, 1, 67, 216, 65, 60, 36, 165, 18, 212, 171, 18, 211, 4, 35, 25, 72, 174, 215, 40, 207, 198, 2, 215, 65, 22, 116, 255, 192, 248, 147, 3, 121, 121, 9, 1, 24, 1, 7, 32, 22, 75, 38, 250, 199, 198, 162, 49, 64, 34, 57, 244, 197, 133, 119, 7, 253, 126, 33, 188, 231, 199, 204, 218, 58, 28, 233, 9, 251, 91, 225, 63, 15, 47, 189, 254, 112, 216, 199, 135, 121, 47, 175, 118, 143, 245, 170, 196, 52, 193, 72, 6, 74, 213, 107, 88, 179, 156, 89, 161, 81, 126, 237, 161, 128, 151, 75, 15, 12, 244, 72, 47, 26, 28, 181, 151, 141, 68, 35, 209, 88, 114, 36, 26, 11, 98, 52, 4, 0, 196, 6, 49, 66, 102, 240, 18, 198, 196, 109, 145, 244, 171, 196, 210, 131, 145, 12, 148, 170, 215, 80, 122, 196, 45, 152, 234, 72, 107, 26, 107, 77, 19, 18, 18, 103, 55, 30, 64, 125, 187, 49, 32, 70, 24, 250, 169, 149, 52, 0, 6, 251, 34, 145, 232, 208, 8, 16, 201, 16, 67, 16, 24, 196, 236, 208, 72, 127, 132, 124, 203, 224, 37, 76, 246, 71, 39, 100, 80, 37, 198, 168, 131, 17, 99, 82, 212, 107, 20, 43, 100, 128, 134, 144, 105, 231, 166, 141, 31, 180, 137, 121, 133, 77, 27, 1, 175, 125, 212, 219, 50, 200, 45, 41, 0, 192, 182, 203, 233, 225, 145, 88, 180, 43, 24, 133, 246, 67, 44, 24, 13, 10, 95, 208, 83, 102, 178, 156, 33, 0, 102, 43, 72, 25, 249, 160, 71, 224, 202, 88, 173, 180, 185, 202, 122, 141, 20, 11, 144, 17, 63, 173, 7, 109, 85, 190, 17, 163, 201, 128, 215, 203, 120, 3, 161, 125, 36, 73, 160, 101, 27, 74, 41, 0, 190, 32, 217, 143, 145, 254, 104, 48, 8, 124, 223, 21, 140, 141, 37, 163, 93, 93, 35, 201, 161, 200, 160, 204, 31, 229, 25, 162, 51, 179, 21, 164, 140, 124, 208, 35, 28, 19, 246, 82, 169, 81, 214, 107, 40, 134, 138, 3, 217, 1, 144, 83, 75, 236, 143, 189, 52, 231, 26, 8, 24, 48, 0, 103, 25, 28, 28, 236, 237, 21, 142, 251, 143, 15, 6, 163, 208, 246, 40, 166, 62, 250, 41, 19, 16, 110, 136, 246, 41, 4, 132, 203, 128, 128, 217, 10, 82, 70, 62, 24, 145, 167, 49, 16, 240, 84, 168, 234, 53, 108, 210, 207, 146, 129, 164, 128, 118, 128, 65, 37, 2, 18, 7, 132, 90, 48, 172, 228, 54, 137, 3, 230, 186, 63, 38, 15, 143, 247, 146, 216, 159, 80, 116, 100, 44, 58, 20, 13, 70, 131, 152, 13, 27, 188, 160, 4, 128, 171, 176, 26, 120, 155, 146, 196, 250, 48, 52, 12, 103, 106, 159, 49, 129, 38, 19, 37, 160, 177, 209, 102, 211, 197, 170, 49, 100, 192, 202, 74, 106, 33, 249, 240, 198, 181, 107, 215, 178, 108, 11, 178, 81, 27, 230, 25, 245, 207, 149, 1, 56, 222, 43, 3, 16, 139, 38, 163, 132, 19, 128, 255, 21, 253, 127, 129, 140, 17, 25, 165, 233, 37, 137, 221, 211, 145, 191, 110, 158, 56, 148, 226, 51, 12, 246, 116, 169, 88, 236, 198, 98, 162, 210, 177, 102, 7, 255, 50, 234, 147, 196, 174, 12, 120, 165, 131, 30, 209, 1, 87, 83, 250, 86, 6, 96, 211, 166, 227, 66, 127, 159, 32, 28, 63, 30, 18, 130, 99, 9, 4, 32, 56, 150, 12, 166, 25, 9, 122, 46, 171, 227, 153, 201, 18, 27, 111, 90, 24, 167, 57, 100, 62, 34, 228, 130, 128, 56, 116, 33, 147, 88, 170, 193, 128, 167, 171, 248, 61, 194, 212, 59, 55, 98, 186, 156, 13, 168, 228, 81, 27, 7, 50, 242, 193, 152, 100, 0, 176, 1, 21, 84, 118, 132, 232, 208, 88, 144, 48, 66, 255, 23, 105, 46, 2, 29, 38, 100, 11, 180, 174, 153, 36, 177, 124, 71, 254, 148, 217, 116, 60, 53, 60, 187, 41, 135, 4, 73, 133, 53, 253, 19, 41, 188, 145, 218, 79, 236, 121, 35, 152, 56, 239, 166, 181, 192, 210, 7, 2, 94, 149, 194, 101, 53, 55, 96, 228, 131, 49, 201, 0, 144, 177, 93, 17, 0, 104, 250, 80, 127, 44, 58, 54, 18, 73, 247, 145, 6, 197, 241, 129, 34, 157, 100, 45, 253, 185, 248, 237, 29, 224, 21, 163, 38, 8, 3, 0, 112, 108, 5, 157, 48, 48, 0, 135, 204, 163, 204, 213, 198, 234, 85, 108, 88, 128, 72, 113, 0, 147, 254, 79, 147, 190, 10, 164, 143, 16, 234, 63, 81, 166, 95, 85, 0, 32, 158, 25, 32, 0, 4, 135, 198, 200, 120, 128, 214, 75, 148, 16, 168, 176, 106, 152, 128, 254, 92, 252, 246, 72, 216, 239, 139, 207, 190, 123, 74, 83, 124, 118, 126, 254, 228, 221, 241, 149, 43, 167, 172, 220, 63, 27, 249, 34, 211, 131, 84, 27, 182, 95, 236, 217, 0, 117, 127, 2, 33, 15, 81, 184, 88, 247, 47, 149, 180, 210, 75, 205, 39, 208, 149, 148, 14, 64, 69, 11, 2, 16, 67, 103, 32, 184, 107, 80, 11, 64, 106, 2, 131, 230, 231, 200, 245, 76, 120, 247, 237, 243, 34, 66, 213, 236, 184, 48, 121, 120, 246, 194, 184, 112, 251, 240, 194, 187, 227, 241, 42, 248, 63, 37, 146, 155, 78, 36, 100, 147, 37, 32, 68, 45, 57, 56, 249, 84, 225, 178, 7, 151, 206, 156, 105, 45, 175, 46, 22, 155, 46, 87, 18, 148, 231, 4, 132, 197, 37, 35, 143, 13, 104, 220, 4, 50, 38, 136, 198, 96, 151, 78, 251, 213, 37, 35, 58, 249, 39, 79, 56, 190, 238, 118, 97, 225, 148, 252, 252, 219, 135, 103, 239, 246, 11, 147, 251, 86, 174, 228, 253, 155, 31, 12, 135, 239, 233, 200, 40, 3, 250, 121, 143, 130, 10, 177, 96, 136, 20, 189, 5, 72, 213, 119, 128, 40, 220, 34, 235, 143, 193, 202, 21, 96, 227, 139, 241, 218, 114, 150, 45, 199, 199, 177, 229, 150, 20, 179, 216, 237, 46, 183, 252, 142, 186, 15, 8, 64, 52, 18, 17, 116, 1, 80, 149, 75, 84, 131, 217, 182, 169, 77, 2, 207, 251, 133, 121, 85, 43, 87, 162, 228, 207, 174, 226, 227, 183, 15, 172, 92, 7, 0, 204, 227, 179, 1, 160, 175, 3, 138, 173, 98, 199, 66, 219, 219, 209, 163, 67, 43, 80, 141, 10, 203, 170, 144, 110, 188, 182, 218, 106, 53, 40, 45, 204, 72, 22, 206, 229, 176, 75, 63, 237, 161, 74, 240, 120, 172, 63, 12, 13, 57, 174, 15, 64, 122, 213, 144, 58, 115, 231, 17, 22, 86, 85, 77, 238, 137, 76, 222, 188, 127, 243, 192, 236, 187, 155, 86, 230, 15, 44, 204, 14, 64, 113, 65, 145, 126, 18, 159, 227, 30, 16, 27, 5, 166, 29, 213, 30, 169, 4, 10, 181, 51, 56, 216, 199, 164, 158, 193, 90, 93, 174, 204, 154, 228, 144, 76, 70, 29, 0, 28, 224, 34, 111, 164, 41, 87, 125, 208, 205, 188, 112, 65, 31, 128, 11, 122, 3, 197, 69, 214, 2, 49, 19, 31, 223, 188, 112, 101, 79, 152, 239, 89, 185, 178, 41, 222, 20, 89, 183, 46, 30, 238, 232, 240, 251, 123, 154, 248, 240, 110, 93, 175, 128, 58, 215, 198, 121, 124, 182, 160, 72, 52, 0, 196, 171, 15, 144, 33, 240, 16, 131, 166, 33, 5, 0, 176, 162, 234, 34, 43, 253, 227, 53, 161, 116, 168, 18, 100, 237, 174, 20, 0, 255, 6, 186, 207, 15, 188, 124, 193, 96, 212, 84, 127, 164, 156, 45, 178, 146, 166, 120, 253, 60, 15, 61, 237, 3, 81, 240, 250, 225, 181, 151, 212, 77, 251, 253, 30, 47, 175, 117, 10, 224, 34, 155, 153, 222, 146, 74, 94, 100, 98, 80, 236, 25, 195, 243, 69, 53, 96, 224, 254, 171, 72, 180, 2, 110, 242, 15, 188, 140, 192, 241, 65, 177, 145, 126, 253, 230, 95, 248, 194, 196, 3, 155, 164, 98, 35, 190, 79, 39, 16, 254, 55, 10, 183, 97, 203, 247, 110, 45, 44, 44, 180, 22, 88, 51, 207, 164, 96, 169, 73, 108, 52, 138, 128, 20, 148, 170, 22, 167, 76, 128, 5, 179, 4, 0, 159, 158, 13, 76, 87, 130, 90, 42, 150, 187, 148, 205, 125, 84, 199, 144, 14, 22, 22, 110, 107, 59, 136, 134, 224, 13, 60, 22, 102, 59, 191, 45, 176, 148, 252, 5, 196, 178, 213, 39, 167, 252, 0, 151, 172, 11, 1, 130, 94, 174, 245, 130, 129, 8, 100, 169, 25, 243, 135, 79, 190, 7, 188, 206, 119, 111, 43, 216, 242, 225, 135, 130, 112, 67, 70, 74, 80, 245, 43, 74, 125, 151, 102, 195, 54, 20, 58, 72, 186, 190, 45, 7, 14, 160, 50, 32, 210, 241, 11, 199, 249, 11, 6, 74, 48, 51, 0, 173, 145, 252, 252, 252, 15, 252, 194, 254, 123, 226, 92, 252, 158, 41, 224, 254, 129, 33, 43, 174, 0, 109, 228, 81, 245, 69, 121, 65, 78, 142, 155, 56, 5, 64, 44, 166, 202, 90, 53, 32, 135, 255, 109, 57, 112, 0, 144, 211, 73, 255, 82, 61, 96, 0, 64, 230, 106, 161, 240, 131, 85, 241, 205, 15, 198, 215, 205, 190, 29, 0, 152, 12, 206, 0, 156, 206, 11, 66, 55, 196, 6, 66, 167, 205, 106, 35, 161, 46, 14, 225, 229, 232, 183, 42, 203, 70, 2, 129, 242, 204, 3, 160, 98, 121, 5, 121, 153, 141, 5, 84, 0, 176, 78, 84, 223, 199, 37, 214, 223, 165, 39, 3, 10, 55, 160, 145, 78, 136, 241, 146, 40, 141, 22, 153, 115, 252, 202, 121, 241, 121, 235, 132, 72, 252, 246, 184, 71, 152, 2, 177, 16, 244, 189, 112, 247, 131, 249, 147, 87, 230, 79, 89, 24, 102, 43, 138, 208, 94, 149, 143, 195, 97, 73, 33, 64, 154, 84, 148, 137, 9, 196, 124, 16, 125, 200, 44, 247, 213, 76, 157, 85, 100, 64, 122, 95, 210, 65, 32, 5, 64, 235, 224, 133, 65, 15, 138, 11, 9, 144, 224, 13, 66, 224, 139, 223, 125, 123, 126, 220, 231, 3, 0, 188, 194, 148, 123, 38, 175, 132, 15, 133, 41, 85, 194, 186, 124, 33, 114, 123, 142, 131, 231, 42, 162, 89, 173, 84, 253, 151, 54, 122, 78, 17, 130, 117, 176, 212, 76, 129, 161, 22, 0, 231, 235, 89, 16, 72, 1, 64, 202, 136, 125, 100, 154, 1, 207, 209, 19, 7, 123, 135, 87, 206, 139, 204, 94, 25, 230, 0, 0, 100, 253, 248, 228, 30, 47, 0, 208, 225, 175, 154, 231, 143, 95, 23, 0, 56, 30, 0, 189, 153, 170, 2, 51, 230, 34, 226, 208, 238, 125, 35, 27, 0, 52, 179, 166, 225, 128, 134, 215, 187, 84, 105, 192, 193, 116, 115, 40, 159, 201, 247, 246, 10, 131, 130, 7, 186, 254, 184, 71, 212, 26, 23, 122, 113, 146, 128, 48, 69, 64, 0, 60, 97, 8, 11, 238, 238, 1, 136, 166, 244, 212, 85, 205, 131, 176, 192, 72, 127, 230, 50, 100, 108, 130, 2, 98, 70, 52, 227, 73, 222, 0, 213, 143, 218, 217, 227, 205, 175, 55, 71, 101, 141, 15, 62, 221, 247, 150, 190, 117, 82, 9, 64, 42, 37, 201, 99, 171, 27, 249, 227, 66, 10, 35, 33, 252, 224, 131, 251, 103, 47, 36, 28, 224, 239, 88, 88, 53, 15, 140, 1, 2, 224, 203, 8, 64, 78, 67, 198, 217, 137, 170, 139, 204, 194, 223, 40, 105, 73, 157, 233, 243, 108, 243, 235, 255, 20, 19, 219, 195, 97, 174, 172, 188, 96, 102, 97, 39, 47, 32, 47, 12, 14, 42, 22, 52, 160, 115, 108, 252, 190, 148, 200, 0, 176, 241, 166, 117, 77, 113, 31, 23, 7, 207, 63, 190, 123, 221, 126, 172, 26, 226, 247, 11, 117, 61, 29, 62, 161, 202, 40, 89, 156, 219, 144, 177, 68, 134, 245, 67, 1, 227, 36, 184, 76, 109, 18, 68, 186, 235, 7, 116, 116, 93, 184, 16, 5, 165, 70, 4, 94, 78, 207, 87, 20, 96, 202, 90, 161, 124, 143, 83, 33, 241, 202, 137, 67, 68, 199, 227, 231, 209, 247, 241, 250, 189, 228, 37, 178, 217, 218, 86, 223, 206, 202, 214, 181, 107, 247, 24, 217, 228, 92, 134, 140, 83, 100, 53, 250, 194, 43, 27, 193, 12, 212, 38, 78, 169, 51, 88, 64, 65, 232, 123, 189, 11, 186, 142, 186, 189, 140, 124, 0, 213, 163, 240, 222, 121, 177, 223, 143, 167, 68, 64, 215, 79, 206, 58, 35, 53, 151, 33, 227, 20, 21, 233, 4, 18, 226, 248, 28, 27, 48, 17, 6, 81, 50, 92, 65, 162, 193, 233, 150, 50, 37, 140, 124, 160, 4, 17, 42, 141, 189, 5, 218, 106, 159, 191, 55, 29, 0, 159, 32, 8, 189, 189, 95, 244, 246, 242, 186, 119, 72, 167, 28, 134, 140, 101, 10, 4, 116, 66, 41, 243, 43, 188, 72, 100, 188, 132, 134, 203, 110, 23, 227, 35, 70, 62, 136, 196, 22, 219, 200, 248, 141, 31, 92, 128, 65, 210, 106, 17, 10, 201, 77, 228, 133, 54, 95, 43, 88, 193, 222, 11, 90, 30, 210, 37, 198, 236, 144, 113, 138, 192, 37, 88, 47, 15, 164, 74, 148, 113, 124, 14, 7, 94, 53, 238, 83, 166, 53, 68, 92, 246, 74, 206, 229, 202, 228, 118, 159, 20, 88, 239, 113, 210, 203, 148, 9, 36, 39, 65, 230, 8, 81, 101, 50, 242, 193, 128, 178, 158, 160, 67, 242, 112, 103, 234, 9, 51, 175, 128, 96, 43, 214, 153, 131, 149, 113, 17, 149, 74, 187, 211, 9, 49, 162, 34, 105, 40, 147, 203, 225, 112, 186, 185, 109, 75, 11, 196, 62, 224, 121, 190, 55, 21, 40, 137, 0, 12, 230, 50, 221, 202, 24, 0, 253, 129, 119, 175, 12, 64, 129, 28, 24, 100, 95, 1, 193, 150, 30, 68, 100, 4, 128, 117, 216, 237, 228, 159, 83, 253, 219, 24, 234, 187, 92, 240, 49, 97, 15, 116, 201, 192, 45, 190, 144, 90, 241, 166, 77, 52, 162, 230, 211, 224, 140, 124, 208, 33, 125, 193, 110, 73, 13, 120, 203, 243, 170, 50, 172, 128, 32, 105, 111, 101, 53, 29, 152, 170, 214, 204, 28, 224, 178, 59, 42, 57, 183, 203, 254, 29, 187, 83, 228, 2, 55, 70, 140, 118, 59, 125, 13, 106, 66, 252, 165, 162, 15, 85, 61, 222, 171, 208, 7, 148, 52, 133, 252, 57, 144, 66, 176, 149, 61, 145, 2, 128, 149, 68, 59, 131, 189, 145, 165, 223, 138, 135, 234, 226, 114, 120, 47, 4, 227, 81, 75, 134, 220, 18, 91, 233, 182, 83, 21, 0, 189, 141, 92, 224, 118, 83, 0, 228, 75, 88, 208, 146, 78, 23, 126, 96, 91, 223, 185, 145, 239, 245, 3, 248, 24, 240, 130, 98, 80, 79, 186, 109, 9, 181, 141, 99, 84, 132, 146, 82, 176, 93, 82, 222, 138, 35, 0, 72, 247, 76, 137, 54, 35, 31, 212, 148, 42, 183, 196, 135, 47, 40, 40, 2, 133, 88, 16, 127, 225, 133, 102, 75, 121, 134, 188, 28, 203, 166, 84, 160, 203, 238, 116, 124, 199, 46, 126, 14, 128, 200, 39, 33, 0, 128, 78, 249, 67, 51, 31, 2, 70, 181, 205, 252, 94, 129, 182, 132, 192, 67, 166, 242, 55, 142, 143, 11, 84, 130, 237, 118, 218, 157, 34, 6, 33, 69, 209, 135, 60, 221, 154, 145, 15, 42, 162, 69, 167, 34, 137, 77, 246, 133, 71, 95, 120, 33, 110, 161, 115, 244, 179, 231, 239, 92, 174, 74, 55, 138, 129, 195, 85, 233, 4, 3, 233, 80, 126, 199, 186, 128, 45, 22, 216, 37, 70, 101, 53, 217, 10, 82, 214, 210, 34, 251, 167, 222, 156, 22, 18, 72, 19, 108, 214, 101, 151, 111, 154, 242, 247, 165, 252, 18, 35, 31, 84, 164, 170, 34, 171, 102, 113, 62, 176, 63, 250, 122, 215, 240, 235, 175, 139, 58, 160, 200, 116, 141, 185, 219, 233, 112, 161, 13, 168, 84, 125, 234, 176, 131, 85, 120, 166, 242, 219, 208, 61, 162, 5, 178, 89, 139, 228, 164, 79, 139, 152, 161, 144, 122, 204, 212, 92, 64, 153, 100, 193, 198, 62, 128, 247, 149, 34, 248, 158, 128, 34, 222, 81, 117, 177, 134, 104, 165, 165, 28, 116, 122, 133, 96, 115, 56, 254, 66, 48, 46, 244, 189, 32, 43, 193, 34, 235, 67, 38, 98, 82, 41, 227, 155, 202, 160, 74, 244, 226, 139, 168, 40, 236, 246, 239, 124, 199, 69, 205, 38, 91, 76, 147, 215, 96, 50, 82, 217, 28, 236, 122, 47, 166, 53, 114, 226, 1, 134, 30, 92, 78, 7, 128, 207, 85, 234, 158, 147, 177, 154, 150, 106, 64, 57, 232, 12, 119, 125, 167, 89, 8, 190, 16, 108, 126, 61, 18, 73, 89, 129, 141, 203, 11, 204, 199, 164, 218, 156, 55, 48, 234, 139, 118, 199, 51, 79, 61, 227, 64, 7, 178, 18, 254, 195, 57, 196, 74, 90, 173, 197, 138, 165, 63, 73, 185, 78, 91, 246, 116, 181, 146, 24, 249, 224, 6, 223, 44, 247, 116, 187, 168, 33, 228, 160, 179, 53, 30, 253, 78, 100, 160, 225, 245, 230, 174, 23, 4, 2, 0, 239, 85, 126, 157, 243, 253, 233, 221, 25, 230, 9, 208, 133, 12, 209, 83, 196, 123, 112, 74, 58, 19, 7, 206, 170, 27, 113, 100, 83, 177, 222, 225, 56, 1, 112, 216, 53, 188, 39, 145, 241, 44, 25, 197, 172, 81, 49, 232, 228, 187, 94, 136, 135, 227, 241, 224, 235, 130, 101, 73, 29, 186, 113, 32, 81, 244, 235, 153, 21, 185, 197, 164, 202, 199, 100, 29, 110, 250, 152, 132, 63, 48, 150, 176, 139, 131, 142, 92, 69, 53, 142, 223, 21, 203, 11, 220, 100, 11, 86, 13, 8, 180, 45, 0, 107, 240, 101, 69, 182, 217, 34, 202, 160, 83, 120, 253, 245, 248, 192, 11, 175, 199, 125, 150, 218, 197, 111, 250, 133, 4, 31, 15, 251, 194, 62, 248, 122, 99, 129, 245, 31, 199, 247, 112, 233, 10, 184, 18, 4, 150, 117, 43, 236, 54, 87, 94, 4, 182, 119, 91, 168, 165, 197, 212, 28, 8, 61, 114, 179, 174, 74, 206, 161, 175, 4, 56, 156, 33, 168, 87, 86, 94, 46, 115, 134, 34, 232, 244, 198, 95, 104, 142, 14, 8, 62, 206, 82, 91, 187, 122, 117, 19, 159, 136, 183, 250, 54, 210, 175, 43, 30, 210, 137, 153, 76, 80, 107, 15, 186, 126, 12, 190, 186, 50, 6, 47, 235, 98, 61, 110, 3, 251, 202, 22, 93, 79, 253, 187, 203, 225, 52, 132, 64, 179, 216, 128, 186, 118, 138, 73, 5, 157, 225, 230, 23, 200, 228, 6, 0, 160, 182, 118, 241, 146, 58, 175, 242, 235, 114, 163, 106, 200, 76, 52, 92, 114, 85, 116, 204, 132, 146, 18, 136, 138, 134, 239, 59, 75, 134, 197, 116, 33, 208, 121, 208, 28, 8, 32, 72, 221, 85, 29, 174, 106, 199, 155, 148, 63, 197, 200, 7, 46, 28, 161, 115, 252, 8, 0, 0, 65, 9, 171, 252, 154, 213, 71, 32, 211, 120, 187, 39, 121, 115, 18, 180, 201, 240, 240, 216, 192, 112, 222, 137, 100, 220, 155, 60, 113, 109, 56, 121, 101, 152, 119, 234, 201, 44, 107, 118, 92, 216, 128, 92, 14, 41, 66, 5, 173, 160, 144, 50, 173, 63, 80, 161, 12, 128, 25, 249, 0, 1, 60, 81, 195, 46, 17, 128, 218, 29, 139, 151, 108, 86, 124, 205, 166, 164, 128, 31, 144, 114, 153, 252, 213, 171, 56, 208, 117, 69, 55, 189, 239, 63, 149, 55, 204, 241, 151, 243, 242, 142, 214, 39, 39, 213, 231, 149, 12, 95, 158, 159, 44, 169, 207, 203, 75, 180, 218, 117, 181, 214, 245, 1, 64, 114, 21, 34, 185, 29, 74, 4, 210, 74, 164, 88, 149, 56, 51, 242, 65, 188, 212, 105, 151, 0, 0, 90, 253, 88, 73, 141, 142, 27, 57, 54, 63, 41, 190, 138, 207, 175, 7, 44, 174, 228, 37, 244, 74, 93, 194, 245, 37, 113, 248, 174, 254, 218, 164, 250, 203, 127, 86, 127, 237, 230, 107, 239, 207, 31, 203, 203, 187, 156, 87, 31, 134, 144, 210, 32, 171, 98, 203, 232, 189, 100, 166, 202, 20, 2, 172, 177, 90, 228, 50, 10, 179, 27, 92, 22, 203, 142, 20, 2, 192, 6, 143, 151, 84, 201, 95, 211, 146, 35, 95, 98, 210, 21, 16, 235, 100, 50, 121, 229, 202, 164, 139, 201, 97, 95, 242, 98, 18, 139, 8, 211, 83, 220, 3, 243, 223, 15, 3, 23, 192, 73, 103, 223, 207, 187, 50, 118, 243, 181, 146, 250, 228, 159, 93, 76, 230, 189, 207, 115, 123, 48, 117, 224, 209, 161, 234, 2, 107, 133, 222, 231, 166, 168, 210, 33, 189, 218, 227, 176, 87, 42, 239, 202, 166, 94, 23, 20, 43, 190, 224, 210, 124, 15, 183, 29, 4, 201, 178, 100, 113, 173, 138, 86, 63, 190, 100, 29, 173, 215, 167, 234, 51, 124, 116, 254, 0, 39, 156, 154, 148, 7, 13, 154, 52, 127, 82, 201, 240, 169, 249, 99, 243, 75, 38, 77, 74, 164, 10, 94, 240, 222, 220, 149, 73, 151, 7, 134, 235, 231, 15, 95, 188, 57, 89, 82, 50, 144, 152, 148, 204, 59, 117, 118, 210, 149, 228, 205, 151, 137, 222, 128, 56, 138, 117, 234, 176, 1, 171, 19, 58, 154, 37, 201, 33, 192, 216, 84, 169, 7, 20, 53, 167, 122, 137, 99, 197, 143, 99, 146, 195, 194, 85, 45, 89, 173, 134, 160, 118, 7, 128, 80, 178, 153, 165, 213, 219, 200, 247, 222, 107, 147, 206, 38, 111, 62, 113, 234, 207, 78, 93, 195, 230, 37, 111, 94, 2, 61, 27, 230, 195, 97, 65, 136, 39, 40, 9, 87, 111, 62, 123, 54, 81, 159, 119, 53, 47, 239, 90, 222, 124, 224, 124, 208, 137, 8, 7, 178, 15, 37, 183, 93, 39, 179, 54, 174, 201, 158, 98, 179, 193, 217, 36, 81, 7, 188, 114, 56, 149, 122, 160, 194, 92, 225, 17, 71, 242, 43, 232, 10, 107, 33, 64, 16, 22, 63, 190, 228, 190, 191, 222, 92, 53, 156, 119, 214, 31, 134, 166, 36, 111, 190, 246, 243, 249, 3, 208, 172, 249, 239, 39, 254, 236, 90, 114, 210, 217, 163, 171, 255, 110, 245, 142, 29, 171, 69, 170, 189, 152, 151, 151, 247, 254, 181, 249, 121, 39, 234, 129, 249, 243, 74, 46, 95, 94, 114, 185, 254, 216, 192, 169, 146, 129, 212, 35, 143, 175, 165, 6, 132, 78, 177, 3, 250, 222, 69, 34, 117, 104, 139, 50, 64, 47, 199, 49, 156, 236, 51, 207, 49, 189, 66, 131, 161, 170, 116, 65, 144, 96, 88, 178, 26, 154, 92, 123, 172, 164, 228, 242, 177, 73, 215, 230, 215, 159, 61, 145, 151, 156, 116, 249, 253, 188, 139, 87, 225, 83, 53, 157, 186, 120, 246, 236, 169, 163, 103, 47, 94, 62, 123, 236, 226, 197, 139, 167, 106, 63, 61, 123, 236, 236, 167, 171, 143, 157, 104, 82, 254, 160, 75, 255, 65, 198, 227, 25, 130, 163, 9, 189, 190, 243, 41, 135, 152, 161, 98, 237, 89, 47, 73, 39, 22, 217, 70, 138, 6, 107, 74, 30, 215, 178, 1, 208, 209, 179, 147, 234, 235, 47, 214, 231, 157, 200, 155, 15, 42, 224, 196, 164, 19, 87, 39, 93, 91, 82, 114, 241, 68, 222, 69, 93, 196, 180, 180, 250, 241, 18, 121, 226, 156, 107, 193, 19, 250, 17, 183, 45, 119, 207, 112, 227, 90, 231, 51, 107, 49, 120, 149, 186, 222, 48, 72, 50, 110, 191, 19, 254, 43, 146, 162, 235, 150, 44, 222, 161, 121, 254, 159, 159, 45, 41, 41, 57, 123, 13, 254, 159, 72, 214, 31, 155, 127, 226, 98, 162, 254, 114, 61, 244, 245, 251, 159, 150, 148, 212, 214, 126, 211, 12, 4, 224, 98, 136, 166, 101, 227, 51, 236, 198, 181, 172, 94, 196, 157, 177, 224, 67, 151, 52, 193, 171, 211, 145, 91, 93, 26, 75, 178, 155, 78, 85, 86, 184, 166, 100, 201, 226, 52, 62, 184, 47, 250, 217, 216, 246, 99, 181, 167, 78, 157, 58, 118, 244, 83, 56, 212, 30, 251, 244, 232, 167, 120, 172, 253, 230, 15, 106, 107, 255, 66, 159, 105, 128, 78, 169, 62, 90, 188, 164, 70, 122, 232, 5, 118, 221, 136, 59, 211, 124, 52, 67, 0, 212, 3, 170, 46, 162, 21, 76, 223, 192, 73, 148, 50, 155, 158, 22, 103, 55, 151, 60, 174, 2, 97, 226, 185, 117, 37, 186, 29, 123, 223, 125, 6, 0, 92, 155, 15, 116, 236, 152, 250, 67, 234, 107, 103, 24, 5, 54, 158, 47, 160, 79, 250, 3, 170, 149, 134, 193, 114, 58, 185, 68, 163, 164, 55, 46, 80, 179, 185, 100, 9, 160, 176, 154, 8, 196, 15, 244, 155, 95, 91, 91, 2, 28, 112, 159, 226, 61, 90, 132, 197, 143, 63, 190, 100, 201, 229, 19, 39, 78, 76, 58, 11, 65, 166, 234, 244, 29, 143, 173, 147, 30, 218, 105, 191, 17, 149, 16, 250, 3, 170, 14, 209, 54, 102, 37, 105, 160, 195, 120, 96, 4, 96, 40, 89, 178, 228, 241, 199, 23, 3, 20, 96, 239, 106, 107, 63, 189, 136, 164, 70, 1, 49, 90, 189, 24, 44, 230, 146, 146, 146, 205, 85, 84, 219, 121, 255, 13, 28, 162, 209, 64, 205, 58, 64, 81, 169, 84, 86, 47, 113, 139, 15, 237, 92, 160, 219, 221, 24, 184, 7, 52, 107, 65, 25, 18, 163, 51, 160, 74, 108, 131, 60, 138, 147, 133, 200, 242, 14, 38, 54, 88, 168, 169, 218, 188, 110, 29, 168, 66, 240, 113, 242, 242, 78, 212, 62, 254, 248, 99, 139, 101, 33, 129, 191, 59, 74, 84, 21, 41, 237, 161, 208, 241, 171, 147, 46, 31, 39, 137, 223, 52, 219, 242, 216, 102, 241, 161, 23, 232, 50, 106, 185, 149, 20, 110, 28, 216, 100, 174, 94, 136, 145, 15, 50, 129, 83, 136, 90, 192, 149, 77, 14, 88, 7, 246, 0, 46, 239, 160, 209, 1, 198, 228, 25, 187, 122, 53, 49, 233, 42, 169, 86, 15, 253, 178, 68, 238, 92, 17, 0, 113, 17, 119, 4, 96, 120, 126, 201, 112, 168, 189, 133, 118, 165, 202, 197, 88, 125, 31, 125, 104, 183, 190, 168, 150, 91, 201, 20, 215, 118, 115, 185, 89, 70, 62, 40, 155, 134, 113, 23, 171, 239, 116, 166, 200, 141, 63, 79, 150, 119, 112, 231, 176, 197, 70, 75, 104, 116, 126, 125, 183, 148, 214, 11, 200, 16, 236, 88, 34, 87, 112, 6, 112, 105, 211, 127, 59, 59, 41, 25, 10, 144, 210, 94, 178, 50, 88, 167, 2, 130, 29, 75, 104, 222, 129, 77, 31, 111, 165, 84, 108, 13, 208, 132, 169, 153, 220, 44, 35, 31, 52, 228, 178, 103, 183, 6, 116, 121, 135, 92, 0, 224, 120, 104, 217, 65, 107, 106, 237, 183, 93, 127, 183, 67, 82, 241, 202, 101, 237, 199, 242, 142, 125, 17, 106, 75, 141, 5, 52, 42, 125, 237, 29, 143, 213, 144, 135, 118, 59, 244, 213, 128, 181, 58, 16, 106, 99, 217, 92, 235, 133, 210, 169, 210, 110, 224, 114, 33, 81, 135, 148, 46, 239, 224, 204, 5, 128, 43, 121, 39, 132, 80, 123, 185, 60, 53, 25, 36, 65, 20, 113, 240, 117, 222, 147, 218, 251, 111, 137, 146, 177, 80, 232, 233, 64, 91, 75, 75, 27, 206, 114, 36, 227, 97, 235, 30, 79, 41, 2, 234, 21, 177, 6, 193, 193, 198, 141, 129, 208, 206, 156, 235, 133, 52, 119, 121, 209, 254, 162, 145, 32, 209, 68, 173, 184, 188, 195, 79, 114, 0, 64, 128, 56, 31, 84, 129, 183, 113, 223, 65, 9, 0, 128, 224, 49, 145, 11, 86, 175, 254, 165, 136, 192, 23, 23, 142, 135, 26, 89, 17, 165, 182, 70, 58, 12, 86, 147, 98, 130, 199, 69, 191, 208, 165, 47, 169, 107, 25, 235, 250, 239, 85, 231, 86, 47, 164, 33, 6, 212, 161, 225, 32, 71, 138, 1, 190, 251, 221, 151, 44, 47, 167, 125, 107, 88, 181, 233, 187, 54, 233, 34, 102, 57, 219, 66, 1, 91, 1, 66, 208, 40, 66, 32, 121, 208, 171, 23, 75, 16, 132, 66, 21, 41, 137, 144, 234, 181, 214, 45, 214, 32, 96, 244, 240, 229, 15, 205, 156, 57, 147, 25, 71, 187, 21, 247, 224, 156, 79, 232, 186, 92, 106, 127, 217, 205, 89, 190, 49, 87, 29, 144, 27, 86, 109, 134, 47, 190, 79, 67, 91, 156, 184, 81, 100, 45, 88, 106, 45, 22, 87, 254, 147, 245, 225, 234, 197, 187, 196, 86, 115, 84, 39, 182, 181, 41, 38, 187, 87, 61, 150, 142, 0, 169, 47, 208, 62, 188, 124, 40, 182, 142, 119, 129, 32, 104, 197, 51, 246, 23, 245, 4, 201, 169, 84, 62, 160, 139, 45, 111, 189, 245, 163, 7, 10, 20, 191, 97, 60, 66, 198, 147, 44, 152, 199, 227, 1, 217, 198, 228, 18, 139, 249, 150, 162, 130, 245, 104, 186, 118, 137, 158, 193, 142, 199, 137, 46, 8, 52, 74, 166, 17, 57, 160, 93, 28, 7, 170, 145, 17, 120, 76, 12, 16, 117, 181, 117, 10, 0, 142, 44, 17, 53, 174, 25, 177, 107, 159, 128, 192, 64, 175, 240, 206, 173, 148, 59, 183, 203, 101, 217, 250, 214, 91, 191, 249, 205, 143, 30, 144, 115, 40, 90, 127, 189, 149, 238, 140, 224, 229, 121, 222, 227, 209, 14, 233, 85, 88, 109, 68, 31, 138, 66, 142, 114, 176, 148, 85, 24, 133, 246, 22, 121, 36, 172, 70, 82, 24, 181, 143, 137, 63, 151, 153, 3, 20, 191, 81, 80, 94, 93, 156, 75, 238, 136, 197, 225, 73, 109, 225, 157, 86, 235, 88, 214, 35, 2, 255, 252, 207, 191, 248, 209, 3, 52, 85, 175, 13, 50, 124, 137, 184, 23, 32, 104, 77, 24, 45, 162, 185, 141, 174, 127, 251, 30, 133, 96, 113, 103, 104, 169, 212, 120, 52, 134, 100, 133, 99, 113, 40, 159, 93, 34, 251, 14, 242, 213, 230, 226, 183, 234, 162, 2, 100, 5, 189, 149, 11, 244, 233, 169, 74, 183, 211, 197, 164, 125, 232, 86, 115, 28, 190, 181, 172, 23, 17, 248, 231, 127, 254, 13, 128, 240, 192, 3, 86, 208, 63, 214, 153, 214, 2, 235, 204, 130, 2, 91, 81, 81, 113, 121, 5, 23, 78, 36, 18, 126, 31, 175, 72, 131, 170, 200, 182, 84, 178, 249, 20, 130, 37, 235, 247, 42, 24, 64, 222, 1, 141, 122, 138, 50, 2, 37, 210, 67, 228, 16, 193, 234, 175, 92, 160, 79, 12, 58, 188, 105, 206, 6, 155, 38, 113, 36, 37, 246, 95, 231, 110, 123, 67, 68, 128, 208, 111, 148, 244, 139, 95, 252, 226, 165, 31, 33, 189, 244, 214, 111, 63, 51, 0, 0, 52, 65, 202, 233, 41, 129, 246, 45, 94, 26, 82, 2, 160, 120, 221, 162, 64, 96, 117, 149, 226, 65, 100, 206, 196, 250, 187, 204, 161, 76, 177, 201, 201, 86, 12, 30, 158, 80, 141, 24, 96, 10, 196, 67, 146, 212, 244, 200, 97, 17, 164, 101, 251, 244, 123, 191, 215, 249, 174, 2, 1, 125, 250, 13, 34, 241, 0, 114, 136, 173, 168, 92, 235, 193, 200, 221, 12, 14, 207, 142, 101, 33, 35, 194, 167, 144, 52, 97, 74, 8, 56, 57, 126, 115, 99, 89, 166, 211, 5, 214, 129, 36, 43, 88, 183, 78, 146, 167, 58, 151, 68, 122, 165, 50, 85, 10, 6, 128, 143, 71, 226, 97, 111, 24, 142, 126, 206, 23, 143, 52, 227, 138, 146, 101, 211, 190, 254, 176, 91, 232, 254, 215, 44, 8, 164, 56, 68, 134, 162, 192, 6, 50, 82, 84, 12, 82, 82, 205, 226, 212, 110, 148, 246, 191, 67, 13, 103, 8, 0, 198, 71, 146, 45, 216, 161, 104, 27, 169, 253, 130, 70, 75, 85, 105, 110, 0, 196, 109, 39, 164, 144, 16, 86, 100, 141, 92, 70, 213, 20, 169, 82, 112, 188, 252, 29, 83, 102, 79, 105, 138, 111, 190, 123, 246, 20, 129, 23, 242, 243, 123, 0, 128, 89, 51, 166, 79, 187, 243, 235, 47, 30, 234, 25, 29, 251, 223, 127, 52, 139, 130, 36, 44, 191, 32, 244, 18, 21, 19, 4, 229, 129, 249, 208, 180, 249, 7, 117, 183, 52, 18, 89, 192, 87, 133, 30, 209, 225, 195, 181, 37, 202, 7, 117, 187, 156, 105, 202, 128, 117, 3, 177, 232, 41, 0, 35, 112, 88, 186, 110, 48, 178, 144, 145, 228, 65, 84, 114, 53, 78, 234, 107, 154, 61, 60, 89, 136, 207, 171, 26, 200, 95, 71, 0, 24, 218, 78, 16, 120, 120, 237, 139, 135, 34, 209, 254, 243, 163, 99, 72, 9, 252, 255, 191, 79, 254, 246, 221, 93, 47, 1, 253, 34, 155, 124, 168, 233, 127, 125, 252, 209, 59, 123, 183, 110, 45, 45, 44, 213, 89, 54, 156, 243, 38, 18, 181, 59, 160, 253, 135, 15, 255, 165, 217, 36, 152, 211, 142, 255, 41, 60, 162, 186, 48, 157, 63, 115, 42, 24, 8, 231, 174, 220, 195, 231, 239, 142, 63, 248, 96, 211, 221, 17, 94, 232, 32, 0, 212, 5, 251, 23, 0, 2, 119, 222, 251, 15, 63, 101, 185, 61, 117, 117, 117, 29, 193, 161, 100, 236, 208, 33, 120, 37, 206, 240, 168, 46, 182, 89, 161, 111, 65, 19, 254, 226, 55, 166, 160, 248, 95, 95, 138, 180, 117, 155, 14, 11, 8, 137, 196, 98, 2, 192, 142, 7, 205, 182, 66, 17, 56, 137, 234, 194, 180, 30, 144, 148, 0, 245, 57, 125, 241, 123, 38, 223, 35, 240, 252, 237, 183, 47, 140, 111, 170, 108, 34, 0, 112, 92, 93, 52, 56, 235, 206, 59, 239, 159, 118, 255, 79, 201, 175, 120, 19, 177, 161, 228, 80, 157, 206, 99, 148, 23, 17, 36, 8, 20, 153, 184, 66, 6, 224, 203, 207, 239, 210, 2, 224, 227, 195, 77, 171, 17, 128, 195, 181, 198, 131, 186, 154, 223, 150, 35, 20, 55, 229, 5, 211, 19, 111, 69, 25, 160, 85, 10, 225, 217, 85, 241, 170, 252, 225, 41, 61, 241, 217, 155, 235, 188, 18, 0, 156, 231, 80, 172, 236, 206, 167, 124, 143, 78, 251, 111, 4, 1, 33, 113, 168, 95, 23, 129, 212, 227, 212, 108, 43, 42, 32, 88, 0, 125, 247, 71, 34, 201, 12, 242, 101, 138, 222, 181, 165, 139, 0, 161, 175, 149, 16, 4, 76, 179, 128, 42, 66, 193, 241, 48, 243, 139, 76, 186, 148, 75, 132, 8, 83, 34, 225, 248, 228, 129, 201, 241, 112, 213, 131, 126, 31, 238, 1, 36, 230, 4, 247, 28, 9, 222, 123, 239, 158, 67, 247, 222, 139, 125, 18, 78, 240, 61, 209, 100, 226, 144, 225, 61, 189, 32, 54, 66, 171, 15, 11, 94, 113, 204, 217, 159, 72, 128, 147, 196, 130, 215, 110, 67, 84, 126, 84, 88, 90, 186, 117, 235, 222, 119, 62, 166, 60, 96, 211, 105, 63, 199, 222, 74, 0, 56, 108, 218, 7, 82, 71, 40, 88, 44, 107, 53, 117, 29, 43, 150, 252, 187, 40, 179, 241, 85, 119, 207, 187, 123, 243, 192, 236, 252, 7, 167, 116, 248, 124, 205, 41, 0, 16, 130, 167, 190, 254, 112, 248, 181, 233, 143, 178, 60, 14, 246, 70, 1, 1, 35, 30, 240, 29, 234, 234, 106, 110, 238, 138, 14, 37, 112, 108, 216, 239, 5, 196, 6, 52, 181, 51, 108, 197, 242, 130, 15, 17, 129, 223, 45, 106, 9, 4, 210, 218, 207, 113, 15, 255, 53, 182, 255, 247, 111, 230, 2, 128, 34, 66, 1, 78, 157, 105, 210, 34, 84, 58, 112, 190, 131, 20, 117, 133, 193, 250, 133, 125, 225, 158, 142, 120, 221, 78, 79, 36, 200, 43, 179, 194, 123, 94, 188, 255, 235, 175, 133, 103, 204, 120, 109, 24, 218, 227, 241, 68, 13, 164, 192, 227, 111, 238, 34, 212, 140, 32, 32, 88, 62, 136, 18, 116, 127, 251, 142, 207, 17, 129, 143, 138, 7, 18, 3, 97, 127, 155, 106, 219, 7, 207, 159, 19, 14, 56, 173, 217, 149, 217, 128, 180, 17, 138, 211, 101, 110, 137, 134, 74, 187, 3, 43, 140, 193, 45, 38, 162, 224, 241, 145, 137, 124, 62, 15, 46, 156, 133, 252, 164, 200, 8, 177, 255, 240, 223, 190, 126, 239, 180, 105, 211, 23, 12, 97, 123, 234, 98, 201, 168, 78, 53, 167, 151, 23, 219, 47, 129, 16, 236, 231, 189, 58, 28, 64, 104, 46, 69, 224, 109, 82, 65, 192, 171, 238, 230, 36, 44, 240, 201, 251, 38, 154, 64, 72, 59, 12, 194, 162, 69, 200, 126, 161, 251, 59, 162, 115, 85, 73, 10, 171, 232, 82, 207, 12, 57, 144, 220, 160, 50, 37, 198, 174, 216, 120, 255, 180, 59, 167, 77, 159, 177, 189, 206, 195, 133, 99, 137, 164, 86, 13, 248, 142, 40, 219, 79, 168, 33, 193, 199, 19, 250, 245, 99, 62, 155, 10, 1, 250, 161, 136, 3, 213, 2, 151, 205, 178, 128, 222, 48, 72, 5, 182, 32, 115, 228, 224, 86, 70, 192, 136, 0, 93, 234, 153, 220, 133, 113, 160, 137, 84, 111, 188, 252, 253, 71, 94, 188, 119, 218, 244, 233, 51, 202, 162, 29, 231, 207, 31, 209, 10, 1, 31, 211, 180, 191, 171, 57, 8, 141, 211, 221, 74, 176, 39, 22, 179, 17, 77, 248, 241, 250, 243, 68, 86, 144, 3, 253, 3, 62, 63, 26, 180, 175, 19, 67, 112, 250, 132, 116, 178, 39, 203, 226, 131, 140, 124, 144, 201, 202, 97, 128, 227, 146, 93, 100, 13, 177, 78, 117, 209, 128, 203, 177, 138, 46, 245, 76, 111, 70, 68, 72, 157, 20, 253, 217, 35, 223, 127, 246, 225, 25, 211, 167, 79, 127, 180, 63, 26, 246, 196, 146, 65, 233, 78, 212, 12, 111, 74, 104, 154, 143, 8, 36, 18, 3, 58, 129, 162, 135, 172, 201, 104, 21, 93, 162, 245, 239, 157, 79, 224, 54, 164, 132, 23, 188, 96, 208, 158, 254, 26, 81, 131, 71, 59, 252, 180, 225, 158, 176, 224, 207, 180, 210, 148, 22, 0, 185, 172, 194, 165, 90, 13, 75, 209, 94, 205, 24, 153, 75, 187, 212, 115, 90, 86, 248, 251, 143, 60, 249, 236, 166, 89, 211, 203, 182, 207, 232, 226, 185, 67, 99, 146, 37, 16, 205, 48, 159, 72, 99, 128, 104, 20, 143, 231, 195, 94, 157, 76, 17, 135, 0, 196, 86, 21, 74, 62, 209, 222, 210, 45, 229, 8, 192, 64, 36, 220, 195, 111, 220, 249, 52, 85, 131, 181, 71, 19, 116, 153, 61, 47, 120, 136, 153, 22, 92, 209, 0, 160, 152, 2, 200, 146, 25, 108, 105, 141, 117, 59, 181, 249, 182, 85, 170, 165, 158, 49, 26, 86, 3, 112, 224, 192, 147, 143, 172, 120, 246, 103, 220, 130, 89, 253, 209, 89, 219, 235, 184, 126, 73, 11, 136, 102, 56, 158, 38, 1, 177, 161, 161, 32, 162, 16, 231, 117, 246, 81, 227, 124, 100, 65, 182, 254, 245, 31, 203, 110, 209, 71, 91, 75, 75, 215, 151, 174, 47, 180, 109, 59, 224, 89, 123, 47, 117, 134, 106, 207, 38, 112, 122, 157, 15, 218, 159, 219, 246, 20, 234, 130, 2, 172, 153, 82, 125, 173, 55, 68, 90, 161, 94, 234, 153, 84, 8, 168, 1, 168, 121, 18, 56, 96, 39, 119, 192, 53, 163, 121, 104, 86, 89, 115, 84, 146, 1, 209, 12, 243, 93, 105, 237, 143, 225, 159, 96, 208, 160, 235, 120, 4, 96, 40, 241, 198, 59, 95, 106, 232, 227, 119, 230, 254, 237, 95, 80, 0, 142, 129, 76, 12, 8, 3, 67, 67, 130, 222, 42, 141, 134, 164, 221, 124, 192, 141, 147, 183, 228, 119, 122, 202, 49, 109, 169, 103, 151, 6, 0, 174, 243, 201, 71, 158, 125, 22, 144, 237, 172, 155, 181, 125, 100, 142, 35, 153, 20, 55, 23, 18, 205, 176, 247, 144, 138, 255, 135, 98, 253, 244, 149, 96, 48, 251, 33, 74, 0, 72, 68, 108, 123, 63, 215, 98, 240, 249, 127, 36, 0, 36, 146, 167, 136, 94, 232, 143, 13, 213, 84, 20, 120, 175, 111, 135, 6, 201, 44, 152, 43, 17, 32, 222, 145, 26, 128, 157, 43, 86, 60, 251, 83, 174, 21, 247, 113, 157, 227, 24, 177, 63, 58, 38, 217, 1, 209, 12, 239, 81, 49, 64, 127, 87, 16, 68, 32, 216, 223, 229, 195, 237, 135, 116, 126, 1, 132, 96, 23, 105, 221, 153, 183, 215, 151, 254, 46, 29, 129, 149, 203, 136, 29, 72, 94, 62, 9, 103, 12, 197, 98, 31, 36, 60, 75, 195, 137, 204, 171, 244, 100, 39, 55, 106, 126, 199, 120, 1, 120, 249, 251, 207, 62, 91, 45, 110, 90, 248, 232, 130, 232, 140, 57, 35, 65, 159, 207, 135, 218, 153, 33, 102, 120, 163, 74, 4, 64, 254, 1, 129, 161, 46, 172, 160, 214, 127, 240, 104, 100, 127, 66, 162, 238, 109, 133, 91, 127, 253, 241, 231, 41, 94, 248, 156, 200, 192, 239, 147, 201, 83, 165, 219, 222, 139, 197, 130, 171, 122, 184, 165, 137, 156, 214, 161, 85, 82, 42, 94, 52, 157, 43, 112, 75, 0, 164, 24, 248, 167, 143, 60, 255, 51, 143, 180, 109, 227, 2, 176, 135, 179, 162, 177, 4, 209, 78, 12, 126, 205, 120, 52, 54, 48, 58, 20, 237, 138, 67, 60, 196, 235, 74, 1, 120, 2, 212, 7, 226, 19, 113, 76, 170, 151, 47, 47, 42, 42, 90, 52, 215, 86, 72, 20, 227, 87, 169, 47, 148, 76, 254, 235, 151, 123, 31, 88, 186, 75, 240, 122, 121, 147, 237, 215, 201, 140, 42, 226, 69, 179, 41, 51, 146, 36, 182, 212, 213, 237, 241, 138, 59, 54, 115, 220, 147, 143, 60, 95, 157, 218, 200, 215, 1, 14, 193, 140, 134, 126, 98, 158, 68, 0, 210, 253, 160, 32, 232, 193, 243, 9, 31, 154, 118, 61, 242, 199, 250, 177, 253, 97, 31, 215, 202, 135, 201, 41, 7, 106, 58, 59, 55, 117, 118, 238, 91, 100, 219, 250, 229, 58, 81, 6, 146, 103, 81, 45, 22, 70, 192, 159, 50, 183, 73, 77, 250, 10, 196, 228, 217, 82, 241, 162, 217, 100, 9, 229, 128, 177, 177, 68, 16, 100, 158, 118, 224, 247, 31, 217, 164, 0, 32, 24, 4, 30, 152, 177, 189, 63, 5, 128, 79, 211, 254, 40, 56, 130, 130, 215, 80, 114, 99, 49, 1, 1, 144, 223, 123, 17, 0, 32, 92, 91, 172, 200, 70, 100, 224, 147, 171, 127, 164, 34, 241, 206, 27, 102, 55, 233, 41, 208, 177, 22, 227, 88, 135, 132, 234, 128, 161, 161, 177, 100, 76, 106, 242, 147, 43, 42, 170, 229, 246, 119, 118, 36, 187, 102, 1, 19, 148, 197, 68, 43, 239, 225, 59, 52, 237, 71, 63, 144, 247, 27, 114, 110, 52, 22, 197, 240, 90, 86, 237, 98, 251, 1, 1, 79, 103, 162, 155, 4, 68, 135, 255, 248, 127, 68, 165, 240, 59, 179, 21, 179, 86, 157, 207, 198, 177, 14, 9, 73, 178, 90, 60, 158, 67, 67, 201, 126, 113, 251, 230, 21, 10, 0, 186, 35, 184, 194, 246, 172, 233, 51, 230, 148, 69, 105, 7, 183, 38, 162, 205, 154, 246, 67, 68, 44, 24, 75, 46, 223, 195, 251, 19, 66, 152, 79, 157, 80, 83, 33, 221, 255, 124, 231, 50, 154, 22, 73, 142, 137, 16, 236, 53, 201, 188, 186, 217, 32, 69, 188, 104, 114, 0, 141, 250, 1, 208, 185, 117, 253, 201, 40, 121, 166, 125, 43, 158, 175, 150, 1, 136, 196, 130, 201, 145, 40, 63, 203, 1, 14, 65, 148, 56, 105, 224, 199, 170, 21, 96, 148, 4, 2, 188, 42, 140, 81, 215, 23, 192, 221, 89, 175, 106, 243, 203, 206, 206, 214, 3, 7, 184, 198, 206, 78, 65, 144, 0, 248, 36, 153, 28, 123, 151, 32, 176, 200, 28, 0, 250, 196, 200, 241, 162, 118, 145, 109, 93, 114, 99, 157, 146, 69, 192, 25, 110, 35, 99, 29, 216, 230, 106, 0, 96, 159, 4, 64, 52, 58, 148, 196, 189, 54, 231, 148, 141, 60, 250, 104, 23, 185, 34, 158, 72, 15, 6, 154, 99, 233, 94, 160, 186, 190, 192, 163, 225, 215, 214, 3, 28, 189, 255, 0, 223, 249, 173, 195, 34, 11, 36, 63, 173, 93, 143, 0, 148, 94, 39, 0, 82, 180, 144, 121, 193, 77, 66, 210, 144, 145, 133, 52, 0, 34, 95, 68, 128, 251, 233, 203, 41, 29, 40, 45, 178, 207, 207, 177, 143, 216, 231, 28, 225, 60, 254, 214, 1, 112, 88, 212, 8, 52, 159, 79, 215, 255, 234, 236, 157, 22, 0, 128, 128, 222, 255, 139, 78, 137, 3, 14, 127, 90, 91, 91, 123, 116, 47, 6, 11, 215, 51, 155, 142, 145, 15, 102, 200, 206, 186, 201, 152, 129, 133, 136, 183, 39, 154, 28, 27, 138, 117, 236, 169, 60, 116, 36, 34, 68, 212, 0, 196, 248, 71, 31, 29, 41, 155, 211, 213, 74, 44, 122, 90, 56, 212, 220, 159, 72, 115, 0, 212, 218, 216, 195, 85, 232, 9, 108, 39, 192, 220, 219, 217, 185, 75, 4, 224, 40, 14, 149, 109, 53, 47, 3, 218, 5, 10, 228, 182, 51, 38, 219, 207, 58, 196, 177, 114, 11, 72, 176, 223, 207, 181, 70, 113, 191, 229, 61, 47, 214, 29, 249, 32, 26, 149, 0, 24, 162, 8, 244, 243, 79, 204, 73, 52, 204, 138, 6, 251, 81, 233, 165, 137, 64, 34, 29, 0, 181, 54, 246, 112, 213, 186, 10, 9, 0, 16, 224, 55, 68, 25, 216, 81, 15, 0, 252, 193, 52, 0, 6, 179, 26, 13, 79, 215, 171, 122, 114, 209, 226, 250, 74, 167, 197, 159, 8, 131, 167, 7, 108, 217, 209, 21, 243, 251, 195, 117, 235, 196, 246, 119, 131, 115, 154, 160, 59, 237, 68, 59, 157, 179, 202, 166, 207, 24, 26, 218, 189, 121, 221, 126, 185, 237, 13, 20, 128, 129, 244, 223, 83, 101, 239, 12, 231, 136, 119, 30, 64, 0, 34, 20, 129, 13, 103, 107, 79, 37, 175, 33, 0, 54, 51, 187, 213, 152, 216, 121, 75, 201, 34, 233, 85, 79, 36, 78, 192, 177, 5, 214, 141, 0, 196, 7, 60, 97, 31, 109, 115, 205, 63, 84, 134, 7, 146, 67, 93, 162, 17, 0, 4, 250, 147, 100, 167, 5, 190, 211, 133, 46, 81, 176, 106, 247, 238, 85, 155, 165, 190, 95, 69, 254, 36, 180, 75, 228, 49, 138, 236, 157, 241, 36, 121, 15, 2, 208, 93, 79, 1, 56, 119, 10, 248, 15, 109, 225, 86, 214, 196, 106, 88, 214, 172, 237, 87, 157, 146, 86, 245, 4, 238, 79, 101, 37, 153, 222, 15, 193, 179, 203, 105, 1, 255, 179, 213, 67, 182, 45, 71, 35, 80, 9, 78, 65, 50, 17, 4, 141, 72, 196, 63, 218, 65, 16, 0, 173, 80, 133, 8, 52, 40, 152, 127, 243, 238, 46, 146, 15, 212, 250, 128, 140, 124, 200, 4, 0, 215, 141, 63, 121, 142, 234, 193, 111, 157, 131, 136, 224, 18, 6, 72, 115, 179, 239, 86, 99, 198, 200, 165, 3, 160, 244, 17, 89, 92, 136, 194, 97, 119, 218, 237, 110, 223, 145, 30, 75, 92, 72, 180, 122, 5, 14, 226, 129, 206, 242, 21, 24, 9, 116, 196, 198, 146, 35, 65, 10, 64, 108, 85, 48, 57, 4, 175, 144, 61, 102, 76, 119, 136, 10, 96, 55, 182, 125, 85, 51, 213, 129, 90, 239, 53, 13, 0, 35, 126, 37, 160, 11, 162, 12, 28, 5, 22, 24, 35, 222, 224, 115, 25, 87, 195, 34, 100, 66, 3, 40, 1, 208, 245, 17, 113, 37, 24, 167, 47, 62, 47, 223, 194, 129, 4, 8, 224, 172, 183, 182, 118, 62, 191, 162, 156, 88, 193, 142, 232, 24, 8, 63, 34, 16, 217, 21, 141, 37, 163, 40, 3, 157, 157, 117, 79, 240, 61, 34, 2, 11, 197, 255, 232, 6, 104, 157, 192, 52, 0, 244, 28, 119, 36, 31, 178, 64, 183, 104, 9, 191, 117, 254, 34, 149, 129, 47, 109, 89, 87, 195, 50, 67, 42, 53, 161, 55, 181, 194, 141, 203, 99, 245, 204, 94, 25, 177, 248, 195, 100, 222, 163, 183, 85, 25, 9, 116, 244, 211, 205, 182, 86, 225, 86, 51, 67, 251, 99, 221, 146, 251, 74, 17, 216, 45, 169, 128, 230, 104, 150, 4, 142, 71, 220, 253, 64, 143, 252, 66, 183, 240, 197, 31, 14, 203, 44, 112, 145, 248, 66, 95, 206, 53, 92, 13, 43, 7, 170, 38, 33, 177, 180, 235, 8, 163, 25, 83, 112, 145, 185, 166, 194, 228, 224, 107, 22, 49, 93, 113, 30, 58, 89, 25, 9, 116, 196, 112, 159, 169, 88, 16, 141, 225, 174, 88, 68, 14, 16, 210, 236, 96, 71, 150, 7, 241, 232, 237, 125, 34, 145, 215, 15, 63, 188, 129, 34, 176, 44, 113, 106, 44, 65, 92, 129, 223, 253, 173, 209, 106, 88, 50, 153, 198, 70, 100, 4, 70, 62, 136, 4, 237, 71, 91, 208, 117, 207, 238, 168, 4, 64, 130, 135, 72, 224, 217, 106, 101, 44, 136, 149, 18, 201, 88, 52, 56, 118, 53, 152, 66, 0, 152, 64, 1, 65, 179, 241, 16, 50, 37, 84, 130, 198, 70, 11, 1, 16, 93, 129, 13, 39, 206, 94, 75, 246, 254, 154, 140, 33, 148, 115, 89, 0, 176, 102, 249, 85, 36, 50, 106, 32, 206, 29, 102, 228, 3, 33, 151, 180, 58, 92, 93, 48, 63, 95, 6, 64, 80, 71, 2, 148, 186, 18, 160, 1, 48, 38, 138, 69, 37, 41, 232, 228, 21, 92, 208, 156, 45, 128, 207, 188, 86, 12, 74, 223, 9, 145, 5, 54, 36, 64, 15, 190, 77, 18, 102, 54, 150, 243, 50, 153, 174, 179, 102, 249, 85, 36, 162, 121, 172, 116, 13, 107, 70, 62, 16, 74, 21, 232, 242, 241, 184, 12, 64, 162, 123, 185, 22, 128, 206, 32, 0, 64, 54, 28, 236, 82, 50, 129, 16, 36, 99, 195, 93, 205, 217, 36, 32, 51, 0, 30, 50, 243, 90, 84, 131, 203, 206, 163, 41, 164, 195, 40, 115, 189, 137, 112, 171, 241, 165, 166, 130, 61, 60, 167, 220, 102, 211, 170, 96, 87, 74, 132, 124, 66, 79, 85, 10, 128, 196, 198, 21, 202, 124, 152, 232, 15, 39, 163, 253, 65, 220, 119, 110, 36, 24, 12, 166, 62, 230, 187, 19, 231, 163, 193, 174, 96, 214, 12, 78, 102, 14, 104, 21, 20, 44, 112, 52, 113, 234, 218, 177, 29, 165, 100, 212, 96, 17, 124, 49, 192, 235, 100, 25, 196, 45, 72, 77, 76, 169, 178, 234, 126, 10, 206, 159, 61, 85, 107, 230, 239, 152, 189, 82, 1, 128, 239, 249, 229, 229, 105, 237, 239, 24, 75, 4, 163, 137, 100, 48, 26, 77, 142, 140, 13, 241, 10, 4, 128, 123, 195, 188, 63, 107, 246, 5, 1, 200, 96, 182, 125, 137, 148, 22, 248, 86, 34, 1, 65, 81, 9, 25, 69, 249, 117, 49, 10, 165, 206, 5, 104, 31, 159, 179, 62, 103, 98, 74, 149, 190, 241, 133, 0, 80, 57, 102, 192, 135, 249, 20, 0, 231, 189, 222, 231, 170, 211, 0, 8, 146, 141, 7, 49, 36, 130, 87, 81, 41, 157, 213, 217, 205, 175, 245, 122, 127, 108, 102, 17, 84, 4, 192, 200, 17, 64, 82, 176, 192, 134, 207, 18, 103, 33, 38, 90, 72, 18, 198, 91, 107, 48, 149, 232, 215, 48, 16, 113, 17, 171, 205, 76, 169, 178, 89, 213, 203, 135, 16, 210, 169, 40, 72, 1, 16, 247, 250, 170, 211, 1, 136, 129, 31, 24, 13, 10, 208, 245, 53, 29, 29, 171, 36, 0, 60, 94, 31, 121, 0, 38, 235, 83, 80, 14, 200, 80, 207, 68, 98, 108, 17, 128, 101, 126, 239, 155, 181, 71, 47, 83, 111, 96, 107, 185, 162, 164, 32, 69, 0, 64, 181, 201, 212, 103, 185, 118, 51, 48, 183, 93, 59, 85, 43, 5, 64, 79, 101, 101, 221, 145, 104, 164, 67, 41, 1, 201, 161, 126, 63, 231, 241, 122, 224, 117, 77, 133, 216, 254, 3, 228, 65, 228, 67, 102, 34, 93, 104, 77, 251, 80, 25, 159, 122, 253, 16, 142, 19, 33, 248, 214, 39, 171, 56, 238, 40, 120, 196, 84, 17, 190, 67, 170, 42, 210, 252, 44, 246, 161, 153, 214, 239, 129, 91, 203, 102, 77, 125, 234, 149, 207, 176, 14, 157, 249, 25, 20, 0, 232, 231, 232, 17, 127, 56, 62, 76, 246, 154, 149, 49, 8, 38, 131, 173, 100, 240, 186, 243, 128, 167, 174, 2, 8, 184, 159, 114, 37, 35, 31, 244, 136, 143, 156, 233, 139, 68, 120, 9, 128, 244, 103, 209, 204, 202, 217, 2, 8, 108, 56, 124, 250, 36, 199, 53, 93, 77, 38, 71, 75, 105, 142, 120, 61, 65, 64, 148, 130, 138, 7, 170, 177, 83, 203, 215, 50, 255, 104, 157, 105, 181, 150, 103, 147, 1, 189, 213, 67, 116, 198, 203, 1, 0, 127, 103, 247, 121, 178, 201, 104, 120, 207, 158, 61, 135, 58, 130, 49, 176, 121, 67, 168, 225, 35, 193, 14, 0, 64, 220, 26, 196, 203, 121, 168, 244, 155, 24, 187, 20, 206, 156, 19, 41, 204, 241, 242, 249, 138, 209, 106, 237, 172, 156, 85, 27, 118, 156, 6, 250, 229, 22, 238, 3, 120, 146, 51, 20, 129, 207, 11, 187, 81, 50, 121, 98, 106, 138, 37, 119, 138, 65, 183, 246, 123, 15, 49, 89, 30, 66, 171, 120, 244, 234, 5, 16, 0, 14, 56, 125, 4, 68, 61, 248, 227, 127, 144, 66, 161, 68, 82, 218, 123, 55, 25, 83, 168, 33, 31, 15, 148, 117, 12, 63, 114, 238, 156, 4, 64, 31, 128, 113, 46, 218, 71, 181, 185, 98, 126, 148, 206, 24, 198, 178, 79, 17, 128, 211, 27, 182, 112, 152, 25, 56, 81, 64, 211, 228, 191, 222, 114, 254, 252, 182, 210, 82, 149, 44, 147, 150, 51, 89, 199, 209, 53, 28, 192, 218, 245, 75, 170, 0, 128, 142, 177, 36, 154, 120, 69, 36, 208, 209, 21, 3, 153, 128, 64, 48, 57, 100, 114, 172, 70, 34, 63, 182, 63, 122, 46, 122, 230, 92, 4, 219, 207, 133, 17, 140, 51, 61, 234, 147, 116, 226, 211, 234, 183, 9, 0, 167, 151, 85, 215, 36, 78, 30, 171, 173, 181, 210, 49, 212, 207, 75, 145, 25, 62, 87, 38, 202, 24, 249, 144, 145, 216, 244, 8, 68, 191, 255, 9, 0, 67, 164, 253, 7, 86, 60, 169, 52, 2, 88, 177, 146, 0, 11, 192, 251, 114, 89, 249, 48, 34, 246, 253, 25, 145, 1, 56, 207, 25, 124, 25, 225, 84, 14, 152, 110, 124, 250, 30, 69, 160, 138, 91, 133, 11, 22, 212, 47, 250, 181, 98, 32, 253, 215, 229, 169, 139, 25, 249, 144, 153, 210, 0, 48, 154, 77, 204, 90, 246, 116, 209, 113, 33, 140, 4, 106, 82, 0, 28, 240, 144, 48, 185, 231, 208, 17, 243, 60, 224, 239, 59, 23, 147, 229, 255, 220, 57, 84, 130, 194, 153, 230, 186, 14, 4, 0, 87, 108, 177, 187, 20, 173, 72, 175, 121, 91, 181, 131, 34, 176, 165, 224, 77, 204, 17, 191, 95, 92, 168, 24, 72, 95, 158, 94, 17, 146, 107, 250, 156, 53, 154, 155, 85, 105, 129, 136, 143, 104, 253, 138, 21, 207, 111, 98, 21, 38, 176, 21, 66, 53, 33, 220, 115, 228, 136, 233, 202, 157, 72, 20, 27, 221, 23, 197, 46, 223, 222, 16, 62, 132, 53, 59, 117, 118, 187, 195, 33, 62, 2, 86, 173, 74, 43, 64, 203, 135, 20, 173, 58, 76, 0, 216, 245, 215, 92, 61, 120, 3, 201, 15, 216, 20, 19, 160, 12, 168, 3, 128, 204, 75, 68, 105, 200, 229, 48, 250, 198, 105, 25, 195, 33, 17, 80, 126, 47, 63, 242, 228, 179, 63, 83, 134, 2, 94, 193, 239, 241, 241, 71, 122, 76, 214, 172, 180, 246, 33, 227, 115, 100, 153, 245, 86, 182, 210, 225, 194, 150, 179, 78, 123, 144, 85, 172, 111, 35, 173, 1, 199, 200, 7, 5, 45, 251, 4, 218, 255, 201, 239, 55, 172, 242, 28, 189, 150, 76, 94, 107, 226, 158, 183, 237, 21, 235, 41, 62, 90, 196, 170, 57, 192, 56, 201, 160, 71, 172, 211, 112, 174, 9, 107, 233, 32, 174, 77, 231, 129, 239, 211, 242, 40, 232, 122, 9, 128, 156, 244, 159, 40, 253, 125, 135, 28, 78, 178, 234, 52, 46, 255, 90, 233, 178, 151, 53, 171, 24, 200, 69, 22, 69, 118, 56, 159, 192, 55, 76, 250, 61, 14, 64, 251, 193, 35, 186, 239, 222, 154, 95, 190, 151, 76, 94, 109, 130, 118, 46, 95, 62, 151, 50, 65, 193, 76, 117, 81, 152, 53, 203, 243, 40, 57, 196, 173, 55, 97, 82, 34, 75, 39, 125, 66, 246, 145, 71, 158, 125, 86, 124, 14, 17, 129, 44, 63, 161, 164, 214, 62, 0, 160, 15, 0, 104, 112, 116, 69, 35, 132, 103, 80, 230, 88, 71, 115, 36, 130, 85, 70, 78, 186, 16, 115, 37, 93, 146, 27, 254, 232, 79, 149, 216, 117, 154, 56, 197, 255, 249, 222, 42, 136, 140, 147, 151, 201, 34, 84, 207, 125, 68, 17, 248, 184, 112, 145, 18, 2, 171, 209, 163, 208, 97, 133, 135, 190, 151, 58, 217, 237, 200, 52, 215, 72, 170, 17, 194, 26, 209, 159, 138, 175, 69, 14, 200, 220, 102, 37, 17, 219, 127, 38, 118, 46, 184, 221, 190, 253, 92, 95, 68, 112, 224, 66, 95, 206, 237, 107, 153, 200, 153, 136, 189, 172, 161, 199, 229, 112, 145, 249, 95, 164, 217, 248, 100, 78, 29, 153, 132, 223, 171, 22, 115, 3, 95, 187, 31, 60, 194, 177, 99, 181, 187, 241, 243, 69, 82, 149, 221, 231, 133, 138, 110, 45, 54, 146, 1, 58, 172, 96, 83, 228, 84, 141, 12, 32, 37, 9, 128, 151, 1, 128, 151, 197, 215, 7, 48, 25, 97, 126, 243, 163, 86, 236, 250, 115, 193, 134, 237, 142, 50, 135, 195, 1, 29, 30, 105, 112, 108, 47, 43, 179, 55, 56, 252, 168, 23, 206, 52, 160, 19, 232, 112, 176, 149, 84, 21, 145, 137, 112, 96, 15, 100, 197, 212, 218, 215, 231, 231, 248, 62, 172, 77, 119, 46, 164, 8, 252, 197, 195, 77, 215, 142, 45, 91, 182, 140, 204, 230, 45, 159, 43, 21, 152, 109, 53, 161, 252, 53, 195, 10, 238, 204, 53, 229, 18, 0, 207, 2, 0, 255, 67, 249, 133, 233, 205, 143, 122, 160, 141, 205, 208, 228, 221, 117, 145, 51, 29, 101, 14, 103, 228, 76, 20, 154, 220, 16, 117, 148, 117, 81, 104, 136, 67, 224, 74, 49, 189, 219, 233, 116, 57, 237, 169, 79, 124, 224, 43, 156, 57, 215, 135, 30, 163, 243, 254, 175, 137, 8, 60, 177, 159, 76, 46, 36, 197, 237, 220, 242, 82, 209, 36, 170, 227, 59, 189, 128, 71, 179, 201, 70, 6, 5, 168, 2, 96, 5, 26, 1, 229, 23, 102, 23, 154, 70, 237, 215, 188, 189, 57, 122, 238, 76, 95, 95, 15, 91, 233, 239, 139, 110, 183, 59, 42, 215, 70, 250, 182, 67, 203, 225, 203, 24, 89, 182, 153, 85, 61, 44, 46, 178, 73, 38, 133, 186, 113, 50, 187, 0, 119, 232, 106, 112, 190, 214, 5, 74, 211, 125, 255, 127, 166, 8, 220, 250, 148, 19, 218, 127, 153, 234, 1, 142, 93, 100, 35, 170, 96, 175, 202, 5, 174, 208, 25, 36, 5, 239, 242, 57, 171, 114, 88, 193, 145, 101, 82, 129, 4, 0, 53, 2, 105, 0, 152, 136, 188, 209, 248, 7, 237, 209, 115, 61, 2, 196, 61, 108, 165, 243, 80, 7, 206, 80, 217, 184, 137, 139, 136, 118, 33, 170, 119, 21, 107, 39, 171, 111, 131, 165, 116, 56, 92, 125, 219, 203, 236, 101, 219, 241, 29, 230, 106, 239, 253, 47, 34, 2, 143, 218, 107, 65, 21, 94, 251, 64, 188, 228, 185, 173, 162, 71, 164, 188, 141, 118, 109, 90, 112, 48, 173, 140, 114, 88, 193, 46, 110, 4, 98, 8, 0, 21, 114, 133, 17, 16, 201, 76, 209, 145, 31, 2, 191, 232, 185, 134, 178, 32, 225, 114, 14, 55, 31, 112, 56, 220, 149, 78, 215, 83, 224, 231, 118, 40, 28, 194, 116, 194, 137, 60, 116, 233, 117, 28, 163, 114, 108, 7, 253, 129, 11, 244, 147, 185, 163, 211, 36, 4, 54, 98, 108, 156, 76, 74, 8, 216, 244, 134, 207, 53, 245, 130, 16, 41, 226, 226, 27, 140, 252, 129, 203, 105, 16, 5, 137, 74, 206, 178, 118, 83, 15, 248, 58, 74, 35, 32, 82, 150, 205, 143, 90, 59, 34, 125, 49, 108, 96, 179, 35, 122, 46, 162, 106, 165, 219, 69, 252, 92, 136, 4, 163, 125, 18, 52, 106, 114, 59, 20, 51, 58, 201, 94, 37, 14, 167, 179, 18, 193, 112, 185, 28, 83, 239, 19, 17, 216, 212, 132, 214, 48, 121, 148, 86, 119, 47, 210, 42, 1, 221, 109, 5, 136, 195, 192, 40, 63, 209, 103, 1, 81, 201, 89, 58, 234, 48, 120, 105, 85, 24, 1, 137, 24, 157, 181, 186, 100, 226, 203, 28, 246, 237, 196, 242, 219, 29, 13, 231, 116, 199, 200, 57, 254, 181, 142, 142, 115, 61, 154, 175, 56, 140, 141, 85, 43, 89, 224, 104, 181, 211, 141, 79, 233, 118, 58, 42, 43, 239, 76, 33, 112, 49, 153, 60, 85, 91, 75, 16, 208, 5, 64, 39, 247, 57, 83, 250, 249, 44, 36, 42, 57, 203, 185, 142, 62, 8, 94, 127, 150, 110, 4, 184, 204, 113, 151, 191, 204, 222, 28, 41, 3, 159, 191, 172, 204, 105, 119, 234, 12, 15, 224, 117, 236, 19, 206, 237, 93, 0, 64, 214, 169, 77, 44, 56, 10, 226, 73, 61, 188, 175, 149, 187, 83, 244, 7, 110, 221, 89, 119, 150, 172, 222, 182, 31, 190, 121, 254, 99, 115, 85, 84, 218, 199, 118, 233, 46, 110, 45, 42, 57, 203, 185, 158, 158, 51, 2, 175, 49, 2, 186, 119, 146, 201, 191, 187, 161, 185, 140, 181, 59, 65, 153, 243, 29, 187, 245, 236, 12, 94, 199, 58, 236, 101, 193, 8, 167, 251, 243, 106, 114, 87, 98, 247, 123, 234, 234, 26, 26, 182, 219, 65, 156, 238, 164, 147, 41, 118, 220, 202, 122, 176, 122, 166, 246, 242, 9, 15, 87, 141, 14, 209, 214, 172, 183, 210, 187, 187, 174, 33, 20, 149, 156, 5, 133, 220, 163, 53, 2, 92, 70, 0, 58, 202, 206, 0, 111, 87, 210, 53, 169, 140, 167, 239, 1, 103, 55, 235, 195, 175, 71, 158, 237, 101, 13, 205, 13, 46, 59, 250, 178, 19, 40, 2, 37, 203, 88, 14, 162, 99, 16, 132, 179, 77, 28, 154, 129, 95, 107, 7, 26, 171, 179, 77, 162, 101, 13, 6, 83, 169, 146, 179, 128, 144, 243, 125, 94, 141, 17, 200, 72, 188, 35, 72, 179, 28, 198, 63, 202, 162, 203, 135, 118, 206, 120, 41, 252, 116, 242, 159, 59, 35, 244, 113, 236, 51, 118, 240, 101, 215, 254, 121, 137, 56, 92, 82, 205, 237, 63, 134, 170, 240, 234, 7, 152, 28, 250, 152, 218, 65, 207, 219, 89, 72, 113, 91, 227, 133, 213, 24, 84, 114, 22, 6, 247, 70, 255, 25, 0, 240, 83, 131, 211, 180, 196, 55, 52, 159, 203, 12, 128, 3, 181, 26, 110, 198, 101, 24, 135, 235, 81, 43, 137, 63, 159, 118, 61, 221, 211, 225, 103, 111, 221, 65, 71, 11, 190, 85, 193, 213, 157, 34, 25, 202, 165, 41, 59, 248, 118, 247, 133, 75, 25, 232, 66, 119, 10, 1, 214, 56, 20, 98, 240, 96, 97, 34, 62, 117, 36, 144, 157, 58, 202, 208, 252, 101, 203, 19, 184, 156, 196, 213, 113, 186, 42, 115, 42, 117, 0, 223, 171, 174, 239, 76, 31, 235, 16, 231, 87, 31, 254, 214, 74, 142, 251, 0, 60, 130, 83, 4, 128, 185, 139, 158, 135, 219, 189, 157, 177, 253, 128, 64, 10, 128, 12, 43, 235, 49, 120, 176, 144, 163, 38, 18, 200, 68, 126, 112, 124, 98, 193, 51, 244, 141, 164, 0, 244, 27, 201, 186, 221, 24, 2, 131, 28, 144, 181, 143, 43, 77, 204, 100, 1, 199, 203, 221, 115, 46, 118, 38, 82, 247, 147, 122, 113, 220, 120, 21, 199, 53, 157, 189, 92, 187, 80, 140, 139, 183, 46, 98, 223, 206, 220, 254, 75, 151, 222, 174, 144, 30, 200, 97, 188, 222, 48, 105, 58, 117, 133, 159, 212, 49, 2, 74, 234, 233, 233, 59, 87, 86, 86, 22, 36, 189, 222, 188, 29, 195, 95, 158, 104, 23, 220, 80, 198, 137, 17, 103, 70, 102, 119, 211, 93, 16, 92, 70, 203, 236, 43, 9, 125, 47, 247, 153, 190, 136, 155, 173, 92, 32, 149, 14, 128, 42, 4, 93, 88, 34, 207, 58, 90, 148, 29, 128, 98, 209, 73, 206, 186, 180, 34, 5, 64, 207, 8, 40, 72, 32, 78, 125, 164, 185, 172, 25, 172, 186, 223, 14, 126, 3, 58, 120, 96, 4, 64, 195, 147, 121, 171, 208, 176, 44, 186, 14, 60, 28, 167, 221, 196, 58, 143, 212, 247, 226, 207, 96, 233, 137, 235, 190, 13, 146, 24, 128, 242, 175, 169, 95, 42, 1, 240, 142, 8, 64, 119, 119, 247, 201, 75, 151, 122, 155, 122, 241, 205, 96, 231, 165, 94, 248, 160, 187, 151, 114, 64, 193, 34, 91, 233, 214, 194, 69, 11, 244, 23, 110, 76, 3, 64, 27, 9, 168, 136, 151, 242, 188, 125, 142, 230, 62, 161, 161, 225, 12, 25, 244, 65, 35, 151, 203, 178, 46, 232, 238, 153, 178, 8, 140, 124, 112, 45, 147, 120, 96, 3, 78, 247, 124, 179, 80, 202, 14, 81, 0, 6, 191, 114, 215, 93, 63, 188, 212, 125, 219, 95, 221, 214, 13, 239, 254, 230, 43, 151, 106, 238, 186, 235, 174, 175, 84, 17, 0, 108, 123, 201, 153, 239, 175, 203, 182, 155, 9, 1, 32, 179, 17, 192, 176, 14, 58, 61, 22, 59, 215, 215, 12, 109, 46, 235, 19, 67, 252, 172, 145, 102, 58, 85, 58, 76, 77, 103, 99, 228, 3, 142, 153, 81, 0, 14, 147, 128, 192, 179, 255, 237, 111, 144, 28, 41, 5, 224, 228, 29, 120, 252, 171, 170, 75, 111, 223, 117, 233, 82, 211, 93, 183, 225, 187, 222, 219, 6, 9, 0, 20, 169, 238, 55, 247, 127, 249, 78, 230, 250, 99, 2, 64, 102, 35, 128, 13, 38, 97, 15, 230, 61, 236, 219, 207, 192, 123, 129, 196, 82, 110, 123, 101, 150, 132, 81, 250, 160, 138, 203, 97, 98, 201, 95, 70, 62, 112, 116, 220, 20, 0, 120, 243, 244, 233, 38, 100, 130, 58, 58, 116, 76, 1, 232, 188, 163, 24, 204, 1, 116, 255, 224, 109, 151, 122, 239, 232, 253, 10, 126, 246, 195, 121, 151, 82, 0, 156, 124, 243, 205, 127, 193, 234, 211, 140, 225, 48, 30, 50, 27, 1, 225, 92, 80, 12, 237, 207, 52, 59, 28, 93, 208, 253, 81, 116, 36, 129, 165, 157, 217, 18, 70, 218, 81, 37, 205, 62, 117, 25, 136, 60, 119, 245, 183, 54, 108, 216, 80, 242, 254, 233, 211, 167, 137, 57, 176, 41, 0, 232, 253, 225, 202, 187, 238, 184, 116, 27, 8, 253, 87, 46, 221, 213, 116, 9, 1, 184, 112, 91, 111, 10, 128, 223, 109, 126, 243, 36, 57, 221, 154, 225, 71, 8, 0, 89, 140, 0, 116, 249, 153, 62, 204, 238, 156, 11, 118, 149, 69, 34, 231, 130, 61, 24, 74, 64, 247, 103, 77, 24, 105, 1, 96, 215, 226, 142, 100, 36, 117, 235, 206, 166, 56, 69, 90, 184, 108, 179, 125, 59, 25, 52, 57, 122, 128, 91, 244, 121, 10, 0, 164, 175, 244, 222, 209, 13, 124, 255, 246, 109, 63, 252, 225, 87, 86, 94, 186, 180, 242, 175, 68, 43, 240, 229, 231, 165, 139, 158, 47, 127, 168, 84, 30, 88, 201, 8, 64, 22, 35, 64, 9, 60, 85, 224, 132, 51, 103, 98, 68, 1, 60, 5, 65, 118, 246, 132, 145, 22, 0, 12, 65, 158, 177, 59, 158, 217, 84, 233, 116, 178, 166, 116, 72, 37, 186, 149, 39, 232, 184, 217, 233, 63, 44, 35, 203, 114, 136, 74, 16, 152, 224, 43, 23, 254, 230, 135, 151, 170, 238, 234, 94, 183, 110, 29, 170, 63, 162, 14, 137, 18, 92, 206, 226, 234, 201, 236, 34, 98, 59, 11, 53, 119, 197, 181, 153, 128, 123, 31, 126, 130, 0, 240, 8, 157, 50, 157, 133, 34, 56, 208, 201, 247, 69, 207, 181, 226, 146, 36, 14, 51, 179, 212, 180, 0, 144, 40, 156, 125, 2, 130, 4, 187, 9, 167, 128, 60, 42, 154, 207, 147, 167, 37, 58, 252, 128, 173, 180, 144, 2, 80, 117, 219, 29, 183, 85, 129, 244, 223, 65, 155, 13, 34, 80, 117, 151, 228, 7, 112, 210, 250, 249, 132, 101, 52, 81, 244, 253, 95, 173, 169, 217, 127, 203, 137, 19, 223, 254, 54, 2, 96, 58, 18, 8, 99, 0, 176, 187, 3, 58, 197, 101, 110, 183, 108, 125, 0, 128, 115, 88, 135, 41, 167, 128, 80, 37, 180, 100, 11, 25, 55, 163, 244, 89, 231, 50, 81, 4, 46, 244, 74, 156, 160, 113, 132, 240, 66, 162, 111, 73, 30, 165, 148, 251, 153, 136, 118, 77, 83, 211, 7, 39, 78, 156, 45, 249, 11, 136, 49, 111, 73, 38, 187, 102, 89, 184, 64, 224, 229, 71, 254, 251, 190, 125, 166, 118, 43, 7, 39, 61, 226, 119, 80, 61, 198, 100, 74, 24, 25, 2, 32, 167, 26, 95, 52, 161, 13, 165, 245, 95, 192, 203, 100, 87, 41, 32, 56, 157, 221, 19, 196, 139, 201, 118, 143, 239, 124, 249, 229, 191, 252, 185, 197, 114, 235, 254, 15, 78, 156, 58, 90, 82, 242, 181, 178, 228, 208, 212, 169, 142, 163, 255, 161, 182, 246, 44, 0, 16, 155, 106, 9, 133, 66, 251, 214, 188, 186, 239, 87, 242, 186, 159, 153, 8, 93, 194, 136, 35, 195, 0, 103, 86, 0, 20, 156, 3, 194, 237, 204, 28, 44, 139, 38, 83, 76, 59, 108, 233, 148, 33, 200, 30, 12, 157, 61, 246, 194, 15, 234, 143, 157, 61, 75, 204, 230, 222, 251, 190, 153, 220, 30, 76, 206, 250, 249, 215, 254, 195, 15, 110, 25, 154, 213, 21, 180, 124, 10, 0, 156, 154, 48, 150, 28, 154, 136, 0, 188, 10, 0, 224, 10, 128, 217, 119, 65, 140, 40, 194, 224, 113, 2, 160, 228, 28, 136, 148, 204, 56, 135, 114, 2, 117, 213, 6, 17, 130, 15, 179, 133, 195, 221, 201, 19, 63, 248, 121, 237, 169, 100, 55, 169, 58, 92, 248, 131, 255, 148, 116, 148, 37, 23, 252, 228, 47, 255, 178, 246, 155, 13, 211, 167, 151, 53, 3, 0, 245, 239, 207, 154, 184, 96, 194, 189, 8, 192, 43, 0, 128, 185, 141, 13, 90, 207, 164, 234, 93, 174, 3, 0, 197, 117, 149, 68, 21, 24, 36, 149, 196, 134, 167, 226, 57, 118, 225, 178, 63, 80, 4, 50, 231, 67, 186, 199, 146, 255, 19, 0, 248, 52, 73, 43, 111, 151, 254, 245, 173, 99, 13, 115, 146, 101, 223, 190, 239, 107, 181, 223, 156, 245, 147, 91, 127, 50, 189, 230, 38, 188, 231, 253, 95, 125, 152, 67, 0, 214, 172, 217, 183, 207, 228, 78, 184, 125, 173, 17, 221, 52, 175, 1, 101, 7, 128, 174, 42, 91, 169, 171, 15, 196, 65, 173, 180, 145, 212, 3, 59, 78, 43, 233, 147, 195, 245, 157, 187, 150, 45, 92, 181, 5, 44, 91, 77, 211, 254, 55, 235, 235, 143, 93, 188, 124, 249, 234, 213, 159, 255, 160, 182, 246, 211, 107, 165, 226, 184, 234, 127, 220, 59, 243, 166, 205, 183, 126, 245, 153, 9, 220, 19, 247, 114, 15, 127, 245, 254, 84, 248, 14, 0, 4, 214, 128, 14, 84, 46, 244, 197, 53, 182, 27, 73, 67, 142, 43, 124, 152, 0, 0, 139, 103, 92, 14, 93, 73, 192, 205, 50, 200, 110, 64, 106, 170, 88, 182, 225, 247, 167, 211, 233, 147, 63, 156, 252, 229, 134, 101, 203, 182, 120, 86, 173, 90, 181, 165, 198, 83, 83, 245, 147, 205, 53, 64, 54, 49, 126, 252, 234, 151, 95, 222, 241, 231, 204, 83, 236, 211, 154, 233, 120, 0, 192, 175, 214, 188, 178, 239, 87, 82, 139, 27, 165, 197, 243, 233, 170, 225, 1, 67, 40, 76, 145, 169, 2, 43, 178, 224, 157, 174, 42, 192, 96, 222, 169, 99, 45, 217, 45, 7, 126, 171, 129, 64, 134, 226, 232, 225, 223, 118, 239, 90, 118, 223, 178, 42, 0, 163, 32, 181, 130, 205, 59, 140, 238, 116, 60, 139, 100, 4, 136, 10, 104, 108, 9, 4, 90, 26, 197, 157, 177, 189, 141, 109, 109, 129, 212, 178, 160, 184, 127, 132, 118, 93, 188, 27, 0, 0, 137, 148, 221, 110, 157, 40, 9, 139, 108, 12, 18, 58, 21, 203, 150, 29, 54, 196, 224, 244, 233, 79, 127, 254, 79, 20, 140, 151, 10, 183, 138, 121, 148, 143, 172, 11, 171, 117, 166, 227, 89, 36, 35, 208, 40, 238, 144, 205, 109, 82, 237, 12, 238, 85, 47, 141, 44, 66, 97, 118, 39, 24, 211, 37, 118, 46, 169, 150, 74, 69, 118, 123, 101, 134, 253, 82, 182, 108, 233, 252, 195, 39, 6, 0, 188, 255, 243, 247, 229, 215, 191, 221, 69, 108, 97, 233, 39, 191, 255, 195, 7, 245, 16, 89, 117, 119, 195, 97, 195, 135, 226, 209, 34, 26, 129, 128, 184, 65, 56, 18, 89, 16, 223, 219, 6, 127, 60, 45, 235, 3, 102, 123, 91, 143, 114, 169, 49, 212, 225, 117, 220, 51, 38, 163, 153, 172, 94, 181, 108, 89, 73, 247, 201, 223, 107, 112, 248, 167, 159, 159, 82, 188, 35, 170, 112, 239, 250, 127, 213, 131, 202, 66, 141, 64, 64, 177, 60, 182, 204, 236, 129, 150, 64, 104, 95, 233, 191, 23, 0, 227, 216, 46, 78, 186, 114, 213, 194, 101, 203, 90, 171, 54, 236, 56, 44, 41, 199, 19, 10, 6, 0, 18, 215, 51, 219, 186, 94, 23, 0, 98, 4, 168, 238, 11, 52, 182, 164, 51, 123, 142, 143, 146, 86, 87, 147, 17, 128, 198, 64, 91, 118, 223, 211, 60, 49, 108, 245, 150, 31, 238, 252, 199, 101, 64, 157, 251, 127, 80, 127, 88, 97, 41, 118, 73, 203, 153, 149, 106, 33, 176, 16, 35, 176, 147, 112, 62, 110, 239, 162, 106, 191, 66, 212, 205, 173, 202, 145, 94, 87, 147, 17, 0, 175, 98, 247, 137, 27, 64, 170, 130, 134, 234, 45, 91, 86, 113, 53, 4, 140, 247, 54, 28, 62, 188, 116, 175, 84, 104, 85, 186, 75, 3, 192, 190, 53, 175, 112, 191, 2, 77, 23, 240, 166, 107, 187, 84, 251, 43, 76, 110, 19, 156, 94, 87, 147, 89, 4, 2, 57, 237, 192, 158, 149, 82, 81, 134, 122, 129, 122, 182, 98, 203, 150, 10, 171, 92, 107, 86, 186, 158, 168, 191, 110, 241, 104, 1, 35, 240, 202, 190, 246, 0, 23, 106, 107, 76, 147, 129, 113, 60, 67, 122, 93, 77, 54, 0, 188, 28, 215, 22, 16, 235, 241, 204, 151, 165, 25, 254, 186, 20, 101, 232, 213, 69, 149, 203, 149, 183, 159, 151, 46, 34, 37, 75, 44, 57, 90, 66, 175, 172, 0, 43, 40, 234, 192, 64, 203, 38, 89, 252, 205, 239, 122, 150, 162, 244, 186, 154, 236, 34, 80, 46, 47, 54, 107, 186, 44, 205, 144, 24, 249, 160, 191, 137, 147, 77, 134, 160, 48, 149, 35, 179, 16, 35, 192, 114, 162, 18, 132, 235, 169, 62, 108, 131, 174, 25, 199, 51, 164, 165, 73, 50, 2, 128, 91, 212, 216, 222, 144, 89, 205, 108, 89, 154, 49, 49, 226, 193, 229, 54, 200, 53, 61, 111, 251, 72, 134, 64, 42, 183, 178, 4, 0, 128, 87, 65, 4, 218, 229, 39, 9, 72, 182, 112, 156, 15, 161, 72, 147, 100, 145, 241, 64, 160, 218, 35, 27, 155, 113, 172, 133, 165, 253, 109, 114, 200, 48, 28, 152, 130, 224, 119, 115, 233, 239, 88, 246, 97, 36, 112, 156, 184, 123, 180, 197, 237, 215, 161, 3, 210, 67, 157, 44, 0, 224, 98, 188, 30, 178, 43, 89, 227, 184, 214, 194, 210, 167, 204, 165, 161, 207, 217, 164, 33, 198, 66, 50, 102, 100, 121, 121, 205, 43, 175, 254, 10, 52, 126, 74, 235, 7, 254, 221, 0, 16, 9, 126, 11, 21, 161, 169, 44, 163, 9, 202, 82, 26, 154, 130, 160, 20, 17, 176, 128, 17, 120, 181, 5, 122, 32, 197, 241, 84, 29, 180, 175, 63, 168, 185, 52, 251, 2, 47, 227, 2, 160, 69, 228, 61, 198, 68, 150, 49, 59, 153, 72, 182, 47, 167, 11, 62, 227, 52, 117, 206, 242, 202, 154, 159, 66, 147, 21, 249, 64, 209, 16, 182, 180, 104, 1, 200, 190, 192, 75, 26, 153, 180, 243, 45, 212, 18, 50, 242, 225, 122, 200, 204, 94, 131, 220, 162, 189, 210, 128, 137, 101, 205, 154, 103, 95, 85, 5, 184, 200, 0, 123, 245, 21, 160, 92, 137, 109, 182, 230, 35, 35, 0, 154, 13, 194, 25, 249, 112, 61, 148, 65, 5, 74, 244, 188, 173, 116, 235, 151, 226, 128, 9, 2, 176, 15, 85, 160, 152, 248, 160, 221, 95, 168, 235, 3, 236, 92, 203, 206, 44, 32, 202, 186, 194, 106, 110, 233, 191, 76, 0, 136, 19, 219, 216, 20, 14, 140, 124, 208, 57, 219, 172, 155, 100, 130, 1, 82, 147, 145, 182, 202, 0, 112, 180, 253, 141, 162, 9, 56, 168, 224, 0, 182, 162, 192, 106, 125, 0, 159, 118, 83, 206, 11, 188, 152, 16, 129, 34, 115, 19, 192, 76, 187, 73, 58, 243, 163, 211, 104, 121, 106, 165, 103, 194, 1, 43, 20, 35, 227, 41, 71, 152, 178, 0, 139, 155, 184, 23, 20, 225, 42, 166, 44, 173, 94, 201, 109, 129, 151, 27, 231, 235, 155, 117, 147, 12, 118, 113, 84, 82, 169, 220, 126, 44, 186, 179, 172, 81, 141, 140, 183, 75, 94, 128, 94, 42, 80, 84, 209, 140, 249, 199, 54, 6, 192, 6, 200, 154, 191, 143, 105, 55, 41, 203, 244, 16, 164, 114, 226, 10, 189, 243, 206, 71, 191, 46, 69, 135, 24, 57, 64, 53, 50, 30, 200, 228, 4, 48, 242, 193, 28, 221, 56, 14, 48, 235, 38, 185, 179, 106, 128, 229, 104, 2, 11, 171, 171, 233, 250, 42, 0, 64, 218, 200, 184, 2, 0, 114, 74, 185, 92, 143, 93, 94, 164, 6, 192, 196, 66, 224, 55, 48, 220, 53, 229, 38, 233, 101, 86, 185, 180, 217, 95, 68, 5, 164, 198, 203, 17, 0, 61, 14, 104, 199, 36, 64, 117, 81, 129, 85, 177, 58, 165, 205, 198, 224, 31, 134, 190, 51, 51, 119, 81, 23, 0, 157, 26, 127, 51, 196, 152, 112, 147, 244, 156, 192, 198, 52, 113, 174, 64, 27, 176, 87, 238, 244, 52, 29, 32, 110, 149, 25, 90, 127, 112, 169, 181, 160, 184, 32, 109, 191, 207, 34, 101, 155, 43, 76, 88, 66, 93, 0, 114, 89, 67, 94, 65, 140, 124, 48, 36, 221, 25, 194, 141, 233, 121, 39, 50, 33, 93, 158, 123, 131, 0, 188, 172, 60, 91, 180, 131, 223, 216, 167, 247, 11, 229, 218, 117, 73, 50, 146, 30, 0, 134, 107, 139, 101, 33, 70, 62, 24, 146, 91, 47, 17, 224, 73, 31, 245, 86, 207, 189, 177, 172, 81, 151, 8, 138, 237, 95, 106, 144, 15, 41, 55, 189, 229, 37, 253, 113, 157, 207, 198, 187, 72, 26, 35, 31, 140, 200, 148, 19, 44, 151, 76, 136, 100, 121, 229, 251, 42, 45, 136, 219, 196, 182, 238, 45, 204, 57, 31, 172, 79, 55, 52, 231, 151, 149, 76, 56, 193, 72, 213, 123, 149, 50, 96, 121, 117, 197, 10, 85, 137, 28, 246, 127, 97, 166, 177, 47, 253, 85, 82, 245, 73, 5, 128, 137, 88, 242, 186, 200, 36, 3, 136, 50, 32, 217, 1, 203, 190, 53, 170, 249, 98, 168, 4, 182, 22, 30, 12, 25, 103, 236, 193, 51, 30, 159, 39, 136, 126, 236, 63, 126, 239, 58, 87, 74, 52, 38, 214, 110, 142, 1, 68, 67, 40, 149, 206, 89, 218, 1, 0, 165, 12, 4, 140, 61, 65, 137, 204, 171, 66, 21, 0, 232, 199, 62, 84, 156, 97, 177, 208, 235, 35, 211, 12, 32, 178, 0, 89, 200, 220, 195, 91, 56, 148, 1, 133, 39, 112, 240, 96, 218, 144, 136, 14, 145, 69, 125, 204, 144, 18, 0, 226, 199, 206, 44, 42, 202, 176, 88, 232, 245, 144, 51, 135, 194, 229, 229, 180, 120, 176, 154, 45, 111, 250, 204, 194, 237, 83, 26, 194, 10, 235, 250, 128, 137, 116, 152, 21, 245, 64, 65, 81, 86, 109, 160, 4, 128, 248, 177, 223, 120, 104, 39, 136, 64, 69, 110, 11, 96, 152, 33, 77, 251, 51, 198, 207, 116, 85, 134, 130, 242, 109, 239, 109, 177, 112, 45, 16, 13, 72, 101, 146, 16, 244, 101, 151, 0, 174, 136, 78, 90, 102, 203, 179, 70, 52, 74, 0, 136, 31, 107, 37, 177, 36, 91, 36, 94, 151, 211, 22, 186, 153, 200, 37, 174, 17, 153, 162, 140, 241, 243, 78, 58, 92, 90, 248, 153, 31, 75, 101, 95, 145, 189, 225, 242, 165, 45, 162, 35, 176, 41, 151, 65, 154, 98, 107, 145, 129, 82, 144, 0, 40, 46, 42, 166, 126, 172, 85, 118, 164, 241, 144, 211, 198, 66, 153, 72, 103, 149, 184, 204, 241, 243, 115, 116, 164, 108, 235, 114, 4, 224, 85, 0, 224, 101, 242, 241, 193, 144, 148, 17, 8, 24, 160, 167, 191, 172, 57, 64, 160, 191, 102, 30, 5, 96, 203, 210, 45, 196, 129, 122, 200, 10, 29, 206, 40, 239, 150, 161, 77, 185, 145, 182, 236, 54, 75, 252, 188, 156, 102, 133, 62, 90, 4, 0, 52, 174, 89, 67, 237, 0, 202, 190, 152, 15, 8, 24, 160, 103, 51, 98, 89, 41, 52, 210, 236, 175, 192, 181, 224, 86, 228, 226, 51, 177, 55, 32, 231, 167, 37, 221, 169, 153, 217, 226, 231, 229, 116, 192, 248, 115, 172, 21, 126, 245, 239, 215, 60, 251, 63, 90, 200, 22, 185, 156, 52, 38, 96, 128, 158, 45, 155, 222, 211, 236, 175, 208, 150, 74, 47, 48, 242, 225, 198, 146, 51, 93, 254, 9, 101, 139, 159, 159, 167, 122, 0, 0, 8, 88, 191, 49, 247, 27, 115, 151, 238, 37, 131, 131, 141, 232, 8, 112, 134, 232, 21, 103, 83, 90, 105, 251, 43, 84, 84, 80, 64, 255, 132, 62, 177, 203, 72, 148, 152, 76, 241, 51, 240, 104, 53, 89, 176, 204, 2, 158, 239, 3, 208, 126, 82, 43, 27, 8, 136, 234, 127, 124, 131, 52, 24, 45, 171, 69, 175, 194, 202, 82, 14, 24, 239, 254, 81, 217, 201, 216, 254, 51, 242, 65, 135, 200, 16, 7, 174, 206, 2, 0, 188, 81, 248, 234, 220, 53, 88, 45, 173, 40, 137, 97, 198, 53, 72, 83, 108, 85, 139, 30, 113, 25, 3, 127, 66, 14, 216, 201, 64, 251, 141, 194, 11, 70, 62, 232, 16, 29, 226, 88, 190, 21, 1, 104, 95, 31, 152, 139, 117, 82, 164, 62, 194, 204, 197, 198, 4, 45, 86, 48, 143, 184, 33, 148, 167, 165, 237, 186, 170, 45, 51, 208, 166, 103, 22, 56, 13, 243, 244, 140, 124, 208, 146, 184, 216, 76, 203, 65, 27, 0, 176, 180, 61, 180, 232, 239, 73, 189, 184, 185, 139, 51, 81, 185, 53, 197, 60, 172, 149, 213, 233, 120, 15, 231, 53, 87, 26, 101, 38, 120, 124, 186, 242, 219, 142, 140, 123, 17, 24, 17, 29, 226, 216, 20, 106, 217, 104, 9, 60, 0, 206, 255, 190, 191, 127, 5, 100, 32, 144, 146, 84, 70, 62, 168, 169, 58, 91, 28, 84, 173, 186, 52, 29, 0, 111, 91, 75, 40, 144, 33, 210, 84, 61, 99, 246, 129, 72, 232, 199, 103, 236, 11, 50, 238, 69, 96, 64, 132, 71, 61, 237, 109, 155, 88, 139, 21, 107, 52, 2, 180, 86, 208, 4, 85, 88, 179, 157, 193, 200, 7, 21, 0, 94, 172, 8, 18, 139, 79, 76, 62, 99, 150, 157, 38, 104, 63, 62, 179, 192, 62, 142, 8, 155, 240, 40, 184, 123, 88, 33, 66, 180, 52, 86, 139, 154, 171, 9, 202, 110, 8, 171, 173, 224, 247, 210, 133, 237, 37, 0, 196, 18, 108, 60, 122, 2, 230, 0, 80, 46, 9, 5, 142, 38, 254, 42, 155, 254, 211, 164, 31, 157, 11, 198, 181, 23, 1, 131, 53, 90, 12, 169, 21, 70, 194, 58, 17, 208, 2, 227, 168, 139, 210, 33, 136, 117, 196, 32, 71, 6, 32, 164, 32, 115, 38, 81, 61, 16, 73, 83, 208, 5, 105, 142, 56, 233, 71, 183, 157, 25, 207, 67, 50, 40, 138, 140, 12, 128, 100, 7, 110, 12, 2, 50, 165, 3, 224, 229, 26, 219, 76, 90, 68, 221, 129, 200, 162, 244, 224, 147, 193, 209, 176, 39, 198, 243, 104, 213, 208, 254, 138, 185, 165, 165, 34, 0, 161, 229, 127, 251, 234, 206, 95, 253, 201, 0, 144, 38, 33, 228, 112, 169, 60, 16, 153, 49, 235, 192, 224, 34, 13, 204, 56, 158, 44, 16, 218, 86, 196, 22, 80, 79, 144, 208, 175, 230, 254, 247, 87, 247, 153, 124, 66, 157, 13, 47, 13, 72, 2, 160, 197, 235, 201, 21, 0, 89, 155, 102, 180, 59, 140, 124, 200, 141, 218, 193, 229, 45, 250, 6, 186, 194, 255, 23, 173, 60, 17, 44, 251, 36, 61, 236, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130]; +// Will be replaced automatically by GitHub Actions +final landTileBytes = []; diff --git a/tile_server/static/generated/sea.dart b/tile_server/static/generated/sea.dart index f3707c9e..196bd5af 100644 --- a/tile_server/static/generated/sea.dart +++ b/tile_server/static/generated/sea.dart @@ -1 +1,2 @@ -final seaTileBytes = [137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 1, 0, 0, 0, 1, 0, 1, 3, 0, 0, 0, 102, 188, 58, 37, 0, 0, 0, 3, 80, 76, 84, 69, 170, 211, 223, 207, 236, 188, 245, 0, 0, 0, 31, 73, 68, 65, 84, 104, 129, 237, 193, 1, 13, 0, 0, 0, 194, 160, 247, 79, 109, 14, 55, 160, 0, 0, 0, 0, 0, 0, 0, 0, 190, 13, 33, 0, 0, 1, 154, 96, 225, 213, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130]; +// Will be replaced automatically by GitHub Actions +final seaTileBytes = []; From 0b5d495fe1afa4b3addb39f0411ad9356159c8ef Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Tue, 27 Feb 2024 21:44:27 +0000 Subject: [PATCH 116/168] Updated GitHub Workflow Former-commit-id: d34118ef2f167879bcbe719c2e815ca1da30c6da [formerly 2bc5c3f850b8d52f7a09c13a5161d846d961fa28] Former-commit-id: 19fc07672c7f2d32a17d80995e7adfa454308d4c --- .github/workflows/main.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2721f15d..62b5538d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,7 +15,7 @@ jobs: - name: Checkout Repository uses: actions/checkout@v3 - name: Run Dart Package Analyser - uses: axel-op/dart-package-analyzer@master + uses: JaffaKetchup/dart-package-analyzer@master id: analysis with: githubToken: ${{ secrets.GITHUB_TOKEN }} @@ -40,10 +40,14 @@ jobs: uses: subosito/flutter-action@v2 with: channel: "stable" - - name: Get Package & Example Dependencies + - name: Get Package Dependencies run: flutter pub get + - name: Get Example Dependencies + run: flutter pub get -C example - name: Get Test Tile Server Dependencies run: dart pub get -C tile_server + - name: Generate Code + run: dart run build_runner build - name: Check Formatting run: dart format --output=none --set-exit-if-changed . - name: Check Lints @@ -59,6 +63,8 @@ jobs: uses: subosito/flutter-action@v2 with: channel: "stable" + - name: Generate Code + run: dart run build_runner build - name: Run Tests run: flutter test -r expanded From c0fe062dce08339c3c6ce29e087b2ecd030eb368 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Tue, 27 Feb 2024 22:00:34 +0000 Subject: [PATCH 117/168] Updated GitHub Workflow Former-commit-id: fff2195f46ee555641660e65bb02709d2510a3b9 [formerly 84b0a6090a62f884bb522d4c43277e2f6d96f38d] Former-commit-id: e80e620c4269840ab9f9c59a3af33c43cf8ff43f --- .github/workflows/main.yml | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 62b5538d..08cb885d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,9 +13,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Repository - uses: actions/checkout@v3 + uses: actions/checkout@master - name: Run Dart Package Analyser - uses: JaffaKetchup/dart-package-analyzer@master + uses: axel-op/dart-package-analyzer@master id: analysis with: githubToken: ${{ secrets.GITHUB_TOKEN }} @@ -35,9 +35,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Repository - uses: actions/checkout@v3 + uses: actions/checkout@master - name: Setup Flutter Environment - uses: subosito/flutter-action@v2 + uses: subosito/flutter-action@master with: channel: "stable" - name: Get Package Dependencies @@ -58,9 +58,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Repository - uses: actions/checkout@v3 + uses: actions/checkout@master - name: Setup Flutter Environment - uses: subosito/flutter-action@v2 + uses: subosito/flutter-action@master with: channel: "stable" - name: Generate Code @@ -71,20 +71,20 @@ jobs: build-demo-android: name: "Build Demo App (Android)" runs-on: ubuntu-latest - needs: [analyse-code, run-tests, score-package] + needs: [analyse-code, run-tests] defaults: run: working-directory: ./example steps: - name: Checkout Repository - uses: actions/checkout@v3 + uses: actions/checkout@master - name: Setup Java 17 Environment uses: actions/setup-java@v3 with: distribution: "temurin" java-version: "17" - name: Setup Flutter Environment - uses: subosito/flutter-action@v2 + uses: subosito/flutter-action@master with: channel: "stable" - name: Generate Code @@ -101,15 +101,15 @@ jobs: build-demo-windows: name: "Build Demo App (Windows)" runs-on: windows-latest - needs: [analyse-code, run-tests, score-package] + needs: [analyse-code, run-tests] defaults: run: working-directory: ./example steps: - name: Checkout Repository - uses: actions/checkout@v3 + uses: actions/checkout@master - name: Setup Flutter Environment - uses: subosito/flutter-action@v2 + uses: subosito/flutter-action@master with: channel: "stable" - name: Generate Code @@ -129,15 +129,15 @@ jobs: build-tile-server-windows: name: "Build Tile Server (Windows)" runs-on: windows-latest - needs: [analyse-code, run-tests, score-package] + needs: [analyse-code, run-tests] defaults: run: working-directory: ./tile_server steps: - name: Checkout Repository - uses: actions/checkout@v3 + uses: actions/checkout@master - name: Setup Dart Environment - uses: dart-lang/setup-dart@v1.5.0 + uses: dart-lang/setup-dart@v1.6.2 - name: Get Dependencies run: dart pub get - name: Generate Tile Images @@ -145,7 +145,7 @@ jobs: - name: Compile run: dart compile exe bin/tile_server.dart - name: Upload Artifact - uses: actions/upload-artifact@v3.1.2 + uses: actions/upload-artifact@v4.3.1 with: name: windows-ts path: tile_server/bin/tile_server.exe @@ -154,15 +154,15 @@ jobs: build-tile-server-linux: name: "Build Tile Server (Linux/Ubuntu)" runs-on: ubuntu-latest - needs: [analyse-code, run-tests, score-package] + needs: [analyse-code, run-tests] defaults: run: working-directory: ./tile_server steps: - name: Checkout Repository - uses: actions/checkout@v3 + uses: actions/checkout@master - name: Setup Dart Environment - uses: dart-lang/setup-dart@v1.5.0 + uses: dart-lang/setup-dart@v1.6.2 - name: Get Dependencies run: dart pub get - name: Run Pre-Compile Generator From 6345ea44fc665ff4473544684f30114b959b8dd8 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Tue, 27 Feb 2024 22:06:58 +0000 Subject: [PATCH 118/168] Updated GitHub Workflow Former-commit-id: 850dd35b5272fa431fc918ed90f12abb724258d7 [formerly 364cab2b94b10289c790179bad8d4032a8014767] Former-commit-id: 993322e48d4cd1f78f0438985892951f7191a4eb --- .github/workflows/main.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 08cb885d..5c5d5680 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -63,6 +63,8 @@ jobs: uses: subosito/flutter-action@master with: channel: "stable" + - name: Get Dependencies + run: flutter pub get - name: Generate Code run: dart run build_runner build - name: Run Tests @@ -87,6 +89,8 @@ jobs: uses: subosito/flutter-action@master with: channel: "stable" + - name: Get Dependencies + run: flutter pub get - name: Generate Code run: dart run build_runner build - name: Build @@ -112,6 +116,8 @@ jobs: uses: subosito/flutter-action@master with: channel: "stable" + - name: Get Dependencies + run: flutter pub get - name: Generate Code run: dart run build_runner build - name: Build From 987c1d99fe5add671eff88378e5b1bff6eb161c2 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Tue, 27 Feb 2024 22:18:51 +0000 Subject: [PATCH 119/168] Updated GitHub Workflow Former-commit-id: 2c23791dbad4f2cbc9fb317ccca4ebffbd20fcd7 [formerly 6edf96fb1c0d370c0a04244187ebf19d30db8792] Former-commit-id: b19504ff2b2179306629b4834844c9df87d49c75 --- .github/workflows/main.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5c5d5680..5f886060 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -42,6 +42,8 @@ jobs: channel: "stable" - name: Get Package Dependencies run: flutter pub get + - name: Get Dart Package Dependencies + run: dart pub get - name: Get Example Dependencies run: flutter pub get -C example - name: Get Test Tile Server Dependencies @@ -65,6 +67,8 @@ jobs: channel: "stable" - name: Get Dependencies run: flutter pub get + - name: Get Dart Dependencies + run: dart pub get - name: Generate Code run: dart run build_runner build - name: Run Tests @@ -91,6 +95,8 @@ jobs: channel: "stable" - name: Get Dependencies run: flutter pub get + - name: Get Dart Dependencies + run: dart pub get - name: Generate Code run: dart run build_runner build - name: Build @@ -118,6 +124,8 @@ jobs: channel: "stable" - name: Get Dependencies run: flutter pub get + - name: Get Dart Dependencies + run: dart pub get - name: Generate Code run: dart run build_runner build - name: Build @@ -146,6 +154,8 @@ jobs: uses: dart-lang/setup-dart@v1.6.2 - name: Get Dependencies run: dart pub get + - name: Get Dart Dependencies + run: dart pub get - name: Generate Tile Images run: dart run bin/generate_dart_images.dart - name: Compile From ae854c9f3902c9b3f3dc23bc7b2e58c4efcf93f5 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Tue, 27 Feb 2024 22:34:26 +0000 Subject: [PATCH 120/168] Updated GitHub Workflow Former-commit-id: f2f4a6c3ea63281f7b41eb50100546457a845dff [formerly 7e0b89d627e7d90636fbc4cb7b4f6b8ba1d912bf] Former-commit-id: 09a6bd26ee14ceed0c535d9b73b75574db0b0155 --- .github/workflows/main.yml | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5f886060..916c13af 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -42,8 +42,6 @@ jobs: channel: "stable" - name: Get Package Dependencies run: flutter pub get - - name: Get Dart Package Dependencies - run: dart pub get - name: Get Example Dependencies run: flutter pub get -C example - name: Get Test Tile Server Dependencies @@ -67,8 +65,6 @@ jobs: channel: "stable" - name: Get Dependencies run: flutter pub get - - name: Get Dart Dependencies - run: dart pub get - name: Generate Code run: dart run build_runner build - name: Run Tests @@ -95,10 +91,10 @@ jobs: channel: "stable" - name: Get Dependencies run: flutter pub get - - name: Get Dart Dependencies - run: dart pub get + working-directory: . - name: Generate Code run: dart run build_runner build + working-directory: . - name: Build run: flutter build apk --obfuscate --split-debug-info=./symbols - name: Upload Artifact @@ -124,10 +120,10 @@ jobs: channel: "stable" - name: Get Dependencies run: flutter pub get - - name: Get Dart Dependencies - run: dart pub get + working-directory: . - name: Generate Code run: dart run build_runner build + working-directory: . - name: Build run: flutter build windows --obfuscate --split-debug-info=./symbols - name: Create Installer From 7a3adac4d55832065626bdf24535952cf445b7b0 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Tue, 27 Feb 2024 22:49:48 +0000 Subject: [PATCH 121/168] Updated Windows installation bundler setup config Former-commit-id: 0d6ba16622d27da05102f31b27211bac9862184c [formerly ac4fa8451a6eb275f8e9ac40073002ba5dfe2762] Former-commit-id: 1671660fb0ad8f55b09e675066a5ebfd74db90d9 --- windowsApplicationInstallerSetup.iss | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/windowsApplicationInstallerSetup.iss b/windowsApplicationInstallerSetup.iss index d8fbeadb..a2b52f32 100644 --- a/windowsApplicationInstallerSetup.iss +++ b/windowsApplicationInstallerSetup.iss @@ -65,13 +65,11 @@ Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{ ; Specify all files within 'build/windows/runner/Release' except 'example.exe' [Files] -Source: "example\build\windows\runner\Release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion -Source: "example\build\windows\runner\Release\flutter_windows.dll"; DestDir: "{app}"; Flags: ignoreversion -Source: "example\build\windows\runner\Release\isar_flutter_libs_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion -Source: "example\build\windows\runner\Release\isar.dll"; DestDir: "{app}"; Flags: ignoreversion -Source: "example\build\windows\runner\Release\share_plus_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion -Source: "example\build\windows\runner\Release\url_launcher_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion -Source: "example\build\windows\runner\Release\data\*"; DestDir: "{app}\data"; Flags: ignoreversion recursesubdirs createallsubdirs +Source: "example\build\windows\x64\runner\Release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion +Source: "example\build\windows\x64\runner\Release\flutter_windows.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "example\build\windows\x64\runner\Release\objectbox_flutter_libs_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "example\build\windows\x64\runner\Release\objectbox.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "example\build\windows\x64\runner\Release\data\*"; DestDir: "{app}\data"; Flags: ignoreversion recursesubdirs createallsubdirs [Registry] Root: HKA; Subkey: "Software\Classes\{#MyAppAssocExt}\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocKey}"; ValueData: ""; Flags: uninsdeletevalue From 28f731ec433bd24caedaf436eec23322631a5c2d Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Wed, 28 Feb 2024 20:20:19 +0000 Subject: [PATCH 122/168] Moved `tileImage` back to `StoreStats` Former-commit-id: 1012afa6b8ae3d3fd4d6fbeb11ec1cfef2e9e538 [formerly fe97307f768cc5631cde3e733d8cdca6c8c78e8d] Former-commit-id: 3473e2e32b6882cc696ace28b871e2f79a827b41 --- .../pages/stores/components/store_tile.dart | 2 +- lib/src/backend/utils/errors.dart | 3 +- lib/src/misc/deprecations.dart | 74 +++++++++---------- lib/src/store/manage.dart | 54 -------------- lib/src/store/statistics.dart | 54 ++++++++++++++ 5 files changed, 94 insertions(+), 93 deletions(-) diff --git a/example/lib/screens/main/pages/stores/components/store_tile.dart b/example/lib/screens/main/pages/stores/components/store_tile.dart index 0302464a..5a02f8b5 100644 --- a/example/lib/screens/main/pages/stores/components/store_tile.dart +++ b/example/lib/screens/main/pages/stores/components/store_tile.dart @@ -107,7 +107,7 @@ class _StoreTileState extends State { borderRadius: BorderRadius.circular(16), child: FutureBuilder( - future: store.manage.tileImage( + future: store.stats.tileImage( gaplessPlayback: true, ), builder: (context, snapshot) { diff --git a/lib/src/backend/utils/errors.dart b/lib/src/backend/utils/errors.dart index d3dd5595..0d706c20 100644 --- a/lib/src/backend/utils/errors.dart +++ b/lib/src/backend/utils/errors.dart @@ -24,7 +24,8 @@ import '../export_external.dart'; /// /// If the callback returns `true`, then FMTC will not continue to handle the /// error. Otherwise, FMTC will also throw the exception/error. If the callback -/// is not defined (in [FMTCBackend.initialise]), then FMTC will throw the exception/error. +/// is not defined (in [FMTCBackend.initialise]), then FMTC will throw the +/// exception/error. typedef FMTCExceptionHandler = bool Function({ required Object? exception, required StackTrace stackTrace, diff --git a/lib/src/misc/deprecations.dart b/lib/src/misc/deprecations.dart index 65688891..72eb7dab 100644 --- a/lib/src/misc/deprecations.dart +++ b/lib/src/misc/deprecations.dart @@ -82,6 +82,43 @@ extension StoreManagementDeprecations on StoreManagement { /// {@macro fmtc.backend.renameStore} @Deprecated('Migrate to `rename`. $_syncRemoval') Future renameAsync(String newStoreName) => rename(newStoreName); +} + +/// Provides deprecations where possible for previous methods in [StoreStats] +/// after the v9 release. +/// +/// Synchronous operations have been removed throughout FMTC v9, therefore the +/// distinction between sync and async operations has been removed. +/// +/// Provided in an extension method for easy differentiation and quick removal. +@Deprecated( + 'Migrate to the suggested replacements for each operation. $_syncRemoval', +) +extension StoreStatsDeprecations on StoreStats { + /// Retrieve the total number of KiBs of all tiles' bytes (not 'real total' + /// size) + /// + /// {@macro fmtc.frontend.storestats.efficiency} + @Deprecated('Migrate to `size`. $_syncRemoval') + Future get storeSizeAsync => size; + + /// Retrieve the number of tiles belonging to this store + /// + /// {@macro fmtc.frontend.storestats.efficiency} + @Deprecated('Migrate to `length`. $_syncRemoval') + Future get storeLengthAsync => length; + + /// Retrieve the number of successful tile retrievals when browsing + /// + /// {@macro fmtc.frontend.storestats.efficiency} + @Deprecated('Migrate to `hits`.$_syncRemoval') + Future get cacheHitsAsync => hits; + + /// Retrieve the number of unsuccessful tile retrievals when browsing + /// + /// {@macro fmtc.frontend.storestats.efficiency} + @Deprecated('Migrate to `misses`. $_syncRemoval') + Future get cacheMissesAsync => misses; /// {@macro fmtc.backend.tileImage} /// , then render the bytes to an [Image] @@ -132,43 +169,6 @@ extension StoreManagementDeprecations on StoreManagement { ); } -/// Provides deprecations where possible for previous methods in [StoreStats] -/// after the v9 release. -/// -/// Synchronous operations have been removed throughout FMTC v9, therefore the -/// distinction between sync and async operations has been removed. -/// -/// Provided in an extension method for easy differentiation and quick removal. -@Deprecated( - 'Migrate to the suggested replacements for each operation. $_syncRemoval', -) -extension StoreStatsDeprecations on StoreStats { - /// Retrieve the total number of KiBs of all tiles' bytes (not 'real total' - /// size) - /// - /// {@macro fmtc.frontend.storestats.efficiency} - @Deprecated('Migrate to `size`. $_syncRemoval') - Future get storeSizeAsync => size; - - /// Retrieve the number of tiles belonging to this store - /// - /// {@macro fmtc.frontend.storestats.efficiency} - @Deprecated('Migrate to `length`. $_syncRemoval') - Future get storeLengthAsync => length; - - /// Retrieve the number of successful tile retrievals when browsing - /// - /// {@macro fmtc.frontend.storestats.efficiency} - @Deprecated('Migrate to `hits`.$_syncRemoval') - Future get cacheHitsAsync => hits; - - /// Retrieve the number of unsuccessful tile retrievals when browsing - /// - /// {@macro fmtc.frontend.storestats.efficiency} - @Deprecated('Migrate to `misses`. $_syncRemoval') - Future get cacheMissesAsync => misses; -} - /// Provides deprecations where possible for previous methods in [StoreMetadata] /// after the v9 release. /// diff --git a/lib/src/store/manage.dart b/lib/src/store/manage.dart index 55de4c25..fef3c720 100644 --- a/lib/src/store/manage.dart +++ b/lib/src/store/manage.dart @@ -48,58 +48,4 @@ class StoreManagement { Future removeTilesOlderThan({required DateTime expiry}) => FMTCBackendAccess.internal .removeTilesOlderThan(storeName: _storeName, expiry: expiry); - - /// {@macro fmtc.backend.readLatestTile} - /// , then render the bytes to an [Image] - Future tileImage({ - double? size, - Key? key, - double scale = 1.0, - ImageFrameBuilder? frameBuilder, - ImageErrorWidgetBuilder? errorBuilder, - String? semanticLabel, - bool excludeFromSemantics = false, - Color? color, - Animation? opacity, - BlendMode? colorBlendMode, - BoxFit? fit, - AlignmentGeometry alignment = Alignment.center, - ImageRepeat repeat = ImageRepeat.noRepeat, - Rect? centerSlice, - bool matchTextDirection = false, - bool gaplessPlayback = false, - bool isAntiAlias = false, - FilterQuality filterQuality = FilterQuality.low, - int? cacheWidth, - int? cacheHeight, - }) async { - final latestTile = - await FMTCBackendAccess.internal.readLatestTile(storeName: _storeName); - if (latestTile == null) return null; - - return Image.memory( - Uint8List.fromList(latestTile.bytes), - key: key, - scale: scale, - frameBuilder: frameBuilder, - errorBuilder: errorBuilder, - semanticLabel: semanticLabel, - excludeFromSemantics: excludeFromSemantics, - width: size, - height: size, - color: color, - opacity: opacity, - colorBlendMode: colorBlendMode, - fit: fit, - alignment: alignment, - repeat: repeat, - centerSlice: centerSlice, - matchTextDirection: matchTextDirection, - gaplessPlayback: gaplessPlayback, - isAntiAlias: isAntiAlias, - filterQuality: filterQuality, - cacheWidth: cacheWidth, - cacheHeight: cacheHeight, - ); - } } diff --git a/lib/src/store/statistics.dart b/lib/src/store/statistics.dart index bb43b5d8..660a1595 100644 --- a/lib/src/store/statistics.dart +++ b/lib/src/store/statistics.dart @@ -57,4 +57,58 @@ class StoreStats { ); yield* stream; } + + /// {@macro fmtc.backend.readLatestTile} + /// , then render the bytes to an [Image] + Future tileImage({ + double? size, + Key? key, + double scale = 1.0, + ImageFrameBuilder? frameBuilder, + ImageErrorWidgetBuilder? errorBuilder, + String? semanticLabel, + bool excludeFromSemantics = false, + Color? color, + Animation? opacity, + BlendMode? colorBlendMode, + BoxFit? fit, + AlignmentGeometry alignment = Alignment.center, + ImageRepeat repeat = ImageRepeat.noRepeat, + Rect? centerSlice, + bool matchTextDirection = false, + bool gaplessPlayback = false, + bool isAntiAlias = false, + FilterQuality filterQuality = FilterQuality.low, + int? cacheWidth, + int? cacheHeight, + }) async { + final latestTile = + await FMTCBackendAccess.internal.readLatestTile(storeName: _storeName); + if (latestTile == null) return null; + + return Image.memory( + Uint8List.fromList(latestTile.bytes), + key: key, + scale: scale, + frameBuilder: frameBuilder, + errorBuilder: errorBuilder, + semanticLabel: semanticLabel, + excludeFromSemantics: excludeFromSemantics, + width: size, + height: size, + color: color, + opacity: opacity, + colorBlendMode: colorBlendMode, + fit: fit, + alignment: alignment, + repeat: repeat, + centerSlice: centerSlice, + matchTextDirection: matchTextDirection, + gaplessPlayback: gaplessPlayback, + isAntiAlias: isAntiAlias, + filterQuality: filterQuality, + cacheWidth: cacheWidth, + cacheHeight: cacheHeight, + ); + } } From c7147266bfd0c85377fe634f50eb1bbf043488df Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Wed, 28 Feb 2024 20:25:43 +0000 Subject: [PATCH 123/168] Updated GitHub Workflow Former-commit-id: 11bcf4e4be8f6d2071674fe1006fa0a36321931a [formerly 5b6e667d38295d871f8221e86285fe50cd7cfd53] Former-commit-id: 3c8e1303da00a45a2a4025e270b08c476246e557 --- .github/workflows/main.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 916c13af..9b825930 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -19,6 +19,8 @@ jobs: id: analysis with: githubToken: ${{ secrets.GITHUB_TOKEN }} + - name: Disable Git Security False Alarm + run: git config --global --add safe.directory /github/workspace - name: Check Package Scores env: TOTAL: ${{ steps.analysis.outputs.total }} From ab7a6a7f17f206b4fa2cf1fb8d41eb9a5d74e44c Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Wed, 28 Feb 2024 20:26:30 +0000 Subject: [PATCH 124/168] Updated GitHub Workflow Former-commit-id: f52812d231a60fd0bfa8b62e12e0f44a857807b6 [formerly c74f1f908c5641c732ae3addb5ef3cc3bf730ec0] Former-commit-id: 0a7af3ae4318e990c8e76a12dd9780b33a89ddf7 --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9b825930..af3857a4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -14,13 +14,13 @@ jobs: steps: - name: Checkout Repository uses: actions/checkout@master + - name: Disable Git Security False Alarm + run: git config --global --add safe.directory /github/workspace - name: Run Dart Package Analyser uses: axel-op/dart-package-analyzer@master id: analysis with: githubToken: ${{ secrets.GITHUB_TOKEN }} - - name: Disable Git Security False Alarm - run: git config --global --add safe.directory /github/workspace - name: Check Package Scores env: TOTAL: ${{ steps.analysis.outputs.total }} From 27e82913186a80dd376c20e9b55351a0f0fa7137 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Wed, 28 Feb 2024 20:48:40 +0000 Subject: [PATCH 125/168] Fixed bug in `removeOldestTilesAboveLimit` Former-commit-id: 4a79e1d394718b7f6044d0fa28c52aee6c69aeba [formerly 4153e1c7a9eda2763f82771e30ffca05db05c32b] Former-commit-id: 11aef64a64fc0b410ec2c1a3ff1ae1231a7fb809 --- .../objectbox/backend/internal_worker.dart | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/lib/src/backend/impls/objectbox/backend/internal_worker.dart b/lib/src/backend/impls/objectbox/backend/internal_worker.dart index 31120c49..3476c451 100644 --- a/lib/src/backend/impls/objectbox/backend/internal_worker.dart +++ b/lib/src/backend/impls/objectbox/backend/internal_worker.dart @@ -551,17 +551,19 @@ Future _worker( final numToRemove = store.length - tilesLimit; - if (numToRemove <= 0) sendRes(id: cmd.id, data: {'numOrphans': 0}); - - tilesQuery.limit = numToRemove; - - sendRes( - id: cmd.id, - data: { - 'numOrphans': - deleteTiles(storeName: storeName, tilesQuery: tilesQuery), - }, - ); + if (numToRemove <= 0) { + sendRes(id: cmd.id, data: {'numOrphans': 0}); + } else { + tilesQuery.limit = numToRemove; + + sendRes( + id: cmd.id, + data: { + 'numOrphans': + deleteTiles(storeName: storeName, tilesQuery: tilesQuery), + }, + ); + } storeQuery.close(); tilesQuery.close(); From 29bafba093b3fc0cd7ec0d018633874f203d3fc0 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Wed, 28 Feb 2024 21:41:46 +0000 Subject: [PATCH 126/168] Fixed bug in bulk downloader (`htWriteTile(s)`) Discovered bug in `rootSize` calculation Former-commit-id: da7a296f12df95e93c3080f36dc4f6824d939f03 [formerly 2745f0e1f6e7ccfe64adbad19ac39bce644a2e7e] Former-commit-id: 25e26eb0d7900617b7c2c00ba84f67e42f7177e7 --- .../backend/internal_thread_safe.dart | 33 +++++++++++++++---- .../objectbox/backend/internal_worker.dart | 7 ++-- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/lib/src/backend/impls/objectbox/backend/internal_thread_safe.dart b/lib/src/backend/impls/objectbox/backend/internal_thread_safe.dart index 72a046bf..449df4ca 100644 --- a/lib/src/backend/impls/objectbox/backend/internal_thread_safe.dart +++ b/lib/src/backend/impls/objectbox/backend/internal_thread_safe.dart @@ -78,19 +78,30 @@ class _ObjectBoxBackendThreadSafeImpl implements FMTCBackendInternalThreadSafe { (throw StoreNotExists(storeName: storeName)); if (existingTile == null) { + // If tile doesn't exist, just add to this store storesToUpdate[storeName] = store ..length += 1 ..size += bytes.lengthInBytes; } else { - storesToUpdate[storeName] = store - ..size += -existingTile.bytes.lengthInBytes + bytes.lengthInBytes; + // If tile exists in this store, just update size, otherwise + // length and size + // Also update size of all related stores + bool didContainAlready = false; for (final relatedStore in existingTile.stores) { + if (relatedStore.name == storeName) didContainAlready = true; + storesToUpdate[relatedStore.name] = (storesToUpdate[relatedStore.name] ?? relatedStore) ..size += -existingTile.bytes.lengthInBytes + bytes.lengthInBytes; } + + if (!didContainAlready) { + storesToUpdate[storeName] = store + ..length += 1 + ..size += bytes.lengthInBytes; + } } tiles.put( @@ -139,20 +150,30 @@ class _ObjectBoxBackendThreadSafeImpl implements FMTCBackendInternalThreadSafe { .findUnique(); if (existingTile == null) { - storesToUpdate[storeName] = store + // If tile doesn't exist, just add to this store + storesToUpdate[storeName] = (storesToUpdate[storeName] ?? store) ..length += 1 ..size += bytess[i].lengthInBytes; } else { - storesToUpdate[storeName] = store - ..size += - -existingTile.bytes.lengthInBytes + bytess[i].lengthInBytes; + // If tile exists in this store, just update size, otherwise + // length and size + // Also update size of all related stores + bool didContainAlready = false; for (final relatedStore in existingTile.stores) { + if (relatedStore.name == storeName) didContainAlready = true; + storesToUpdate[relatedStore.name] = (storesToUpdate[relatedStore.name] ?? relatedStore) ..size += -existingTile.bytes.lengthInBytes + bytess[i].lengthInBytes; } + + if (!didContainAlready) { + storesToUpdate[storeName] = store + ..length += 1 + ..size += bytess[i].lengthInBytes; + } } return ObjectBoxTile( diff --git a/lib/src/backend/impls/objectbox/backend/internal_worker.dart b/lib/src/backend/impls/objectbox/backend/internal_worker.dart index 3476c451..2b5b82b3 100644 --- a/lib/src/backend/impls/objectbox/backend/internal_worker.dart +++ b/lib/src/backend/impls/objectbox/backend/internal_worker.dart @@ -180,6 +180,7 @@ Future _worker( }, ); case _WorkerCmdType.rootSize: + // TODO: Invalid, considers ALL tiles (related ones multiple times), not UNIQUE tiles final query = root .box() .query() @@ -583,10 +584,8 @@ Future _worker( sendRes( id: cmd.id, data: { - 'numOrphans': deleteTiles( - storeName: storeName, - tilesQuery: tilesQuery, - ), + 'numOrphans': + deleteTiles(storeName: storeName, tilesQuery: tilesQuery), }, ); From 84b14c3760228a5fe32768475c638b0d23963e22 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Wed, 28 Feb 2024 22:18:06 +0000 Subject: [PATCH 127/168] Fixed bug in `rootSize` calculation Former-commit-id: b771c38ab1c73b1eb93cb5afae3cf7d23b0078d7 [formerly 71cace8cfc85ee72d463469fb3a750c897019a40] Former-commit-id: 422a4aaedac947719bbf1c6593b554bc91309bfd --- .../lib/screens/main/pages/stores/stores.dart | 8 ++-- .../objectbox/backend/internal_worker.dart | 38 +++++++++---------- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/example/lib/screens/main/pages/stores/stores.dart b/example/lib/screens/main/pages/stores/stores.dart index c60bec30..008300fe 100644 --- a/example/lib/screens/main/pages/stores/stores.dart +++ b/example/lib/screens/main/pages/stores/stores.dart @@ -184,10 +184,10 @@ class _StoresPageState extends State { builder: (context) => AlertDialog.adaptive( title: const Text('Database Size'), content: const Text( - ''' -This measurement refers to the actual size of the database root (which may be a -flat/file or another structure). Includes database overheads, and may not follow -the total tiles size in a linear relationship, or any relationship at all.''', + 'This measurement refers to the actual size of the database root ' + '(which may be a flat/file or another structure).\nIncludes database ' + 'overheads, and may not follow the total tiles size in a linear ' + 'relationship, or any relationship at all.', ), actions: [ TextButton( diff --git a/lib/src/backend/impls/objectbox/backend/internal_worker.dart b/lib/src/backend/impls/objectbox/backend/internal_worker.dart index 2b5b82b3..36f3afda 100644 --- a/lib/src/backend/impls/objectbox/backend/internal_worker.dart +++ b/lib/src/backend/impls/objectbox/backend/internal_worker.dart @@ -180,19 +180,18 @@ Future _worker( }, ); case _WorkerCmdType.rootSize: - // TODO: Invalid, considers ALL tiles (related ones multiple times), not UNIQUE tiles - final query = root - .box() - .query() - .build() - .property(ObjectBoxStore_.size); - + // TODO: Consider caching root stats in a model as well sendRes( id: cmd.id, - data: {'size': query.find().sum / 1024}, // Convert to KiB + data: { + 'size': root + .box() + .getAll() + .map((t) => t.bytes.lengthInBytes) + .sum / + 1024, // Convert to KiB + }, ); - - query.close(); case _WorkerCmdType.rootLength: final query = root.box().query().build(); @@ -200,16 +199,15 @@ Future _worker( query.close(); case _WorkerCmdType.listStores: - sendRes( - id: cmd.id, - data: { - 'stores': root - .box() - .getAll() - .map((e) => e.name) - .toList(growable: false), - }, - ); + final query = root + .box() + .query() + .build() + .property(ObjectBoxStore_.name); + + sendRes(id: cmd.id, data: {'stores': query.find()}); + + query.close(); case _WorkerCmdType.storeExists: final query = root .box() From 417cdfe31ae79380ddc7e319b684c00beece1f61 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 29 Feb 2024 23:01:43 +0000 Subject: [PATCH 128/168] Improved error handling Added root model to cache length and size Fixed bugs when errors occured Improved example application Former-commit-id: c0f6217e837f4efb8ae75e6ff780111e60ffd478 [formerly 9b14bcd844cb77c8d1c3597df3cc429f95a3aa31] Former-commit-id: 7f742bacbb6a72b91aab17ca137b39bea208d114 --- example/lib/main.dart | 17 +- .../stores/components/root_stats_pane.dart | 106 +++++++++++ .../lib/screens/main/pages/stores/stores.dart | 174 +++++------------ .../impls/objectbox/backend/backend.dart | 9 +- .../impls/objectbox/backend/internal.dart | 80 ++++---- .../backend/internal_thread_safe.dart | 130 +++++++------ .../objectbox/backend/internal_worker.dart | 179 ++++++++++-------- .../models/generated/objectbox-model.json | 28 ++- .../impls/objectbox/models/src/root.dart | 18 ++ .../impls/objectbox/models/src/store.dart | 2 +- .../backend/interfaces/backend/backend.dart | 3 - .../backend/interfaces/backend/internal.dart | 2 +- lib/src/backend/utils/errors.dart | 43 ----- lib/src/store/manage.dart | 4 +- lib/src/store/statistics.dart | 5 +- 15 files changed, 410 insertions(+), 390 deletions(-) create mode 100644 example/lib/screens/main/pages/stores/components/root_stats_pane.dart create mode 100644 lib/src/backend/impls/objectbox/models/src/root.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index a996816d..54898542 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,4 +1,3 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; @@ -21,20 +20,8 @@ void main() async { ), ); - await FMTCObjectBoxBackend().initialise( - exceptionHandler: ({ - required exception, - required initialisationFailure, - required stackTrace, - }) { - if (kDebugMode) { - print( - 'Caught error externally (was init error: $initialisationFailure)', - ); - } - return false; - }, - ); + // TODO: Implement handling + await FMTCObjectBoxBackend().initialise(); runApp(const _AppContainer()); } diff --git a/example/lib/screens/main/pages/stores/components/root_stats_pane.dart b/example/lib/screens/main/pages/stores/components/root_stats_pane.dart new file mode 100644 index 00000000..552dba6d --- /dev/null +++ b/example/lib/screens/main/pages/stores/components/root_stats_pane.dart @@ -0,0 +1,106 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; + +import '../../../../../shared/components/loading_indicator.dart'; +import '../../../../../shared/misc/exts/size_formatter.dart'; +import '../components/stat_display.dart'; + +class RootStatsPane extends StatefulWidget { + const RootStatsPane({super.key}); + + @override + State createState() => _RootStatsPaneState(); +} + +class _RootStatsPaneState extends State { + late final watchStream = FMTCRoot.stats.watchStores( + triggerImmediately: true, + ); + + @override + Widget build(BuildContext context) => Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + margin: const EdgeInsets.symmetric(vertical: 16), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.onSecondary, + borderRadius: BorderRadius.circular(16), + ), + child: StreamBuilder( + stream: watchStream, + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const Padding( + padding: EdgeInsets.all(12), + child: LoadingIndicator('Retrieving Stores'), + ); + } + + return Wrap( + alignment: WrapAlignment.spaceEvenly, + runAlignment: WrapAlignment.spaceEvenly, + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 20, + children: [ + FutureBuilder( + future: FMTCRoot.stats.length, + builder: (context, snapshot) => StatDisplay( + statistic: snapshot.data?.toString(), + description: 'total tiles', + ), + ), + FutureBuilder( + future: FMTCRoot.stats.size, + builder: (context, snapshot) => StatDisplay( + statistic: snapshot.data == null + ? null + : ((snapshot.data! * 1024).asReadableSize), + description: 'total tiles size', + ), + ), + FutureBuilder( + future: FMTCRoot.stats.realSize, + builder: (context, snapshot) => Row( + mainAxisSize: MainAxisSize.min, + children: [ + StatDisplay( + statistic: snapshot.data == null + ? null + : ((snapshot.data! * 1024).asReadableSize), + description: 'database size', + ), + const SizedBox.square(dimension: 6), + IconButton( + icon: const Icon(Icons.help_outline), + onPressed: () => _showDatabaseSizeInfoDialog(context), + ), + ], + ), + ), + ], + ); + }, + ), + ); + + void _showDatabaseSizeInfoDialog(BuildContext context) { + showAdaptiveDialog( + context: context, + builder: (context) => AlertDialog.adaptive( + title: const Text('Database Size'), + content: const Text( + 'This measurement refers to the actual size of the database root ' + '(which may be a flat/file or another structure).\nIncludes database ' + 'overheads, and may not follow the total tiles size in a linear ' + 'relationship, or any relationship at all.', + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Close'), + ), + ], + ), + ); + } +} diff --git a/example/lib/screens/main/pages/stores/stores.dart b/example/lib/screens/main/pages/stores/stores.dart index 008300fe..02548e31 100644 --- a/example/lib/screens/main/pages/stores/stores.dart +++ b/example/lib/screens/main/pages/stores/stores.dart @@ -2,12 +2,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; import '../../../../shared/components/loading_indicator.dart'; -import '../../../../shared/misc/exts/size_formatter.dart'; import '../../../import_store/import_store.dart'; import '../../../store_editor/store_editor.dart'; import 'components/empty_indicator.dart'; import 'components/header.dart'; -import 'components/stat_display.dart'; +import 'components/root_stats_pane.dart'; import 'components/store_tile.dart'; class StoresPage extends StatefulWidget { @@ -18,9 +17,9 @@ class StoresPage extends StatefulWidget { } class _StoresPageState extends State { - late final watchStream = FMTCRoot.stats.watchStores( - triggerImmediately: true, - ); + late final storesStream = FMTCRoot.stats + .watchStores(triggerImmediately: true) + .asyncMap((_) => FMTCRoot.stats.storesAvailable); @override Widget build(BuildContext context) { @@ -30,116 +29,60 @@ class _StoresPageState extends State { body: SafeArea( child: Padding( padding: const EdgeInsets.all(12), - child: StreamBuilder( - stream: watchStream, - builder: (context, snapshot) { - if (!snapshot.hasData) return loadingIndicator; + child: Column( + children: [ + const Header(), + const SizedBox(height: 16), + Expanded( + child: StreamBuilder( + stream: storesStream, + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const Padding( + padding: EdgeInsets.all(12), + child: loadingIndicator, + ); + } - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Header(), - const SizedBox(height: 16), - Expanded( - child: FutureBuilder( - future: FMTCRoot.stats.storesAvailable, - builder: (context, snapshot) { - if (!snapshot.hasData) { - return const Padding( - padding: EdgeInsets.all(12), - child: loadingIndicator, - ); - } + if (snapshot.data!.isEmpty) { + return const Column( + children: [ + RootStatsPane(), + Expanded(child: EmptyIndicator()), + ], + ); + } - if (snapshot.data!.isEmpty) { - return const EmptyIndicator(); + return ListView.builder( + itemCount: snapshot.data!.length + 2, + itemBuilder: (context, index) { + if (index == 0) { + return const RootStatsPane(); } - return ListView.builder( - itemCount: snapshot.data!.length + 2, - itemBuilder: (context, index) { - final addRootStats = index == 0; + // Ensure the store buttons are not obscured by the FABs + if (index >= snapshot.data!.length + 1) { + return const SizedBox(height: 124); + } - if (addRootStats) { - return Container( - width: double.infinity, - padding: const EdgeInsets.all(16), - margin: - const EdgeInsets.symmetric(vertical: 16), - decoration: BoxDecoration( - color: - Theme.of(context).colorScheme.onSecondary, - borderRadius: BorderRadius.circular(16), - ), - child: Wrap( - alignment: WrapAlignment.spaceEvenly, - runAlignment: WrapAlignment.spaceEvenly, - crossAxisAlignment: WrapCrossAlignment.center, - spacing: 20, - children: [ - FutureBuilder( - future: FMTCRoot.stats.length, - builder: (context, snapshot) => - StatDisplay( - statistic: snapshot.data?.toString(), - description: 'total tiles', - ), - ), - FutureBuilder( - future: FMTCRoot.stats.size, - builder: (context, snapshot) => - StatDisplay( - statistic: snapshot.data == null - ? null - : ((snapshot.data! * 1024) - .asReadableSize), - description: 'total tiles size', - ), - ), - FutureBuilder( - future: FMTCRoot.stats.realSize, - builder: (context, snapshot) => Row( - mainAxisSize: MainAxisSize.min, - children: [ - StatDisplay( - statistic: snapshot.data == null - ? null - : ((snapshot.data! * 1024) - .asReadableSize), - description: 'database size', - ), - const SizedBox.square(dimension: 6), - IconButton( - icon: - const Icon(Icons.help_outline), - onPressed: () => - showDatabaseSizeInfoDialog( - context, - ), - ), - ], - ), - ), - ], - ), - ); + final storeName = + snapshot.data!.elementAt(index - 1).storeName; + return FutureBuilder( + future: FMTCStore(storeName).manage.ready, + builder: (context, snapshot) { + if (snapshot.data == null) { + return const SizedBox.shrink(); } - final addSpace = index >= snapshot.data!.length + 1; - if (addSpace) return const SizedBox(height: 124); - - return StoreTile( - storeName: - snapshot.data!.elementAt(index - 1).storeName, - ); + return StoreTile(storeName: storeName); }, ); }, - ), - ), - ], - ); - }, + ); + }, + ), + ), + ], ), ), ), @@ -177,25 +120,4 @@ class _StoresPageState extends State { ), ); } - - void showDatabaseSizeInfoDialog(BuildContext context) { - showAdaptiveDialog( - context: context, - builder: (context) => AlertDialog.adaptive( - title: const Text('Database Size'), - content: const Text( - 'This measurement refers to the actual size of the database root ' - '(which may be a flat/file or another structure).\nIncludes database ' - 'overheads, and may not follow the total tiles size in a linear ' - 'relationship, or any relationship at all.', - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('Close'), - ), - ], - ), - ); - } } diff --git a/lib/src/backend/impls/objectbox/backend/backend.dart b/lib/src/backend/impls/objectbox/backend/backend.dart index 0d201ea4..a3739159 100644 --- a/lib/src/backend/impls/objectbox/backend/backend.dart +++ b/lib/src/backend/impls/objectbox/backend/backend.dart @@ -6,8 +6,6 @@ import 'dart:convert'; import 'dart:io'; import 'dart:isolate'; -import 'package:collection/collection.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; @@ -16,6 +14,7 @@ import '../../../../../flutter_map_tile_caching.dart'; import '../../../export_internal.dart'; import '../models/generated/objectbox.g.dart'; import '../models/src/recovery.dart'; +import '../models/src/root.dart'; import '../models/src/store.dart'; import '../models/src/tile.dart'; @@ -29,10 +28,6 @@ part 'internal_worker.dart'; final class FMTCObjectBoxBackend implements FMTCBackend { /// {@macro fmtc.backend.initialise} /// - /// Consider handling [StorageException], which is an exception thrown by - /// ObjectBox which indicates an issue when reading/writing data to the - /// database - for example, due to exceeding the [maxDatabaseSize]. - /// /// --- /// /// [maxDatabaseSize] is the maximum size the database file can grow @@ -47,13 +42,11 @@ final class FMTCObjectBoxBackend implements FMTCBackend { String? rootDirectory, int maxDatabaseSize = 10000000, String? macosApplicationGroup, - FMTCExceptionHandler? exceptionHandler, }) => FMTCObjectBoxBackendInternal._instance.initialise( rootDirectory: rootDirectory, maxDatabaseSize: maxDatabaseSize, macosApplicationGroup: macosApplicationGroup, - exceptionHandler: exceptionHandler, ); /// {@macro fmtc.backend.uninitialise} diff --git a/lib/src/backend/impls/objectbox/backend/internal.dart b/lib/src/backend/impls/objectbox/backend/internal.dart index b3051f72..a15337c3 100644 --- a/lib/src/backend/impls/objectbox/backend/internal.dart +++ b/lib/src/backend/impls/objectbox/backend/internal.dart @@ -42,32 +42,20 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { final id = ++_workerId; // Create new unique ID _workerResOneShot[id] = Completer(); // Will be completed by direct handler _sendPort!.send((id: id, type: type, args: args)); // Send cmd - final res = await _workerResOneShot[id]!.future; // Await response - _workerResOneShot.remove(id); // Free memory - - // Handle errors (if missed by direct handler) - final err = res?['error']; - if (err != null) { - // Thrown by immediate shutdown - if (err is RootUnavailable && kDebugMode) { - debugPrint( - '[FMTC] Immediate uninitialisation caused an operation to be lost.', - ); - } - debugPrint( - 'An unexpected error in the FMTC ObjectBox Backend occurred, and should not have occurred at this point:', - ); + try { + return await _workerResOneShot[id]!.future; // Await response + } catch (err, stackTrace) { Error.throwWithStackTrace( err, StackTrace.fromString( - (res?['stackTrace']! as StackTrace).toString() + - StackTrace.current.toString(), + '$stackTrace\n${StackTrace.current}' + '#+ [FMTC Debug Info] $type: $args\n', ), ); + } finally { + _workerResOneShot.remove(id); // Free memory } - - return res; } Stream?> _sendCmdStreamed({ @@ -90,6 +78,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { controller.sink; // Will be inserted into by direct handler _sendPort!.send((id: id, type: type, args: args)); // Send cmd + // TODO: error handle try { yield* controller.stream; // Return responses } finally { @@ -102,7 +91,6 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { required String? rootDirectory, required int maxDatabaseSize, required String? macosApplicationGroup, - FMTCExceptionHandler? exceptionHandler, }) async { if (_sendPort != null) throw RootAlreadyInitialised(); @@ -115,10 +103,19 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { ).create(recursive: true); // Prepare to recieve `SendPort` from worker - ByteData? storeReference; + late ByteData storeReference; _workerResOneShot[0] = Completer(); - final workerInitialRes = _workerResOneShot[0]!.future.then((res) { - if (res!['error'] != null) { + final workerInitialRes = _workerResOneShot[0]! + .future + .then<({Object err, StackTrace stackTrace})?>( + (res) { + storeReference = res!['storeReference']; + _sendPort = res['sendPort']; + _workerResOneShot.remove(0); + + return null; + }, + onError: (err, stackTrace) { _workerHandler.cancel(); _workerComplete.complete(); @@ -126,13 +123,9 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { _workerResOneShot.clear(); _workerResStreamed.clear(); - return; - } - - storeReference = res['storeReference']; - _sendPort = res['sendPort']; - _workerResOneShot.remove(0); - }); + return (err: err, stackTrace: stackTrace); + }, + ); // Setup worker comms/response handler final receivePort = ReceivePort(); @@ -152,23 +145,18 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { // Handle errors final err = evt.data?['error']; if (err != null) { - if (err is FMTCBackendError) throw err; - final stackTrace = StackTrace.fromString( - (evt.data?['stackTrace']! as StackTrace).toString() + - StackTrace.current.toString(), + '${evt.data?['stackTrace']! as StackTrace}' + '\n${StackTrace.current}', ); - if (exceptionHandler != null) { - final handled = exceptionHandler( - exception: err, - stackTrace: stackTrace, - initialisationFailure: evt.id == 0, - ); - if (!handled) Error.throwWithStackTrace(err, stackTrace); + if (evt.data?['expectStream'] == true) { + _workerResStreamed[evt.id]!.addError(err, stackTrace); } else { - Error.throwWithStackTrace(err, stackTrace); + _workerResOneShot[evt.id]!.completeError(err, stackTrace); } + + return; } if (evt.data?['expectStream'] == true) { @@ -195,14 +183,16 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { ); // Wait for initial response from isolate - await workerInitialRes; + final initResult = await workerInitialRes; // Check whether initialisation was successful - if (storeReference != null) { + if (initResult == null) { FMTCBackendAccess.internal = this; FMTCBackendAccessThreadSafe.internal = _ObjectBoxBackendThreadSafeImpl._( - storeReference: storeReference!, + storeReference: storeReference, ); + } else { + Error.throwWithStackTrace(initResult.err, initResult.stackTrace); } } diff --git a/lib/src/backend/impls/objectbox/backend/internal_thread_safe.dart b/lib/src/backend/impls/objectbox/backend/internal_thread_safe.dart index 449df4ca..6a0c400a 100644 --- a/lib/src/backend/impls/objectbox/backend/internal_thread_safe.dart +++ b/lib/src/backend/impls/objectbox/backend/internal_thread_safe.dart @@ -8,13 +8,14 @@ class _ObjectBoxBackendThreadSafeImpl implements FMTCBackendInternalThreadSafe { required this.storeReference, }); - final ByteData storeReference; - Store? root; - void get expectInitialised => root ?? (throw RootUnavailable()); - @override String get friendlyIdentifier => 'ObjectBox'; + void get expectInitialised => root ?? (throw RootUnavailable()); + + final ByteData storeReference; + Store? root; + @override void initialise() { if (root != null) throw RootAlreadyInitialised(); @@ -63,6 +64,7 @@ class _ObjectBoxBackendThreadSafeImpl implements FMTCBackendInternalThreadSafe { final tiles = root!.box(); final stores = root!.box(); + final rootBox = root!.box(); final tilesQuery = tiles.query(ObjectBoxTile_.url.equals(url)).build(); final storeQuery = @@ -77,17 +79,12 @@ class _ObjectBoxBackendThreadSafeImpl implements FMTCBackendInternalThreadSafe { final store = storeQuery.findUnique() ?? (throw StoreNotExists(storeName: storeName)); - if (existingTile == null) { - // If tile doesn't exist, just add to this store - storesToUpdate[storeName] = store - ..length += 1 - ..size += bytes.lengthInBytes; - } else { - // If tile exists in this store, just update size, otherwise - // length and size - // Also update size of all related stores - bool didContainAlready = false; + // If tile exists in this store, just update size, otherwise + // length and size + // Also update size of all related stores + bool didContainAlready = false; + if (existingTile != null) { for (final relatedStore in existingTile.stores) { if (relatedStore.name == storeName) didContainAlready = true; @@ -97,11 +94,24 @@ class _ObjectBoxBackendThreadSafeImpl implements FMTCBackendInternalThreadSafe { -existingTile.bytes.lengthInBytes + bytes.lengthInBytes; } - if (!didContainAlready) { - storesToUpdate[storeName] = store + rootBox.put( + rootBox.get(1)! + ..size += -existingTile.bytes.lengthInBytes + bytes.lengthInBytes, + mode: PutMode.update, + ); + } + + if (!didContainAlready || existingTile == null) { + storesToUpdate[storeName] = store + ..length += 1 + ..size += bytes.lengthInBytes; + + rootBox.put( + rootBox.get(1)! ..length += 1 - ..size += bytes.lengthInBytes; - } + ..size += bytes.lengthInBytes, + mode: PutMode.update, + ); } tiles.put( @@ -129,6 +139,7 @@ class _ObjectBoxBackendThreadSafeImpl implements FMTCBackendInternalThreadSafe { final tiles = root!.box(); final stores = root!.box(); + final rootBox = root!.box(); final tilesQuery = tiles.query(ObjectBoxTile_.url.equals('')).build(); final storeQuery = @@ -139,53 +150,56 @@ class _ObjectBoxBackendThreadSafeImpl implements FMTCBackendInternalThreadSafe { root!.runInTransaction( TxMode.write, () { + final rootData = rootBox.get(1)!; final store = storeQuery.findUnique() ?? (throw StoreNotExists(storeName: storeName)); - final tilesToUpdate = List.generate( - urls.length, - (i) { - final existingTile = (tilesQuery - ..param(ObjectBoxTile_.url).value = urls[i]) - .findUnique(); - - if (existingTile == null) { - // If tile doesn't exist, just add to this store - storesToUpdate[storeName] = (storesToUpdate[storeName] ?? store) - ..length += 1 - ..size += bytess[i].lengthInBytes; - } else { - // If tile exists in this store, just update size, otherwise - // length and size - // Also update size of all related stores - bool didContainAlready = false; - - for (final relatedStore in existingTile.stores) { - if (relatedStore.name == storeName) didContainAlready = true; - - storesToUpdate[relatedStore.name] = - (storesToUpdate[relatedStore.name] ?? relatedStore) - ..size += -existingTile.bytes.lengthInBytes + - bytess[i].lengthInBytes; - } - - if (!didContainAlready) { - storesToUpdate[storeName] = store - ..length += 1 - ..size += bytess[i].lengthInBytes; - } + for (int i = 0; i <= urls.length; i++) { + final url = urls[i]; + final bytes = bytess[i]; + + final existingTile = + (tilesQuery..param(ObjectBoxTile_.url).value = url).findUnique(); + + // If tile exists in this store, just update size, otherwise + // length and size + // Also update size of all related stores + bool didContainAlready = false; + + if (existingTile != null) { + for (final relatedStore in existingTile.stores) { + if (relatedStore.name == storeName) didContainAlready = true; + + storesToUpdate[relatedStore.name] = + (storesToUpdate[relatedStore.name] ?? relatedStore) + ..size += + -existingTile.bytes.lengthInBytes + bytes.lengthInBytes; } - return ObjectBoxTile( - url: urls[i], + rootData.size += + -existingTile.bytes.lengthInBytes + bytes.lengthInBytes; + } + + if (!didContainAlready || existingTile == null) { + storesToUpdate[storeName] = store + ..length += 1 + ..size += bytes.lengthInBytes; + + rootData + ..length += 1 + ..size += bytes.lengthInBytes; + } + + tiles.put( + ObjectBoxTile( + url: url, lastModified: DateTime.timestamp(), - bytes: bytess[i], - )..stores.addAll({store, ...?existingTile?.stores}); - }, - growable: false, - ); + bytes: bytes, + )..stores.addAll({store, ...?existingTile?.stores}), + ); + } - tiles.putMany(tilesToUpdate); + rootBox.put(rootData, mode: PutMode.update); stores.putMany(storesToUpdate.values.toList(), mode: PutMode.update); }, ); diff --git a/lib/src/backend/impls/objectbox/backend/internal_worker.dart b/lib/src/backend/impls/objectbox/backend/internal_worker.dart index 36f3afda..07d11e33 100644 --- a/lib/src/backend/impls/objectbox/backend/internal_worker.dart +++ b/lib/src/backend/impls/objectbox/backend/internal_worker.dart @@ -56,16 +56,13 @@ Future _worker( // Setup comms final receivePort = ReceivePort(); - void sendRes({ - required int id, - Map? data, - }) => + void sendRes({required int id, Map? data}) => input.sendPort.send((id: id, data: data)); - /// Enable ObjectBox usage from this background isolate + // Enable ObjectBox usage from this background isolate BackgroundIsolateBinaryMessenger.ensureInitialized(input.rootIsolateToken); - // Initialise database + // Open database, kill self if failed late final Store root; try { root = await openStore( @@ -73,19 +70,43 @@ Future _worker( maxDBSizeInKB: input.maxDatabaseSize, // Defaults to 10 GB macosApplicationGroup: input.macosApplicationGroup, ); + + // If the database is new, create the root statistics object + final rootBox = root.box(); + if (!rootBox.contains(1)) { + rootBox.put(ObjectBoxRoot(length: 0, size: 0), mode: PutMode.insert); + } } catch (e, s) { sendRes(id: 0, data: {'error': e, 'stackTrace': s}); Isolate.exit(); } + // Create memory for streamed output subscription storage + final streamedOutputSubscriptions = {}; + // Respond with comms channel for future cmds sendRes( id: 0, data: {'sendPort': receivePort.sendPort, 'storeReference': root.reference}, ); - // Create memory for streamed output subscription storage - final streamedOutputSubscriptions = {}; + //! UTIL METHODS !// + + /// Update the root statistics object (ID 0) with the existing values plus + /// the specified values respectively + /// + /// Should be run within a transaction. + /// + /// Specified values may be negative. + /// + /// Handles cases where there is no root statistics object yet. + void updateRootStatistics({int deltaLength = 0, int deltaSize = 0}) => + root.box().put( + root.box().get(1)! + ..length += deltaLength + ..size += deltaSize, + mode: PutMode.update, + ); /// Delete the specified tiles from the specified store /// @@ -105,47 +126,48 @@ Future _worker( final storeQuery = stores.query(ObjectBoxStore_.name.equals(storeName)).build(); - final tilesToUpdate = []; + int rootDeltaLength = 0; + int rootDeltaSize = 0; - final deletedNum = root.runInTransaction( + root.runInTransaction( TxMode.write, () { final queriedTiles = tilesQuery.find(); final store = storeQuery.findUnique() ?? (throw StoreNotExists(storeName: storeName)); - final tilesToDelete = List.generate( - queriedTiles.length, - (i) { - final tile = queriedTiles[i]; - - // Modify current store - store - ..length -= 1 - ..size -= tile.bytes.lengthInBytes; - - // Remove the store relation from the tile - tile.stores.removeWhere((store) => store.name == storeName); + for (final tile in queriedTiles) { + // Modify current store + store + ..length -= 1 + ..size -= tile.bytes.lengthInBytes; + + // Remove the store relation from the tile + tile.stores.removeWhere((store) => store.name == storeName); + + // Delete the tile if it is now orphaned + if (tile.stores.isEmpty) { + rootDeltaLength -= 1; + rootDeltaSize -= tile.bytes.lengthInBytes; + tiles.remove(tile.id); + continue; + } - // Register the tile for deletion if it belongs to no stores - if (tile.stores.isEmpty) return tile.id; + // Otherwise apply the new relation + tile.stores.applyToDb(mode: PutMode.update); + } - // Otherwise register the tile to be updated (the new relation applied) - tilesToUpdate.add(tile); - return null; - }, - growable: false, + updateRootStatistics( + deltaLength: rootDeltaLength, + deltaSize: rootDeltaSize, ); - stores.put(store, mode: PutMode.update); - return (tiles..putMany(tilesToUpdate, mode: PutMode.update)) - .removeMany(tilesToDelete.whereNotNull().toList(growable: false)); }, ); storeQuery.close(); - return deletedNum; + return rootDeltaLength.abs(); } //! MAIN LOOP !// @@ -180,24 +202,15 @@ Future _worker( }, ); case _WorkerCmdType.rootSize: - // TODO: Consider caching root stats in a model as well sendRes( id: cmd.id, - data: { - 'size': root - .box() - .getAll() - .map((t) => t.bytes.lengthInBytes) - .sum / - 1024, // Convert to KiB - }, + data: {'size': root.box().get(1)!.size / 1024}, ); case _WorkerCmdType.rootLength: - final query = root.box().query().build(); - - sendRes(id: cmd.id, data: {'length': query.count()}); - - query.close(); + sendRes( + id: cmd.id, + data: {'length': root.box().get(1)!.length}, + ); case _WorkerCmdType.listStores: final query = root .box() @@ -259,13 +272,13 @@ Future _worker( mode: PutMode.insert, ); } on UniqueViolationException { - throw StoreAlreadyExists(storeName: storeName); + sendRes(id: cmd.id); + break; } sendRes(id: cmd.id); case _WorkerCmdType.resetStore: final storeName = cmd.args['storeName']! as String; - final removeIds = []; final tiles = root.box(); final stores = root.box(); @@ -283,23 +296,7 @@ Future _worker( root.runInTransaction( TxMode.write, () { - tiles.putMany( - tilesQuery - .find() - .map((tile) { - tile.stores - .removeWhere((store) => store.name == storeName); - if (tile.stores.isNotEmpty) return tile; - removeIds.add(tile.id); - return null; - }) - .whereNotNull() - .toList(growable: false), - mode: PutMode.update, - ); - tilesQuery.close(); - - tiles.removeMany(removeIds); + deleteTiles(storeName: storeName, tilesQuery: tilesQuery); final store = storeQuery.findUnique() ?? (throw StoreNotExists(storeName: storeName)); @@ -342,8 +339,10 @@ Future _worker( case _WorkerCmdType.deleteStore: final storeName = cmd.args['storeName']! as String; - final stores = root.box(); - + final storesQuery = root + .box() + .query(ObjectBoxStore_.name.equals(storeName)) + .build(); final tilesQuery = (root.box().query() ..linkMany( ObjectBoxTile_.stores, @@ -351,14 +350,17 @@ Future _worker( )) .build(); - deleteTiles(storeName: storeName, tilesQuery: tilesQuery); - - stores.query(ObjectBoxStore_.name.equals(storeName)).build() - ..remove() - ..close(); + root.runInTransaction( + TxMode.write, + () { + deleteTiles(storeName: storeName, tilesQuery: tilesQuery); + storesQuery.remove(); + }, + ); sendRes(id: cmd.id); + storesQuery.close(); tilesQuery.close(); case _WorkerCmdType.tileExistsInStore: final storeName = cmd.args['storeName']! as String; @@ -420,7 +422,6 @@ Future _worker( final tilesQuery = tiles.query(ObjectBoxTile_.url.equals(url)).build(); - final storeQuery = stores.query(ObjectBoxStore_.name.equals(storeName)).build(); @@ -428,11 +429,9 @@ Future _worker( TxMode.write, () { final existingTile = tilesQuery.findUnique(); - tilesQuery.close(); final store = storeQuery.findUnique() ?? (throw StoreNotExists(storeName: storeName)); - storeQuery.close(); switch ((existingTile == null, bytes == null)) { case (true, false): // No existing tile @@ -448,6 +447,10 @@ Future _worker( ..length += 1 ..size += bytes.lengthInBytes, ); + updateRootStatistics( + deltaLength: 1, + deltaSize: bytes.lengthInBytes, + ); case (false, true): // Existing tile, no update // Only take action if it's not already belonging to the store if (!existingTile!.stores.contains(store)) { @@ -457,8 +460,13 @@ Future _worker( ..length += 1 ..size += existingTile.bytes.lengthInBytes, ); + updateRootStatistics( + deltaLength: 1, + deltaSize: existingTile.bytes.lengthInBytes, + ); } case (false, false): // Existing tile, update required + int rootDeltaSize = 0; tiles.put( existingTile! ..lastModified = DateTime.timestamp() @@ -466,14 +474,16 @@ Future _worker( mode: PutMode.update, ); stores.putMany( - existingTile.stores - .map( - (store) => store - ..size += (bytes.lengthInBytes - - existingTile.bytes.lengthInBytes), - ) - .toList(growable: false), + existingTile.stores.map( + (store) { + final diff = bytes.lengthInBytes - + existingTile.bytes.lengthInBytes; + rootDeltaSize += diff; + return store..size += diff; + }, + ).toList(growable: false), ); + updateRootStatistics(deltaSize: rootDeltaSize); case (true, true): // FMTC internal error throw StateError( 'FMTC ObjectBox backend internal state error: $url', @@ -483,6 +493,9 @@ Future _worker( ); sendRes(id: cmd.id); + + storeQuery.close(); + tilesQuery.close(); case _WorkerCmdType.deleteTile: final storeName = cmd.args['storeName']! as String; final url = cmd.args['url']! as String; diff --git a/lib/src/backend/impls/objectbox/models/generated/objectbox-model.json b/lib/src/backend/impls/objectbox/models/generated/objectbox-model.json index 564ca372..d8e27ba4 100644 --- a/lib/src/backend/impls/objectbox/models/generated/objectbox-model.json +++ b/lib/src/backend/impls/objectbox/models/generated/objectbox-model.json @@ -145,7 +145,7 @@ { "id": "4:7781853256122686511", "name": "size", - "type": 8 + "type": 6 }, { "id": "5:3183925806131180531", @@ -203,9 +203,33 @@ "targetId": "2:632249766926720928" } ] + }, + { + "id": "4:8718814737097934474", + "lastPropertyId": "3:6574336219794969200", + "name": "ObjectBoxRoot", + "properties": [ + { + "id": "1:3527394784453371799", + "name": "id", + "type": 6, + "flags": 1 + }, + { + "id": "2:2833017356902860570", + "name": "length", + "type": 6 + }, + { + "id": "3:6574336219794969200", + "name": "size", + "type": 6 + } + ], + "relations": [] } ], - "lastEntityId": "3:8691708694767276679", + "lastEntityId": "4:8718814737097934474", "lastIndexId": "4:4857742396480146668", "lastRelationId": "1:7496298295217061586", "lastSequenceId": "0:0", diff --git a/lib/src/backend/impls/objectbox/models/src/root.dart b/lib/src/backend/impls/objectbox/models/src/root.dart new file mode 100644 index 00000000..2459152e --- /dev/null +++ b/lib/src/backend/impls/objectbox/models/src/root.dart @@ -0,0 +1,18 @@ +// Copyright © Luka S (JaffaKetchup) under GPL-v3 +// A full license can be found at .\LICENSE + +import 'package:objectbox/objectbox.dart'; + +@Entity() +class ObjectBoxRoot { + ObjectBoxRoot({ + required this.length, + required this.size, + }); + + @Id() + int id = 0; + + int length; + int size; +} diff --git a/lib/src/backend/impls/objectbox/models/src/store.dart b/lib/src/backend/impls/objectbox/models/src/store.dart index d3aa8a91..6f26b1c6 100644 --- a/lib/src/backend/impls/objectbox/models/src/store.dart +++ b/lib/src/backend/impls/objectbox/models/src/store.dart @@ -28,7 +28,7 @@ class ObjectBoxStore { final tiles = ToMany(); int length; - double size; + int size; int hits; int misses; String metadataJson; diff --git a/lib/src/backend/interfaces/backend/backend.dart b/lib/src/backend/interfaces/backend/backend.dart index 96ba1cc1..813dad78 100644 --- a/lib/src/backend/interfaces/backend/backend.dart +++ b/lib/src/backend/interfaces/backend/backend.dart @@ -30,12 +30,9 @@ abstract interface class FMTCBackend { /// `getApplicationDocumentsDirectory()`. Alternatively, pass a custom /// directory - it is recommended to not use a typical cache directory, as the /// OS can clear these without notice at any time. - /// - /// For information on [exceptionHandler], see [FMTCExceptionHandler]'s docs. /// {@endtemplate} Future initialise({ String? rootDirectory, - FMTCExceptionHandler? exceptionHandler, }); /// {@template fmtc.backend.uninitialise} diff --git a/lib/src/backend/interfaces/backend/internal.dart b/lib/src/backend/interfaces/backend/internal.dart index 82b72f09..0b328014 100644 --- a/lib/src/backend/interfaces/backend/internal.dart +++ b/lib/src/backend/interfaces/backend/internal.dart @@ -74,7 +74,7 @@ abstract interface class FMTCBackendInternal /// {@template fmtc.backend.createStore} /// Create a new store with the specified name /// - /// Throws [StoreAlreadyExists] if the specified store already exists. + /// Does nothing if the store already exists. /// {@endtemplate} Future createStore({ required String storeName, diff --git a/lib/src/backend/utils/errors.dart b/lib/src/backend/utils/errors.dart index 0d706c20..5df7ee83 100644 --- a/lib/src/backend/utils/errors.dart +++ b/lib/src/backend/utils/errors.dart @@ -1,37 +1,6 @@ // Copyright © Luka S (JaffaKetchup) under GPL-v3 // A full license can be found at .\LICENSE -import '../export_external.dart'; - -/// A method called by FMTC internals when an exception occurs in a backend -/// -/// A thrown [FMTCBackendError] will NOT result in this method being invoked, as -/// that error should be fixed in code, and should not occur at runtime. It will -/// always be thrown. -/// -/// [initialisationFailure] indicates whether the error occured during -/// intialisation. If it is `true`, then the error was fatal and will have -/// killed the backend. Otherwise, the backend should still recieve and respond -/// to future operations. -/// -/// Other [Exception]s/[Error]s will result in this method being invoked, with a -/// modified [StackTrace] that gives an indication as to where the issue occured -/// internally, useful should a bug need to be reported. The trace will not -/// include where the original method that caused the error was invoked. This -/// exception/error may or may not be an issue directly in FMTC, it may be an -/// issue in the usage of a dependency (such as an exception caused by -/// exceeding a database's storage limit). -/// -/// If the callback returns `true`, then FMTC will not continue to handle the -/// error. Otherwise, FMTC will also throw the exception/error. If the callback -/// is not defined (in [FMTCBackend.initialise]), then FMTC will throw the -/// exception/error. -typedef FMTCExceptionHandler = bool Function({ - required Object? exception, - required StackTrace stackTrace, - required bool initialisationFailure, -}); - /// An error to be thrown by backend implementations in known events only /// /// A backend can create custom errors of this type, which is useful to show @@ -69,15 +38,3 @@ final class StoreNotExists extends FMTCBackendError { String toString() => 'StoreNotExists: The requested store "$storeName" did not exist'; } - -/// Indicates that the specified store structure could not be created because it -/// already existed -final class StoreAlreadyExists extends FMTCBackendError { - StoreAlreadyExists({required this.storeName}); - - final String storeName; - - @override - String toString() => - 'StoreAlreadyExists: The requested store "$storeName" already existed'; -} diff --git a/lib/src/store/manage.dart b/lib/src/store/manage.dart index fef3c720..070182d1 100644 --- a/lib/src/store/manage.dart +++ b/lib/src/store/manage.dart @@ -7,8 +7,8 @@ part of flutter_map_tile_caching; /// creation and deletion /// /// If the store is not in the expected state (of existence) when invoking an -/// operation, then an error will be thrown (likely [StoreNotExists] or -/// [StoreAlreadyExists]). It is recommended to check [ready] when necessary. +/// operation, then an error will be thrown ([StoreNotExists]). It is +/// recommended to check [ready] when necessary. class StoreManagement { StoreManagement._(FMTCStore store) : _storeName = store.storeName; diff --git a/lib/src/store/statistics.dart b/lib/src/store/statistics.dart index 660a1595..6893a91c 100644 --- a/lib/src/store/statistics.dart +++ b/lib/src/store/statistics.dart @@ -8,9 +8,8 @@ part of flutter_map_tile_caching; /// Provides statistics about an [FMTCStore] /// /// If the store is not in the expected state (of existence) when invoking an -/// operation, then an error will be thrown (likely [StoreNotExists] or -/// [StoreAlreadyExists]). It is recommended to check [StoreManagement.ready] -/// when necessary. +/// operation, then an error will be thrown ([StoreNotExists]). It is +/// recommended to check [StoreManagement.ready] when necessary. class StoreStats { StoreStats._(FMTCStore store) : _storeName = store.storeName; From cfca5f5b2a12a2af4fd07ccb54cc172f81cd9a04 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Fri, 1 Mar 2024 10:09:33 +0000 Subject: [PATCH 129/168] Improved error handling Former-commit-id: 2538cdbf8cf837bc544ff0bcdcc2f2f49f229b5b [formerly 29bffd8393841ba7cd539b63eedf88319d05127f] Former-commit-id: 23c55eb07f0ed17a0195fb0e8d6e48b74960caff --- .../impls/objectbox/backend/internal.dart | 49 +++++++++++-------- .../objectbox/backend/internal_worker.dart | 9 +++- 2 files changed, 37 insertions(+), 21 deletions(-) diff --git a/lib/src/backend/impls/objectbox/backend/internal.dart b/lib/src/backend/impls/objectbox/backend/internal.dart index a15337c3..3e439595 100644 --- a/lib/src/backend/impls/objectbox/backend/internal.dart +++ b/lib/src/backend/impls/objectbox/backend/internal.dart @@ -78,9 +78,21 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { controller.sink; // Will be inserted into by direct handler _sendPort!.send((id: id, type: type, args: args)); // Send cmd - // TODO: error handle try { - yield* controller.stream; // Return responses + // Not using yield* as it doesn't allow for correct error handling + await for (final evt in controller.stream) { + // Listen to responses + yield evt; + } + } catch (err, stackTrace) { + yield Error.throwWithStackTrace( + err, + StackTrace.fromString( + '$stackTrace\n#+ [FMTC] Unable to ' + 'attach final `StackTrace` when streaming results\n\n#+ [FMTC] (Debug Info) $type: $args\n', + ), + ); } finally { // Goto `onCancel` once output listening cancelled await controller.close(); @@ -103,17 +115,19 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { ).create(recursive: true); // Prepare to recieve `SendPort` from worker - late ByteData storeReference; _workerResOneShot[0] = Completer(); final workerInitialRes = _workerResOneShot[0]! - .future - .then<({Object err, StackTrace stackTrace})?>( + .future // Completed directly by handler below + .then<({ByteData? storeRef, Object? err, StackTrace? stackTrace})>( (res) { - storeReference = res!['storeReference']; - _sendPort = res['sendPort']; _workerResOneShot.remove(0); + _sendPort = res!['sendPort']; - return null; + return ( + storeRef: res['storeReference'] as ByteData, + err: null, + stackTrace: null, + ); }, onError: (err, stackTrace) { _workerHandler.cancel(); @@ -123,7 +137,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { _workerResOneShot.clear(); _workerResStreamed.clear(); - return (err: err, stackTrace: stackTrace); + return (storeRef: null, err: err, stackTrace: stackTrace); }, ); @@ -145,17 +159,12 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { // Handle errors final err = evt.data?['error']; if (err != null) { - final stackTrace = StackTrace.fromString( - '${evt.data?['stackTrace']! as StackTrace}' - '\n${StackTrace.current}', - ); - if (evt.data?['expectStream'] == true) { - _workerResStreamed[evt.id]!.addError(err, stackTrace); + _workerResStreamed[evt.id]!.addError(err, evt.data!['stackTrace']); } else { - _workerResOneShot[evt.id]!.completeError(err, stackTrace); + _workerResOneShot[evt.id]! + .completeError(err, evt.data!['stackTrace']); } - return; } @@ -186,13 +195,13 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { final initResult = await workerInitialRes; // Check whether initialisation was successful - if (initResult == null) { + if (initResult.storeRef != null) { FMTCBackendAccess.internal = this; FMTCBackendAccessThreadSafe.internal = _ObjectBoxBackendThreadSafeImpl._( - storeReference: storeReference, + storeReference: initResult.storeRef!, ); } else { - Error.throwWithStackTrace(initResult.err, initResult.stackTrace); + Error.throwWithStackTrace(initResult.err!, initResult.stackTrace!); } } diff --git a/lib/src/backend/impls/objectbox/backend/internal_worker.dart b/lib/src/backend/impls/objectbox/backend/internal_worker.dart index 07d11e33..9af8c494 100644 --- a/lib/src/backend/impls/objectbox/backend/internal_worker.dart +++ b/lib/src/backend/impls/objectbox/backend/internal_worker.dart @@ -819,7 +819,14 @@ Future _worker( sendRes(id: cmd.id); } } catch (e, s) { - sendRes(id: cmd.id, data: {'error': e, 'stackTrace': s}); + sendRes( + id: cmd.id, + data: { + if (cmd.type.streamCancel != null) 'expectStream': true, + 'error': e, + 'stackTrace': s, + }, + ); } }).asFuture(); } From d98070e97e0b595d091ee152691ec004bd03e7ba Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sat, 2 Mar 2024 22:01:52 +0000 Subject: [PATCH 130/168] Updated GitHub Workflow Former-commit-id: de2ca2aa34fef6bf36185c32fa28ce7c13c7591f [formerly d279c38fe27629f4310eae1fb06b1687f776cb39] Former-commit-id: cf7f1202e87bd99864bc7751f886460538c06c81 --- .github/workflows/main.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index af3857a4..916c13af 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -14,8 +14,6 @@ jobs: steps: - name: Checkout Repository uses: actions/checkout@master - - name: Disable Git Security False Alarm - run: git config --global --add safe.directory /github/workspace - name: Run Dart Package Analyser uses: axel-op/dart-package-analyzer@master id: analysis From 2f8be26ae6c175667680451aa98d0bc4217528a3 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Mon, 4 Mar 2024 21:18:31 +0000 Subject: [PATCH 131/168] Implemented exporting stores functionality Partially implemented importing stores functionality Former-commit-id: bbc821a6d8d238a72dd915d1c891339e064458c9 [formerly 69196095c81a9dd4c4202222d31a19f4a1ffc389] Former-commit-id: 3914a4fb647fc2e84b5cb0c48aae6f4f8c7df66a --- .../screens/import_store/import_store.dart | 12 + lib/flutter_map_tile_caching.dart | 3 +- .../{utils/errors.dart => errors/basic.dart} | 10 +- lib/src/backend/errors/errors.dart | 12 + lib/src/backend/errors/import_export.dart | 49 + lib/src/backend/export_external.dart | 2 +- .../impls/objectbox/backend/backend.dart | 1 + .../impls/objectbox/backend/errors.dart | 24 + .../impls/objectbox/backend/internal.dart | 67 +- .../objectbox/backend/internal_worker.dart | 1496 ++++++++++------- .../backend/interfaces/backend/internal.dart | 10 + lib/src/root/directory.dart | 2 +- lib/src/root/external.dart | 56 + lib/src/root/import.dart | 12 - lib/src/store/directory.dart | 8 +- lib/src/store/export.dart | 19 - 16 files changed, 1116 insertions(+), 667 deletions(-) rename lib/src/backend/{utils/errors.dart => errors/basic.dart} (76%) create mode 100644 lib/src/backend/errors/errors.dart create mode 100644 lib/src/backend/errors/import_export.dart create mode 100644 lib/src/backend/impls/objectbox/backend/errors.dart create mode 100644 lib/src/root/external.dart delete mode 100644 lib/src/root/import.dart delete mode 100644 lib/src/store/export.dart diff --git a/example/lib/screens/import_store/import_store.dart b/example/lib/screens/import_store/import_store.dart index aa8c2fa8..94a46dee 100644 --- a/example/lib/screens/import_store/import_store.dart +++ b/example/lib/screens/import_store/import_store.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; //import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; //import 'package:fmtc_plus_sharing/fmtc_plus_sharing.dart'; @@ -14,6 +15,17 @@ class ImportStorePopup extends StatefulWidget { class _ImportStorePopupState extends State { final Map importStores = {}; + @override + void initState() { + super.initState(); + + FMTCRoot.external + .import(path: r'C:\Users\lukas\Documents\fmtc_export\wow.fmtc') + .listen((_) { + print('evt'); + }); + } + // TODO: Implement @override Widget build(BuildContext context) => Scaffold( diff --git a/lib/flutter_map_tile_caching.dart b/lib/flutter_map_tile_caching.dart index c8ca09eb..fbee4b80 100644 --- a/lib/flutter_map_tile_caching.dart +++ b/lib/flutter_map_tile_caching.dart @@ -55,12 +55,11 @@ part 'src/regions/line.dart'; part 'src/regions/recovered_region.dart'; part 'src/regions/rectangle.dart'; part 'src/root/directory.dart'; -part 'src/root/import.dart'; +part 'src/root/external.dart'; part 'src/root/recovery.dart'; part 'src/root/statistics.dart'; part 'src/store/directory.dart'; part 'src/store/download.dart'; -part 'src/store/export.dart'; part 'src/store/manage.dart'; part 'src/store/metadata.dart'; part 'src/store/statistics.dart'; diff --git a/lib/src/backend/utils/errors.dart b/lib/src/backend/errors/basic.dart similarity index 76% rename from lib/src/backend/utils/errors.dart rename to lib/src/backend/errors/basic.dart index 5df7ee83..4f6b3ab4 100644 --- a/lib/src/backend/utils/errors.dart +++ b/lib/src/backend/errors/basic.dart @@ -1,12 +1,7 @@ // Copyright © Luka S (JaffaKetchup) under GPL-v3 // A full license can be found at .\LICENSE -/// An error to be thrown by backend implementations in known events only -/// -/// A backend can create custom errors of this type, which is useful to show -/// that the backend is throwing a known expected error, rather than an -/// unexpected one. -base class FMTCBackendError extends Error {} +part of 'errors.dart'; /// Indicates that the backend/root structure (ie. database and/or directory) was /// not available for use in operations, because either: @@ -24,7 +19,8 @@ final class RootUnavailable extends FMTCBackendError { final class RootAlreadyInitialised extends FMTCBackendError { @override String toString() => - 'RootAlreadyInitialised: The requested backend/root could not be initialised because it was already initialised'; + 'RootAlreadyInitialised: The requested backend/root could not be ' + 'initialised because it was already initialised'; } /// Indicates that the specified store structure was not available for use in diff --git a/lib/src/backend/errors/errors.dart b/lib/src/backend/errors/errors.dart new file mode 100644 index 00000000..52b25b88 --- /dev/null +++ b/lib/src/backend/errors/errors.dart @@ -0,0 +1,12 @@ +// Copyright © Luka S (JaffaKetchup) under GPL-v3 +// A full license can be found at .\LICENSE + +part 'basic.dart'; +part 'import_export.dart'; + +/// An error to be thrown by backend implementations in known events only +/// +/// A backend can create custom errors of this type, which is useful to show +/// that the backend is throwing a known expected error, rather than an +/// unexpected one. +base class FMTCBackendError extends Error {} diff --git a/lib/src/backend/errors/import_export.dart b/lib/src/backend/errors/import_export.dart new file mode 100644 index 00000000..e9be18e4 --- /dev/null +++ b/lib/src/backend/errors/import_export.dart @@ -0,0 +1,49 @@ +// Copyright © Luka S (JaffaKetchup) under GPL-v3 +// A full license can be found at .\LICENSE + +part of 'errors.dart'; + +/// A subset of [FMTCBackendError]s that indicates a failure during import, due +/// to the extended reason +base class ImportError extends FMTCBackendError {} + +/// Indicates that the specified file to import did not exist/could not be found +final class ImportFileNotExists extends ImportError { + ImportFileNotExists({required this.path}); + + /// The specified path to the import file + final String path; + + @override + String toString() => + 'FileNotExists: The specified import file ($path) did not exist.'; +} + +/// Indicates that the import file was not of the expected standard, because it +/// either: +/// * did not contain the appropriate footer signature: hex "FF FF 46 4D 54 43" +/// ("**FMTC") +/// * did not contain all required header information within the file +final class ImportFileNotFMTCStandard extends ImportError { + ImportFileNotFMTCStandard(); + + @override + String toString() => + 'ImportFileNotFMTCStandard: The import file was not of the expected ' + 'standard.'; +} + +/// Indicates that the import file was exported from a different FMTC backend, +/// and is not compatible with the current backend +/// +/// The bytes prior to the header signature (hex "FF FF 46 4D 54 43" ("**FMTC")) +/// should an identifier (eg. the name) of the exporting backend proceeded by +/// hex "FF FE". +final class ImportFileNotBackendCompatible extends ImportError { + ImportFileNotBackendCompatible(); + + @override + String toString() => + 'ImportFileNotBackendCompatible: The import file was exported from a ' + 'different FMTC backend, and is not compatible with the current backend'; +} diff --git a/lib/src/backend/export_external.dart b/lib/src/backend/export_external.dart index 47f063f3..4d2890e0 100644 --- a/lib/src/backend/export_external.dart +++ b/lib/src/backend/export_external.dart @@ -1,8 +1,8 @@ // Copyright © Luka S (JaffaKetchup) under GPL-v3 // A full license can be found at .\LICENSE +export 'errors/errors.dart'; export 'impls/objectbox/backend/backend.dart'; export 'interfaces/backend/backend.dart'; export 'interfaces/backend/internal.dart'; export 'interfaces/backend/internal_thread_safe.dart'; -export 'utils/errors.dart'; diff --git a/lib/src/backend/impls/objectbox/backend/backend.dart b/lib/src/backend/impls/objectbox/backend/backend.dart index a3739159..d9157c94 100644 --- a/lib/src/backend/impls/objectbox/backend/backend.dart +++ b/lib/src/backend/impls/objectbox/backend/backend.dart @@ -20,6 +20,7 @@ import '../models/src/tile.dart'; export 'package:objectbox/objectbox.dart' show StorageException; +part 'errors.dart'; part 'internal_thread_safe.dart'; part 'internal.dart'; part 'internal_worker.dart'; diff --git a/lib/src/backend/impls/objectbox/backend/errors.dart b/lib/src/backend/impls/objectbox/backend/errors.dart new file mode 100644 index 00000000..8b41245e --- /dev/null +++ b/lib/src/backend/impls/objectbox/backend/errors.dart @@ -0,0 +1,24 @@ +// Copyright © Luka S (JaffaKetchup) under GPL-v3 +// A full license can be found at .\LICENSE + +part of 'backend.dart'; + +/// An [FMTCBackendError] that originates specifically from the +/// [FMTCObjectBoxBackend] +/// +/// The [FMTCObjectBoxBackend] may also emit errors directly of type +/// [FMTCBackendError]. +base class FMTCObjectBoxBackendError extends FMTCBackendError {} + +/// Indicates that an export failed because the specified output path directory +/// was the same as the root directory +final class ExportInRootDirectoryForbidden extends FMTCObjectBoxBackendError { + ExportInRootDirectoryForbidden({required this.directory}); + + final String directory; + + @override + String toString() => + 'ExportInRootDirectoryForbidden: It is forbidden to export stores to the ' + 'same directory ($directory) as the `rootDirectory`'; +} diff --git a/lib/src/backend/impls/objectbox/backend/internal.dart b/lib/src/backend/impls/objectbox/backend/internal.dart index 3e439595..4be26c3d 100644 --- a/lib/src/backend/impls/objectbox/backend/internal.dart +++ b/lib/src/backend/impls/objectbox/backend/internal.dart @@ -423,45 +423,6 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { return -1; } - /* FOR ABOVE METHOD - // Attempts to avoid flooding worker with requests to delete oldest tile, - // and 'batches' them instead - - if (_dotStore != storeName) { - // If the store has changed, failing to reset the batch/queue will mean - // tiles are removed from the wrong store - _dotStore = storeName; - if (_dotDebouncer.isActive) { - _dotDebouncer.cancel(); - _sendROTCmd(storeName); - _dotLength += numToRemove; - } - } - - if (_dotDebouncer.isActive) { - _dotDebouncer.cancel(); - _dotDebouncer = Timer( - const Duration(milliseconds: 500), - () => _sendROTCmd(storeName), - ); - _dotLength += numToRemove; - return; - } - - _dotDebouncer = - Timer(const Duration(seconds: 1), () => _sendROTCmd(storeName)); - _dotLength += numToRemove; - - // may need to be moved out - void _sendROTCmd(String storeName) { - _sendCmd( - type: _WorkerCmdType.removeOldestTile, - args: {'storeName': storeName, 'number': _dotLength}, - ); - _dotLength = 0; - } - */ - @override Future removeTilesOlderThan({ required String storeName, @@ -573,4 +534,32 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { 'triggerImmediately': triggerImmediately, }, ); + + @override + Future exportStores({ + required List storeNames, + required String outputPath, + }) => + _sendCmdOneShot( + type: _WorkerCmdType.exportStores, + args: {'storeNames': storeNames, 'outputPath': outputPath}, + ); + + @override + Future>> importStores({ + required String path, + required ImportConflictStrategy strategy, + }) async { + final storeStatuses = >{}; + + await for (final evt in _sendCmdStreamed( + type: _WorkerCmdType.importStores, + args: {'path': path, 'strategy': strategy}, + )) { + if (evt!.containsKey('finished')) break; + if (evt['storeName'] case final String storeName) { + storeStatuses[storeName] = Completer(); + } + } + } } diff --git a/lib/src/backend/impls/objectbox/backend/internal_worker.dart b/lib/src/backend/impls/objectbox/backend/internal_worker.dart index 9af8c494..4f1358bd 100644 --- a/lib/src/backend/impls/objectbox/backend/internal_worker.dart +++ b/lib/src/backend/impls/objectbox/backend/internal_worker.dart @@ -3,6 +3,12 @@ part of 'backend.dart'; +typedef _IncomingCmd = ({ + int id, + _WorkerCmdType type, + Map args +}); + enum _WorkerCmdType { initialise_, // Only valid as a request destroy, @@ -33,13 +39,18 @@ enum _WorkerCmdType { getRecoverableRegion, startRecovery, cancelRecovery, - watchRecovery(streamCancel: cancelWatch), - watchStores(streamCancel: cancelWatch), - cancelWatch; + watchRecovery(streamCancel: cancelStreamedOutputs), + watchStores(streamCancel: cancelStreamedOutputs), + exportStores, + importStores(streamCancel: cancelStreamedOutputs), + cancelStreamedOutputs, + ; const _WorkerCmdType({this.streamCancel}); /// Command to execute when cancelling a streamed result + /// + /// All streamed cmds must specify a cancel cmd. final _WorkerCmdType? streamCancel; } @@ -170,427 +181,394 @@ Future _worker( return rootDeltaLength.abs(); } - //! MAIN LOOP !// - - await receivePort.listen((cmd) { - cmd as ({ - int id, - _WorkerCmdType type, - Map args, - }); - - try { - switch (cmd.type) { - case _WorkerCmdType.initialise_: - throw UnsupportedError('Invalid operation'); - case _WorkerCmdType.destroy: - root.close(); - - if (cmd.args['deleteRoot'] == true) { - input.rootDirectory.deleteSync(recursive: true); - } - - sendRes(id: cmd.id); - - Isolate.exit(); - case _WorkerCmdType.realSize: - sendRes( - id: cmd.id, - data: { - 'size': Store.dbFileSize(input.rootDirectory.absolute.path) / - 1024, // Convert to KiB - }, - ); - case _WorkerCmdType.rootSize: - sendRes( - id: cmd.id, - data: {'size': root.box().get(1)!.size / 1024}, - ); - case _WorkerCmdType.rootLength: - sendRes( - id: cmd.id, - data: {'length': root.box().get(1)!.length}, - ); - case _WorkerCmdType.listStores: - final query = root - .box() - .query() - .build() - .property(ObjectBoxStore_.name); - - sendRes(id: cmd.id, data: {'stores': query.find()}); - - query.close(); - case _WorkerCmdType.storeExists: - final query = root - .box() - .query( - ObjectBoxStore_.name.equals(cmd.args['storeName']! as String), - ) - .build(); - - sendRes(id: cmd.id, data: {'exists': query.count() == 1}); + //! MAIN HANDLER !// - query.close(); - case _WorkerCmdType.getStoreStats: - final storeName = cmd.args['storeName']! as String; + void mainHandler(_IncomingCmd cmd) { + switch (cmd.type) { + case _WorkerCmdType.initialise_: + throw UnsupportedError('Invalid operation'); + case _WorkerCmdType.destroy: + root.close(); - final query = root - .box() - .query(ObjectBoxStore_.name.equals(storeName)) - .build(); + if (cmd.args['deleteRoot'] == true) { + input.rootDirectory.deleteSync(recursive: true); + } - final store = query.findUnique() ?? - (throw StoreNotExists(storeName: storeName)); + sendRes(id: cmd.id); - sendRes( - id: cmd.id, - data: { - 'stats': ( - size: store.size / 1024, // Convert to KiB - length: store.length, - hits: store.hits, - misses: store.misses, - ), - }, - ); - - query.close(); - case _WorkerCmdType.createStore: - final storeName = cmd.args['storeName']! as String; - - try { - root.box().put( - ObjectBoxStore( - name: storeName, - length: 0, - size: 0, - hits: 0, - misses: 0, - metadataJson: '', - ), - mode: PutMode.insert, - ); - } on UniqueViolationException { - sendRes(id: cmd.id); - break; - } + Isolate.exit(); + case _WorkerCmdType.realSize: + sendRes( + id: cmd.id, + data: { + 'size': Store.dbFileSize(input.rootDirectory.absolute.path) / + 1024, // Convert to KiB + }, + ); + case _WorkerCmdType.rootSize: + sendRes( + id: cmd.id, + data: {'size': root.box().get(1)!.size / 1024}, + ); + case _WorkerCmdType.rootLength: + sendRes( + id: cmd.id, + data: {'length': root.box().get(1)!.length}, + ); + case _WorkerCmdType.listStores: + final query = root + .box() + .query() + .build() + .property(ObjectBoxStore_.name); + + sendRes(id: cmd.id, data: {'stores': query.find()}); + + query.close(); + case _WorkerCmdType.storeExists: + final query = root + .box() + .query( + ObjectBoxStore_.name.equals(cmd.args['storeName']! as String), + ) + .build(); + + sendRes(id: cmd.id, data: {'exists': query.count() == 1}); + + query.close(); + case _WorkerCmdType.getStoreStats: + final storeName = cmd.args['storeName']! as String; + + final query = root + .box() + .query(ObjectBoxStore_.name.equals(storeName)) + .build(); + + final store = + query.findUnique() ?? (throw StoreNotExists(storeName: storeName)); + + sendRes( + id: cmd.id, + data: { + 'stats': ( + size: store.size / 1024, // Convert to KiB + length: store.length, + hits: store.hits, + misses: store.misses, + ), + }, + ); - sendRes(id: cmd.id); - case _WorkerCmdType.resetStore: - final storeName = cmd.args['storeName']! as String; - - final tiles = root.box(); - final stores = root.box(); - - final tilesQuery = (tiles.query() - ..linkMany( - ObjectBoxTile_.stores, - ObjectBoxStore_.name.equals(storeName), - )) - .build(); - - final storeQuery = - stores.query(ObjectBoxStore_.name.equals(storeName)).build(); - - root.runInTransaction( - TxMode.write, - () { - deleteTiles(storeName: storeName, tilesQuery: tilesQuery); - - final store = storeQuery.findUnique() ?? - (throw StoreNotExists(storeName: storeName)); - storeQuery.close(); - - stores.put( - store - ..length = 0 - ..size = 0 - ..hits = 0 - ..misses = 0, - mode: PutMode.update, + query.close(); + case _WorkerCmdType.createStore: + final storeName = cmd.args['storeName']! as String; + + try { + root.box().put( + ObjectBoxStore( + name: storeName, + length: 0, + size: 0, + hits: 0, + misses: 0, + metadataJson: '', + ), + mode: PutMode.insert, ); - }, - ); - + } on UniqueViolationException { sendRes(id: cmd.id); - case _WorkerCmdType.renameStore: - final currentStoreName = cmd.args['currentStoreName']! as String; - final newStoreName = cmd.args['newStoreName']! as String; + break; + } - final stores = root.box(); + sendRes(id: cmd.id); + case _WorkerCmdType.resetStore: + final storeName = cmd.args['storeName']! as String; + + final tiles = root.box(); + final stores = root.box(); + + final tilesQuery = (tiles.query() + ..linkMany( + ObjectBoxTile_.stores, + ObjectBoxStore_.name.equals(storeName), + )) + .build(); + + final storeQuery = + stores.query(ObjectBoxStore_.name.equals(storeName)).build(); + + root.runInTransaction( + TxMode.write, + () { + deleteTiles(storeName: storeName, tilesQuery: tilesQuery); + + final store = storeQuery.findUnique() ?? + (throw StoreNotExists(storeName: storeName)); + storeQuery.close(); + + stores.put( + store + ..length = 0 + ..size = 0 + ..hits = 0 + ..misses = 0, + mode: PutMode.update, + ); + }, + ); - final query = stores - .query(ObjectBoxStore_.name.equals(currentStoreName)) - .build(); + sendRes(id: cmd.id); + case _WorkerCmdType.renameStore: + final currentStoreName = cmd.args['currentStoreName']! as String; + final newStoreName = cmd.args['newStoreName']! as String; - root.runInTransaction( - TxMode.write, - () { - final store = query.findUnique() ?? - (throw StoreNotExists(storeName: currentStoreName)); - query.close(); + final stores = root.box(); - stores.put(store..name = newStoreName, mode: PutMode.update); - }, - ); + final query = + stores.query(ObjectBoxStore_.name.equals(currentStoreName)).build(); - sendRes(id: cmd.id); - case _WorkerCmdType.deleteStore: - final storeName = cmd.args['storeName']! as String; - - final storesQuery = root - .box() - .query(ObjectBoxStore_.name.equals(storeName)) - .build(); - final tilesQuery = (root.box().query() - ..linkMany( - ObjectBoxTile_.stores, - ObjectBoxStore_.name.equals(storeName), - )) - .build(); - - root.runInTransaction( - TxMode.write, - () { - deleteTiles(storeName: storeName, tilesQuery: tilesQuery); - storesQuery.remove(); - }, - ); + root.runInTransaction( + TxMode.write, + () { + final store = query.findUnique() ?? + (throw StoreNotExists(storeName: currentStoreName)); + query.close(); - sendRes(id: cmd.id); + stores.put(store..name = newStoreName, mode: PutMode.update); + }, + ); - storesQuery.close(); - tilesQuery.close(); - case _WorkerCmdType.tileExistsInStore: - final storeName = cmd.args['storeName']! as String; - final url = cmd.args['url']! as String; - - final query = - (root.box().query(ObjectBoxTile_.url.equals(url)) - ..linkMany( - ObjectBoxTile_.stores, - ObjectBoxStore_.name.equals(storeName), - )) - .build(); - - sendRes(id: cmd.id, data: {'exists': query.count() == 1}); - - query.close(); - case _WorkerCmdType.readTile: - final url = cmd.args['url']! as String; - final storeName = cmd.args['storeName'] as String?; - - final stores = root.box(); - - final query = storeName == null - ? stores.query(ObjectBoxTile_.url.equals(url)).build() - : (stores.query(ObjectBoxTile_.url.equals(url)) - ..linkMany( - ObjectBoxTile_.stores, - ObjectBoxStore_.name.equals(storeName), - )) - .build(); - - sendRes(id: cmd.id, data: {'tile': query.findUnique()}); - - query.close(); - case _WorkerCmdType.readLatestTile: - final storeName = cmd.args['storeName']! as String; - - final query = (root - .box() - .query() - .order(ObjectBoxTile_.lastModified, flags: Order.descending) - ..linkMany( - ObjectBoxTile_.stores, - ObjectBoxStore_.name.equals(storeName), - )) - .build(); - - sendRes(id: cmd.id, data: {'tile': query.findFirst()}); - - query.close(); - case _WorkerCmdType.writeTile: - // TODO: Test all - final storeName = cmd.args['storeName']! as String; - final url = cmd.args['url']! as String; - final bytes = cmd.args['bytes'] as Uint8List?; - - final tiles = root.box(); - final stores = root.box(); - - final tilesQuery = - tiles.query(ObjectBoxTile_.url.equals(url)).build(); - final storeQuery = - stores.query(ObjectBoxStore_.name.equals(storeName)).build(); - - root.runInTransaction( - TxMode.write, - () { - final existingTile = tilesQuery.findUnique(); - - final store = storeQuery.findUnique() ?? - (throw StoreNotExists(storeName: storeName)); - - switch ((existingTile == null, bytes == null)) { - case (true, false): // No existing tile - tiles.put( - ObjectBoxTile( - url: url, - lastModified: DateTime.timestamp(), - bytes: bytes!, - )..stores.add(store), - ); + sendRes(id: cmd.id); + case _WorkerCmdType.deleteStore: + final storeName = cmd.args['storeName']! as String; + + final storesQuery = root + .box() + .query(ObjectBoxStore_.name.equals(storeName)) + .build(); + final tilesQuery = (root.box().query() + ..linkMany( + ObjectBoxTile_.stores, + ObjectBoxStore_.name.equals(storeName), + )) + .build(); + + root.runInTransaction( + TxMode.write, + () { + deleteTiles(storeName: storeName, tilesQuery: tilesQuery); + storesQuery.remove(); + }, + ); + + sendRes(id: cmd.id); + + storesQuery.close(); + tilesQuery.close(); + case _WorkerCmdType.tileExistsInStore: + final storeName = cmd.args['storeName']! as String; + final url = cmd.args['url']! as String; + + final query = + (root.box().query(ObjectBoxTile_.url.equals(url)) + ..linkMany( + ObjectBoxTile_.stores, + ObjectBoxStore_.name.equals(storeName), + )) + .build(); + + sendRes(id: cmd.id, data: {'exists': query.count() == 1}); + + query.close(); + case _WorkerCmdType.readTile: + final url = cmd.args['url']! as String; + final storeName = cmd.args['storeName'] as String?; + + final stores = root.box(); + + final query = storeName == null + ? stores.query(ObjectBoxTile_.url.equals(url)).build() + : (stores.query(ObjectBoxTile_.url.equals(url)) + ..linkMany( + ObjectBoxTile_.stores, + ObjectBoxStore_.name.equals(storeName), + )) + .build(); + + sendRes(id: cmd.id, data: {'tile': query.findUnique()}); + + query.close(); + case _WorkerCmdType.readLatestTile: + final storeName = cmd.args['storeName']! as String; + + final query = (root + .box() + .query() + .order(ObjectBoxTile_.lastModified, flags: Order.descending) + ..linkMany( + ObjectBoxTile_.stores, + ObjectBoxStore_.name.equals(storeName), + )) + .build(); + + sendRes(id: cmd.id, data: {'tile': query.findFirst()}); + + query.close(); + case _WorkerCmdType.writeTile: + // TODO: Test all + final storeName = cmd.args['storeName']! as String; + final url = cmd.args['url']! as String; + final bytes = cmd.args['bytes'] as Uint8List?; + + final tiles = root.box(); + final stores = root.box(); + + final tilesQuery = tiles.query(ObjectBoxTile_.url.equals(url)).build(); + final storeQuery = + stores.query(ObjectBoxStore_.name.equals(storeName)).build(); + + root.runInTransaction( + TxMode.write, + () { + final existingTile = tilesQuery.findUnique(); + + final store = storeQuery.findUnique() ?? + (throw StoreNotExists(storeName: storeName)); + + switch ((existingTile == null, bytes == null)) { + case (true, false): // No existing tile + tiles.put( + ObjectBoxTile( + url: url, + lastModified: DateTime.timestamp(), + bytes: bytes!, + )..stores.add(store), + ); + stores.put( + store + ..length += 1 + ..size += bytes.lengthInBytes, + ); + updateRootStatistics( + deltaLength: 1, + deltaSize: bytes.lengthInBytes, + ); + case (false, true): // Existing tile, no update + // Only take action if it's not already belonging to the store + if (!existingTile!.stores.contains(store)) { + tiles.put(existingTile..stores.add(store)); stores.put( store ..length += 1 - ..size += bytes.lengthInBytes, + ..size += existingTile.bytes.lengthInBytes, ); updateRootStatistics( deltaLength: 1, - deltaSize: bytes.lengthInBytes, - ); - case (false, true): // Existing tile, no update - // Only take action if it's not already belonging to the store - if (!existingTile!.stores.contains(store)) { - tiles.put(existingTile..stores.add(store)); - stores.put( - store - ..length += 1 - ..size += existingTile.bytes.lengthInBytes, - ); - updateRootStatistics( - deltaLength: 1, - deltaSize: existingTile.bytes.lengthInBytes, - ); - } - case (false, false): // Existing tile, update required - int rootDeltaSize = 0; - tiles.put( - existingTile! - ..lastModified = DateTime.timestamp() - ..bytes = bytes!, - mode: PutMode.update, - ); - stores.putMany( - existingTile.stores.map( - (store) { - final diff = bytes.lengthInBytes - - existingTile.bytes.lengthInBytes; - rootDeltaSize += diff; - return store..size += diff; - }, - ).toList(growable: false), - ); - updateRootStatistics(deltaSize: rootDeltaSize); - case (true, true): // FMTC internal error - throw StateError( - 'FMTC ObjectBox backend internal state error: $url', + deltaSize: existingTile.bytes.lengthInBytes, ); - } - }, - ); + } + case (false, false): // Existing tile, update required + int rootDeltaSize = 0; + tiles.put( + existingTile! + ..lastModified = DateTime.timestamp() + ..bytes = bytes!, + mode: PutMode.update, + ); + stores.putMany( + existingTile.stores.map( + (store) { + final diff = bytes.lengthInBytes - + existingTile.bytes.lengthInBytes; + rootDeltaSize += diff; + return store..size += diff; + }, + ).toList(growable: false), + ); + updateRootStatistics(deltaSize: rootDeltaSize); + case (true, true): // FMTC internal error + throw StateError( + 'FMTC ObjectBox backend internal state error: $url', + ); + } + }, + ); - sendRes(id: cmd.id); + sendRes(id: cmd.id); + + storeQuery.close(); + tilesQuery.close(); + case _WorkerCmdType.deleteTile: + final storeName = cmd.args['storeName']! as String; + final url = cmd.args['url']! as String; + + final query = root + .box() + .query(ObjectBoxTile_.url.equals(url)) + .build(); + + sendRes( + id: cmd.id, + data: { + 'wasOrphan': + deleteTiles(storeName: storeName, tilesQuery: query) == 1, + }, + ); - storeQuery.close(); - tilesQuery.close(); - case _WorkerCmdType.deleteTile: - final storeName = cmd.args['storeName']! as String; - final url = cmd.args['url']! as String; + query.close(); + case _WorkerCmdType.registerHitOrMiss: + final storeName = cmd.args['storeName']! as String; + final hit = cmd.args['hit']! as bool; - final query = root - .box() - .query(ObjectBoxTile_.url.equals(url)) - .build(); + final stores = root.box(); - sendRes( - id: cmd.id, - data: { - 'wasOrphan': - deleteTiles(storeName: storeName, tilesQuery: query) == 1, - }, - ); + final query = + stores.query(ObjectBoxStore_.name.equals(storeName)).build(); - query.close(); - case _WorkerCmdType.registerHitOrMiss: - final storeName = cmd.args['storeName']! as String; - final hit = cmd.args['hit']! as bool; + root.runInTransaction( + TxMode.write, + () { + final store = query.findUnique() ?? + (throw StoreNotExists(storeName: storeName)); + query.close(); - final stores = root.box(); + stores.put( + store + ..hits += hit ? 1 : 0 + ..misses += hit ? 0 : 1, + ); + }, + ); - final query = - stores.query(ObjectBoxStore_.name.equals(storeName)).build(); + sendRes(id: cmd.id); + case _WorkerCmdType.removeOldestTilesAboveLimit: + final storeName = cmd.args['storeName']! as String; + final tilesLimit = cmd.args['tilesLimit']! as int; + + final tilesQuery = (root + .box() + .query() + .order(ObjectBoxTile_.lastModified) + ..linkMany( + ObjectBoxTile_.stores, + ObjectBoxStore_.name.equals(storeName), + )) + .build(); + + final storeQuery = root + .box() + .query(ObjectBoxStore_.name.equals(storeName)) + .build(); - root.runInTransaction( - TxMode.write, - () { - final store = query.findUnique() ?? - (throw StoreNotExists(storeName: storeName)); - query.close(); + final store = storeQuery.findUnique() ?? + (throw StoreNotExists(storeName: storeName)); - stores.put( - store - ..hits += hit ? 1 : 0 - ..misses += hit ? 0 : 1, - ); - }, - ); + final numToRemove = store.length - tilesLimit; - sendRes(id: cmd.id); - case _WorkerCmdType.removeOldestTilesAboveLimit: - final storeName = cmd.args['storeName']! as String; - final tilesLimit = cmd.args['tilesLimit']! as int; - - final tilesQuery = (root - .box() - .query() - .order(ObjectBoxTile_.lastModified) - ..linkMany( - ObjectBoxTile_.stores, - ObjectBoxStore_.name.equals(storeName), - )) - .build(); - - final storeQuery = root - .box() - .query(ObjectBoxStore_.name.equals(storeName)) - .build(); - - final store = storeQuery.findUnique() ?? - (throw StoreNotExists(storeName: storeName)); - - final numToRemove = store.length - tilesLimit; - - if (numToRemove <= 0) { - sendRes(id: cmd.id, data: {'numOrphans': 0}); - } else { - tilesQuery.limit = numToRemove; - - sendRes( - id: cmd.id, - data: { - 'numOrphans': - deleteTiles(storeName: storeName, tilesQuery: tilesQuery), - }, - ); - } - - storeQuery.close(); - tilesQuery.close(); - case _WorkerCmdType.removeTilesOlderThan: - final storeName = cmd.args['storeName']! as String; - final expiry = cmd.args['expiry']! as DateTime; - - final tilesQuery = (root.box().query( - ObjectBoxTile_.lastModified - .greaterThan(expiry.millisecondsSinceEpoch), - )..linkMany( - ObjectBoxTile_.stores, - ObjectBoxStore_.name.equals(storeName), - )) - .build(); + if (numToRemove <= 0) { + sendRes(id: cmd.id, data: {'numOrphans': 0}); + } else { + tilesQuery.limit = numToRemove; sendRes( id: cmd.id, @@ -599,226 +577,586 @@ Future _worker( deleteTiles(storeName: storeName, tilesQuery: tilesQuery), }, ); + } - tilesQuery.close(); - case _WorkerCmdType.readMetadata: - final storeName = cmd.args['storeName']! as String; + storeQuery.close(); + tilesQuery.close(); + case _WorkerCmdType.removeTilesOlderThan: + final storeName = cmd.args['storeName']! as String; + final expiry = cmd.args['expiry']! as DateTime; + + final tilesQuery = (root.box().query( + ObjectBoxTile_.lastModified + .greaterThan(expiry.millisecondsSinceEpoch), + )..linkMany( + ObjectBoxTile_.stores, + ObjectBoxStore_.name.equals(storeName), + )) + .build(); + + sendRes( + id: cmd.id, + data: { + 'numOrphans': + deleteTiles(storeName: storeName, tilesQuery: tilesQuery), + }, + ); - final query = root - .box() - .query(ObjectBoxStore_.name.equals(storeName)) - .build(); + tilesQuery.close(); + case _WorkerCmdType.readMetadata: + final storeName = cmd.args['storeName']! as String; - final store = query.findUnique() ?? - (throw StoreNotExists(storeName: storeName)); + final query = root + .box() + .query(ObjectBoxStore_.name.equals(storeName)) + .build(); - sendRes( - id: cmd.id, - data: { - 'metadata': - (jsonDecode(store.metadataJson) as Map) - .cast(), - }, - ); + final store = + query.findUnique() ?? (throw StoreNotExists(storeName: storeName)); - query.close(); - case _WorkerCmdType.setMetadata: - final storeName = cmd.args['storeName']! as String; - final key = cmd.args['key']! as String; - final value = cmd.args['value']! as String; + sendRes( + id: cmd.id, + data: { + 'metadata': (jsonDecode(store.metadataJson) as Map) + .cast(), + }, + ); - final stores = root.box(); + query.close(); + case _WorkerCmdType.setMetadata: + final storeName = cmd.args['storeName']! as String; + final key = cmd.args['key']! as String; + final value = cmd.args['value']! as String; - final query = - stores.query(ObjectBoxStore_.name.equals(storeName)).build(); + final stores = root.box(); - root.runInTransaction( - TxMode.write, - () { - final store = query.findUnique() ?? - (throw StoreNotExists(storeName: storeName)); - query.close(); + final query = + stores.query(ObjectBoxStore_.name.equals(storeName)).build(); - final Map json = store.metadataJson == '' - ? {} - : jsonDecode(store.metadataJson); - json[key] = value; + root.runInTransaction( + TxMode.write, + () { + final store = query.findUnique() ?? + (throw StoreNotExists(storeName: storeName)); + query.close(); - stores.put( - store..metadataJson = jsonEncode(json), - mode: PutMode.update, - ); - }, - ); + final Map json = + store.metadataJson == '' ? {} : jsonDecode(store.metadataJson); + json[key] = value; - sendRes(id: cmd.id); - case _WorkerCmdType.setBulkMetadata: - final storeName = cmd.args['storeName']! as String; - final kvs = cmd.args['kvs']! as Map; - - final stores = root.box(); - - final query = - stores.query(ObjectBoxStore_.name.equals(storeName)).build(); - - root.runInTransaction( - TxMode.write, - () { - final store = query.findUnique() ?? - (throw StoreNotExists(storeName: storeName)); - query.close(); - - final Map json = store.metadataJson == '' - ? {} - : jsonDecode(store.metadataJson); - // ignore: cascade_invocations - json.addAll(kvs); - - stores.put( - store..metadataJson = jsonEncode(json), - mode: PutMode.update, - ); - }, - ); + stores.put( + store..metadataJson = jsonEncode(json), + mode: PutMode.update, + ); + }, + ); - sendRes(id: cmd.id); - case _WorkerCmdType.removeMetadata: - final storeName = cmd.args['storeName']! as String; - final key = cmd.args['key']! as String; + sendRes(id: cmd.id); + case _WorkerCmdType.setBulkMetadata: + final storeName = cmd.args['storeName']! as String; + final kvs = cmd.args['kvs']! as Map; - final stores = root.box(); + final stores = root.box(); - final query = - stores.query(ObjectBoxStore_.name.equals(storeName)).build(); + final query = + stores.query(ObjectBoxStore_.name.equals(storeName)).build(); - sendRes( - id: cmd.id, - data: { - 'removedValue': root.runInTransaction( - TxMode.write, - () { - final store = query.findUnique() ?? - (throw StoreNotExists(storeName: storeName)); - query.close(); + root.runInTransaction( + TxMode.write, + () { + final store = query.findUnique() ?? + (throw StoreNotExists(storeName: storeName)); + query.close(); - final metadata = - jsonDecode(store.metadataJson) as Map; - final removedVal = metadata.remove(key); + final Map json = + store.metadataJson == '' ? {} : jsonDecode(store.metadataJson); + // ignore: cascade_invocations + json.addAll(kvs); - stores.put( - store..metadataJson = jsonEncode(metadata), - mode: PutMode.update, - ); + stores.put( + store..metadataJson = jsonEncode(json), + mode: PutMode.update, + ); + }, + ); - return removedVal; - }, + sendRes(id: cmd.id); + case _WorkerCmdType.removeMetadata: + final storeName = cmd.args['storeName']! as String; + final key = cmd.args['key']! as String; + + final stores = root.box(); + + final query = + stores.query(ObjectBoxStore_.name.equals(storeName)).build(); + + sendRes( + id: cmd.id, + data: { + 'removedValue': root.runInTransaction( + TxMode.write, + () { + final store = query.findUnique() ?? + (throw StoreNotExists(storeName: storeName)); + query.close(); + + final metadata = + jsonDecode(store.metadataJson) as Map; + final removedVal = metadata.remove(key); + + stores.put( + store..metadataJson = jsonEncode(metadata), + mode: PutMode.update, + ); + + return removedVal; + }, + ), + }, + ); + case _WorkerCmdType.resetMetadata: + final storeName = cmd.args['storeName']! as String; + + final stores = root.box(); + + final query = + stores.query(ObjectBoxStore_.name.equals(storeName)).build(); + + root.runInTransaction( + TxMode.write, + () { + final store = query.findUnique() ?? + (throw StoreNotExists(storeName: storeName)); + query.close(); + + stores.put( + store..metadataJson = jsonEncode({}), + mode: PutMode.update, + ); + }, + ); + + sendRes(id: cmd.id); + case _WorkerCmdType.listRecoverableRegions: + sendRes( + id: cmd.id, + data: { + 'recoverableRegions': root + .box() + .getAll() + .map((r) => r.toRegion()) + .toList(growable: false), + }, + ); + case _WorkerCmdType.getRecoverableRegion: + final id = cmd.args['id']! as int; + + sendRes( + id: cmd.id, + data: { + 'recoverableRegion': (root + .box() + .query(ObjectBoxRecovery_.refId.equals(id)) + .build() + ..close()) + .findUnique() + ?.toRegion(), + }, + ); + case _WorkerCmdType.startRecovery: + final id = cmd.args['id']! as int; + final storeName = cmd.args['storeName']! as String; + final region = cmd.args['region']! as DownloadableRegion; + + root.box().put( + ObjectBoxRecovery.fromRegion( + refId: id, + storeName: storeName, + region: region, ), - }, - ); - case _WorkerCmdType.resetMetadata: - final storeName = cmd.args['storeName']! as String; + ); - final stores = root.box(); + sendRes(id: cmd.id); + case _WorkerCmdType.cancelRecovery: + final id = cmd.args['id']! as int; + + root + .box() + .query(ObjectBoxRecovery_.refId.equals(id)) + .build() + ..remove() + ..close(); + + sendRes(id: cmd.id); + case _WorkerCmdType.watchRecovery: + final triggerImmediately = cmd.args['triggerImmediately']! as bool; + + streamedOutputSubscriptions[cmd.id] = root + .box() + .query() + .watch(triggerImmediately: triggerImmediately) + .listen((_) => sendRes(id: cmd.id, data: {'expectStream': true})); + case _WorkerCmdType.watchStores: + final storeNames = cmd.args['storeNames']! as List; + final triggerImmediately = cmd.args['triggerImmediately']! as bool; + + streamedOutputSubscriptions[cmd.id] = root + .box() + .query( + storeNames.isEmpty + ? null + : ObjectBoxStore_.name.oneOf(storeNames), + ) + .watch(triggerImmediately: triggerImmediately) + .listen((_) => sendRes(id: cmd.id, data: {'expectStream': true})); + case _WorkerCmdType.cancelStreamedOutputs: + final id = cmd.args['id']! as int; + + streamedOutputSubscriptions[id]?.cancel(); + streamedOutputSubscriptions.remove(id); + + sendRes(id: cmd.id); + case _WorkerCmdType.exportStores: + final storeNames = cmd.args['storeNames']! as List; + final outputPath = cmd.args['outputPath']! as String; + + final outputDir = path.dirname(outputPath); + + if (outputDir == input.rootDirectory.absolute.path) { + throw ExportInRootDirectoryForbidden(directory: outputDir); + } - final query = - stores.query(ObjectBoxStore_.name.equals(storeName)).build(); + Directory(outputDir).createSync(recursive: true); - root.runInTransaction( - TxMode.write, - () { - final store = query.findUnique() ?? - (throw StoreNotExists(storeName: storeName)); - query.close(); + final exportableStore = Store( + getObjectBoxModel(), + directory: outputDir, + maxDBSizeInKB: input.maxDatabaseSize, // Defaults to 10 GB + macosApplicationGroup: input.macosApplicationGroup, + ); - stores.put( - store..metadataJson = jsonEncode({}), - mode: PutMode.update, - ); - }, - ); + final storesQuery = root + .box() + .query(ObjectBoxStore_.name.oneOf(storeNames)) + .build(); + + final tilesQuery = (root.box().query() + ..linkMany( + ObjectBoxTile_.stores, + ObjectBoxStore_.name.oneOf(storeNames), + )) + .build(); + + final storesRaw = root.runInTransaction( + TxMode.read, + storesQuery.stream, + ); - sendRes(id: cmd.id); - case _WorkerCmdType.listRecoverableRegions: - sendRes( - id: cmd.id, - data: { - 'recoverableRegions': root - .box() - .getAll() - .map((r) => r.toRegion()) - .toList(growable: false), - }, + final newStores = storesRaw.map( + (s) => ObjectBoxStore( + name: s.name, + length: s.length, + size: s.size, + hits: s.hits, + misses: s.misses, + metadataJson: s.metadataJson, + ), + ); + + exportableStore + .runInTransaction( + TxMode.write, + () => newStores.listen( + (store) { + exportableStore + .box() + .put(store, mode: PutMode.insert); + }, + ), + ) + .asFuture() + .then((_) { + final tilesRaw = root.runInTransaction( + TxMode.read, + tilesQuery.stream, ); - case _WorkerCmdType.getRecoverableRegion: - final id = cmd.args['id']! as int; - sendRes( - id: cmd.id, - data: { - 'recoverableRegion': (root - .box() - .query(ObjectBoxRecovery_.refId.equals(id)) - .build() - ..close()) - .findUnique() - ?.toRegion(), - }, + final newTiles = tilesRaw.map( + (t) => ObjectBoxTile( + url: t.url, + bytes: t.bytes, + lastModified: t.lastModified, + )..stores.addAll(t.stores), ); - case _WorkerCmdType.startRecovery: - final id = cmd.args['id']! as int; - final storeName = cmd.args['storeName']! as String; - final region = cmd.args['region']! as DownloadableRegion; - - root.box().put( - ObjectBoxRecovery.fromRegion( - refId: id, - storeName: storeName, - region: region, + + exportableStore + .runInTransaction( + TxMode.write, + () => newTiles.listen( + (tile) { + exportableStore + .box() + .put(tile, mode: PutMode.insert); + }, ), - ); + ) + .asFuture() + .then((_) { + storesQuery.close(); + tilesQuery.close(); + exportableStore.close(); + + File(path.join(outputDir, 'lock.mdb')).delete(); + + final ram = File(path.join(outputDir, 'data.mdb')) + .renameSync(outputPath) + .openSync(mode: FileMode.writeOnlyAppend); + try { + ram + ..writeFromSync(List.filled(4, 255)) + ..writeStringSync('ObjectBox') // Backend identifier + ..writeByteSync(255) + ..writeByteSync(255) + ..writeStringSync('FMTC'); // Signature + } finally { + ram.closeSync(); + } - sendRes(id: cmd.id); - case _WorkerCmdType.cancelRecovery: - final id = cmd.args['id']! as int; + sendRes(id: cmd.id); + }); + }); + case _WorkerCmdType.importStores: + final importPath = cmd.args['path']! as String; + final strategy = cmd.args['strategy'] as ImportConflictStrategy; - root - .box() - .query(ObjectBoxRecovery_.refId.equals(id)) - .build() - ..remove() - ..close(); + final importFileRaw = File(importPath); - sendRes(id: cmd.id); - case _WorkerCmdType.watchRecovery: - final triggerImmediately = cmd.args['triggerImmediately']! as bool; - - streamedOutputSubscriptions[cmd.id] = root - .box() - .query() - .watch(triggerImmediately: triggerImmediately) - .listen((_) => sendRes(id: cmd.id, data: {'expectStream': true})); - case _WorkerCmdType.watchStores: - final storeNames = cmd.args['storeNames']! as List; - final triggerImmediately = cmd.args['triggerImmediately']! as bool; - - streamedOutputSubscriptions[cmd.id] = root - .box() - .query( - storeNames.isEmpty - ? null - : ObjectBoxStore_.name.oneOf(storeNames), - ) - .watch(triggerImmediately: triggerImmediately) - .listen((_) => sendRes(id: cmd.id, data: {'expectStream': true})); - case _WorkerCmdType.cancelWatch: - final id = cmd.args['id']! as int; + if (!importFileRaw.existsSync()) { + throw ImportFileNotExists(path: importPath); + } + + final importDir = + path.join(input.rootDirectory.absolute.path, 'import_tmp'); + final importDirIO = Directory(importDir)..createSync(); + + final importFile = + importFileRaw.copySync(path.join(importDir, 'data.mdb')); + + // Verify file is valid for import + final ram = importFile.openSync(mode: FileMode.append); + try { + int cursorPos = ram.positionSync() - 1; + ram.setPositionSync(cursorPos); + + // Check for FMTC footer signature ("**FMTC") + const signature = [255, 255, 70, 77, 84, 67]; + for (int i = 5; i >= 0; i--) { + if (signature[i] != ram.readByteSync()) { + throw ImportFileNotFMTCStandard(); + } + ram.setPositionSync(--cursorPos); + } - streamedOutputSubscriptions[id]?.cancel(); - streamedOutputSubscriptions.remove(id); + // Check for expected backend identifier ("**ObjectBox") + const id = [255, 255, 79, 98, 106, 101, 99, 116, 66, 111, 120]; + for (int i = 10; i >= 0; i--) { + if (id[i] != ram.readByteSync()) { + throw ImportFileNotBackendCompatible(); + } + ram.setPositionSync(--cursorPos); + } - sendRes(id: cmd.id); - } + ram.truncateSync(--cursorPos); + } catch (e) { + ram.closeSync(); + importFile.deleteSync(); + importDirIO.deleteSync(); + rethrow; + } + ram.closeSync(); + + final importingRoot = Store( + getObjectBoxModel(), + directory: importDir, + maxDBSizeInKB: input.maxDatabaseSize, // Defaults to 10 GB + macosApplicationGroup: input.macosApplicationGroup, + ); + + final storesQuery = importingRoot.box().query().build(); + //final tilesQuery = importingRoot.box().query().build(); + + final specificStoresQuery = root + .box() + .query(ObjectBoxStore_.name.equals('')) + .build(); + + final importingStores = importingRoot.runInTransaction( + TxMode.read, + storesQuery.stream, + ); + + root.runInTransaction( + TxMode.write, + // ignore: prefer_expression_function_bodies + () { + return importingStores.listen( + (importingStore) { + final existingStore = (specificStoresQuery + ..param(ObjectBoxStore_.name).value = importingStore.name) + .findUnique(); + + if (existingStore != null) { + if (strategy == ImportConflictStrategy.skip) { + sendRes( + id: cmd.id, + data: { + 'expectStream': true, + 'storeName': importingStore.name, + 'newStoreName': null, + 'conflict': true, + }, + ); + return; + } + + String? newName; + if (strategy == ImportConflictStrategy.rename) { + newName = + '${existingStore.name} (Imported ${DateTime.now()})'; + importingRoot.box().put( + importingStore..name = newName, + mode: PutMode.update, + ); + } + + sendRes( + id: cmd.id, + data: { + 'expectStream': true, + 'storeName': importingStore.name, + 'newStoreName': newName, + 'conflict': true, + }, + ); + + if (strategy == ImportConflictStrategy.replace) { + final tilesQuery = (root.box().query() + ..linkMany( + ObjectBoxTile_.stores, + ObjectBoxStore_.name.equals(existingStore.name), + )) + .build(); + + deleteTiles( + storeName: existingStore.name, + tilesQuery: tilesQuery, + ); + root.box().remove(existingStore.id); + + tilesQuery.close(); + } + + switch (strategy) { + case ImportConflictStrategy.rename || + ImportConflictStrategy.replace: + // These can now be handled identically, because they have + // been pre-processed as necessary + // TODO: Implement + case ImportConflictStrategy.merge: + // TODO: Implement + case ImportConflictStrategy.skip: + throw Error(); + } + } else { + sendRes( + id: cmd.id, + data: { + 'expectStream': true, + 'storeName': importingStore.name, + 'newStoreName': null, + 'conflict': false, + }, + ); + + // TODO: Implement + } + + /*root.box().put( + ObjectBoxStore( + name: importingStore.name, + length: importingStore.length, + size: importingStore.size, + hits: importingStore.hits, + misses: importingStore.misses, + metadataJson: importingStore.metadataJson, + ), + mode: PutMode.insert, + );*/ + }, + ); + }, + ).asFuture(); + /*.then( + (_) { + sendRes( + id: cmd.id, + data: {'expectStream': true, 'tiles': null}, + ); + + final importingTiles = importingRoot.runInTransaction( + TxMode.read, + tilesQuery.stream, + ); + + /*final newTiles = tilesRaw.map( + (t) => ObjectBoxTile( + url: t.url, + bytes: t.bytes, + lastModified: t.lastModified, + )..stores.addAll(t.stores), + );*/ + + root + .runInTransaction( + TxMode.write, + () => newTiles.listen( + (tile) { + root + .box() + .put(tile, mode: PutMode.insert); + }, + ), + ) + .asFuture() + .then( + (_) { + storesQuery.close(); + tilesQuery.close(); + importingRoot.close(); + + importFile.deleteSync(); + File(path.join(importDir, 'lock.mdb')).deleteSync(); + importDirIO.deleteSync(); + + sendRes( + id: cmd.id, + data: {'expectStream': true, 'finished': null}, + ); + }, + ); + }, + );*/ + } + } + + //! CMD/COMM RECIEVER !// + + await receivePort.listen((cmd) { + try { + mainHandler(cmd); } catch (e, s) { + cmd as _IncomingCmd; + sendRes( id: cmd.id, data: { diff --git a/lib/src/backend/interfaces/backend/internal.dart b/lib/src/backend/interfaces/backend/internal.dart index 0b328014..e4c35f80 100644 --- a/lib/src/backend/interfaces/backend/internal.dart +++ b/lib/src/backend/interfaces/backend/internal.dart @@ -318,4 +318,14 @@ abstract interface class FMTCBackendInternal required List storeNames, required bool triggerImmediately, }); + + Future exportStores({ + required List storeNames, + required String outputPath, + }); + + Future>> importStores({ + required String path, + required ImportConflictStrategy strategy, + }); } diff --git a/lib/src/root/directory.dart b/lib/src/root/directory.dart index cc0456e7..52bd020a 100644 --- a/lib/src/root/directory.dart +++ b/lib/src/root/directory.dart @@ -39,5 +39,5 @@ abstract class FMTCRoot { /// /// The 'fmtc_plus_sharing' module must be installed to add the functionality, /// without it, this object provides no functionality. - static RootImport get import => const RootImport._(); + static RootExternal get external => const RootExternal._(); } diff --git a/lib/src/root/external.dart b/lib/src/root/external.dart new file mode 100644 index 00000000..3507c2c9 --- /dev/null +++ b/lib/src/root/external.dart @@ -0,0 +1,56 @@ +// Copyright © Luka S (JaffaKetchup) under GPL-v3 +// A full license can be found at .\LICENSE + +part of flutter_map_tile_caching; + +class RootExternal { + const RootExternal._(); + + Future export({ + required List storeNames, + required String outputPath, + }) => + FMTCBackendAccess.internal + .exportStores(storeNames: storeNames, outputPath: outputPath); + + Stream import({ + required String path, + ImportConflictStrategy strategy = ImportConflictStrategy.skip, + }) async* { + yield* FMTCBackendAccess.internal.importStores( + path: path, + strategy: strategy, + ); + //throw UnimplementedError(); + } +} + +/// Determines what action should be taken when an importing store conflicts +/// with an existing store of the same name +/// +/// If speed is a necessity, prefer using [skip] or [replace]. +/// +/// See documentation on individual values for more information. +enum ImportConflictStrategy { + /// Skips the importing of the store + skip, + + /// Entirely replaces the existing store with the importing store + /// + /// Tiles from the existing store are deleted if they become orphaned (and do + /// not belong to the importing store). + replace, + + /// Renames the importing store by appending it with the current time (which + /// should be unique in all reasonable usecases) + /// + /// All tiles are retained. In the event of a conflict between two tiles, only + /// the one modified most recently is retained. + rename, + + /// Merges the importing and existing stores' tiles and metadata together + /// + /// All tiles are retained. In the event of a conflict between two tiles, only + /// the one modified most recently is retained. + merge; +} diff --git a/lib/src/root/import.dart b/lib/src/root/import.dart deleted file mode 100644 index 6c2bf838..00000000 --- a/lib/src/root/import.dart +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright © Luka S (JaffaKetchup) under GPL-v3 -// A full license can be found at .\LICENSE - -part of flutter_map_tile_caching; - -/// Extension access point for the 'fmtc_plus_sharing' module to add store export -/// functionality -/// -/// Does not include any functionality without the module. -class RootImport { - const RootImport._(); -} diff --git a/lib/src/store/directory.dart b/lib/src/store/directory.dart index d8203a1e..85388beb 100644 --- a/lib/src/store/directory.dart +++ b/lib/src/store/directory.dart @@ -50,13 +50,7 @@ class FMTCStore { /// advanced requirements should use another package, as this is a basic /// implementation. StoreMetadata get metadata => StoreMetadata._(this); - - /// Provides export functionality for this store - /// - /// The 'fmtc_plus_sharing' module must be installed to add the functionality, - /// without it, this object provides no functionality. - StoreExport get export => StoreExport._(this); - + /// Get tools to manage bulk downloading to this store /// /// The 'fmtc_plus_background_downloading' module must be installed to add the diff --git a/lib/src/store/export.dart b/lib/src/store/export.dart deleted file mode 100644 index 888e63e9..00000000 --- a/lib/src/store/export.dart +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright © Luka S (JaffaKetchup) under GPL-v3 -// A full license can be found at .\LICENSE - -part of flutter_map_tile_caching; - -/// Extension access point for the 'fmtc_plus_sharing' module to add store export -/// functionality -/// -/// Does not include any functionality without the module. -class StoreExport { - const StoreExport._(this.storeDirectory); - - /// Used in the 'fmtc_plus_sharing' module - /// - /// Do not use in normal applications. - @internal - @protected - final FMTCStore storeDirectory; -} From 06e70e93064231fe1867dfba0ed9f5e1d6dd70c6 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Tue, 5 Mar 2024 22:17:39 +0000 Subject: [PATCH 132/168] Progressed with importing implementation Fixed bugs Started reimplementation of exporting/importing in example app Former-commit-id: 74398a9db15dc280f12e9b81a4a9d65eae80bf06 [formerly 6a403049cf5bf13a43544c6a804458f7be1a10cb] Former-commit-id: d8163ef22c706f065a0d0d34871398c18ef0e6d4 --- .../components/directory_selected.dart | 17 + .../export_import/components/export.dart | 84 ++++ .../export_import/components/import.dart | 34 ++ .../components/no_path_selected.dart | 17 + .../export_import/components/path_picker.dart | 149 +++++++ .../screens/export_import/export_import.dart | 205 +++++++++ .../screens/import_store/import_store.dart | 162 -------- example/lib/screens/main/main.dart | 4 +- .../stores/components/empty_indicator.dart | 2 +- .../pages/stores/components/store_tile.dart | 38 -- .../lib/screens/main/pages/stores/stores.dart | 10 +- example/pubspec.yaml | 2 +- .../impls/objectbox/backend/internal.dart | 45 +- .../backend/internal_thread_safe.dart | 2 +- .../objectbox/backend/internal_worker.dart | 392 +++++++++++------- .../backend/interfaces/backend/internal.dart | 2 +- lib/src/root/external.dart | 19 +- 17 files changed, 794 insertions(+), 390 deletions(-) create mode 100644 example/lib/screens/export_import/components/directory_selected.dart create mode 100644 example/lib/screens/export_import/components/export.dart create mode 100644 example/lib/screens/export_import/components/import.dart create mode 100644 example/lib/screens/export_import/components/no_path_selected.dart create mode 100644 example/lib/screens/export_import/components/path_picker.dart create mode 100644 example/lib/screens/export_import/export_import.dart delete mode 100644 example/lib/screens/import_store/import_store.dart diff --git a/example/lib/screens/export_import/components/directory_selected.dart b/example/lib/screens/export_import/components/directory_selected.dart new file mode 100644 index 00000000..e7cf2362 --- /dev/null +++ b/example/lib/screens/export_import/components/directory_selected.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; + +class DirectorySelected extends StatelessWidget { + const DirectorySelected({super.key}); + + @override + Widget build(BuildContext context) => const Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.snippet_folder_rounded, size: 48), + Text( + 'Input/select a file (not a directory)', + style: TextStyle(fontSize: 15), + ), + ], + ); +} diff --git a/example/lib/screens/export_import/components/export.dart b/example/lib/screens/export_import/components/export.dart new file mode 100644 index 00000000..4cae849d --- /dev/null +++ b/example/lib/screens/export_import/components/export.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; + +import '../../../shared/components/loading_indicator.dart'; + +class Export extends StatefulWidget { + const Export({ + super.key, + required this.selectedStores, + }); + + final Set selectedStores; + + @override + State createState() => _ExportState(); +} + +class _ExportState extends State { + late final stores = FMTCRoot.stats.storesAvailable; + + @override + Widget build(BuildContext context) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Export Stores To Archive', + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 16), + Expanded( + child: FutureBuilder( + future: stores, + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const LoadingIndicator('Loading available stores'); + } + + if (snapshot.data!.isEmpty) { + return const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.folder_off_rounded, size: 48), + Text( + "There aren't any stores to export!", + style: TextStyle(fontSize: 15), + ), + ], + ), + ); + } + + final availableStores = + snapshot.data!.map((e) => e.storeName).toList(); + + return Wrap( + spacing: 10, + runSpacing: 10, + children: List.generate( + availableStores.length, + (i) { + final storeName = availableStores[i]; + return ChoiceChip( + label: Text(storeName), + selected: widget.selectedStores.contains(storeName), + onSelected: (selected) { + if (selected) { + widget.selectedStores.add(storeName); + } else { + widget.selectedStores.remove(storeName); + } + setState(() {}); + }, + ); + }, + growable: false, + ), + ); + }, + ), + ), + ], + ); +} diff --git a/example/lib/screens/export_import/components/import.dart b/example/lib/screens/export_import/components/import.dart new file mode 100644 index 00000000..d9de6b1f --- /dev/null +++ b/example/lib/screens/export_import/components/import.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; + +class Import extends StatelessWidget { + const Import({ + super.key, + required this.changeForceOverrideExisting, + }); + + final void Function({required bool forceOverrideExisting}) + changeForceOverrideExisting; + + @override + Widget build(BuildContext context) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + 'Import Stores From Archive', + style: Theme.of(context).textTheme.titleLarge, + ), + ), + OutlinedButton.icon( + onPressed: () => + changeForceOverrideExisting(forceOverrideExisting: true), + icon: const Icon(Icons.file_upload_outlined), + label: const Text('Force Overwrite'), + ), + ], + ), + ], + ); +} diff --git a/example/lib/screens/export_import/components/no_path_selected.dart b/example/lib/screens/export_import/components/no_path_selected.dart new file mode 100644 index 00000000..7e4b6b35 --- /dev/null +++ b/example/lib/screens/export_import/components/no_path_selected.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; + +class NoPathSelected extends StatelessWidget { + const NoPathSelected({super.key}); + + @override + Widget build(BuildContext context) => const Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.keyboard_rounded, size: 48), + Text( + 'To get started, input/select a path to a file', + style: TextStyle(fontSize: 15), + ), + ], + ); +} diff --git a/example/lib/screens/export_import/components/path_picker.dart b/example/lib/screens/export_import/components/path_picker.dart new file mode 100644 index 00000000..a703eb5c --- /dev/null +++ b/example/lib/screens/export_import/components/path_picker.dart @@ -0,0 +1,149 @@ +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:path/path.dart' as path; + +class PathPicker extends StatelessWidget { + const PathPicker({ + super.key, + required this.pathController, + required this.onPathChanged, + }); + + final TextEditingController pathController; + final void Function({required bool forceOverrideExisting}) onPathChanged; + + @override + Widget build(BuildContext context) { + final isDesktop = Theme.of(context).platform == TargetPlatform.linux || + Theme.of(context).platform == TargetPlatform.windows || + Theme.of(context).platform == TargetPlatform.macOS; + + return IntrinsicWidth( + child: Column( + children: [ + if (isDesktop) + Row( + children: [ + OutlinedButton.icon( + onPressed: () async { + final picked = await FilePicker.platform.saveFile( + type: FileType.custom, + allowedExtensions: ['fmtc'], + dialogTitle: 'Export To File', + ); + if (picked != null) { + pathController.value = TextEditingValue( + text: picked, + selection: TextSelection.collapsed( + offset: picked.length, + ), + ); + onPathChanged(forceOverrideExisting: true); + } + }, + icon: const Icon(Icons.file_upload_outlined), + label: const Text('Export'), + ), + const SizedBox.square(dimension: 8), + SizedBox.square( + dimension: 32, + child: IconButton.outlined( + onPressed: () async { + final picked = await FilePicker.platform.getDirectoryPath( + dialogTitle: 'Export To Directory', + ); + if (picked != null) { + final finalPath = path.join(picked, 'archive.fmtc'); + + pathController.value = TextEditingValue( + text: finalPath, + selection: TextSelection.collapsed( + offset: finalPath.length, + ), + ); + + onPathChanged(forceOverrideExisting: true); + } + }, + iconSize: 16, + icon: Icon( + Icons.folder, + color: Theme.of(context) + .buttonTheme + .colorScheme! + .primaryFixedDim, + ), + ), + ), + ], + ) + else + OutlinedButton.icon( + onPressed: () async { + if (isDesktop) { + final picked = await FilePicker.platform.saveFile( + type: FileType.custom, + allowedExtensions: ['fmtc'], + dialogTitle: 'Export', + ); + if (picked != null) { + pathController.value = TextEditingValue( + text: picked, + selection: TextSelection.collapsed( + offset: picked.length, + ), + ); + + onPathChanged(forceOverrideExisting: true); + } + } else { + final picked = await FilePicker.platform.getDirectoryPath( + dialogTitle: 'Export', + ); + if (picked != null) { + final finalPath = path.join(picked, 'archive.fmtc'); + + pathController.value = TextEditingValue( + text: finalPath, + selection: TextSelection.collapsed( + offset: finalPath.length, + ), + ); + + onPathChanged(forceOverrideExisting: true); + } + } + }, + icon: const Icon(Icons.file_upload_outlined), + label: const Text('Export'), + ), + const SizedBox.square(dimension: 12), + SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + onPressed: () async { + final picked = await FilePicker.platform.pickFiles( + type: FileType.custom, + allowedExtensions: ['fmtc'], + dialogTitle: 'Import', + ); + if (picked != null) { + pathController.value = TextEditingValue( + text: picked.files.single.path!, + selection: TextSelection.collapsed( + offset: picked.files.single.path!.length, + ), + ); + + onPathChanged(forceOverrideExisting: false); + } + }, + icon: const Icon(Icons.file_download_outlined), + label: const Text('Import'), + ), + ), + ], + ), + ); + } +} diff --git a/example/lib/screens/export_import/export_import.dart b/example/lib/screens/export_import/export_import.dart new file mode 100644 index 00000000..e79caeea --- /dev/null +++ b/example/lib/screens/export_import/export_import.dart @@ -0,0 +1,205 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; + +import '../../shared/components/loading_indicator.dart'; +import 'components/directory_selected.dart'; +import 'components/export.dart'; +import 'components/import.dart'; +import 'components/no_path_selected.dart'; +import 'components/path_picker.dart'; + +class ExportImportPopup extends StatefulWidget { + const ExportImportPopup({super.key}); + + @override + State createState() => _ExportImportPopupState(); +} + +class _ExportImportPopupState extends State { + final pathController = TextEditingController(); + + final selectedStores = {}; + Future? typeOfPath; + bool forceOverrideExisting = false; + bool isProcessingExporting = false; + + void onPathChanged({required bool forceOverrideExisting}) => setState(() { + this.forceOverrideExisting = forceOverrideExisting; + typeOfPath = FileSystemEntity.type(pathController.text); + }); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar( + title: const Text('Export/Import Stores'), + ), + body: Padding( + padding: const EdgeInsets.all(12), + child: Column( + children: [ + Container( + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).dividerColor, + width: 2, + ), + borderRadius: BorderRadius.circular(12), + ), + padding: const EdgeInsets.all(12), + child: Row( + children: [ + Expanded( + child: TextField( + controller: pathController, + decoration: const InputDecoration( + label: Text('Path To Archive'), + hintText: 'folder/archive.fmtc', + isDense: true, + ), + onEditingComplete: () => + onPathChanged(forceOverrideExisting: false), + ), + ), + const SizedBox.square(dimension: 12), + PathPicker( + pathController: pathController, + onPathChanged: onPathChanged, + ), + ], + ), + ), + Expanded( + child: pathController.text != '' && !isProcessingExporting + ? SizedBox( + width: double.infinity, + child: FutureBuilder( + future: typeOfPath, + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const LoadingIndicator( + 'Checking whether the path exists', + ); + } + + if (snapshot.data! == + FileSystemEntityType.notFound || + forceOverrideExisting) { + return Padding( + padding: const EdgeInsets.only( + top: 24, + left: 12, + right: 12, + ), + child: Export( + selectedStores: selectedStores, + ), + ); + } + + if (snapshot.data! != FileSystemEntityType.file) { + return const DirectorySelected(); + } + + return Padding( + padding: const EdgeInsets.only( + top: 24, + left: 12, + right: 12, + ), + child: Import( + changeForceOverrideExisting: onPathChanged, + ), + ); + }, + ), + ) + : pathController.text == '' + ? const NoPathSelected() + : const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator.adaptive(), + SizedBox(height: 12), + Text( + 'Exporting your stores, tiles, and metadata', + textAlign: TextAlign.center, + ), + Text( + 'This could take a while, please be patient', + textAlign: TextAlign.center, + style: TextStyle(fontWeight: FontWeight.bold), + ), + ], + ), + ), + ), + ], + ), + ), + floatingActionButton: FutureBuilder( + future: typeOfPath, + builder: (context, snapshot) { + if (!snapshot.hasData || + (snapshot.data! != FileSystemEntityType.file && + snapshot.data! != FileSystemEntityType.notFound)) { + return const SizedBox.shrink(); + } + + late final bool isExporting; + late final Icon icon; + if (snapshot.data! == FileSystemEntityType.notFound && + !forceOverrideExisting) { + icon = const Icon(Icons.save); + isExporting = true; + } else if (snapshot.data! == FileSystemEntityType.file && + forceOverrideExisting) { + icon = const Icon(Icons.save_as); + isExporting = true; + } else { + icon = const Icon(Icons.file_open_rounded); + isExporting = false; + } + + return FloatingActionButton( + heroTag: 'importExport', + onPressed: isProcessingExporting + ? null + : () async { + if (isExporting) { + setState(() => isProcessingExporting = true); + final stopwatch = Stopwatch()..start(); + await FMTCRoot.external.export( + storeNames: selectedStores.toList(), + outputPath: pathController.text, + ); + stopwatch.stop(); + if (context.mounted) { + final elapsedTime = + (stopwatch.elapsedMilliseconds / 1000) + .toStringAsFixed(1); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Successfully exported stores (in $elapsedTime ' + 'secs)', + ), + ), + ); + Navigator.pop(context); + } + } + }, + child: isProcessingExporting + ? const SizedBox.square( + dimension: 26, + child: CircularProgressIndicator.adaptive(), + ) + : icon, + ); + }, + ), + ); +} diff --git a/example/lib/screens/import_store/import_store.dart b/example/lib/screens/import_store/import_store.dart deleted file mode 100644 index 94a46dee..00000000 --- a/example/lib/screens/import_store/import_store.dart +++ /dev/null @@ -1,162 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; -//import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; -//import 'package:fmtc_plus_sharing/fmtc_plus_sharing.dart'; - -class ImportStorePopup extends StatefulWidget { - const ImportStorePopup({super.key}); - - @override - State createState() => _ImportStorePopupState(); -} - -class _ImportStorePopupState extends State { - final Map importStores = {}; - - @override - void initState() { - super.initState(); - - FMTCRoot.external - .import(path: r'C:\Users\lukas\Documents\fmtc_export\wow.fmtc') - .listen((_) { - print('evt'); - }); - } - - // TODO: Implement - @override - Widget build(BuildContext context) => Scaffold( - appBar: AppBar( - title: const Text('Import Stores'), - ), - body: const Padding( - padding: EdgeInsets.all(12), - /*child: ListView.separated( - itemCount: importStores.length + 1, - itemBuilder: (context, i) { - if (i == importStores.length) { - return ListTile( - leading: const Icon(Icons.add), - title: const Text('Choose New Store(s)'), - subtitle: const Text('Select any valid store files (.fmtc)'), - onTap: () async { - /*importStores.addAll( - (await FMTCRoot.import.withGUI( - collisionHandler: (fn, sn) { - setState( - () => importStores[fn]!.collisionInfo = [ - fn, - sn, - ], - ); - return importStores[fn]! - .collisionResolution - .future; - }, - ) ?? - {}) - .map( - (name, status) => MapEntry( - name, - _ImportStore(status, collisionInfo: null), - ), - ), - );*/ - - if (mounted) setState(() {}); - }, - ); - } - - final filename = importStores.keys.toList()[i]; - return FutureBuilder( - future: importStores[filename]?.result, - builder: (context, s1) => FutureBuilder( - future: importStores[filename]?.collisionResolution.future, - builder: (context, s2) { - final result = s1.data; - final conflict = s2.data; - - T stateSwitcher({ - required T loading, - required T successful, - required T failed, - required T cancelled, - required T collided, - }) { - if (importStores[filename]!.collisionInfo != null && - conflict == null) return collided; - if (conflict == false) return cancelled; - if (result == null) return loading; - return result.successful ? successful : failed; - } - - final storeName = result?.storeName; - - return ListTile( - leading: stateSwitcher( - loading: const CircularProgressIndicator.adaptive(), - successful: const Icon(Icons.done, color: Colors.green), - failed: const Icon(Icons.error, color: Colors.red), - cancelled: const Icon(Icons.cancel), - collided: - const Icon(Icons.merge_type, color: Colors.amber), - ), - title: Text(filename), - subtitle: stateSwitcher( - loading: const Text('Loading...'), - successful: Text('Imported as: $storeName'), - failed: null, - cancelled: null, - collided: Text( - 'Collision with ${importStores[filename]!.collisionInfo?[1]}', - ), - ), - trailing: importStores[filename]!.collisionInfo != null && - conflict == null - ? Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - onPressed: () => importStores[filename]! - .collisionResolution - .complete(true), - icon: const Icon(Icons.edit), - tooltip: 'Overwrite store', - ), - IconButton( - onPressed: () => importStores[filename]! - .collisionResolution - .complete(false), - icon: const Icon(Icons.cancel), - tooltip: 'Cancel import', - ), - ], - ) - : null, - ); - }, - ), - ); - }, - separatorBuilder: (context, i) => i == importStores.length - 1 - ? const Divider() - : const SizedBox.shrink(), - ),*/ - ), - ); -} - -class _ImportStore { - _ImportStore( - this.result, { - required this.collisionInfo, - }) : collisionResolution = Completer(); - - final Future result; - List? collisionInfo; - Completer collisionResolution; -} diff --git a/example/lib/screens/main/main.dart b/example/lib/screens/main/main.dart index 4b02b067..bb756d9e 100644 --- a/example/lib/screens/main/main.dart +++ b/example/lib/screens/main/main.dart @@ -30,8 +30,8 @@ class _MainScreenState extends State { ), const NavigationDestination( label: 'Stores', - icon: Icon(Icons.inventory_2_outlined), - selectedIcon: Icon(Icons.inventory_2), + icon: Icon(Icons.folder_outlined), + selectedIcon: Icon(Icons.folder), ), const NavigationDestination( label: 'Download', diff --git a/example/lib/screens/main/pages/stores/components/empty_indicator.dart b/example/lib/screens/main/pages/stores/components/empty_indicator.dart index 82af5ebd..c15e7ad0 100644 --- a/example/lib/screens/main/pages/stores/components/empty_indicator.dart +++ b/example/lib/screens/main/pages/stores/components/empty_indicator.dart @@ -12,7 +12,7 @@ class EmptyIndicator extends StatelessWidget { children: [ Icon(Icons.folder_off, size: 36), SizedBox(height: 10), - Text('No Stores Found'), + Text('Get started by creating a store!'), ], ), ); diff --git a/example/lib/screens/main/pages/stores/components/store_tile.dart b/example/lib/screens/main/pages/stores/components/store_tile.dart index 5a02f8b5..83a88365 100644 --- a/example/lib/screens/main/pages/stores/components/store_tile.dart +++ b/example/lib/screens/main/pages/stores/components/store_tile.dart @@ -21,7 +21,6 @@ class StoreTile extends StatefulWidget { class _StoreTileState extends State { bool _deletingProgress = false; bool _emptyingProgress = false; - bool _exportingProgress = false; @override Widget build(BuildContext context) => Selector( @@ -225,43 +224,6 @@ class _StoreTileState extends State { ); }, ), - IconButton( - icon: _exportingProgress - ? const CircularProgressIndicator( - strokeWidth: 3, - ) - : const Icon( - Icons.send_time_extension_rounded, - ), - tooltip: 'Export Store', - onPressed: _exportingProgress - ? null - : () async { - // TODO: Implement - /* setState( - () => _exportingProgress = true, - ); - final bool result = await _store - .export - .withGUI(context: context); - setState( - () => - _exportingProgress = false, - ); - if (mounted) { - ScaffoldMessenger.of(context) - .showSnackBar( - SnackBar( - content: Text( - result - ? 'Exported Sucessfully' - : 'Export Cancelled', - ), - ), - ); - }*/ - }, - ), IconButton( icon: const Icon(Icons.edit), tooltip: 'Edit Store', diff --git a/example/lib/screens/main/pages/stores/stores.dart b/example/lib/screens/main/pages/stores/stores.dart index 02548e31..4b2024c0 100644 --- a/example/lib/screens/main/pages/stores/stores.dart +++ b/example/lib/screens/main/pages/stores/stores.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; import '../../../../shared/components/loading_indicator.dart'; -import '../../../import_store/import_store.dart'; +import '../../../export_import/export_import.dart'; import '../../../store_editor/store_editor.dart'; import 'components/empty_indicator.dart'; import 'components/header.dart'; @@ -91,13 +91,13 @@ class _StoresPageState extends State { crossAxisAlignment: CrossAxisAlignment.end, children: [ FloatingActionButton.small( - heroTag: null, - tooltip: 'Import Store', + heroTag: 'importExport', + tooltip: 'Export/Import', shape: const CircleBorder(), - child: const Icon(Icons.file_open_rounded), + child: const Icon(Icons.folder_zip_rounded), onPressed: () => Navigator.of(context).push( MaterialPageRoute( - builder: (BuildContext context) => const ImportStorePopup(), + builder: (BuildContext context) => const ExportImportPopup(), fullscreenDialog: true, ), ), diff --git a/example/pubspec.yaml b/example/pubspec.yaml index d93f9111..7bf08d96 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -15,7 +15,7 @@ dependencies: better_open_file: ^3.6.4 collection: ^1.18.0 dart_earcut: ^1.0.1 - file_picker: ^5.2.10 + file_picker: ^6.2.0 flutter: sdk: flutter flutter_foreground_task: ^6.1.2 diff --git a/lib/src/backend/impls/objectbox/backend/internal.dart b/lib/src/backend/impls/objectbox/backend/internal.dart index 4be26c3d..12e71a8a 100644 --- a/lib/src/backend/impls/objectbox/backend/internal.dart +++ b/lib/src/backend/impls/objectbox/backend/internal.dart @@ -546,20 +546,45 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { ); @override - Future>> importStores({ + ImportResult importStores({ required String path, required ImportConflictStrategy strategy, - }) async { - final storeStatuses = >{}; + }) { + final storesStreamController = StreamController< + ({String importingName, bool conflict, String? newName})>(); + + final complete = Completer(); - await for (final evt in _sendCmdStreamed( + late final StreamSubscription?> listener; + listener = _sendCmdStreamed( type: _WorkerCmdType.importStores, args: {'path': path, 'strategy': strategy}, - )) { - if (evt!.containsKey('finished')) break; - if (evt['storeName'] case final String storeName) { - storeStatuses[storeName] = Completer(); - } - } + ).listen( + cancelOnError: true, + (evt) { + if (evt!.containsKey('finished')) { + complete.complete(); + listener.cancel(); + return; + } + if (evt.containsKey('tiles')) { + storesStreamController.close(); + return; + } + + storesStreamController.add( + ( + importingName: evt['storeName'], + conflict: evt['conflict'], + newName: evt['newStoreName'], + ), + ); + }, + ); + + return ( + stores: storesStreamController.stream.toList(), + complete: complete.future, + ); } } diff --git a/lib/src/backend/impls/objectbox/backend/internal_thread_safe.dart b/lib/src/backend/impls/objectbox/backend/internal_thread_safe.dart index 6a0c400a..22aabb9a 100644 --- a/lib/src/backend/impls/objectbox/backend/internal_thread_safe.dart +++ b/lib/src/backend/impls/objectbox/backend/internal_thread_safe.dart @@ -154,7 +154,7 @@ class _ObjectBoxBackendThreadSafeImpl implements FMTCBackendInternalThreadSafe { final store = storeQuery.findUnique() ?? (throw StoreNotExists(storeName: storeName)); - for (int i = 0; i <= urls.length; i++) { + for (int i = 0; i <= urls.length - 1; i++) { final url = urls[i]; final bytes = bytess[i]; diff --git a/lib/src/backend/impls/objectbox/backend/internal_worker.dart b/lib/src/backend/impls/objectbox/backend/internal_worker.dart index 4f1358bd..3d3a16c8 100644 --- a/lib/src/backend/impls/objectbox/backend/internal_worker.dart +++ b/lib/src/backend/impls/objectbox/backend/internal_worker.dart @@ -828,7 +828,7 @@ Future _worker( Directory(outputDir).createSync(recursive: true); - final exportableStore = Store( + final exportingRoot = Store( getObjectBoxModel(), directory: outputDir, maxDBSizeInKB: input.maxDatabaseSize, // Defaults to 10 GB @@ -847,84 +847,89 @@ Future _worker( )) .build(); - final storesRaw = root.runInTransaction( + final storesObjectsForRelations = {}; + + final exportingStores = root.runInTransaction( TxMode.read, storesQuery.stream, ); - final newStores = storesRaw.map( - (s) => ObjectBoxStore( - name: s.name, - length: s.length, - size: s.size, - hits: s.hits, - misses: s.misses, - metadataJson: s.metadataJson, - ), - ); - - exportableStore + exportingRoot .runInTransaction( TxMode.write, - () => newStores.listen( - (store) { - exportableStore - .box() - .put(store, mode: PutMode.insert); + () => exportingStores.listen( + (exportingStore) { + exportingRoot.box().put( + storesObjectsForRelations[exportingStore.name] = + ObjectBoxStore( + name: exportingStore.name, + length: exportingStore.length, + size: exportingStore.size, + hits: exportingStore.hits, + misses: exportingStore.misses, + metadataJson: exportingStore.metadataJson, + ), + mode: PutMode.insert, + ); }, ), ) .asFuture() - .then((_) { - final tilesRaw = root.runInTransaction( - TxMode.read, - tilesQuery.stream, - ); - - final newTiles = tilesRaw.map( - (t) => ObjectBoxTile( - url: t.url, - bytes: t.bytes, - lastModified: t.lastModified, - )..stores.addAll(t.stores), - ); + .then( + (_) { + final exportingTiles = root.runInTransaction( + TxMode.read, + tilesQuery.stream, + ); - exportableStore - .runInTransaction( - TxMode.write, - () => newTiles.listen( - (tile) { - exportableStore - .box() - .put(tile, mode: PutMode.insert); - }, - ), - ) - .asFuture() - .then((_) { - storesQuery.close(); - tilesQuery.close(); - exportableStore.close(); - - File(path.join(outputDir, 'lock.mdb')).delete(); - - final ram = File(path.join(outputDir, 'data.mdb')) - .renameSync(outputPath) - .openSync(mode: FileMode.writeOnlyAppend); - try { - ram - ..writeFromSync(List.filled(4, 255)) - ..writeStringSync('ObjectBox') // Backend identifier - ..writeByteSync(255) - ..writeByteSync(255) - ..writeStringSync('FMTC'); // Signature - } finally { - ram.closeSync(); - } + exportingRoot + .runInTransaction( + TxMode.write, + () => exportingTiles.listen( + (exportingTile) { + exportingRoot.box().put( + ObjectBoxTile( + url: exportingTile.url, + bytes: exportingTile.bytes, + lastModified: exportingTile.lastModified, + )..stores.addAll( + exportingTile.stores.map( + (s) => storesObjectsForRelations[s.name]!, + ), + ), + mode: PutMode.insert, + ); + }, + ), + ) + .asFuture() + .then( + (_) { + storesQuery.close(); + tilesQuery.close(); + exportingRoot.close(); + + File(path.join(outputDir, 'lock.mdb')).delete(); + + final ram = File(path.join(outputDir, 'data.mdb')) + .renameSync(outputPath) + .openSync(mode: FileMode.writeOnlyAppend); + try { + ram + ..writeFromSync(List.filled(4, 255)) + ..writeStringSync('ObjectBox') // Backend identifier + ..writeByteSync(255) + ..writeByteSync(255) + ..writeStringSync('FMTC'); // Signature + } finally { + ram.closeSync(); + } - sendRes(id: cmd.id); - }); - }); + sendRes(id: cmd.id); + }, + ); + }, + ); case _WorkerCmdType.importStores: final importPath = cmd.args['path']! as String; final strategy = cmd.args['strategy'] as ImportConflictStrategy; @@ -983,30 +988,66 @@ Future _worker( ); final storesQuery = importingRoot.box().query().build(); - //final tilesQuery = importingRoot.box().query().build(); + final tilesQuery = importingRoot.box().query().build(); final specificStoresQuery = root .box() .query(ObjectBoxStore_.name.equals('')) .build(); + final specificTilesQuery = root + .box() + .query(ObjectBoxTile_.url.equals('')) + .build(); + + final storesObjectsForRelations = {}; + final storesToSkipTiles = []; + int rootDeltaLength = 0; + int rootDeltaSize = 0; final importingStores = importingRoot.runInTransaction( TxMode.read, storesQuery.stream, ); - root.runInTransaction( - TxMode.write, - // ignore: prefer_expression_function_bodies - () { - return importingStores.listen( - (importingStore) { - final existingStore = (specificStoresQuery - ..param(ObjectBoxStore_.name).value = importingStore.name) - .findUnique(); + root + .runInTransaction( + TxMode.write, + () => importingStores.listen( + (importingStore) { + final existingStore = (specificStoresQuery + ..param(ObjectBoxStore_.name).value = + importingStore.name) + .findUnique(); + + if (existingStore == null) { + sendRes( + id: cmd.id, + data: { + 'expectStream': true, + 'storeName': importingStore.name, + 'newStoreName': null, + 'conflict': false, + }, + ); + + root.box().put( + storesObjectsForRelations[importingStore.name] = + ObjectBoxStore( + name: importingStore.name, + length: importingStore.length, + size: importingStore.size, + hits: importingStore.hits, + misses: importingStore.misses, + metadataJson: importingStore.metadataJson, + ), + mode: PutMode.insert, + ); + + return; + } - if (existingStore != null) { if (strategy == ImportConflictStrategy.skip) { + storesToSkipTiles.add(importingStore.name); sendRes( id: cmd.id, data: { @@ -1056,96 +1097,125 @@ Future _worker( tilesQuery.close(); } - switch (strategy) { - case ImportConflictStrategy.rename || - ImportConflictStrategy.replace: - // These can now be handled identically, because they have - // been pre-processed as necessary - // TODO: Implement - case ImportConflictStrategy.merge: - // TODO: Implement - case ImportConflictStrategy.skip: - throw Error(); + if (strategy != ImportConflictStrategy.merge) { + root.box().put( + storesObjectsForRelations[importingStore.name] = + ObjectBoxStore( + name: newName ?? importingStore.name, + length: importingStore.length, + size: importingStore.size, + hits: importingStore.hits, + misses: importingStore.misses, + metadataJson: importingStore.metadataJson, + ), + mode: PutMode.insert, + ); } - } else { - sendRes( - id: cmd.id, - data: { - 'expectStream': true, - 'storeName': importingStore.name, - 'newStoreName': null, - 'conflict': false, + }, + ), + ) + .asFuture() + .then( + (_) { + sendRes( + id: cmd.id, + data: {'expectStream': true, 'tiles': null}, + ); + + final importingTiles = importingRoot.runInTransaction( + TxMode.read, + tilesQuery.stream, + ); + + root + .runInTransaction( + TxMode.write, + () => importingTiles.listen( + (importingTile) { + if (strategy == ImportConflictStrategy.skip && + importingTile.stores.length == 1 && + storesToSkipTiles.contains( + importingTile.stores[0].name, + )) return; + + importingTile.stores.removeWhere( + (s) => storesToSkipTiles.contains(s.name), + ); + + try { + root.box().put( + ObjectBoxTile( + url: importingTile.url, + bytes: importingTile.bytes, + lastModified: importingTile.lastModified, + )..stores.addAll( + importingTile.stores.map( + (e) => storesObjectsForRelations[e.name]!, + ), + ), + mode: PutMode.insert, + ); + + rootDeltaLength++; + rootDeltaSize += importingTile.bytes.lengthInBytes; + } on UniqueViolationException { + final existingTile = (specificTilesQuery + ..param(ObjectBoxTile_.url).value = + importingTile.url) + .findUnique()!; + + if (existingTile.lastModified + .isAfter(importingTile.lastModified)) return; + + root.box().put( + ObjectBoxTile( + url: importingTile.url, + bytes: importingTile.bytes, + lastModified: importingTile.lastModified, + )..stores.addAll( + { + ...existingTile.stores, + ...importingTile.stores.map( + (e) => storesObjectsForRelations[e.name]!, + ), + }, + ), + mode: PutMode.update, + ); + + rootDeltaSize += -existingTile.bytes.lengthInBytes + + importingTile.bytes.lengthInBytes; + } + + //TODO: mergeing strategy }, - ); + ), + ) + .asFuture() + .then( + (_) { + print(rootDeltaLength); + updateRootStatistics( + deltaLength: rootDeltaLength, + deltaSize: rootDeltaSize, + ); - // TODO: Implement - } + storesQuery.close(); + tilesQuery.close(); + importingRoot.close(); - /*root.box().put( - ObjectBoxStore( - name: importingStore.name, - length: importingStore.length, - size: importingStore.size, - hits: importingStore.hits, - misses: importingStore.misses, - metadataJson: importingStore.metadataJson, - ), - mode: PutMode.insert, - );*/ + importFile.deleteSync(); + File(path.join(importDir, 'lock.mdb')).deleteSync(); + importDirIO.deleteSync(); + + sendRes( + id: cmd.id, + data: {'expectStream': true, 'finished': null}, + ); }, ); }, - ).asFuture(); - /*.then( - (_) { - sendRes( - id: cmd.id, - data: {'expectStream': true, 'tiles': null}, - ); - - final importingTiles = importingRoot.runInTransaction( - TxMode.read, - tilesQuery.stream, - ); - - /*final newTiles = tilesRaw.map( - (t) => ObjectBoxTile( - url: t.url, - bytes: t.bytes, - lastModified: t.lastModified, - )..stores.addAll(t.stores), - );*/ - - root - .runInTransaction( - TxMode.write, - () => newTiles.listen( - (tile) { - root - .box() - .put(tile, mode: PutMode.insert); - }, - ), - ) - .asFuture() - .then( - (_) { - storesQuery.close(); - tilesQuery.close(); - importingRoot.close(); - - importFile.deleteSync(); - File(path.join(importDir, 'lock.mdb')).deleteSync(); - importDirIO.deleteSync(); - - sendRes( - id: cmd.id, - data: {'expectStream': true, 'finished': null}, - ); - }, - ); - }, - );*/ + ); } } diff --git a/lib/src/backend/interfaces/backend/internal.dart b/lib/src/backend/interfaces/backend/internal.dart index e4c35f80..93d32455 100644 --- a/lib/src/backend/interfaces/backend/internal.dart +++ b/lib/src/backend/interfaces/backend/internal.dart @@ -324,7 +324,7 @@ abstract interface class FMTCBackendInternal required String outputPath, }); - Future>> importStores({ + ImportResult importStores({ required String path, required ImportConflictStrategy strategy, }); diff --git a/lib/src/root/external.dart b/lib/src/root/external.dart index 3507c2c9..8660e6d1 100644 --- a/lib/src/root/external.dart +++ b/lib/src/root/external.dart @@ -3,6 +3,11 @@ part of flutter_map_tile_caching; +typedef ImportResult = ({ + Future> stores, + Future complete, +}); + class RootExternal { const RootExternal._(); @@ -13,16 +18,14 @@ class RootExternal { FMTCBackendAccess.internal .exportStores(storeNames: storeNames, outputPath: outputPath); - Stream import({ + ImportResult import({ required String path, ImportConflictStrategy strategy = ImportConflictStrategy.skip, - }) async* { - yield* FMTCBackendAccess.internal.importStores( - path: path, - strategy: strategy, - ); - //throw UnimplementedError(); - } + }) => + FMTCBackendAccess.internal.importStores( + path: path, + strategy: strategy, + ); } /// Determines what action should be taken when an importing store conflicts From f2a2b02f735a1edeb7e7cceefca1dfa22e846261 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 7 Mar 2024 21:18:19 +0000 Subject: [PATCH 133/168] Prepared for v9.0.0-dev.6 prerelease Improved `FMTC.external` Fixed bugs Added documentation Former-commit-id: 6e943051ca3ba43d349b0fc43c3b15286598e47e [formerly 2133427ac91838d8c49efe7bc4e533df20ccec36] Former-commit-id: 34aabe4fe7f370cb7330fd314f96c19dd6f6dc06 --- CHANGELOG.md | 52 ++++- example/lib/main.dart | 2 +- .../export_import/components/export.dart | 2 +- .../export_import/components/import.dart | 181 +++++++++++++++++- .../screens/export_import/export_import.dart | 6 +- lib/src/backend/errors/import_export.dart | 27 ++- .../impls/objectbox/backend/backend.dart | 1 + .../impls/objectbox/backend/internal.dart | 50 ++++- .../backend/internal_thread_safe.dart | 22 +-- .../objectbox/backend/internal_worker.dart | 159 +++++++++++---- .../backend/interfaces/backend/internal.dart | 24 ++- lib/src/root/directory.dart | 9 +- lib/src/root/external.dart | 52 ++++- pubspec.yaml | 6 +- 14 files changed, 493 insertions(+), 100 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 50ca081b..6f7a895c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,11 +14,39 @@ Many thanks to my sponsors, no matter how much or how little they donated. Spons # Changelog -## [9.0.0] - 2023/XX/XX +## [9.0.0] - "Hundreds of hours" - 2024/XX/XX -* Migrated to Flutter 3.13 and Dart 3.1 -* Migrated to flutter_map v6 -* Bulk downloading reimplementation +This update has essentially rewritten FMTC from the ground up, over hundreds of hours. It focuses on: + +* improved future maintainability by modularity +* improved stability & performance across the board +* support of 'tiles across stores': reduced duplication + +I would hugely appricate any donations - please see the documentation site, GitHub repo, or pub.dev package. + +I would also like to thank all those who have been waiting and contributing their feedback throughout the process: it means a lot to me that FMTC is such a crucial component to your application. + +And without further ado, let's get the biggest changes out of the way first: + +* Added support for modular storage/root backends through `FMTCBackend` + * Removed Isar support + Isar unfortunately caused too many stability issues, and is not as actively maintained as I would like (I can sympathise :D). + * Added ObjectBox as the default backend (`FMTCObjectBoxBackend`) + ObjectBox uses the same underlying database technology as Isar (MBDX), but is more maintained, and I'm hoping, more stable. Note that ObjectBox declares it only supports 64-bit systems, whereas Isar was just 'mostly unstable' on 32-bit systems until recently (where is also became 64-bit only): it's time for the future! + * It is expected that backends support a many-to-many relationship between tiles and stores + This has reduced duplication between stores and tiles massively, and now allows for smaller, fine-grained region control. The default backend supports this with as minimal hit to performance as possible, although of course, database operations are now considerably more complex than in previous versions, and so therefore will take slightly longer. In practise, there is no noticeable performance difference. + * It is expected that backends cache statistics instead of calculating them at get time + This has decreased the time spent fetching basic statistics, and allowed for increased efficiency when getting multiple stats at once. Of course, there is some impact on performance at write time: it must all be accurately tracked, else it will be inaccacurate/out-of-sync. + +* Restructured top-level access APIs + * Deprecated `StoreDirectory` & `RootDirectory` in favour of `FMTCStore` and `FMTCRoot` + The term 'directory' has been misleading for a couple of years now, as it hasn't been actual filesystem directories storing information since the introduction of v7. + * Removed the `FlutterMapTileCaching`/`FMTC` access object, in favour of `FMTCStore` and `FMTCRoot` direct constructors + Much of the configuration and state management performed by this top-level object and it's close relatives were transferred to the backend, and as such, there is no longer a requirement for these objects. + * Removed support for synchronous operations (and renamed asynchronous operations to reflect this) + These were incompatible with the new `Isolate`d `FMTCObjectBoxBackend`, and to keep scope reasonable, I decided to remove them, in favour of backends implementing their own `Isolate`ion as well. + +* Reimplemented bulk downloading * Added `CustomPolygonRegion`, a `BaseRegion` that is formed of any* outline * Added pause and resume functionality * Added rate limiting functionality @@ -30,14 +58,22 @@ Many thanks to my sponsors, no matter how much or how little they donated. Spons * Fixed usage of `obscuredQueryParams` * Removed support for bulk download buffering by size capacity * Removed support for custom `HttpClient`s -* Added secondary check to `FMTCImageProvider` to ensure responses are valid images +* Deprecated plugins + * Transfered support for import/export operations to core (`RootExternal`) + * Deprecated support for background bulk downloading +* Migrated to Flutter 3.19 and Dart 3.3 +* Migrated to flutter_map v6 + +With those out of the way, we can take a look at the smaller changes: + +* Improved error handling (especially in backends) * Added `StoreManagement.pruneTilesOlderThan` method -* Improved performance of `tileImage` methods -* Improved error objects +* Added shortcut for getting multiple stats: `StoreStats.all` +* Added secondary check to `FMTCImageProvider` to ensure responses are valid images * Replaced public facing `RegionType`/`type` with Dart 3 exhaustive switch statements through `BaseRegion/DownloadableRegion.when` & `RecoverableRegion.toRegion` * Removed HTTP/2 support -Also: +In addition, there's been more action in the surrounding enviroment: * Created a miniature testing tile server * Created automated tests for tile generation diff --git a/example/lib/main.dart b/example/lib/main.dart index 54898542..14f8cb18 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -20,7 +20,7 @@ void main() async { ), ); - // TODO: Implement handling + // TODO: Implement error handling await FMTCObjectBoxBackend().initialise(); runApp(const _AppContainer()); diff --git a/example/lib/screens/export_import/components/export.dart b/example/lib/screens/export_import/components/export.dart index 4cae849d..5ec8add0 100644 --- a/example/lib/screens/export_import/components/export.dart +++ b/example/lib/screens/export_import/components/export.dart @@ -32,7 +32,7 @@ class _ExportState extends State { future: stores, builder: (context, snapshot) { if (!snapshot.hasData) { - return const LoadingIndicator('Loading available stores'); + return const LoadingIndicator('Loading exportable stores'); } if (snapshot.data!.isEmpty) { diff --git a/example/lib/screens/export_import/components/import.dart b/example/lib/screens/export_import/components/import.dart index d9de6b1f..db09d31e 100644 --- a/example/lib/screens/export_import/components/import.dart +++ b/example/lib/screens/export_import/components/import.dart @@ -1,14 +1,40 @@ import 'package:flutter/material.dart'; +import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; -class Import extends StatelessWidget { +import '../../../shared/components/loading_indicator.dart'; + +class Import extends StatefulWidget { const Import({ super.key, + required this.path, required this.changeForceOverrideExisting, }); + final String path; final void Function({required bool forceOverrideExisting}) changeForceOverrideExisting; + @override + State createState() => _ImportState(); +} + +class _ImportState extends State { + late final _conflictStrategies = + ImportConflictStrategy.values.toList(growable: false); + late Future> importableStores = + FMTCRoot.external(pathToArchive: widget.path).listStores; + + ImportConflictStrategy selectedConflictStrategy = ImportConflictStrategy.skip; + + @override + void didUpdateWidget(covariant Import oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.path != widget.path) { + importableStores = + FMTCRoot.external(pathToArchive: widget.path).listStores; + } + } + @override Widget build(BuildContext context) => Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -22,13 +48,162 @@ class Import extends StatelessWidget { ), ), OutlinedButton.icon( - onPressed: () => - changeForceOverrideExisting(forceOverrideExisting: true), + onPressed: () => widget.changeForceOverrideExisting( + forceOverrideExisting: true, + ), icon: const Icon(Icons.file_upload_outlined), label: const Text('Force Overwrite'), ), ], ), + const SizedBox(height: 16), + Text( + 'Importable Stores', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 8), + Flexible( + child: FutureBuilder( + future: importableStores, + builder: (context, snapshot) { + if (snapshot.hasError) { + return const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.broken_image_rounded, size: 48), + Text( + "We couldn't open that archive.\nAre you sure it's " + 'compatible with FMTC, and is unmodified?', + style: TextStyle(fontSize: 15), + textAlign: TextAlign.center, + ), + ], + ), + ); + } + + if (!snapshot.hasData) { + return const LoadingIndicator('Loading importable stores'); + } + + if (snapshot.data!.isEmpty) { + return const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.folder_off_rounded, size: 48), + Text( + "There aren't any stores to import!\n" + 'Check that you exported it correctly.', + style: TextStyle(fontSize: 15), + ), + ], + ), + ); + } + + return ListView.separated( + shrinkWrap: true, + itemCount: snapshot.data!.length, + itemBuilder: (context, index) { + final storeName = snapshot.data![index]; + + return ListTile( + title: Text(storeName), + subtitle: FutureBuilder( + future: FMTCStore(storeName).manage.ready, + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const Text('Checking for conflicts...'); + } + if (snapshot.data!) { + return const Text('Conflicts with existing store'); + } + return const SizedBox.shrink(); + }, + ), + dense: true, + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + FutureBuilder( + future: FMTCStore(storeName).manage.ready, + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const SizedBox.square( + dimension: 18, + child: CircularProgressIndicator.adaptive( + strokeWidth: 3, + ), + ); + } + if (snapshot.data!) { + return const Icon(Icons.merge_type_rounded); + } + return const SizedBox.shrink(); + }, + ), + const SizedBox(width: 10), + const Icon(Icons.pending_outlined), + ], + ), + ); + }, + separatorBuilder: (context, index) => const Divider(), + ); + }, + ), + ), + const SizedBox(height: 16), + Text( + 'Conflict Strategy', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 8), + SizedBox( + width: double.infinity, + child: DropdownButton( + isExpanded: true, + value: selectedConflictStrategy, + items: _conflictStrategies + .map( + (e) => DropdownMenuItem( + value: e, + child: Row( + children: [ + Icon( + switch (e) { + ImportConflictStrategy.merge => + Icons.merge_rounded, + ImportConflictStrategy.rename => + Icons.edit_rounded, + ImportConflictStrategy.replace => + Icons.save_as_rounded, + ImportConflictStrategy.skip => + Icons.skip_next_rounded, + }, + ), + const SizedBox(width: 8), + Text( + switch (e) { + ImportConflictStrategy.merge => 'Merge', + ImportConflictStrategy.rename => 'Rename', + ImportConflictStrategy.replace => + 'Replace/Overwrite', + ImportConflictStrategy.skip => 'Skip', + }, + style: const TextStyle(color: Colors.white), + ), + ], + ), + ), + ) + .toList(growable: false), + onChanged: (choice) => + setState(() => selectedConflictStrategy = choice!), + ), + ), ], ); } diff --git a/example/lib/screens/export_import/export_import.dart b/example/lib/screens/export_import/export_import.dart index e79caeea..753c9fdb 100644 --- a/example/lib/screens/export_import/export_import.dart +++ b/example/lib/screens/export_import/export_import.dart @@ -109,6 +109,7 @@ class _ExportImportPopupState extends State { right: 12, ), child: Import( + path: pathController.text, changeForceOverrideExisting: onPathChanged, ), ); @@ -171,9 +172,10 @@ class _ExportImportPopupState extends State { if (isExporting) { setState(() => isProcessingExporting = true); final stopwatch = Stopwatch()..start(); - await FMTCRoot.external.export( + await FMTCRoot.external( + pathToArchive: pathController.text, + ).import( storeNames: selectedStores.toList(), - outputPath: pathController.text, ); stopwatch.stop(); if (context.mounted) { diff --git a/lib/src/backend/errors/import_export.dart b/lib/src/backend/errors/import_export.dart index e9be18e4..81d2832d 100644 --- a/lib/src/backend/errors/import_export.dart +++ b/lib/src/backend/errors/import_export.dart @@ -3,20 +3,31 @@ part of 'errors.dart'; -/// A subset of [FMTCBackendError]s that indicates a failure during import, due -/// to the extended reason -base class ImportError extends FMTCBackendError {} +/// A subset of [FMTCBackendError]s that indicates a failure during import or +/// export, due to the extended reason +base class ImportExportError extends FMTCBackendError {} + +/// Indicates that the specified path to import from or export to did exist, but +/// was not a file +final class ImportExportPathNotFile extends ImportExportError { + ImportExportPathNotFile(); + + @override + String toString() => + 'ImportPathNotFile: The specified import/export path existed, but was not ' + 'a file.'; +} /// Indicates that the specified file to import did not exist/could not be found -final class ImportFileNotExists extends ImportError { - ImportFileNotExists({required this.path}); +final class ImportPathNotExists extends ImportExportError { + ImportPathNotExists({required this.path}); /// The specified path to the import file final String path; @override String toString() => - 'FileNotExists: The specified import file ($path) did not exist.'; + 'ImportPathNotExists: The specified import file ($path) did not exist.'; } /// Indicates that the import file was not of the expected standard, because it @@ -24,7 +35,7 @@ final class ImportFileNotExists extends ImportError { /// * did not contain the appropriate footer signature: hex "FF FF 46 4D 54 43" /// ("**FMTC") /// * did not contain all required header information within the file -final class ImportFileNotFMTCStandard extends ImportError { +final class ImportFileNotFMTCStandard extends ImportExportError { ImportFileNotFMTCStandard(); @override @@ -39,7 +50,7 @@ final class ImportFileNotFMTCStandard extends ImportError { /// The bytes prior to the header signature (hex "FF FF 46 4D 54 43" ("**FMTC")) /// should an identifier (eg. the name) of the exporting backend proceeded by /// hex "FF FE". -final class ImportFileNotBackendCompatible extends ImportError { +final class ImportFileNotBackendCompatible extends ImportExportError { ImportFileNotBackendCompatible(); @override diff --git a/lib/src/backend/impls/objectbox/backend/backend.dart b/lib/src/backend/impls/objectbox/backend/backend.dart index d9157c94..eda30734 100644 --- a/lib/src/backend/impls/objectbox/backend/backend.dart +++ b/lib/src/backend/impls/objectbox/backend/backend.dart @@ -7,6 +7,7 @@ import 'dart:io'; import 'dart:isolate'; import 'package:flutter/services.dart'; +import 'package:meta/meta.dart'; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; diff --git a/lib/src/backend/impls/objectbox/backend/internal.dart b/lib/src/backend/impls/objectbox/backend/internal.dart index 12e71a8a..ec03e17a 100644 --- a/lib/src/backend/impls/objectbox/backend/internal.dart +++ b/lib/src/backend/impls/objectbox/backend/internal.dart @@ -538,18 +538,28 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { @override Future exportStores({ required List storeNames, - required String outputPath, - }) => - _sendCmdOneShot( - type: _WorkerCmdType.exportStores, - args: {'storeNames': storeNames, 'outputPath': outputPath}, - ); + required String path, + }) async { + final type = await FileSystemEntity.type(path); + if (type == FileSystemEntityType.directory) { + throw ImportExportPathNotFile(); + } + + await _sendCmdOneShot( + type: _WorkerCmdType.exportStores, + args: {'storeNames': storeNames, 'outputPath': path}, + ); + } @override - ImportResult importStores({ + @experimental // TODO: Finish implementation + Future importStores({ required String path, required ImportConflictStrategy strategy, - }) { + required List? storeNames, + }) async { + await _checkImportPathType(path); + final storesStreamController = StreamController< ({String importingName, bool conflict, String? newName})>(); @@ -558,7 +568,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { late final StreamSubscription?> listener; listener = _sendCmdStreamed( type: _WorkerCmdType.importStores, - args: {'path': path, 'strategy': strategy}, + args: {'path': path, 'strategy': strategy, 'stores': storeNames}, ).listen( cancelOnError: true, (evt) { @@ -587,4 +597,26 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { complete: complete.future, ); } + + @override + Future> listImportableStores({ + required String path, + }) async { + await _checkImportPathType(path); + + return (await _sendCmdOneShot( + type: _WorkerCmdType.listImportableStores, + args: {'path': path}, + ))!['stores']; + } + + Future _checkImportPathType(String path) async { + final type = await FileSystemEntity.type(path); + if (type == FileSystemEntityType.notFound) { + throw ImportPathNotExists(path: path); + } + if (type == FileSystemEntityType.directory) { + throw ImportExportPathNotFile(); + } + } } diff --git a/lib/src/backend/impls/objectbox/backend/internal_thread_safe.dart b/lib/src/backend/impls/objectbox/backend/internal_thread_safe.dart index 22aabb9a..6fdf9a9b 100644 --- a/lib/src/backend/impls/objectbox/backend/internal_thread_safe.dart +++ b/lib/src/backend/impls/objectbox/backend/internal_thread_safe.dart @@ -99,13 +99,7 @@ class _ObjectBoxBackendThreadSafeImpl implements FMTCBackendInternalThreadSafe { ..size += -existingTile.bytes.lengthInBytes + bytes.lengthInBytes, mode: PutMode.update, ); - } - - if (!didContainAlready || existingTile == null) { - storesToUpdate[storeName] = store - ..length += 1 - ..size += bytes.lengthInBytes; - + } else { rootBox.put( rootBox.get(1)! ..length += 1 @@ -114,6 +108,12 @@ class _ObjectBoxBackendThreadSafeImpl implements FMTCBackendInternalThreadSafe { ); } + if (!didContainAlready || existingTile == null) { + storesToUpdate[storeName] = store + ..length += 1 + ..size += bytes.lengthInBytes; + } + tiles.put( ObjectBoxTile( url: url, @@ -178,16 +178,16 @@ class _ObjectBoxBackendThreadSafeImpl implements FMTCBackendInternalThreadSafe { rootData.size += -existingTile.bytes.lengthInBytes + bytes.lengthInBytes; + } else { + rootData + ..length += 1 + ..size += bytes.lengthInBytes; } if (!didContainAlready || existingTile == null) { storesToUpdate[storeName] = store ..length += 1 ..size += bytes.lengthInBytes; - - rootData - ..length += 1 - ..size += bytes.lengthInBytes; } tiles.put( diff --git a/lib/src/backend/impls/objectbox/backend/internal_worker.dart b/lib/src/backend/impls/objectbox/backend/internal_worker.dart index 3d3a16c8..1e3c0376 100644 --- a/lib/src/backend/impls/objectbox/backend/internal_worker.dart +++ b/lib/src/backend/impls/objectbox/backend/internal_worker.dart @@ -43,8 +43,8 @@ enum _WorkerCmdType { watchStores(streamCancel: cancelStreamedOutputs), exportStores, importStores(streamCancel: cancelStreamedOutputs), - cancelStreamedOutputs, - ; + listImportableStores, + cancelStreamedOutputs; const _WorkerCmdType({this.streamCancel}); @@ -181,6 +181,46 @@ Future _worker( return rootDeltaLength.abs(); } + /// Verify that the specified file is a valid FMTC format archive, compatible + /// with this ObjectBox backend + /// + /// If [truncate] is `true` (default), the FMTC information will be stripped + /// from the file. + void verifyImportableArchive( + File importFile, { + bool truncate = true, + }) { + final ram = importFile.openSync(mode: FileMode.append); + try { + int cursorPos = ram.positionSync() - 1; + ram.setPositionSync(cursorPos); + + // Check for FMTC footer signature ("**FMTC") + const signature = [255, 255, 70, 77, 84, 67]; + for (int i = 5; i >= 0; i--) { + if (signature[i] != ram.readByteSync()) { + throw ImportFileNotFMTCStandard(); + } + ram.setPositionSync(--cursorPos); + } + + // Check for expected backend identifier ("**ObjectBox") + const id = [255, 255, 79, 98, 106, 101, 99, 116, 66, 111, 120]; + for (int i = 10; i >= 0; i--) { + if (id[i] != ram.readByteSync()) { + throw ImportFileNotBackendCompatible(); + } + ram.setPositionSync(--cursorPos); + } + + if (truncate) ram.truncateSync(--cursorPos); + } catch (e) { + ram.closeSync(); + rethrow; + } + ram.closeSync(); + } + //! MAIN HANDLER !// void mainHandler(_IncomingCmd cmd) { @@ -933,52 +973,22 @@ Future _worker( case _WorkerCmdType.importStores: final importPath = cmd.args['path']! as String; final strategy = cmd.args['strategy'] as ImportConflictStrategy; - - final importFileRaw = File(importPath); - - if (!importFileRaw.existsSync()) { - throw ImportFileNotExists(path: importPath); - } + final storesToImport = cmd.args['stores'] as List; final importDir = path.join(input.rootDirectory.absolute.path, 'import_tmp'); final importDirIO = Directory(importDir)..createSync(); final importFile = - importFileRaw.copySync(path.join(importDir, 'data.mdb')); + File(importPath).copySync(path.join(importDir, 'data.mdb')); - // Verify file is valid for import - final ram = importFile.openSync(mode: FileMode.append); try { - int cursorPos = ram.positionSync() - 1; - ram.setPositionSync(cursorPos); - - // Check for FMTC footer signature ("**FMTC") - const signature = [255, 255, 70, 77, 84, 67]; - for (int i = 5; i >= 0; i--) { - if (signature[i] != ram.readByteSync()) { - throw ImportFileNotFMTCStandard(); - } - ram.setPositionSync(--cursorPos); - } - - // Check for expected backend identifier ("**ObjectBox") - const id = [255, 255, 79, 98, 106, 101, 99, 116, 66, 111, 120]; - for (int i = 10; i >= 0; i--) { - if (id[i] != ram.readByteSync()) { - throw ImportFileNotBackendCompatible(); - } - ram.setPositionSync(--cursorPos); - } - - ram.truncateSync(--cursorPos); + verifyImportableArchive(importFile); } catch (e) { - ram.closeSync(); importFile.deleteSync(); importDirIO.deleteSync(); rethrow; } - ram.closeSync(); final importingRoot = Store( getObjectBoxModel(), @@ -999,6 +1009,7 @@ Future _worker( .query(ObjectBoxTile_.url.equals('')) .build(); + final storesToUpdate = {}; final storesObjectsForRelations = {}; final storesToSkipTiles = []; int rootDeltaLength = 0; @@ -1014,6 +1025,11 @@ Future _worker( TxMode.write, () => importingStores.listen( (importingStore) { + if (!storesToImport.contains(importingStore.name)) { + storesToSkipTiles.add(importingStore.name); + return; + } + final existingStore = (specificStoresQuery ..param(ObjectBoxStore_.name).value = importingStore.name) @@ -1164,8 +1180,21 @@ Future _worker( importingTile.url) .findUnique()!; + final newRelatedStores = importingTile.stores.map( + (e) => storesObjectsForRelations[e.name]!, + ); + if (existingTile.lastModified - .isAfter(importingTile.lastModified)) return; + .isAfter(importingTile.lastModified)) { + for (final newRelatedStore in newRelatedStores) { + storesToUpdate[newRelatedStore.name] = + (storesToUpdate[newRelatedStore.name] ?? + newRelatedStore) + ..size += -importingTile.bytes.lengthInBytes + + existingTile.bytes.lengthInBytes; + } + return; + } root.box().put( ObjectBoxTile( @@ -1175,26 +1204,33 @@ Future _worker( )..stores.addAll( { ...existingTile.stores, - ...importingTile.stores.map( - (e) => storesObjectsForRelations[e.name]!, - ), + ...newRelatedStores, }, ), mode: PutMode.update, ); + for (final existingTileStore in existingTile.stores) { + storesToUpdate[existingTileStore.name] = + (storesToUpdate[existingTileStore.name] ?? + existingTileStore) + ..size += -existingTile.bytes.lengthInBytes + + importingTile.bytes.lengthInBytes; + } + rootDeltaSize += -existingTile.bytes.lengthInBytes + importingTile.bytes.lengthInBytes; } - //TODO: mergeing strategy + // TODO: mergeing strategy + + // TODO: Write `storesToUpdate` }, ), ) .asFuture() .then( (_) { - print(rootDeltaLength); updateRootStatistics( deltaLength: rootDeltaLength, deltaSize: rootDeltaSize, @@ -1216,6 +1252,47 @@ Future _worker( ); }, ); + case _WorkerCmdType.listImportableStores: + final importPath = cmd.args['path']! as String; + + final importDir = + path.join(input.rootDirectory.absolute.path, 'import_tmp'); + final importDirIO = Directory(importDir)..createSync(); + + final importFile = + File(importPath).copySync(path.join(importDir, 'data.mdb')); + + try { + verifyImportableArchive(importFile); + } catch (e) { + importFile.deleteSync(); + importDirIO.deleteSync(); + rethrow; + } + + final importingRoot = Store( + getObjectBoxModel(), + directory: importDir, + maxDBSizeInKB: input.maxDatabaseSize, // Defaults to 10 GB + macosApplicationGroup: input.macosApplicationGroup, + ); + + sendRes( + id: cmd.id, + data: { + 'stores': importingRoot + .box() + .getAll() + .map((e) => e.name) + .toList(growable: false), + }, + ); + + importingRoot.close(); + + importFile.deleteSync(); + File(path.join(importDir, 'lock.mdb')).deleteSync(); + importDirIO.deleteSync(); } } diff --git a/lib/src/backend/interfaces/backend/internal.dart b/lib/src/backend/interfaces/backend/internal.dart index 93d32455..47ea216b 100644 --- a/lib/src/backend/interfaces/backend/internal.dart +++ b/lib/src/backend/interfaces/backend/internal.dart @@ -319,13 +319,33 @@ abstract interface class FMTCBackendInternal required bool triggerImmediately, }); + /// Create an archive at the file [path] containing the specifed stores and + /// their respective tiles + /// + /// See [RootExternal] for more information about expected behaviour and + /// errors. Future exportStores({ + required String path, required List storeNames, - required String outputPath, }); - ImportResult importStores({ + /// Load the specified stores (or all stores if `null`) from the archive file + /// at [path] into the current root, using [strategy] where there are + /// conflicts + /// + /// See [RootExternal] for more information about expected behaviour and + /// errors. + Future importStores({ required String path, required ImportConflictStrategy strategy, + required List? storeNames, + }); + + /// Check the stores available inside the archive file at [path] + /// + /// See [RootExternal] for more information about expected behaviour and + /// errors. + Future> listImportableStores({ + required String path, }); } diff --git a/lib/src/root/directory.dart b/lib/src/root/directory.dart index 52bd020a..c0e44834 100644 --- a/lib/src/root/directory.dart +++ b/lib/src/root/directory.dart @@ -35,9 +35,8 @@ abstract class FMTCRoot { static RootRecovery get recovery => RootRecovery._instance ?? RootRecovery._(); - /// Provides store import functionality for this root - /// - /// The 'fmtc_plus_sharing' module must be installed to add the functionality, - /// without it, this object provides no functionality. - static RootExternal get external => const RootExternal._(); + /// Export & import 'archives' of selected stores and tiles, outside of the + /// FMTC environment + static RootExternal external({required String pathToArchive}) => + RootExternal._(pathToArchive); } diff --git a/lib/src/root/external.dart b/lib/src/root/external.dart index 8660e6d1..2560925a 100644 --- a/lib/src/root/external.dart +++ b/lib/src/root/external.dart @@ -3,29 +3,69 @@ part of flutter_map_tile_caching; +/// The result of [RootExternal.import] +/// +/// `stores` will complete when the store names become available, and whether +/// they have conflicted with existing stores. +/// +/// `complete` will complete when the import is complete. typedef ImportResult = ({ Future> stores, Future complete, }); +/// Export & import 'archives' of selected stores and tiles, outside of the +/// FMTC environment +/// +/// Archives are backend specific, and FMTC specific. They cannot necessarily +/// be imported by a backend different to the one that exported it. The +/// archive may hold a similar form to the raw format of the database used by +/// the backend, but FMTC specific information has been attached, and therefore +/// the file will be unreadable by non-FMTC database implementations. +/// +/// If the specified archive (at [pathToArchive]) is not of the expected format, +/// an error from the [ImportExportError] group: +/// +/// - Doesn't exist (except [export]): [ImportPathNotExists] +/// - Not a file: [ImportExportPathNotFile] +/// - Not an FMTC archive: [ImportFileNotFMTCStandard] +/// - Not compatible with the current backend: [ImportFileNotBackendCompatible] +/// +/// Importing (especially) and exporting operations are likely to be slow. It is +/// not recommended to attempt to use other FMTC operations during the +/// operation, to avoid slowing it further or potentially causing inconsistent +/// state. class RootExternal { - const RootExternal._(); + const RootExternal._(this.pathToArchive); + + /// The path to an archive file + final String pathToArchive; + /// Creates an archive at [pathToArchive] containing the specified stores and + /// their tiles + /// + /// If a file already exists at [pathToArchive], it will be overwritten. Future export({ required List storeNames, - required String outputPath, }) => FMTCBackendAccess.internal - .exportStores(storeNames: storeNames, outputPath: outputPath); + .exportStores(storeNames: storeNames, path: pathToArchive); - ImportResult import({ - required String path, + /// CAUTION: HIGHLY EXPERIMENTAL, INCOMPLETE, AND UNTESTED + @experimental + Future import({ ImportConflictStrategy strategy = ImportConflictStrategy.skip, + List? storeNames, }) => FMTCBackendAccess.internal.importStores( - path: path, + path: pathToArchive, strategy: strategy, + storeNames: storeNames, ); + + /// List the available store names within the archive at [pathToArchive] + Future> get listStores => + FMTCBackendAccess.internal.listImportableStores(path: pathToArchive); } /// Determines what action should be taken when an importing store conflicts diff --git a/pubspec.yaml b/pubspec.yaml index a3e540a6..33c14daa 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -35,9 +35,9 @@ dependencies: flutter_map: ^6.1.0 http: ^1.2.1 latlong2: ^0.9.0 - meta: any - objectbox: ^2.5.0 - objectbox_flutter_libs: any + meta: ^1.11.0 + objectbox: ^2.5.1 + objectbox_flutter_libs: ^2.5.1 path: ^1.9.0 path_provider: ^2.1.2 From 3e7848f1059baf4606e9a5960e661d243145ec71 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 7 Mar 2024 21:49:36 +0000 Subject: [PATCH 134/168] Added generated code to package Former-commit-id: f27e0550e4e77ba4baa1439971e6f38411865569 [formerly 054f7bb54f9bf5538f7a096d4f8bc79b81998412] Former-commit-id: a8cf5f560c63098cfc447b8bf00408497069eea0 --- .gitignore | 1 - lib/src/backend/impls/objectbox/models/generated/.gitignore | 1 + lib/src/backend/impls/objectbox/models/generated/.pubignore | 0 pubspec.yaml | 3 ++- 4 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 lib/src/backend/impls/objectbox/models/generated/.gitignore create mode 100644 lib/src/backend/impls/objectbox/models/generated/.pubignore diff --git a/.gitignore b/.gitignore index b61e4d04..eb120e3c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ # Custom local/ -*.g.dart # Miscellaneous *.class diff --git a/lib/src/backend/impls/objectbox/models/generated/.gitignore b/lib/src/backend/impls/objectbox/models/generated/.gitignore new file mode 100644 index 00000000..38bc1119 --- /dev/null +++ b/lib/src/backend/impls/objectbox/models/generated/.gitignore @@ -0,0 +1 @@ +objectbox.g.dart \ No newline at end of file diff --git a/lib/src/backend/impls/objectbox/models/generated/.pubignore b/lib/src/backend/impls/objectbox/models/generated/.pubignore new file mode 100644 index 00000000..e69de29b diff --git a/pubspec.yaml b/pubspec.yaml index 33c14daa..681a7374 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_map_tile_caching description: Plugin for 'flutter_map' providing advanced caching functionality, with ability to download map regions for offline use. -version: 9.0.0-dev.6 +version: 9.0.0-dev.7 repository: https://github.com/JaffaKetchup/flutter_map_tile_caching issue_tracker: https://github.com/JaffaKetchup/flutter_map_tile_caching/issues @@ -30,6 +30,7 @@ dependencies: async: ^2.11.0 collection: ^1.18.0 dart_earcut: ^1.1.0 + flat_buffers: ^23.5.26 flutter: sdk: flutter flutter_map: ^6.1.0 From 41ae35ebb60984fc6590a5de9b45e03703ddf574 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 7 Mar 2024 21:54:59 +0000 Subject: [PATCH 135/168] Fixed formatting issue Former-commit-id: 6d791c69c460cfe693a5546c0f539fbbeee22ee8 [formerly 1d16590d8f968f099037443ca5c914db48ff4b32] Former-commit-id: 8aa5b4b4d821c9adb167aa7520061667a1f37f00 --- lib/src/store/directory.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/store/directory.dart b/lib/src/store/directory.dart index 85388beb..b7ef52a2 100644 --- a/lib/src/store/directory.dart +++ b/lib/src/store/directory.dart @@ -50,7 +50,7 @@ class FMTCStore { /// advanced requirements should use another package, as this is a basic /// implementation. StoreMetadata get metadata => StoreMetadata._(this); - + /// Get tools to manage bulk downloading to this store /// /// The 'fmtc_plus_background_downloading' module must be installed to add the From a493ce80dbc7c6f46303e92fa309cecf63e63f95 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 7 Mar 2024 22:03:43 +0000 Subject: [PATCH 136/168] Converted named library part-of directives to string paths Upgraded dependencies of example project Former-commit-id: 43e719e46719f2f2c80bb1d2d3899e74207e3c60 [formerly 53bdacfaa3c3b225056104a5f77a8a7fb2e5dbf3] Former-commit-id: d9456a2bd846573556456f03a0532617f74c3607 --- example/pubspec.yaml | 18 +++++++++--------- jaffa_lints.yaml | 1 + .../objectbox/backend/internal_worker.dart | 4 ++-- lib/src/bulk_download/download_progress.dart | 2 +- lib/src/bulk_download/manager.dart | 2 +- lib/src/bulk_download/thread.dart | 2 +- lib/src/bulk_download/tile_event.dart | 2 +- lib/src/misc/deprecations.dart | 2 +- lib/src/providers/tile_provider.dart | 2 +- lib/src/providers/tile_provider_settings.dart | 2 +- lib/src/regions/base_region.dart | 2 +- lib/src/regions/circle.dart | 2 +- lib/src/regions/custom_polygon.dart | 2 +- lib/src/regions/downloadable_region.dart | 2 +- lib/src/regions/line.dart | 2 +- lib/src/regions/recovered_region.dart | 2 +- lib/src/regions/rectangle.dart | 2 +- lib/src/root/directory.dart | 2 +- lib/src/root/external.dart | 2 +- lib/src/root/recovery.dart | 2 +- lib/src/root/statistics.dart | 2 +- lib/src/store/directory.dart | 2 +- lib/src/store/download.dart | 2 +- lib/src/store/manage.dart | 2 +- lib/src/store/metadata.dart | 2 +- lib/src/store/statistics.dart | 2 +- 26 files changed, 35 insertions(+), 34 deletions(-) diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 7bf08d96..da161563 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -6,30 +6,30 @@ publish_to: "none" version: 9.0.0 environment: - sdk: ">=3.0.0 <4.0.0" - flutter: ">=3.10.0" + sdk: ">=3.3.0 <4.0.0" + flutter: ">=3.19.0" dependencies: auto_size_text: ^3.0.0 badges: ^3.1.2 - better_open_file: ^3.6.4 + better_open_file: ^3.6.5 collection: ^1.18.0 - dart_earcut: ^1.0.1 + dart_earcut: ^1.1.0 file_picker: ^6.2.0 flutter: sdk: flutter - flutter_foreground_task: ^6.1.2 + flutter_foreground_task: ^6.1.3 flutter_map: ^6.1.0 flutter_map_animations: ^0.5.3 flutter_map_tile_caching: - google_fonts: ^6.1.0 - gpx: ^2.2.1 - http: ^1.1.2 + google_fonts: ^6.2.1 + gpx: ^2.2.2 + http: ^1.2.1 intl: ^0.19.0 latlong2: ^0.9.0 osm_nominatim: ^3.0.0 path: ^1.9.0 - provider: ^6.1.1 + provider: ^6.1.2 stream_transform: ^2.1.0 validators: ^3.0.0 version: ^3.0.2 diff --git a/jaffa_lints.yaml b/jaffa_lints.yaml index d6e149b1..00d25b89 100644 --- a/jaffa_lints.yaml +++ b/jaffa_lints.yaml @@ -195,6 +195,7 @@ linter: - use_rethrow_when_possible - use_setters_to_change_properties - use_string_buffers + - use_string_in_part_of_directives - use_super_parameters - use_test_throws_matchers - use_to_and_as_if_applicable diff --git a/lib/src/backend/impls/objectbox/backend/internal_worker.dart b/lib/src/backend/impls/objectbox/backend/internal_worker.dart index 1e3c0376..952b5247 100644 --- a/lib/src/backend/impls/objectbox/backend/internal_worker.dart +++ b/lib/src/backend/impls/objectbox/backend/internal_worker.dart @@ -1222,9 +1222,9 @@ Future _worker( importingTile.bytes.lengthInBytes; } - // TODO: mergeing strategy + // TODO: Implement merging strategy - // TODO: Write `storesToUpdate` + // TODO: Write `storesToUpdate` to db }, ), ) diff --git a/lib/src/bulk_download/download_progress.dart b/lib/src/bulk_download/download_progress.dart index a42e617c..f54ecfa3 100644 --- a/lib/src/bulk_download/download_progress.dart +++ b/lib/src/bulk_download/download_progress.dart @@ -1,7 +1,7 @@ // Copyright © Luka S (JaffaKetchup) under GPL-v3 // A full license can be found at .\LICENSE -part of flutter_map_tile_caching; +part of '../../flutter_map_tile_caching.dart'; /// Statistics and information about the current progress of the download /// diff --git a/lib/src/bulk_download/manager.dart b/lib/src/bulk_download/manager.dart index 86709f5c..6aa19f76 100644 --- a/lib/src/bulk_download/manager.dart +++ b/lib/src/bulk_download/manager.dart @@ -1,7 +1,7 @@ // Copyright © Luka S (JaffaKetchup) under GPL-v3 // A full license can be found at .\LICENSE -part of flutter_map_tile_caching; +part of '../../flutter_map_tile_caching.dart'; Future _downloadManager( ({ diff --git a/lib/src/bulk_download/thread.dart b/lib/src/bulk_download/thread.dart index def9cdd4..cd26a1d3 100644 --- a/lib/src/bulk_download/thread.dart +++ b/lib/src/bulk_download/thread.dart @@ -1,7 +1,7 @@ // Copyright © Luka S (JaffaKetchup) under GPL-v3 // A full license can be found at .\LICENSE -part of flutter_map_tile_caching; +part of '../../flutter_map_tile_caching.dart'; Future _singleDownloadThread( ({ diff --git a/lib/src/bulk_download/tile_event.dart b/lib/src/bulk_download/tile_event.dart index 2a54ec6e..89ab3644 100644 --- a/lib/src/bulk_download/tile_event.dart +++ b/lib/src/bulk_download/tile_event.dart @@ -1,7 +1,7 @@ // Copyright © Luka S (JaffaKetchup) under GPL-v3 // A full license can be found at .\LICENSE -part of flutter_map_tile_caching; +part of '../../flutter_map_tile_caching.dart'; /// A generalized category for [TileEventResult] enum TileEventResultCategory { diff --git a/lib/src/misc/deprecations.dart b/lib/src/misc/deprecations.dart index 72eb7dab..30eccac3 100644 --- a/lib/src/misc/deprecations.dart +++ b/lib/src/misc/deprecations.dart @@ -1,7 +1,7 @@ // Copyright © Luka S (JaffaKetchup) under GPL-v3 // A full license can be found at .\LICENSE -part of flutter_map_tile_caching; +part of '../../flutter_map_tile_caching.dart'; const _syncRemoval = ''' diff --git a/lib/src/providers/tile_provider.dart b/lib/src/providers/tile_provider.dart index f334af61..c3fc0120 100644 --- a/lib/src/providers/tile_provider.dart +++ b/lib/src/providers/tile_provider.dart @@ -1,7 +1,7 @@ // Copyright © Luka S (JaffaKetchup) under GPL-v3 // A full license can be found at .\LICENSE -part of flutter_map_tile_caching; +part of '../../flutter_map_tile_caching.dart'; /// FMTC's custom [TileProvider] for use in a [TileLayer] /// diff --git a/lib/src/providers/tile_provider_settings.dart b/lib/src/providers/tile_provider_settings.dart index aa95c63c..fad00270 100644 --- a/lib/src/providers/tile_provider_settings.dart +++ b/lib/src/providers/tile_provider_settings.dart @@ -1,7 +1,7 @@ // Copyright © Luka S (JaffaKetchup) under GPL-v3 // A full license can be found at .\LICENSE -part of flutter_map_tile_caching; +part of '../../flutter_map_tile_caching.dart'; /// Callback type that takes an [FMTCBrowsingError] exception typedef FMTCBrowsingErrorHandler = void Function(FMTCBrowsingError exception); diff --git a/lib/src/regions/base_region.dart b/lib/src/regions/base_region.dart index 34e3192d..32645262 100644 --- a/lib/src/regions/base_region.dart +++ b/lib/src/regions/base_region.dart @@ -1,7 +1,7 @@ // Copyright © Luka S (JaffaKetchup) under GPL-v3 // A full license can be found at .\LICENSE -part of flutter_map_tile_caching; +part of '../../flutter_map_tile_caching.dart'; /// A geographical region that forms a particular shape /// diff --git a/lib/src/regions/circle.dart b/lib/src/regions/circle.dart index c34bf44c..b87588dd 100644 --- a/lib/src/regions/circle.dart +++ b/lib/src/regions/circle.dart @@ -1,7 +1,7 @@ // Copyright © Luka S (JaffaKetchup) under GPL-v3 // A full license can be found at .\LICENSE -part of flutter_map_tile_caching; +part of '../../flutter_map_tile_caching.dart'; /// A geographically circular region based off a [center] coord and [radius] /// diff --git a/lib/src/regions/custom_polygon.dart b/lib/src/regions/custom_polygon.dart index ba187932..496830f1 100644 --- a/lib/src/regions/custom_polygon.dart +++ b/lib/src/regions/custom_polygon.dart @@ -1,7 +1,7 @@ // Copyright © Luka S (JaffaKetchup) under GPL-v3 // A full license can be found at .\LICENSE -part of flutter_map_tile_caching; +part of '../../flutter_map_tile_caching.dart'; /// A geographical region who's outline is defined by a list of coordinates /// diff --git a/lib/src/regions/downloadable_region.dart b/lib/src/regions/downloadable_region.dart index f0e585e5..69896e7b 100644 --- a/lib/src/regions/downloadable_region.dart +++ b/lib/src/regions/downloadable_region.dart @@ -1,7 +1,7 @@ // Copyright © Luka S (JaffaKetchup) under GPL-v3 // A full license can be found at .\LICENSE -part of flutter_map_tile_caching; +part of '../../flutter_map_tile_caching.dart'; /// A downloadable region to be passed to bulk download functions /// diff --git a/lib/src/regions/line.dart b/lib/src/regions/line.dart index 7abafe11..a23aeff3 100644 --- a/lib/src/regions/line.dart +++ b/lib/src/regions/line.dart @@ -1,7 +1,7 @@ // Copyright © Luka S (JaffaKetchup) under GPL-v3 // A full license can be found at .\LICENSE -part of flutter_map_tile_caching; +part of '../../flutter_map_tile_caching.dart'; /// A geographically line/locus region based off a list of coords and a [radius] /// diff --git a/lib/src/regions/recovered_region.dart b/lib/src/regions/recovered_region.dart index 98e9c563..d20d6764 100644 --- a/lib/src/regions/recovered_region.dart +++ b/lib/src/regions/recovered_region.dart @@ -1,7 +1,7 @@ // Copyright © Luka S (JaffaKetchup) under GPL-v3 // A full license can be found at .\LICENSE -part of flutter_map_tile_caching; +part of '../../flutter_map_tile_caching.dart'; /// A mixture between [BaseRegion] and [DownloadableRegion] containing all the /// salvaged data from a recovered download diff --git a/lib/src/regions/rectangle.dart b/lib/src/regions/rectangle.dart index ae39dda1..e9788039 100644 --- a/lib/src/regions/rectangle.dart +++ b/lib/src/regions/rectangle.dart @@ -1,7 +1,7 @@ // Copyright © Luka S (JaffaKetchup) under GPL-v3 // A full license can be found at .\LICENSE -part of flutter_map_tile_caching; +part of '../../flutter_map_tile_caching.dart'; /// A geographically rectangular region based off coordinate bounds /// diff --git a/lib/src/root/directory.dart b/lib/src/root/directory.dart index c0e44834..e01623d2 100644 --- a/lib/src/root/directory.dart +++ b/lib/src/root/directory.dart @@ -1,7 +1,7 @@ // Copyright © Luka S (JaffaKetchup) under GPL-v3 // A full license can be found at .\LICENSE -part of flutter_map_tile_caching; +part of '../../flutter_map_tile_caching.dart'; /// Equivalent to [FMTCRoot], provided to ease migration only /// diff --git a/lib/src/root/external.dart b/lib/src/root/external.dart index 2560925a..a28f0e31 100644 --- a/lib/src/root/external.dart +++ b/lib/src/root/external.dart @@ -1,7 +1,7 @@ // Copyright © Luka S (JaffaKetchup) under GPL-v3 // A full license can be found at .\LICENSE -part of flutter_map_tile_caching; +part of '../../flutter_map_tile_caching.dart'; /// The result of [RootExternal.import] /// diff --git a/lib/src/root/recovery.dart b/lib/src/root/recovery.dart index 8bff8b69..231580f4 100644 --- a/lib/src/root/recovery.dart +++ b/lib/src/root/recovery.dart @@ -1,7 +1,7 @@ // Copyright © Luka S (JaffaKetchup) under GPL-v3 // A full license can be found at .\LICENSE -part of flutter_map_tile_caching; +part of '../../flutter_map_tile_caching.dart'; /// Manages the download recovery of all sub-stores of this [FMTCRoot] /// diff --git a/lib/src/root/statistics.dart b/lib/src/root/statistics.dart index 0ac60d12..e0ca29ed 100644 --- a/lib/src/root/statistics.dart +++ b/lib/src/root/statistics.dart @@ -1,7 +1,7 @@ // Copyright © Luka S (JaffaKetchup) under GPL-v3 // A full license can be found at .\LICENSE -part of flutter_map_tile_caching; +part of '../../flutter_map_tile_caching.dart'; /// Provides statistics about a [FMTCRoot] class RootStats { diff --git a/lib/src/store/directory.dart b/lib/src/store/directory.dart index b7ef52a2..95c3fd99 100644 --- a/lib/src/store/directory.dart +++ b/lib/src/store/directory.dart @@ -1,7 +1,7 @@ // Copyright © Luka S (JaffaKetchup) under GPL-v3 // A full license can be found at .\LICENSE -part of flutter_map_tile_caching; +part of '../../flutter_map_tile_caching.dart'; /// Equivalent to [FMTCStore], provided to ease migration only /// diff --git a/lib/src/store/download.dart b/lib/src/store/download.dart index f0fce636..71829dcd 100644 --- a/lib/src/store/download.dart +++ b/lib/src/store/download.dart @@ -1,7 +1,7 @@ // Copyright © Luka S (JaffaKetchup) under GPL-v3 // A full license can be found at .\LICENSE -part of flutter_map_tile_caching; +part of '../../flutter_map_tile_caching.dart'; /// Provides tools to manage bulk downloading to a specific [FMTCStore] /// diff --git a/lib/src/store/manage.dart b/lib/src/store/manage.dart index 070182d1..a489093e 100644 --- a/lib/src/store/manage.dart +++ b/lib/src/store/manage.dart @@ -1,7 +1,7 @@ // Copyright © Luka S (JaffaKetchup) under GPL-v3 // A full license can be found at .\LICENSE -part of flutter_map_tile_caching; +part of '../../flutter_map_tile_caching.dart'; /// Manages an [FMTCStore]'s representation on the filesystem, such as /// creation and deletion diff --git a/lib/src/store/metadata.dart b/lib/src/store/metadata.dart index ed49f203..ec96216a 100644 --- a/lib/src/store/metadata.dart +++ b/lib/src/store/metadata.dart @@ -1,7 +1,7 @@ // Copyright © Luka S (JaffaKetchup) under GPL-v3 // A full license can be found at .\LICENSE -part of flutter_map_tile_caching; +part of '../../flutter_map_tile_caching.dart'; /// Manage custom miscellaneous information tied to an [FMTCStore] /// diff --git a/lib/src/store/statistics.dart b/lib/src/store/statistics.dart index 6893a91c..8599b294 100644 --- a/lib/src/store/statistics.dart +++ b/lib/src/store/statistics.dart @@ -3,7 +3,7 @@ // ignore_for_file: use_late_for_private_fields_and_variables -part of flutter_map_tile_caching; +part of '../../flutter_map_tile_caching.dart'; /// Provides statistics about an [FMTCStore] /// From f9641806e6d25da6cd1b2a71f0ed7714fd8df098 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 7 Mar 2024 22:09:33 +0000 Subject: [PATCH 137/168] Minor improvements Former-commit-id: 7fb3c3b3289288c1660a95e168d58a472bee64db [formerly e1eeebccd6cf00b67682e2420b1807eaa539b18e] Former-commit-id: ca5334581a155fdbfb883a100a11061dbb719cf1 --- .../lib/screens/export_import/components/path_picker.dart | 2 +- lib/src/backend/errors/basic.dart | 1 + lib/src/backend/impls/objectbox/backend/errors.dart | 6 ++---- .../backend/impls/objectbox/backend/internal_worker.dart | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/example/lib/screens/export_import/components/path_picker.dart b/example/lib/screens/export_import/components/path_picker.dart index a703eb5c..2ab84817 100644 --- a/example/lib/screens/export_import/components/path_picker.dart +++ b/example/lib/screens/export_import/components/path_picker.dart @@ -71,7 +71,7 @@ class PathPicker extends StatelessWidget { color: Theme.of(context) .buttonTheme .colorScheme! - .primaryFixedDim, + .primaryFixed, ), ), ), diff --git a/lib/src/backend/errors/basic.dart b/lib/src/backend/errors/basic.dart index 4f6b3ab4..210f416f 100644 --- a/lib/src/backend/errors/basic.dart +++ b/lib/src/backend/errors/basic.dart @@ -28,6 +28,7 @@ final class RootAlreadyInitialised extends FMTCBackendError { final class StoreNotExists extends FMTCBackendError { StoreNotExists({required this.storeName}); + /// The referenced store name final String storeName; @override diff --git a/lib/src/backend/impls/objectbox/backend/errors.dart b/lib/src/backend/impls/objectbox/backend/errors.dart index 8b41245e..aceecbf7 100644 --- a/lib/src/backend/impls/objectbox/backend/errors.dart +++ b/lib/src/backend/impls/objectbox/backend/errors.dart @@ -13,12 +13,10 @@ base class FMTCObjectBoxBackendError extends FMTCBackendError {} /// Indicates that an export failed because the specified output path directory /// was the same as the root directory final class ExportInRootDirectoryForbidden extends FMTCObjectBoxBackendError { - ExportInRootDirectoryForbidden({required this.directory}); - - final String directory; + ExportInRootDirectoryForbidden(); @override String toString() => 'ExportInRootDirectoryForbidden: It is forbidden to export stores to the ' - 'same directory ($directory) as the `rootDirectory`'; + 'same directory as the `rootDirectory`'; } diff --git a/lib/src/backend/impls/objectbox/backend/internal_worker.dart b/lib/src/backend/impls/objectbox/backend/internal_worker.dart index 952b5247..aff80041 100644 --- a/lib/src/backend/impls/objectbox/backend/internal_worker.dart +++ b/lib/src/backend/impls/objectbox/backend/internal_worker.dart @@ -863,7 +863,7 @@ Future _worker( final outputDir = path.dirname(outputPath); if (outputDir == input.rootDirectory.absolute.path) { - throw ExportInRootDirectoryForbidden(directory: outputDir); + throw ExportInRootDirectoryForbidden(); } Directory(outputDir).createSync(recursive: true); From 798f5ffab675cafa6ee5fee21f86c85095b9c66d Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 7 Mar 2024 22:15:12 +0000 Subject: [PATCH 138/168] Fixed GitHub Workflow Checked in generated code Former-commit-id: e2f2b42ec49f5ba58d5b56f05a83c896f88a8503 [formerly 82108635e900c75a827e45a8e039c1bf7c2833e9] Former-commit-id: 7af61e8592c715cb1d5ada115f719189c0867af6 --- .github/workflows/main.yml | 16 - .../objectbox/models/generated/.gitignore | 1 - .../objectbox/models/generated/.pubignore | 0 .../models/generated/objectbox.g.dart | 699 ++++++++++++++++++ 4 files changed, 699 insertions(+), 17 deletions(-) delete mode 100644 lib/src/backend/impls/objectbox/models/generated/.gitignore delete mode 100644 lib/src/backend/impls/objectbox/models/generated/.pubignore create mode 100644 lib/src/backend/impls/objectbox/models/generated/objectbox.g.dart diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 916c13af..4c4bef09 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -46,8 +46,6 @@ jobs: run: flutter pub get -C example - name: Get Test Tile Server Dependencies run: dart pub get -C tile_server - - name: Generate Code - run: dart run build_runner build - name: Check Formatting run: dart format --output=none --set-exit-if-changed . - name: Check Lints @@ -65,8 +63,6 @@ jobs: channel: "stable" - name: Get Dependencies run: flutter pub get - - name: Generate Code - run: dart run build_runner build - name: Run Tests run: flutter test -r expanded @@ -89,12 +85,6 @@ jobs: uses: subosito/flutter-action@master with: channel: "stable" - - name: Get Dependencies - run: flutter pub get - working-directory: . - - name: Generate Code - run: dart run build_runner build - working-directory: . - name: Build run: flutter build apk --obfuscate --split-debug-info=./symbols - name: Upload Artifact @@ -118,12 +108,6 @@ jobs: uses: subosito/flutter-action@master with: channel: "stable" - - name: Get Dependencies - run: flutter pub get - working-directory: . - - name: Generate Code - run: dart run build_runner build - working-directory: . - name: Build run: flutter build windows --obfuscate --split-debug-info=./symbols - name: Create Installer diff --git a/lib/src/backend/impls/objectbox/models/generated/.gitignore b/lib/src/backend/impls/objectbox/models/generated/.gitignore deleted file mode 100644 index 38bc1119..00000000 --- a/lib/src/backend/impls/objectbox/models/generated/.gitignore +++ /dev/null @@ -1 +0,0 @@ -objectbox.g.dart \ No newline at end of file diff --git a/lib/src/backend/impls/objectbox/models/generated/.pubignore b/lib/src/backend/impls/objectbox/models/generated/.pubignore deleted file mode 100644 index e69de29b..00000000 diff --git a/lib/src/backend/impls/objectbox/models/generated/objectbox.g.dart b/lib/src/backend/impls/objectbox/models/generated/objectbox.g.dart new file mode 100644 index 00000000..558df525 --- /dev/null +++ b/lib/src/backend/impls/objectbox/models/generated/objectbox.g.dart @@ -0,0 +1,699 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// This code was generated by ObjectBox. To update it run the generator again: +// With a Flutter package, run `flutter pub run build_runner build`. +// With a Dart package, run `dart run build_runner build`. +// See also https://docs.objectbox.io/getting-started#generate-objectbox-code + +// ignore_for_file: camel_case_types, depend_on_referenced_packages +// coverage:ignore-file + +import 'dart:typed_data'; + +import 'package:flat_buffers/flat_buffers.dart' as fb; +import 'package:objectbox/internal.dart' + as obx_int; // generated code can access "internal" functionality +import 'package:objectbox/objectbox.dart' as obx; +import 'package:objectbox_flutter_libs/objectbox_flutter_libs.dart'; + +import '../../../../../../src/backend/impls/objectbox/models/src/recovery.dart'; +import '../../../../../../src/backend/impls/objectbox/models/src/root.dart'; +import '../../../../../../src/backend/impls/objectbox/models/src/store.dart'; +import '../../../../../../src/backend/impls/objectbox/models/src/tile.dart'; + +export 'package:objectbox/objectbox.dart'; // so that callers only have to import this file + +final _entities = [ + obx_int.ModelEntity( + id: const obx_int.IdUid(1, 5472631385587455945), + name: 'ObjectBoxRecovery', + lastPropertyId: const obx_int.IdUid(21, 3590067577930145922), + flags: 0, + properties: [ + obx_int.ModelProperty( + id: const obx_int.IdUid(1, 3769282896877713230), + name: 'id', + type: 6, + flags: 1), + obx_int.ModelProperty( + id: const obx_int.IdUid(2, 2496811483091029921), + name: 'refId', + type: 6, + flags: 40, + indexId: const obx_int.IdUid(1, 1036386105099927432)), + obx_int.ModelProperty( + id: const obx_int.IdUid(3, 3612512640999075849), + name: 'storeName', + type: 9, + flags: 0), + obx_int.ModelProperty( + id: const obx_int.IdUid(4, 1095455913099058361), + name: 'creationTime', + type: 10, + flags: 0), + obx_int.ModelProperty( + id: const obx_int.IdUid(5, 1138350672456876624), + name: 'minZoom', + type: 6, + flags: 0), + obx_int.ModelProperty( + id: const obx_int.IdUid(6, 9040433791555820529), + name: 'maxZoom', + type: 6, + flags: 0), + obx_int.ModelProperty( + id: const obx_int.IdUid(7, 6819230045021667310), + name: 'startTile', + type: 6, + flags: 0), + obx_int.ModelProperty( + id: const obx_int.IdUid(8, 8185724925875119436), + name: 'endTile', + type: 6, + flags: 0), + obx_int.ModelProperty( + id: const obx_int.IdUid(9, 7217406424708558740), + name: 'typeId', + type: 6, + flags: 0), + obx_int.ModelProperty( + id: const obx_int.IdUid(10, 5971465387225017460), + name: 'rectNwLat', + type: 8, + flags: 0), + obx_int.ModelProperty( + id: const obx_int.IdUid(11, 6703340231106164623), + name: 'rectNwLng', + type: 8, + flags: 0), + obx_int.ModelProperty( + id: const obx_int.IdUid(12, 741105584939284321), + name: 'rectSeLat', + type: 8, + flags: 0), + obx_int.ModelProperty( + id: const obx_int.IdUid(13, 2939837278126242427), + name: 'rectSeLng', + type: 8, + flags: 0), + obx_int.ModelProperty( + id: const obx_int.IdUid(14, 2393337671661697697), + name: 'circleCenterLat', + type: 8, + flags: 0), + obx_int.ModelProperty( + id: const obx_int.IdUid(15, 8055510540122966413), + name: 'circleCenterLng', + type: 8, + flags: 0), + obx_int.ModelProperty( + id: const obx_int.IdUid(16, 9110709438555760246), + name: 'circleRadius', + type: 8, + flags: 0), + obx_int.ModelProperty( + id: const obx_int.IdUid(17, 8363656194353400366), + name: 'lineLats', + type: 29, + flags: 0), + obx_int.ModelProperty( + id: const obx_int.IdUid(18, 7008680868853575786), + name: 'lineLngs', + type: 29, + flags: 0), + obx_int.ModelProperty( + id: const obx_int.IdUid(19, 7670007285707179405), + name: 'lineRadius', + type: 8, + flags: 0), + obx_int.ModelProperty( + id: const obx_int.IdUid(20, 490933261424375687), + name: 'customPolygonLats', + type: 29, + flags: 0), + obx_int.ModelProperty( + id: const obx_int.IdUid(21, 3590067577930145922), + name: 'customPolygonLngs', + type: 29, + flags: 0) + ], + relations: [], + backlinks: []), + obx_int.ModelEntity( + id: const obx_int.IdUid(2, 632249766926720928), + name: 'ObjectBoxStore', + lastPropertyId: const obx_int.IdUid(7, 7028109958959828879), + flags: 0, + properties: [ + obx_int.ModelProperty( + id: const obx_int.IdUid(1, 1672655555406818874), + name: 'id', + type: 6, + flags: 1), + obx_int.ModelProperty( + id: const obx_int.IdUid(2, 1060752758288526798), + name: 'name', + type: 9, + flags: 2080, + indexId: const obx_int.IdUid(2, 5602852847672696920)), + obx_int.ModelProperty( + id: const obx_int.IdUid(3, 7375048950056890678), + name: 'length', + type: 6, + flags: 0), + obx_int.ModelProperty( + id: const obx_int.IdUid(4, 7781853256122686511), + name: 'size', + type: 6, + flags: 0), + obx_int.ModelProperty( + id: const obx_int.IdUid(5, 3183925806131180531), + name: 'hits', + type: 6, + flags: 0), + obx_int.ModelProperty( + id: const obx_int.IdUid(6, 6484030110235711573), + name: 'misses', + type: 6, + flags: 0), + obx_int.ModelProperty( + id: const obx_int.IdUid(7, 7028109958959828879), + name: 'metadataJson', + type: 9, + flags: 0) + ], + relations: [], + backlinks: [ + obx_int.ModelBacklink( + name: 'tiles', srcEntity: 'ObjectBoxTile', srcField: 'stores') + ]), + obx_int.ModelEntity( + id: const obx_int.IdUid(3, 8691708694767276679), + name: 'ObjectBoxTile', + lastPropertyId: const obx_int.IdUid(4, 1172878417733380836), + flags: 0, + properties: [ + obx_int.ModelProperty( + id: const obx_int.IdUid(1, 5356545328183635928), + name: 'id', + type: 6, + flags: 1), + obx_int.ModelProperty( + id: const obx_int.IdUid(2, 4115905667778721807), + name: 'url', + type: 9, + flags: 34848, + indexId: const obx_int.IdUid(3, 4361441212367179043)), + obx_int.ModelProperty( + id: const obx_int.IdUid(3, 7508139234299399524), + name: 'bytes', + type: 23, + flags: 0), + obx_int.ModelProperty( + id: const obx_int.IdUid(4, 1172878417733380836), + name: 'lastModified', + type: 10, + flags: 8, + indexId: const obx_int.IdUid(4, 4857742396480146668)) + ], + relations: [ + obx_int.ModelRelation( + id: const obx_int.IdUid(1, 7496298295217061586), + name: 'stores', + targetId: const obx_int.IdUid(2, 632249766926720928)) + ], + backlinks: []), + obx_int.ModelEntity( + id: const obx_int.IdUid(4, 8718814737097934474), + name: 'ObjectBoxRoot', + lastPropertyId: const obx_int.IdUid(3, 6574336219794969200), + flags: 0, + properties: [ + obx_int.ModelProperty( + id: const obx_int.IdUid(1, 3527394784453371799), + name: 'id', + type: 6, + flags: 1), + obx_int.ModelProperty( + id: const obx_int.IdUid(2, 2833017356902860570), + name: 'length', + type: 6, + flags: 0), + obx_int.ModelProperty( + id: const obx_int.IdUid(3, 6574336219794969200), + name: 'size', + type: 6, + flags: 0) + ], + relations: [], + backlinks: []) +]; + +/// Shortcut for [Store.new] that passes [getObjectBoxModel] and for Flutter +/// apps by default a [directory] using `defaultStoreDirectory()` from the +/// ObjectBox Flutter library. +/// +/// Note: for desktop apps it is recommended to specify a unique [directory]. +/// +/// See [Store.new] for an explanation of all parameters. +/// +/// For Flutter apps, also calls `loadObjectBoxLibraryAndroidCompat()` from +/// the ObjectBox Flutter library to fix loading the native ObjectBox library +/// on Android 6 and older. +Future openStore( + {String? directory, + int? maxDBSizeInKB, + int? maxDataSizeInKB, + int? fileMode, + int? maxReaders, + bool queriesCaseSensitiveDefault = true, + String? macosApplicationGroup}) async { + await loadObjectBoxLibraryAndroidCompat(); + return obx.Store(getObjectBoxModel(), + directory: directory ?? (await defaultStoreDirectory()).path, + maxDBSizeInKB: maxDBSizeInKB, + maxDataSizeInKB: maxDataSizeInKB, + fileMode: fileMode, + maxReaders: maxReaders, + queriesCaseSensitiveDefault: queriesCaseSensitiveDefault, + macosApplicationGroup: macosApplicationGroup); +} + +/// Returns the ObjectBox model definition for this project for use with +/// [Store.new]. +obx_int.ModelDefinition getObjectBoxModel() { + final model = obx_int.ModelInfo( + entities: _entities, + lastEntityId: const obx_int.IdUid(4, 8718814737097934474), + lastIndexId: const obx_int.IdUid(4, 4857742396480146668), + lastRelationId: const obx_int.IdUid(1, 7496298295217061586), + lastSequenceId: const obx_int.IdUid(0, 0), + retiredEntityUids: const [], + retiredIndexUids: const [], + retiredPropertyUids: const [], + retiredRelationUids: const [], + modelVersion: 5, + modelVersionParserMinimum: 5, + version: 1); + + final bindings = { + ObjectBoxRecovery: obx_int.EntityDefinition( + model: _entities[0], + toOneRelations: (ObjectBoxRecovery object) => [], + toManyRelations: (ObjectBoxRecovery object) => {}, + getId: (ObjectBoxRecovery object) => object.id, + setId: (ObjectBoxRecovery object, int id) { + object.id = id; + }, + objectToFB: (ObjectBoxRecovery object, fb.Builder fbb) { + final storeNameOffset = fbb.writeString(object.storeName); + final lineLatsOffset = object.lineLats == null + ? null + : fbb.writeListFloat64(object.lineLats!); + final lineLngsOffset = object.lineLngs == null + ? null + : fbb.writeListFloat64(object.lineLngs!); + final customPolygonLatsOffset = object.customPolygonLats == null + ? null + : fbb.writeListFloat64(object.customPolygonLats!); + final customPolygonLngsOffset = object.customPolygonLngs == null + ? null + : fbb.writeListFloat64(object.customPolygonLngs!); + fbb.startTable(22); + fbb.addInt64(0, object.id); + fbb.addInt64(1, object.refId); + fbb.addOffset(2, storeNameOffset); + fbb.addInt64(3, object.creationTime.millisecondsSinceEpoch); + fbb.addInt64(4, object.minZoom); + fbb.addInt64(5, object.maxZoom); + fbb.addInt64(6, object.startTile); + fbb.addInt64(7, object.endTile); + fbb.addInt64(8, object.typeId); + fbb.addFloat64(9, object.rectNwLat); + fbb.addFloat64(10, object.rectNwLng); + fbb.addFloat64(11, object.rectSeLat); + fbb.addFloat64(12, object.rectSeLng); + fbb.addFloat64(13, object.circleCenterLat); + fbb.addFloat64(14, object.circleCenterLng); + fbb.addFloat64(15, object.circleRadius); + fbb.addOffset(16, lineLatsOffset); + fbb.addOffset(17, lineLngsOffset); + fbb.addFloat64(18, object.lineRadius); + fbb.addOffset(19, customPolygonLatsOffset); + fbb.addOffset(20, customPolygonLngsOffset); + fbb.finish(fbb.endTable()); + return object.id; + }, + objectFromFB: (obx.Store store, ByteData fbData) { + final buffer = fb.BufferContext(fbData); + final rootOffset = buffer.derefObject(0); + final refIdParam = + const fb.Int64Reader().vTableGet(buffer, rootOffset, 6, 0); + final storeNameParam = const fb.StringReader(asciiOptimization: true) + .vTableGet(buffer, rootOffset, 8, ''); + final creationTimeParam = DateTime.fromMillisecondsSinceEpoch( + const fb.Int64Reader().vTableGet(buffer, rootOffset, 10, 0)); + final typeIdParam = + const fb.Int64Reader().vTableGet(buffer, rootOffset, 20, 0); + final minZoomParam = + const fb.Int64Reader().vTableGet(buffer, rootOffset, 12, 0); + final maxZoomParam = + const fb.Int64Reader().vTableGet(buffer, rootOffset, 14, 0); + final startTileParam = + const fb.Int64Reader().vTableGet(buffer, rootOffset, 16, 0); + final endTileParam = + const fb.Int64Reader().vTableGetNullable(buffer, rootOffset, 18); + final rectNwLatParam = const fb.Float64Reader() + .vTableGetNullable(buffer, rootOffset, 22); + final rectNwLngParam = const fb.Float64Reader() + .vTableGetNullable(buffer, rootOffset, 24); + final rectSeLatParam = const fb.Float64Reader() + .vTableGetNullable(buffer, rootOffset, 26); + final rectSeLngParam = const fb.Float64Reader() + .vTableGetNullable(buffer, rootOffset, 28); + final circleCenterLatParam = const fb.Float64Reader() + .vTableGetNullable(buffer, rootOffset, 30); + final circleCenterLngParam = const fb.Float64Reader() + .vTableGetNullable(buffer, rootOffset, 32); + final circleRadiusParam = const fb.Float64Reader() + .vTableGetNullable(buffer, rootOffset, 34); + final lineLatsParam = + const fb.ListReader(fb.Float64Reader(), lazy: false) + .vTableGetNullable(buffer, rootOffset, 36); + final lineLngsParam = + const fb.ListReader(fb.Float64Reader(), lazy: false) + .vTableGetNullable(buffer, rootOffset, 38); + final lineRadiusParam = const fb.Float64Reader() + .vTableGetNullable(buffer, rootOffset, 40); + final customPolygonLatsParam = + const fb.ListReader(fb.Float64Reader(), lazy: false) + .vTableGetNullable(buffer, rootOffset, 42); + final customPolygonLngsParam = + const fb.ListReader(fb.Float64Reader(), lazy: false) + .vTableGetNullable(buffer, rootOffset, 44); + final object = ObjectBoxRecovery( + refId: refIdParam, + storeName: storeNameParam, + creationTime: creationTimeParam, + typeId: typeIdParam, + minZoom: minZoomParam, + maxZoom: maxZoomParam, + startTile: startTileParam, + endTile: endTileParam, + rectNwLat: rectNwLatParam, + rectNwLng: rectNwLngParam, + rectSeLat: rectSeLatParam, + rectSeLng: rectSeLngParam, + circleCenterLat: circleCenterLatParam, + circleCenterLng: circleCenterLngParam, + circleRadius: circleRadiusParam, + lineLats: lineLatsParam, + lineLngs: lineLngsParam, + lineRadius: lineRadiusParam, + customPolygonLats: customPolygonLatsParam, + customPolygonLngs: customPolygonLngsParam) + ..id = const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0); + + return object; + }), + ObjectBoxStore: obx_int.EntityDefinition( + model: _entities[1], + toOneRelations: (ObjectBoxStore object) => [], + toManyRelations: (ObjectBoxStore object) => { + obx_int.RelInfo.toManyBacklink(1, object.id): + object.tiles + }, + getId: (ObjectBoxStore object) => object.id, + setId: (ObjectBoxStore object, int id) { + object.id = id; + }, + objectToFB: (ObjectBoxStore object, fb.Builder fbb) { + final nameOffset = fbb.writeString(object.name); + final metadataJsonOffset = fbb.writeString(object.metadataJson); + fbb.startTable(8); + fbb.addInt64(0, object.id); + fbb.addOffset(1, nameOffset); + fbb.addInt64(2, object.length); + fbb.addInt64(3, object.size); + fbb.addInt64(4, object.hits); + fbb.addInt64(5, object.misses); + fbb.addOffset(6, metadataJsonOffset); + fbb.finish(fbb.endTable()); + return object.id; + }, + objectFromFB: (obx.Store store, ByteData fbData) { + final buffer = fb.BufferContext(fbData); + final rootOffset = buffer.derefObject(0); + final nameParam = const fb.StringReader(asciiOptimization: true) + .vTableGet(buffer, rootOffset, 6, ''); + final lengthParam = + const fb.Int64Reader().vTableGet(buffer, rootOffset, 8, 0); + final sizeParam = + const fb.Int64Reader().vTableGet(buffer, rootOffset, 10, 0); + final hitsParam = + const fb.Int64Reader().vTableGet(buffer, rootOffset, 12, 0); + final missesParam = + const fb.Int64Reader().vTableGet(buffer, rootOffset, 14, 0); + final metadataJsonParam = + const fb.StringReader(asciiOptimization: true) + .vTableGet(buffer, rootOffset, 16, ''); + final object = ObjectBoxStore( + name: nameParam, + length: lengthParam, + size: sizeParam, + hits: hitsParam, + misses: missesParam, + metadataJson: metadataJsonParam) + ..id = const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0); + obx_int.InternalToManyAccess.setRelInfo( + object.tiles, + store, + obx_int.RelInfo.toManyBacklink(1, object.id)); + return object; + }), + ObjectBoxTile: obx_int.EntityDefinition( + model: _entities[2], + toOneRelations: (ObjectBoxTile object) => [], + toManyRelations: (ObjectBoxTile object) => { + obx_int.RelInfo.toMany(1, object.id): object.stores + }, + getId: (ObjectBoxTile object) => object.id, + setId: (ObjectBoxTile object, int id) { + object.id = id; + }, + objectToFB: (ObjectBoxTile object, fb.Builder fbb) { + final urlOffset = fbb.writeString(object.url); + final bytesOffset = fbb.writeListInt8(object.bytes); + fbb.startTable(5); + fbb.addInt64(0, object.id); + fbb.addOffset(1, urlOffset); + fbb.addOffset(2, bytesOffset); + fbb.addInt64(3, object.lastModified.millisecondsSinceEpoch); + fbb.finish(fbb.endTable()); + return object.id; + }, + objectFromFB: (obx.Store store, ByteData fbData) { + final buffer = fb.BufferContext(fbData); + final rootOffset = buffer.derefObject(0); + final urlParam = const fb.StringReader(asciiOptimization: true) + .vTableGet(buffer, rootOffset, 6, ''); + final bytesParam = const fb.Uint8ListReader(lazy: false) + .vTableGet(buffer, rootOffset, 8, Uint8List(0)) as Uint8List; + final lastModifiedParam = DateTime.fromMillisecondsSinceEpoch( + const fb.Int64Reader().vTableGet(buffer, rootOffset, 10, 0)); + final object = ObjectBoxTile( + url: urlParam, bytes: bytesParam, lastModified: lastModifiedParam) + ..id = const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0); + obx_int.InternalToManyAccess.setRelInfo(object.stores, + store, obx_int.RelInfo.toMany(1, object.id)); + return object; + }), + ObjectBoxRoot: obx_int.EntityDefinition( + model: _entities[3], + toOneRelations: (ObjectBoxRoot object) => [], + toManyRelations: (ObjectBoxRoot object) => {}, + getId: (ObjectBoxRoot object) => object.id, + setId: (ObjectBoxRoot object, int id) { + object.id = id; + }, + objectToFB: (ObjectBoxRoot object, fb.Builder fbb) { + fbb.startTable(4); + fbb.addInt64(0, object.id); + fbb.addInt64(1, object.length); + fbb.addInt64(2, object.size); + fbb.finish(fbb.endTable()); + return object.id; + }, + objectFromFB: (obx.Store store, ByteData fbData) { + final buffer = fb.BufferContext(fbData); + final rootOffset = buffer.derefObject(0); + final lengthParam = + const fb.Int64Reader().vTableGet(buffer, rootOffset, 6, 0); + final sizeParam = + const fb.Int64Reader().vTableGet(buffer, rootOffset, 8, 0); + final object = ObjectBoxRoot(length: lengthParam, size: sizeParam) + ..id = const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0); + + return object; + }) + }; + + return obx_int.ModelDefinition(model, bindings); +} + +/// [ObjectBoxRecovery] entity fields to define ObjectBox queries. +class ObjectBoxRecovery_ { + /// see [ObjectBoxRecovery.id] + static final id = + obx.QueryIntegerProperty(_entities[0].properties[0]); + + /// see [ObjectBoxRecovery.refId] + static final refId = + obx.QueryIntegerProperty(_entities[0].properties[1]); + + /// see [ObjectBoxRecovery.storeName] + static final storeName = + obx.QueryStringProperty(_entities[0].properties[2]); + + /// see [ObjectBoxRecovery.creationTime] + static final creationTime = + obx.QueryDateProperty(_entities[0].properties[3]); + + /// see [ObjectBoxRecovery.minZoom] + static final minZoom = + obx.QueryIntegerProperty(_entities[0].properties[4]); + + /// see [ObjectBoxRecovery.maxZoom] + static final maxZoom = + obx.QueryIntegerProperty(_entities[0].properties[5]); + + /// see [ObjectBoxRecovery.startTile] + static final startTile = + obx.QueryIntegerProperty(_entities[0].properties[6]); + + /// see [ObjectBoxRecovery.endTile] + static final endTile = + obx.QueryIntegerProperty(_entities[0].properties[7]); + + /// see [ObjectBoxRecovery.typeId] + static final typeId = + obx.QueryIntegerProperty(_entities[0].properties[8]); + + /// see [ObjectBoxRecovery.rectNwLat] + static final rectNwLat = + obx.QueryDoubleProperty(_entities[0].properties[9]); + + /// see [ObjectBoxRecovery.rectNwLng] + static final rectNwLng = + obx.QueryDoubleProperty(_entities[0].properties[10]); + + /// see [ObjectBoxRecovery.rectSeLat] + static final rectSeLat = + obx.QueryDoubleProperty(_entities[0].properties[11]); + + /// see [ObjectBoxRecovery.rectSeLng] + static final rectSeLng = + obx.QueryDoubleProperty(_entities[0].properties[12]); + + /// see [ObjectBoxRecovery.circleCenterLat] + static final circleCenterLat = + obx.QueryDoubleProperty(_entities[0].properties[13]); + + /// see [ObjectBoxRecovery.circleCenterLng] + static final circleCenterLng = + obx.QueryDoubleProperty(_entities[0].properties[14]); + + /// see [ObjectBoxRecovery.circleRadius] + static final circleRadius = + obx.QueryDoubleProperty(_entities[0].properties[15]); + + /// see [ObjectBoxRecovery.lineLats] + static final lineLats = obx.QueryDoubleVectorProperty( + _entities[0].properties[16]); + + /// see [ObjectBoxRecovery.lineLngs] + static final lineLngs = obx.QueryDoubleVectorProperty( + _entities[0].properties[17]); + + /// see [ObjectBoxRecovery.lineRadius] + static final lineRadius = + obx.QueryDoubleProperty(_entities[0].properties[18]); + + /// see [ObjectBoxRecovery.customPolygonLats] + static final customPolygonLats = + obx.QueryDoubleVectorProperty( + _entities[0].properties[19]); + + /// see [ObjectBoxRecovery.customPolygonLngs] + static final customPolygonLngs = + obx.QueryDoubleVectorProperty( + _entities[0].properties[20]); +} + +/// [ObjectBoxStore] entity fields to define ObjectBox queries. +class ObjectBoxStore_ { + /// see [ObjectBoxStore.id] + static final id = + obx.QueryIntegerProperty(_entities[1].properties[0]); + + /// see [ObjectBoxStore.name] + static final name = + obx.QueryStringProperty(_entities[1].properties[1]); + + /// see [ObjectBoxStore.length] + static final length = + obx.QueryIntegerProperty(_entities[1].properties[2]); + + /// see [ObjectBoxStore.size] + static final size = + obx.QueryIntegerProperty(_entities[1].properties[3]); + + /// see [ObjectBoxStore.hits] + static final hits = + obx.QueryIntegerProperty(_entities[1].properties[4]); + + /// see [ObjectBoxStore.misses] + static final misses = + obx.QueryIntegerProperty(_entities[1].properties[5]); + + /// see [ObjectBoxStore.metadataJson] + static final metadataJson = + obx.QueryStringProperty(_entities[1].properties[6]); +} + +/// [ObjectBoxTile] entity fields to define ObjectBox queries. +class ObjectBoxTile_ { + /// see [ObjectBoxTile.id] + static final id = + obx.QueryIntegerProperty(_entities[2].properties[0]); + + /// see [ObjectBoxTile.url] + static final url = + obx.QueryStringProperty(_entities[2].properties[1]); + + /// see [ObjectBoxTile.bytes] + static final bytes = + obx.QueryByteVectorProperty(_entities[2].properties[2]); + + /// see [ObjectBoxTile.lastModified] + static final lastModified = + obx.QueryDateProperty(_entities[2].properties[3]); + + /// see [ObjectBoxTile.stores] + static final stores = obx.QueryRelationToMany( + _entities[2].relations[0]); +} + +/// [ObjectBoxRoot] entity fields to define ObjectBox queries. +class ObjectBoxRoot_ { + /// see [ObjectBoxRoot.id] + static final id = + obx.QueryIntegerProperty(_entities[3].properties[0]); + + /// see [ObjectBoxRoot.length] + static final length = + obx.QueryIntegerProperty(_entities[3].properties[1]); + + /// see [ObjectBoxRoot.size] + static final size = + obx.QueryIntegerProperty(_entities[3].properties[2]); +} From be9164b9ac06e5965c911bfd7e00ec70faa06a16 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 7 Mar 2024 22:18:52 +0000 Subject: [PATCH 139/168] Fixed GitHub Workflow Former-commit-id: e088a7cab97d73f18a8e35ecfb5b91447b67337c [formerly 6769e9637aa5d968fd58e74ccbe38be42dff2074] Former-commit-id: 22f3cc1453999cc8b178ff050559e42f5273709d --- .github/workflows/main.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4c4bef09..9f51b020 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -39,7 +39,7 @@ jobs: - name: Setup Flutter Environment uses: subosito/flutter-action@master with: - channel: "stable" + channel: "beta" - name: Get Package Dependencies run: flutter pub get - name: Get Example Dependencies @@ -60,7 +60,7 @@ jobs: - name: Setup Flutter Environment uses: subosito/flutter-action@master with: - channel: "stable" + channel: "beta" - name: Get Dependencies run: flutter pub get - name: Run Tests @@ -84,7 +84,7 @@ jobs: - name: Setup Flutter Environment uses: subosito/flutter-action@master with: - channel: "stable" + channel: "beta" - name: Build run: flutter build apk --obfuscate --split-debug-info=./symbols - name: Upload Artifact @@ -107,7 +107,7 @@ jobs: - name: Setup Flutter Environment uses: subosito/flutter-action@master with: - channel: "stable" + channel: "beta" - name: Build run: flutter build windows --obfuscate --split-debug-info=./symbols - name: Create Installer From 39fc313cbdfa33e3cae797d6f74e236ad3781ca2 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sun, 17 Mar 2024 14:35:00 +0000 Subject: [PATCH 140/168] Added documentation to all remaining public members & improved existing documentation Refactored `RateLimitedStream` into extension method Renamed `TilesCounter` & `TilesGenerator` to `TileCounters` & `TileGenerators` Removed unnecessary permissions from Android example app Former-commit-id: 6320fcc9a8db8c1b8bfce59a50579575ac47281c [formerly b31b096c904f43734c3b745bf503294f48ec59ef] Former-commit-id: e048ffffc312684ea580e4f65db37c239796d4a1 --- analysis_options.yaml | 4 +- .../android/app/src/debug/AndroidManifest.xml | 6 - .../android/app/src/main/AndroidManifest.xml | 6 - .../app/src/profile/AndroidManifest.xml | 6 - lib/src/backend/errors/basic.dart | 2 + lib/src/backend/errors/import_export.dart | 15 ++ .../impls/objectbox/backend/errors.dart | 2 + .../objectbox/backend/internal_worker.dart | 1 - .../impls/objectbox/models/src/recovery.dart | 50 ++++++- .../impls/objectbox/models/src/root.dart | 6 + .../impls/objectbox/models/src/store.dart | 18 +++ .../impls/objectbox/models/src/tile.dart | 4 + lib/src/bulk_download/manager.dart | 23 ++- .../bulk_download/rate_limited_stream.dart | 135 ++++++------------ lib/src/bulk_download/tile_loops/count.dart | 37 ++++- .../bulk_download/tile_loops/generate.dart | 33 ++++- lib/src/bulk_download/tile_loops/shared.dart | 1 + lib/src/misc/int_extremes.dart | 3 + lib/src/misc/obscure_query_params.dart | 2 + lib/src/providers/browsing_errors.dart | 31 ++-- lib/src/providers/tile_provider_settings.dart | 9 +- lib/src/regions/downloadable_region.dart | 6 + lib/src/regions/recovered_region.dart | 8 ++ lib/src/store/directory.dart | 4 + lib/src/store/download.dart | 8 +- test/fmtc_test.dart | 26 ++-- 26 files changed, 294 insertions(+), 152 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index 9431a974..97dbfab7 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -6,6 +6,4 @@ analyzer: linter: rules: - avoid_slow_async_io: false - # TODO: Remove - public_member_api_docs: false \ No newline at end of file + avoid_slow_async_io: false \ No newline at end of file diff --git a/example/android/app/src/debug/AndroidManifest.xml b/example/android/app/src/debug/AndroidManifest.xml index 54ee1d59..2c45d295 100644 --- a/example/android/app/src/debug/AndroidManifest.xml +++ b/example/android/app/src/debug/AndroidManifest.xml @@ -1,12 +1,6 @@ - - - - - - diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index f91b539a..804d6826 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -1,12 +1,6 @@ - - - - - - diff --git a/example/android/app/src/profile/AndroidManifest.xml b/example/android/app/src/profile/AndroidManifest.xml index 54ee1d59..2c45d295 100644 --- a/example/android/app/src/profile/AndroidManifest.xml +++ b/example/android/app/src/profile/AndroidManifest.xml @@ -1,12 +1,6 @@ - - - - - - diff --git a/lib/src/backend/errors/basic.dart b/lib/src/backend/errors/basic.dart index 210f416f..bd911b13 100644 --- a/lib/src/backend/errors/basic.dart +++ b/lib/src/backend/errors/basic.dart @@ -26,6 +26,8 @@ final class RootAlreadyInitialised extends FMTCBackendError { /// Indicates that the specified store structure was not available for use in /// operations, likely because it didn't exist final class StoreNotExists extends FMTCBackendError { + /// Indicates that the specified store structure was not available for use in + /// operations, likely because it didn't exist StoreNotExists({required this.storeName}); /// The referenced store name diff --git a/lib/src/backend/errors/import_export.dart b/lib/src/backend/errors/import_export.dart index 81d2832d..fbbdca5b 100644 --- a/lib/src/backend/errors/import_export.dart +++ b/lib/src/backend/errors/import_export.dart @@ -10,6 +10,8 @@ base class ImportExportError extends FMTCBackendError {} /// Indicates that the specified path to import from or export to did exist, but /// was not a file final class ImportExportPathNotFile extends ImportExportError { + /// Indicates that the specified path to import from or export to did exist, but + /// was not a file ImportExportPathNotFile(); @override @@ -20,6 +22,8 @@ final class ImportExportPathNotFile extends ImportExportError { /// Indicates that the specified file to import did not exist/could not be found final class ImportPathNotExists extends ImportExportError { + /// Indicates that the specified path to import from or export to did exist, but + /// was not a file ImportPathNotExists({required this.path}); /// The specified path to the import file @@ -36,6 +40,11 @@ final class ImportPathNotExists extends ImportExportError { /// ("**FMTC") /// * did not contain all required header information within the file final class ImportFileNotFMTCStandard extends ImportExportError { + /// Indicates that the import file was not of the expected standard, because it + /// either: + /// * did not contain the appropriate footer signature: hex "FF FF 46 4D 54 43" + /// ("**FMTC") + /// * did not contain all required header information within the file ImportFileNotFMTCStandard(); @override @@ -51,6 +60,12 @@ final class ImportFileNotFMTCStandard extends ImportExportError { /// should an identifier (eg. the name) of the exporting backend proceeded by /// hex "FF FE". final class ImportFileNotBackendCompatible extends ImportExportError { + /// Indicates that the import file was exported from a different FMTC backend, + /// and is not compatible with the current backend + /// + /// The bytes prior to the header signature (hex "FF FF 46 4D 54 43" ("**FMTC")) + /// should an identifier (eg. the name) of the exporting backend proceeded by + /// hex "FF FE". ImportFileNotBackendCompatible(); @override diff --git a/lib/src/backend/impls/objectbox/backend/errors.dart b/lib/src/backend/impls/objectbox/backend/errors.dart index aceecbf7..ec1b6018 100644 --- a/lib/src/backend/impls/objectbox/backend/errors.dart +++ b/lib/src/backend/impls/objectbox/backend/errors.dart @@ -13,6 +13,8 @@ base class FMTCObjectBoxBackendError extends FMTCBackendError {} /// Indicates that an export failed because the specified output path directory /// was the same as the root directory final class ExportInRootDirectoryForbidden extends FMTCObjectBoxBackendError { + /// Indicates that an export failed because the specified output path directory + /// was the same as the root directory ExportInRootDirectoryForbidden(); @override diff --git a/lib/src/backend/impls/objectbox/backend/internal_worker.dart b/lib/src/backend/impls/objectbox/backend/internal_worker.dart index aff80041..dd908725 100644 --- a/lib/src/backend/impls/objectbox/backend/internal_worker.dart +++ b/lib/src/backend/impls/objectbox/backend/internal_worker.dart @@ -455,7 +455,6 @@ Future _worker( query.close(); case _WorkerCmdType.writeTile: - // TODO: Test all final storeName = cmd.args['storeName']! as String; final url = cmd.args['url']! as String; final bytes = cmd.args['bytes'] as Uint8List?; diff --git a/lib/src/backend/impls/objectbox/models/src/recovery.dart b/lib/src/backend/impls/objectbox/models/src/recovery.dart index fcf4f9ef..097a69e7 100644 --- a/lib/src/backend/impls/objectbox/models/src/recovery.dart +++ b/lib/src/backend/impls/objectbox/models/src/recovery.dart @@ -8,8 +8,12 @@ import 'package:objectbox/objectbox.dart'; import '../../../../../../flutter_map_tile_caching.dart'; +/// Represents a [RecoveredRegion] in ObjectBox @Entity() base class ObjectBoxRecovery { + /// Create a raw representation of a [RecoveredRegion] in ObjectBox + /// + /// Prefer using [ObjectBoxRecovery.fromRegion]. ObjectBoxRecovery({ required this.refId, required this.storeName, @@ -33,6 +37,8 @@ base class ObjectBoxRecovery { required this.customPolygonLngs, }); + /// Create a raw representation of a [RecoveredRegion] in ObjectBox from a + /// [DownloadableRegion] ObjectBoxRecovery.fromRegion({ required this.refId, required this.storeName, @@ -109,41 +115,83 @@ base class ObjectBoxRecovery { .toList(growable: false) : null; + /// ObjectBox ID + /// + /// Not to be confused with [refId]. @Id() @internal int id = 0; + /// Corresponds to [RecoveredRegion.id] @Index() @Unique() int refId; + /// Corresponds to [RecoveredRegion.storeName] String storeName; + + /// The timestamp of when this object was created/stored @Property(type: PropertyType.date) DateTime creationTime; + /// Corresponds to [RecoveredRegion.minZoom] & [DownloadableRegion.minZoom] int minZoom; + + /// Corresponds to [RecoveredRegion.maxZoom] & [DownloadableRegion.maxZoom] int maxZoom; + + /// Corresponds to [RecoveredRegion.start] & [DownloadableRegion.start] int startTile; + + /// Corresponds to [RecoveredRegion.end] & [DownloadableRegion.end] int? endTile; - int typeId; // 0 - rect, 1 - circle, 2 - line, 3 - custom polygon + /// Corresponds to the generic type of [DownloadableRegion] + /// + /// Values must be as follows: + /// * 0: rect + /// * 1: circle + /// * 2: line + /// * 3: custom polygon + int typeId; + /// Corresponds to [RecoveredRegion.bounds] ([RectangleRegion.bounds]) double? rectNwLat; + + /// Corresponds to [RecoveredRegion.bounds] ([RectangleRegion.bounds]) double? rectNwLng; + + /// Corresponds to [RecoveredRegion.bounds] ([RectangleRegion.bounds]) double? rectSeLat; + + /// Corresponds to [RecoveredRegion.bounds] ([RectangleRegion.bounds]) double? rectSeLng; + /// Corresponds to [RecoveredRegion.center] ([CircleRegion.center]) double? circleCenterLat; + + /// Corresponds to [RecoveredRegion.center] ([CircleRegion.center]) double? circleCenterLng; + + /// Corresponds to [RecoveredRegion.radius] ([CircleRegion.radius]) double? circleRadius; + /// Corresponds to [RecoveredRegion.line] ([LineRegion.line]) List? lineLats; + + /// Corresponds to [RecoveredRegion.line] ([LineRegion.line]) List? lineLngs; + + /// Corresponds to [RecoveredRegion.radius] ([LineRegion.radius]) double? lineRadius; + /// Corresponds to [RecoveredRegion.line] ([CustomPolygonRegion.outline]) List? customPolygonLats; + + /// Corresponds to [RecoveredRegion.line] ([CustomPolygonRegion.outline]) List? customPolygonLngs; + /// Convert this object into a [RecoveredRegion] RecoveredRegion toRegion() => RecoveredRegion( id: refId, storeName: storeName, diff --git a/lib/src/backend/impls/objectbox/models/src/root.dart b/lib/src/backend/impls/objectbox/models/src/root.dart index 2459152e..210c32a4 100644 --- a/lib/src/backend/impls/objectbox/models/src/root.dart +++ b/lib/src/backend/impls/objectbox/models/src/root.dart @@ -3,16 +3,22 @@ import 'package:objectbox/objectbox.dart'; +/// Cache for root-level statistics in ObjectBox @Entity() class ObjectBoxRoot { + /// Create a new cache for root-level statistics in ObjectBox ObjectBoxRoot({ required this.length, required this.size, }); + /// ObjectBox ID @Id() int id = 0; + /// Total number of tiles int length; + + /// Total size (in bytes) of all tiles int size; } diff --git a/lib/src/backend/impls/objectbox/models/src/store.dart b/lib/src/backend/impls/objectbox/models/src/store.dart index 6f26b1c6..4a32f71f 100644 --- a/lib/src/backend/impls/objectbox/models/src/store.dart +++ b/lib/src/backend/impls/objectbox/models/src/store.dart @@ -5,8 +5,12 @@ import 'package:objectbox/objectbox.dart'; import 'tile.dart'; +/// Cache for store-level statistics & storage for metadata, referenced by +/// unique name, in ObjectBox @Entity() class ObjectBoxStore { + /// Create a cache for store-level statistics & storage for metadata, + /// referenced by unique name, in ObjectBox ObjectBoxStore({ required this.name, required this.length, @@ -16,20 +20,34 @@ class ObjectBoxStore { required this.metadataJson, }); + /// ObjectBox ID @Id() int id = 0; + /// Human-readable name of the store @Index() @Unique() String name; + /// Relation to all tiles that belong to this store @Index() @Backlink('stores') final tiles = ToMany(); + /// Number of tiles int length; + + /// Size (in bytes) of all tiles int size; + + /// Number of cache hits (successful retrievals) from this store only int hits; + + /// Number of cache misses (unsuccessful retrievals) from this store only int misses; + + /// Storage for metadata in JSON format + /// + /// Only supports string-string key-value pairs. String metadataJson; } diff --git a/lib/src/backend/impls/objectbox/models/src/tile.dart b/lib/src/backend/impls/objectbox/models/src/tile.dart index 974dd7d1..824e46ff 100644 --- a/lib/src/backend/impls/objectbox/models/src/tile.dart +++ b/lib/src/backend/impls/objectbox/models/src/tile.dart @@ -8,14 +8,17 @@ import 'package:objectbox/objectbox.dart'; import '../../../../interfaces/models.dart'; import 'store.dart'; +/// ObjectBox-specific implementation of [BackendTile] @Entity() base class ObjectBoxTile extends BackendTile { + /// Create an ObjectBox-specific implementation of [BackendTile] ObjectBoxTile({ required this.url, required this.bytes, required this.lastModified, }); + /// ObjectBox ID @Id() int id = 0; @@ -32,6 +35,7 @@ base class ObjectBoxTile extends BackendTile { @Property(type: PropertyType.date) DateTime lastModified; + /// Relation to all stores that this tile belongs to @Index() final stores = ToMany(); } diff --git a/lib/src/bulk_download/manager.dart b/lib/src/bulk_download/manager.dart index 6aa19f76..fb2c5f91 100644 --- a/lib/src/bulk_download/manager.dart +++ b/lib/src/bulk_download/manager.dart @@ -32,10 +32,10 @@ Future _downloadManager( // Count number of tiles final maxTiles = input.region.when( - rectangle: TilesCounter.rectangleTiles, - circle: TilesCounter.circleTiles, - line: TilesCounter.lineTiles, - customPolygon: TilesCounter.customPolygonTiles, + rectangle: TileCounters.rectangleTiles, + circle: TileCounters.circleTiles, + line: TileCounters.lineTiles, + customPolygon: TileCounters.customPolygonTiles, ); // Setup sea tile removal system @@ -70,10 +70,10 @@ Future _downloadManager( final tilereceivePort = ReceivePort(); final tileIsolate = await Isolate.spawn( input.region.when( - rectangle: (_) => TilesGenerator.rectangleTiles, - circle: (_) => TilesGenerator.circleTiles, - line: (_) => TilesGenerator.lineTiles, - customPolygon: (_) => TilesGenerator.customPolygonTiles, + rectangle: (_) => TileGenerators.rectangleTiles, + circle: (_) => TileGenerators.circleTiles, + line: (_) => TileGenerators.lineTiles, + customPolygon: (_) => TileGenerators.customPolygonTiles, ), (sendPort: tilereceivePort.sendPort, region: input.region), onExit: tilereceivePort.sendPort, @@ -87,12 +87,11 @@ Future _downloadManager( final tileQueue = StreamQueue( input.rateLimit == null ? rawTileStream - : RateLimitedStream.fromSourceStream( - emitEvery: Duration( + : rawTileStream.rateLimit( + minimumSpacing: Duration( microseconds: ((1 / input.rateLimit!) * 1000000).ceil(), ), - sourceStream: rawTileStream, - ).stream, + ), ); final requestTilePort = await tileQueue.next as SendPort; diff --git a/lib/src/bulk_download/rate_limited_stream.dart b/lib/src/bulk_download/rate_limited_stream.dart index 5ffa2cd2..02665f19 100644 --- a/lib/src/bulk_download/rate_limited_stream.dart +++ b/lib/src/bulk_download/rate_limited_stream.dart @@ -3,94 +3,53 @@ import 'dart:async'; -/// Transforms a series of events to an output [stream] where a delay of at least -/// [emitEvery] is inserted between every event -/// -/// There are 3 ways to contruct this: -/// - [RateLimitedStream] : No initial stream, remains open until [close] called -/// - [RateLimitedStream.withInitialStream] : Uses initial stream, remains open -/// until [close] called -/// - [RateLimitedStream.fromSourceStream] : One-shot from source stream, closes -/// automatically after output completes -/// -/// Remember, input streams are likely to close before the output [stream]. -/// -/// Do not call [close] if input streams are still outputting or new events are -/// being [add]ed. -/// -/// Optionally pass in a `customStreamController`. If passed in, use only this -/// object's methods to manipulate the stream, not the passed in controller's -/// methods. -/// -/// Illustration of [stream], where one decimal is 500ms, and [emitEvery] is set -/// to 1s: -/// ``` -/// Input: .ABC....DE..F........GH -/// Output: .A..B..C..D..E..F....G..H -/// ``` -class RateLimitedStream { - RateLimitedStream({ - required this.emitEvery, - this.cancelOnError = false, - StreamController? customStreamController, - }) : _streamController = customStreamController ?? StreamController(); - - RateLimitedStream.withInitialStream({ - required this.emitEvery, - required Stream initialStream, - this.cancelOnError = false, - StreamController? customStreamController, - }) : _streamController = customStreamController ?? StreamController() { - _streamController.addStream(initialStream, cancelOnError: cancelOnError); - } - - RateLimitedStream.fromSourceStream({ - required this.emitEvery, - required Stream sourceStream, - this.cancelOnError = false, - StreamController? customStreamController, - }) : _streamController = customStreamController ?? StreamController() { - _streamController - .addStream(sourceStream, cancelOnError: cancelOnError) - .then((_) => close()); - } - - final Duration emitEvery; - final bool cancelOnError; - - final StreamController _streamController; - - void add(E event) => _streamController.sink.add(event); - Future addStream(Stream stream) => - _streamController.sink.addStream(stream); - void addError(Object error, [StackTrace? stackTrace]) => - _streamController.sink.addError(error, stackTrace); - Future close() => _streamController.sink.close(); - Future get done => _streamController.sink.done; - - Stream get stream { +/// Rate limiting extension, see [rateLimit] for more information +extension RateLimitedStream on Stream { + /// Transforms a series of events to an output stream where a delay of at least + /// [minimumSpacing] is inserted between every event + /// + /// The input stream may close before the output stream. + /// + /// Illustration of the output stream, where one decimal is 500ms, and + /// [minimumSpacing] is set to 1s: + /// ``` + /// Input: .ABC....DE..F........GH + /// Output: .A..B..C..D..E..F....G..H + /// ``` + Stream rateLimit({ + required Duration minimumSpacing, + bool cancelOnError = false, + }) { Completer emitEvt = Completer()..complete(); - Timer.periodic(emitEvery, (_) { - if (!emitEvt.isCompleted) emitEvt.complete(); - }); - - return _streamController.stream - .transform>( - StreamTransformer.fromHandlers( - handleData: (data, sink) => sink.add( - (() async { - await emitEvt.future; - emitEvt = Completer(); - return data; - })(), - ), - handleError: (error, stackTrace, sink) { - sink.addError(error, stackTrace); - if (cancelOnError) sink.close(); - }, - handleDone: (sink) => sink.close(), - ), - ) - .asyncMap((e) => e); + final timer = Timer.periodic( + minimumSpacing, + (_) { + /// Trigger an event emission every period + if (!emitEvt.isCompleted) emitEvt.complete(); + }, + ); + + return transform>( + StreamTransformer.fromHandlers( + handleData: (data, sink) => sink.add( + (() async { + await emitEvt.future; // Await for the next signal from [timer] + emitEvt = Completer(); // Get [timer] ready for the next signal + return data; + })(), + ), + handleError: (error, stackTrace, sink) { + sink.addError(error, stackTrace); + if (cancelOnError) { + timer.cancel(); + sink.close(); + } + }, + handleDone: (sink) { + timer.cancel(); + sink.close(); + }, + ), + ).asyncMap((e) => e); } } diff --git a/lib/src/bulk_download/tile_loops/count.dart b/lib/src/bulk_download/tile_loops/count.dart index e87487b1..a0ab80b8 100644 --- a/lib/src/bulk_download/tile_loops/count.dart +++ b/lib/src/bulk_download/tile_loops/count.dart @@ -3,7 +3,30 @@ part of 'shared.dart'; -class TilesCounter { +/// A set of methods for each type of [BaseRegion] that counts the number of +/// tiles within the specified [DownloadableRegion] +/// +/// Each method should handle a [DownloadableRegion] with a specific generic type +/// [BaseRegion]. If a method is passed a non-compatible region, it is expected +/// to throw a `CastError`. +/// +/// These methods should be run within seperate isolates, as they do heavy, +/// potentially lengthy computation. They do not perform multiple-communication, +/// and so only require simple Isolate protocols such as [Isolate.run]. +/// +/// Where possible, these methods do not generate every coordinate for improved +/// efficiency, as the number of tiles can be counted without looping through +/// them all (in most cases). See [TileGenerators] for methods that actually +/// generate the coordinates of each tile, but with added complexity. +/// +/// The number of tiles returned by each method must match the number of tiles +/// returned by the respective method in [TileGenerators]. This is enforced by +/// automated tests. +@internal +class TileCounters { + /// Returns the number of tiles within a [DownloadableRegion] with generic type + /// [RectangleRegion] + @internal static int rectangleTiles(DownloadableRegion region) { region as DownloadableRegion; @@ -30,6 +53,9 @@ class TilesCounter { return numberOfTiles; } + /// Returns the number of tiles within a [DownloadableRegion] with generic type + /// [CircleRegion] + @internal static int circleTiles(DownloadableRegion region) { region as DownloadableRegion; @@ -71,10 +97,14 @@ class TilesCounter { return numberOfTiles; } + /// Returns the number of tiles within a [DownloadableRegion] with generic type + /// [LineRegion] + @internal static int lineTiles(DownloadableRegion region) { region as DownloadableRegion; - // Overlap algorithm originally in Python, available at https://stackoverflow.com/a/56962827/11846040 + // Overlap algorithm originally in Python, available at + // https://stackoverflow.com/a/56962827/11846040 bool overlap(_Polygon a, _Polygon b) { for (int x = 0; x < 2; x++) { final _Polygon polygon = x == 0 ? a : b; @@ -209,6 +239,9 @@ class TilesCounter { return numberOfTiles; } + /// Returns the number of tiles within a [DownloadableRegion] with generic type + /// [CustomPolygonRegion] + @internal static int customPolygonTiles(DownloadableRegion region) { region as DownloadableRegion; diff --git a/lib/src/bulk_download/tile_loops/generate.dart b/lib/src/bulk_download/tile_loops/generate.dart index 82512e15..4982f6a1 100644 --- a/lib/src/bulk_download/tile_loops/generate.dart +++ b/lib/src/bulk_download/tile_loops/generate.dart @@ -3,7 +3,29 @@ part of 'shared.dart'; -class TilesGenerator { +/// A set of methods for each type of [BaseRegion] that generates the coordinates +/// of every tile within the specified [DownloadableRegion] +/// +/// Each method should handle a [DownloadableRegion] with a specific generic type +/// [BaseRegion]. If a method is passed a non-compatible region, it is expected +/// to throw a `CastError`. +/// +/// These methods must be run within seperate isolates, as they do heavy, +/// potentially lengthy computation. They do perform multiple-communication, +/// sending a new coordinate after they recieve a request message only. They will +/// kill themselves after there are no tiles left to generate. +/// +/// See [TileCounters] for methods that do not generate each coordinate, but +/// just count the number of tiles with a more efficient method. +/// +/// The number of tiles returned by each method must match the number of tiles +/// returned by the respective method in [TileCounters]. This is enforced by +/// automated tests. +@internal +class TileGenerators { + /// Generate the coordinates of each tile within a [DownloadableRegion] with + /// generic type [RectangleRegion] + @internal static Future rectangleTiles( ({SendPort sendPort, DownloadableRegion region}) input, ) async { @@ -37,6 +59,9 @@ class TilesGenerator { Isolate.exit(); } + /// Generate the coordinates of each tile within a [DownloadableRegion] with + /// generic type [CircleRegion] + @internal static Future circleTiles( ({SendPort sendPort, DownloadableRegion region}) input, ) async { @@ -96,6 +121,9 @@ class TilesGenerator { Isolate.exit(); } + /// Generate the coordinates of each tile within a [DownloadableRegion] with + /// generic type [LineRegion] + @internal static Future lineTiles( ({SendPort sendPort, DownloadableRegion region}) input, ) async { @@ -245,6 +273,9 @@ class TilesGenerator { Isolate.exit(); } + /// Generate the coordinates of each tile within a [DownloadableRegion] with + /// generic type [CustomPolygonRegion] + @internal static Future customPolygonTiles( ({SendPort sendPort, DownloadableRegion region}) input, ) async { diff --git a/lib/src/bulk_download/tile_loops/shared.dart b/lib/src/bulk_download/tile_loops/shared.dart index a4f3fb9c..e8f423ec 100644 --- a/lib/src/bulk_download/tile_loops/shared.dart +++ b/lib/src/bulk_download/tile_loops/shared.dart @@ -10,6 +10,7 @@ import 'package:dart_earcut/dart_earcut.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/flutter_map.dart' hide Polygon; import 'package:latlong2/latlong.dart'; +import 'package:meta/meta.dart'; import '../../../flutter_map_tile_caching.dart'; import '../../misc/int_extremes.dart'; diff --git a/lib/src/misc/int_extremes.dart b/lib/src/misc/int_extremes.dart index 1ad2b33b..c7471a16 100644 --- a/lib/src/misc/int_extremes.dart +++ b/lib/src/misc/int_extremes.dart @@ -3,7 +3,10 @@ import 'package:meta/meta.dart'; +/// Largest fully representable integer in Dart @internal const largestInt = 9223372036854775807; + +/// Smallest fully representable integer in Dart @internal const smallestInt = -9223372036854775808; diff --git a/lib/src/misc/obscure_query_params.dart b/lib/src/misc/obscure_query_params.dart index ba8eb5f5..59f35aad 100644 --- a/lib/src/misc/obscure_query_params.dart +++ b/lib/src/misc/obscure_query_params.dart @@ -3,6 +3,8 @@ import 'package:meta/meta.dart'; +/// Removes all matches of [obscuredQueryParams] from [url] after the query +/// delimiter '?' @internal String obscureQueryParams({ required String url, diff --git a/lib/src/providers/browsing_errors.dart b/lib/src/providers/browsing_errors.dart index e96fa196..cb0af4f0 100644 --- a/lib/src/providers/browsing_errors.dart +++ b/lib/src/providers/browsing_errors.dart @@ -81,7 +81,7 @@ class FMTCBrowsingError implements Exception { final Object? originalError; @override - String toString() => 'FMTCBrowsingError ($type): $message'; + String toString() => 'FMTCBrowsingError (${type.name}): $message'; } /// Defines the type of issue that a [FMTCBrowsingError] is reporting @@ -104,7 +104,9 @@ enum FMTCBrowsingErrorType { /// /// Check your Internet connection. noConnectionDuringFetch( - 'Failed to load the tile from the cache or the network because it was missing from the cache and a connection to the server could not be established.', + 'Failed to load the tile from the cache or the network because it was ' + 'missing from the cache and a connection to the server could not be ' + 'established.', 'Check your Internet connection.', ), @@ -117,8 +119,13 @@ enum FMTCBrowsingErrorType { /// correct, that any necessary authorization data is correctly included, and /// that the server serves the viewed region. unknownFetchException( - 'Failed to load the tile from the cache or network because it was missing from the cache and there was an unexpected error when requesting from the server.', - 'Try specifying a normal HTTP/1.1 `IOClient` when using `getTileProvider`. Check that the `TileLayer.urlTemplate` is correct, that any necessary authorization data is correctly included, and that the server serves the viewed region.', + 'Failed to load the tile from the cache or network because it was missing ' + 'from the cache and there was an unexpected error when requesting from ' + 'the server.', + 'Try specifying a normal HTTP/1.1 `IOClient` when using `getTileProvider`. ' + 'Check that the `TileLayer.urlTemplate` is correct, that any necessary ' + 'authorization data is correctly included, and that the server serves ' + 'the viewed region.', ), /// Failed to load the tile from the cache or the network because it was @@ -129,8 +136,12 @@ enum FMTCBrowsingErrorType { /// authorization data is correctly included, and that the server serves the /// viewed region. negativeFetchResponse( - 'Failed to load the tile from the cache or the network because it was missing from the cache and the server responded with a HTTP code other than 200 OK.', - 'Check that the `TileLayer.urlTemplate` is correct, that any necessary authorization data is correctly included, and that the server serves the viewed region.', + 'Failed to load the tile from the cache or the network because it was ' + 'missing from the cache and the server responded with a HTTP code other ' + 'than 200 OK.', + 'Check that the `TileLayer.urlTemplate` is correct, that any necessary ' + 'authorization data is correctly included, and that the server serves ' + 'the viewed region.', ), /// Failed to load the tile from the network because it responded with an HTTP @@ -141,8 +152,12 @@ enum FMTCBrowsingErrorType { /// that any necessary authorization data is correctly included, and that the /// server serves the viewed region. invalidImageData( - 'Failed to load the tile from the network because it responded with an HTTP code of 200 OK but an invalid image data.', - 'Your server may be misconfigured and returning an error message or blank response under 200 OK. Check that the `TileLayer.urlTemplate` is correct, that any necessary authorization data is correctly included, and that the server serves the viewed region.', + 'Failed to load the tile from the network because it responded with an ' + 'HTTP code of 200 OK but an invalid image data.', + 'Your server may be misconfigured and returning an error message or blank ' + 'response under 200 OK. Check that the `TileLayer.urlTemplate` is ' + 'correct, that any necessary authorization data is correctly included, ' + 'and that the server serves the viewed region.', ); /// Defines the type of issue that a [FMTCBrowsingError] is reporting diff --git a/lib/src/providers/tile_provider_settings.dart b/lib/src/providers/tile_provider_settings.dart index fad00270..92ad677e 100644 --- a/lib/src/providers/tile_provider_settings.dart +++ b/lib/src/providers/tile_provider_settings.dart @@ -40,6 +40,9 @@ enum CacheBehavior { } /// Settings for an [FMTCTileProvider] +/// +/// This class is a kind of singleton, which maintains a single instance, but +/// allows allows for a one-shot creation where necessary. class FMTCTileProviderSettings { /// Create new settings for an [FMTCTileProvider], and set the [instance] (if /// [setInstance] is `true`, as default) @@ -47,7 +50,7 @@ class FMTCTileProviderSettings { /// To access the existing settings, if any, get [instance]. factory FMTCTileProviderSettings({ CacheBehavior behavior = CacheBehavior.cacheFirst, - bool fallbackToAlternativeStore = true, + bool fallbackToAlternativeStore = false, Duration cachedValidDuration = const Duration(days: 16), int maxStoreLength = 0, List obscuredQueryParams = const [], @@ -94,6 +97,10 @@ class FMTCTileProviderSettings { /// When tiles are retrieved from other stores, it is counted as a miss for the /// specified store. /// + /// This may introduce notable performance reductions, especially if failures + /// occur often or the root is particularly large, as an extra lookup with + /// unbounded constraints is required for each tile. + /// /// See details on [CacheBehavior] for information. Fallback to an alternative /// store is always the last-resort option before throwing an error. final bool fallbackToAlternativeStore; diff --git a/lib/src/regions/downloadable_region.dart b/lib/src/regions/downloadable_region.dart index 69896e7b..7b316176 100644 --- a/lib/src/regions/downloadable_region.dart +++ b/lib/src/regions/downloadable_region.dart @@ -40,11 +40,17 @@ class DownloadableRegion { /// Optionally skip past a number of tiles 'at the start' of a region /// + /// The order of the tiles in a region is directly chosen by the underlying + /// tile generators, and so may not be stable between updates. + /// /// Set to 0 to skip none, which is the default. final int start; /// Optionally skip a number of tiles 'at the end' of a region /// + /// The order of the tiles in a region is directly chosen by the underlying + /// tile generators, and so may not be stable between updates. + /// /// Set to `null` to skip none, which is the default. final int? end; diff --git a/lib/src/regions/recovered_region.dart b/lib/src/regions/recovered_region.dart index d20d6764..c11c6fdf 100644 --- a/lib/src/regions/recovered_region.dart +++ b/lib/src/regions/recovered_region.dart @@ -12,6 +12,14 @@ part of '../../flutter_map_tile_caching.dart'; /// represented type of the recovered region. Use [toDownloadable] to restore a /// valid [DownloadableRegion]. class RecoveredRegion { + /// A mixture between [BaseRegion] and [DownloadableRegion] containing all the + /// salvaged data from a recovered download + /// + /// See [RootRecovery] for information about the recovery system. + /// + /// The availability of [bounds], [line], [center] & [radius] depend on the + /// represented type of the recovered region. Use [toDownloadable] to restore + /// a valid [DownloadableRegion]. @internal RecoveredRegion({ required this.id, diff --git a/lib/src/store/directory.dart b/lib/src/store/directory.dart index 95c3fd99..95a8af23 100644 --- a/lib/src/store/directory.dart +++ b/lib/src/store/directory.dart @@ -59,6 +59,10 @@ class FMTCStore { /// Get the [TileProvider] suitable to connect the [TileLayer] to FMTC's /// internals + /// + /// [settings] defaults to the current ambient + /// [FMTCTileProviderSettings.instance], which defaults to the initial + /// configuration if no other instance has been set. FMTCTileProvider getTileProvider({ FMTCTileProviderSettings? settings, Map? headers, diff --git a/lib/src/store/download.dart b/lib/src/store/download.dart index 71829dcd..0889fe6e 100644 --- a/lib/src/store/download.dart +++ b/lib/src/store/download.dart @@ -245,10 +245,10 @@ class DownloadManagement { /// Returns the number of tiles. Future check(DownloadableRegion region) => compute( region.when( - rectangle: (_) => TilesCounter.rectangleTiles, - circle: (_) => TilesCounter.circleTiles, - line: (_) => TilesCounter.lineTiles, - customPolygon: (_) => TilesCounter.customPolygonTiles, + rectangle: (_) => TileCounters.rectangleTiles, + circle: (_) => TileCounters.circleTiles, + line: (_) => TileCounters.lineTiles, + customPolygon: (_) => TileCounters.customPolygonTiles, ), region, ); diff --git a/test/fmtc_test.dart b/test/fmtc_test.dart index 72b5702f..652bfde6 100644 --- a/test/fmtc_test.dart +++ b/test/fmtc_test.dart @@ -17,10 +17,10 @@ void main() { final tilereceivePort = ReceivePort(); final tileIsolate = await Isolate.spawn( region.when( - rectangle: (_) => TilesGenerator.rectangleTiles, - circle: (_) => TilesGenerator.circleTiles, - line: (_) => TilesGenerator.lineTiles, - customPolygon: (_) => TilesGenerator.customPolygonTiles, + rectangle: (_) => TileGenerators.rectangleTiles, + circle: (_) => TileGenerators.circleTiles, + line: (_) => TileGenerators.lineTiles, + customPolygon: (_) => TileGenerators.customPolygonTiles, ), (sendPort: tilereceivePort.sendPort, region: region), onExit: tilereceivePort.sendPort, @@ -52,7 +52,7 @@ void main() { test( 'Count By Counter', - () => expect(TilesCounter.rectangleTiles(rectRegion), 179196), + () => expect(TileCounters.rectangleTiles(rectRegion), 179196), ); test( @@ -67,7 +67,7 @@ void main() { 2000, (index) { final clock = Stopwatch()..start(); - TilesCounter.rectangleTiles(rectRegion); + TileCounters.rectangleTiles(rectRegion); clock.stop(); return clock.elapsedMilliseconds; }, @@ -97,7 +97,7 @@ void main() { test( 'Count By Counter', - () => expect(TilesCounter.circleTiles(circleRegion), 61564), + () => expect(TileCounters.circleTiles(circleRegion), 61564), ); test( @@ -112,7 +112,7 @@ void main() { 500, (index) { final clock = Stopwatch()..start(); - TilesCounter.circleTiles(circleRegion); + TileCounters.circleTiles(circleRegion); clock.stop(); return clock.elapsedMilliseconds; }, @@ -144,7 +144,7 @@ void main() { test( 'Count By Counter', - () => expect(TilesCounter.lineTiles(lineRegion), 5040), + () => expect(TileCounters.lineTiles(lineRegion), 5040), ); test( @@ -159,7 +159,7 @@ void main() { 300, (index) { final clock = Stopwatch()..start(); - TilesCounter.lineTiles(lineRegion); + TileCounters.lineTiles(lineRegion); clock.stop(); return clock.elapsedMilliseconds; }, @@ -203,7 +203,7 @@ void main() { test( 'Count By Counter', () => expect( - TilesCounter.customPolygonTiles(customPolygonRegion1), + TileCounters.customPolygonTiles(customPolygonRegion1), 15962, ), ); @@ -216,7 +216,7 @@ void main() { test( 'Count By Counter (Compare to Rectangle Region)', () => expect( - TilesCounter.customPolygonTiles(customPolygonRegion2), + TileCounters.customPolygonTiles(customPolygonRegion2), 712096, ), ); @@ -234,7 +234,7 @@ void main() { 500, (index) { final clock = Stopwatch()..start(); - TilesCounter.customPolygonTiles(customPolygonRegion1); + TileCounters.customPolygonTiles(customPolygonRegion1); clock.stop(); return clock.elapsedMilliseconds; }, From ec513e74bd255e65a3fa7a5109bb68ede1f349b7 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sun, 17 Mar 2024 19:12:04 +0000 Subject: [PATCH 141/168] Added initialisation error handler to example app Minor improvements to example app Former-commit-id: 1ee14ebb263911d4d8dc73eb8ea0a6436cd949d4 [formerly 767581f4591241f2a2a34ce9efa1ca825a3461a6] Former-commit-id: 90b6d2eeac523da243bdf052680303ebfc908730 --- example/lib/main.dart | 104 ++++++++++------- .../export_import/components/import.dart | 16 ++- .../initialisation_error.dart | 105 ++++++++++++++++++ .../components/download_layout.dart | 5 +- .../main/pages/downloading/downloading.dart | 1 + example/pubspec.yaml | 1 + .../impls/objectbox/backend/internal.dart | 8 ++ 7 files changed, 187 insertions(+), 53 deletions(-) create mode 100644 example/lib/screens/initialisation_error/initialisation_error.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index 14f8cb18..dd714e5e 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -5,6 +5,7 @@ import 'package:google_fonts/google_fonts.dart'; import 'package:provider/provider.dart'; import 'screens/configure_download/state/configure_download_provider.dart'; +import 'screens/initialisation_error/initialisation_error.dart'; import 'screens/main/main.dart'; import 'screens/main/pages/downloading/state/downloading_provider.dart'; import 'screens/main/pages/map/state/map_provider.dart'; @@ -20,55 +21,74 @@ void main() async { ), ); - // TODO: Implement error handling - await FMTCObjectBoxBackend().initialise(); + Object? initErr; + try { + await FMTCObjectBoxBackend().initialise(); + } catch (err) { + initErr = err; + } - runApp(const _AppContainer()); + runApp(_AppContainer(initialisationError: initErr)); } class _AppContainer extends StatelessWidget { - const _AppContainer(); + const _AppContainer({ + required this.initialisationError, + }); + + final Object? initialisationError; @override - Widget build(BuildContext context) => MultiProvider( - providers: [ - ChangeNotifierProvider(create: (_) => GeneralProvider()), - ChangeNotifierProvider( - create: (_) => MapProvider(), - lazy: true, - ), - ChangeNotifierProvider( - create: (_) => RegionSelectionProvider(), - lazy: true, - ), - ChangeNotifierProvider( - create: (_) => ConfigureDownloadProvider(), - lazy: true, + Widget build(BuildContext context) { + final themeData = ThemeData( + brightness: Brightness.dark, + useMaterial3: true, + textTheme: GoogleFonts.ubuntuTextTheme(ThemeData.dark().textTheme), + colorSchemeSeed: Colors.red, + switchTheme: SwitchThemeData( + thumbIcon: MaterialStateProperty.resolveWith( + (states) => Icon( + states.contains(MaterialState.selected) ? Icons.check : Icons.close, ), - ChangeNotifierProvider( - create: (_) => DownloadingProvider(), - lazy: true, - ), - ], - child: MaterialApp( - title: 'FMTC Demo', - theme: ThemeData( - brightness: Brightness.dark, - useMaterial3: true, - textTheme: GoogleFonts.ubuntuTextTheme(const TextTheme()), - colorSchemeSeed: Colors.red, - switchTheme: SwitchThemeData( - thumbIcon: MaterialStateProperty.resolveWith( - (states) => Icon( - states.contains(MaterialState.selected) - ? Icons.check - : Icons.close, - ), - ), - ), - ), - debugShowCheckedModeBanner: false, - home: const MainScreen(), ), + ), + ); + + if (initialisationError case final err?) { + return MaterialApp( + title: 'FMTC Demo (Initialisation Error)', + theme: themeData, + home: InitialisationError(err: err), ); + } + + return MultiProvider( + providers: [ + ChangeNotifierProvider( + create: (_) => GeneralProvider(), + ), + ChangeNotifierProvider( + create: (_) => MapProvider(), + lazy: true, + ), + ChangeNotifierProvider( + create: (_) => RegionSelectionProvider(), + lazy: true, + ), + ChangeNotifierProvider( + create: (_) => ConfigureDownloadProvider(), + lazy: true, + ), + ChangeNotifierProvider( + create: (_) => DownloadingProvider(), + lazy: true, + ), + ], + child: MaterialApp( + title: 'FMTC Demo', + theme: themeData, + home: const MainScreen(), + ), + ); + } } diff --git a/example/lib/screens/export_import/components/import.dart b/example/lib/screens/export_import/components/import.dart index db09d31e..e514e37e 100644 --- a/example/lib/screens/export_import/components/import.dart +++ b/example/lib/screens/export_import/components/import.dart @@ -113,15 +113,13 @@ class _ImportState extends State { title: Text(storeName), subtitle: FutureBuilder( future: FMTCStore(storeName).manage.ready, - builder: (context, snapshot) { - if (!snapshot.hasData) { - return const Text('Checking for conflicts...'); - } - if (snapshot.data!) { - return const Text('Conflicts with existing store'); - } - return const SizedBox.shrink(); - }, + builder: (context, snapshot) => Text( + switch (snapshot.data) { + null => 'Checking for conflicts...', + true => 'Conflicts with existing store', + false => 'No conflicts', + }, + ), ), dense: true, trailing: Row( diff --git a/example/lib/screens/initialisation_error/initialisation_error.dart b/example/lib/screens/initialisation_error/initialisation_error.dart new file mode 100644 index 00000000..7cae2a7b --- /dev/null +++ b/example/lib/screens/initialisation_error/initialisation_error.dart @@ -0,0 +1,105 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:path/path.dart' as path; +import 'package:path_provider/path_provider.dart'; + +import '../../main.dart'; + +class InitialisationError extends StatelessWidget { + const InitialisationError({super.key, required this.err}); + + final Object? err; + + @override + Widget build(BuildContext context) => Scaffold( + body: SingleChildScrollView( + padding: const EdgeInsets.all(32), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.error, size: 64), + const SizedBox(height: 12), + Text( + 'Whoops, look like FMTC ran into an error initialising', + style: Theme.of(context) + .textTheme + .displaySmall! + .copyWith(color: Colors.white), + textAlign: TextAlign.center, + ), + const SizedBox(height: 32), + SelectableText( + 'Type: ${err.runtimeType}', + style: Theme.of(context).textTheme.headlineSmall, + textAlign: TextAlign.center, + ), + SelectableText( + 'Error: $err', + style: Theme.of(context).textTheme.bodyLarge, + textAlign: TextAlign.center, + ), + const SizedBox(height: 32), + Text( + 'We recommend trying to delete the existing root, as it may ' + 'have become corrupt.\nPlease be aware that this will delete ' + 'any cached data, and will cause the app to restart.', + style: Theme.of(context) + .textTheme + .bodyLarge! + .copyWith(color: Colors.white), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + OutlinedButton( + onPressed: () async { + void showFailure() { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + "Unfortuately, that didn't work. Try clearing " + "the app's storage and cache manually.", + ), + ), + ); + } + } + + final dir = Directory( + path.join( + (await getApplicationDocumentsDirectory()) + .absolute + .path, + 'fmtc', + ), + ); + + if (!await dir.exists()) { + showFailure(); + return; + } + + try { + await dir.delete(recursive: true); + } on FileSystemException { + showFailure(); + rethrow; + } + + runApp(const SizedBox.shrink()); + + main(); + }, + child: const Text( + 'Reset FMTC & attempt re-initialisation', + textAlign: TextAlign.center, + ), + ), + ], + ), + ), + ), + ); +} diff --git a/example/lib/screens/main/pages/downloading/components/download_layout.dart b/example/lib/screens/main/pages/downloading/components/download_layout.dart index 356e5f91..48464765 100644 --- a/example/lib/screens/main/pages/downloading/components/download_layout.dart +++ b/example/lib/screens/main/pages/downloading/components/download_layout.dart @@ -54,6 +54,7 @@ class DownloadLayout extends StatelessWidget { const SizedBox(width: 16), Expanded( child: Table( + defaultVerticalAlignment: TableCellVerticalAlignment.middle, children: [ TableRow( children: [ @@ -73,12 +74,12 @@ class DownloadLayout extends StatelessWidget { children: [ StatDisplay( statistic: - '${download.skippedTiles} (${download.skippedTiles == 0 ? 0 : (100 - ((download.cachedTiles - download.skippedTiles) / download.cachedTiles) * 100).toStringAsFixed(1)}%)', + '${download.skippedTiles} (${download.skippedTiles == 0 ? 0 : (100 - ((download.cachedTiles - download.skippedTiles) / download.cachedTiles) * 100).clamp(double.minPositive, 100).toStringAsFixed(1)}%)', description: 'skipped tiles (% saving)', ), StatDisplay( statistic: - '${(download.skippedSize * 1024).asReadableSize} (${download.skippedTiles == 0 ? 0 : (100 - ((download.cachedSize - download.skippedSize) / download.cachedSize) * 100).toStringAsFixed(1)}%)', + '${(download.skippedSize * 1024).asReadableSize} (${download.skippedTiles == 0 ? 0 : (100 - ((download.cachedSize - download.skippedSize) / download.cachedSize) * 100).clamp(double.minPositive, 100).toStringAsFixed(1)}%)', description: 'skipped size (% saving)', ), ], diff --git a/example/lib/screens/main/pages/downloading/downloading.dart b/example/lib/screens/main/pages/downloading/downloading.dart index f113d800..9a15b526 100644 --- a/example/lib/screens/main/pages/downloading/downloading.dart +++ b/example/lib/screens/main/pages/downloading/downloading.dart @@ -108,6 +108,7 @@ class _DownloadingPageState extends State ); } + // TODO: Make responsive return DownloadLayout( storeDirectory: context.select( diff --git a/example/pubspec.yaml b/example/pubspec.yaml index da161563..09ff84c5 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -29,6 +29,7 @@ dependencies: latlong2: ^0.9.0 osm_nominatim: ^3.0.0 path: ^1.9.0 + path_provider: ^2.1.2 provider: ^6.1.2 stream_transform: ^2.1.0 validators: ^3.0.0 diff --git a/lib/src/backend/impls/objectbox/backend/internal.dart b/lib/src/backend/impls/objectbox/backend/internal.dart index ec03e17a..0c35c8b1 100644 --- a/lib/src/backend/impls/objectbox/backend/internal.dart +++ b/lib/src/backend/impls/objectbox/backend/internal.dart @@ -22,6 +22,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { void get expectInitialised => _sendPort ?? (throw RootUnavailable()); // Worker communication protocol storage + SendPort? _sendPort; final _workerResOneShot = ?>>{}; final _workerResStreamed = ?>>{}; @@ -30,9 +31,12 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { late StreamSubscription _workerHandler; // `removeOldestTilesAboveLimit` tracking & debouncing + Timer? _rotalDebouncer; String? _rotalStore; + // Define communicators + Future?> _sendCmdOneShot({ required _WorkerCmdType type, Map args = const {}, @@ -99,6 +103,8 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { } } + // Lifecycle implementations + Future initialise({ required String? rootDirectory, required int maxDatabaseSize, @@ -249,6 +255,8 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { FMTCBackendAccessThreadSafe.internal = null; } + // Implementation & worker connectors + @override Future realSize() async => (await _sendCmdOneShot(type: _WorkerCmdType.realSize))!['size']; From 52a0aff1192a18470a46ac31c30eec98b1248148 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Tue, 19 Mar 2024 18:48:16 +0000 Subject: [PATCH 142/168] Improved documentation Added 'custom_backend_api' library, which exports semi-public internals used to create custom backend implementations Former-commit-id: 83fe2c69cae3e4ac61ce051c22599500ef3f134f [formerly d3e6c3c238575019455c184cb163944d70ef3df5] Former-commit-id: 049dfdbbf402e889ee3545053fbd04eb3189d6ac --- CHANGELOG.md | 10 +++--- lib/custom_backend_api.dart | 14 ++++++++ lib/flutter_map_tile_caching.dart | 9 +++-- lib/src/backend/backend_access.dart | 35 ++++++++++++++----- lib/src/backend/export_internal.dart | 1 - .../backend/interfaces/backend/backend.dart | 11 ++++++ .../backend/interfaces/backend/internal.dart | 1 + .../backend/internal_thread_safe.dart | 6 ++-- lib/src/root/{directory.dart => root.dart} | 0 lib/src/store/{directory.dart => store.dart} | 0 10 files changed, 66 insertions(+), 21 deletions(-) create mode 100644 lib/custom_backend_api.dart rename lib/src/root/{directory.dart => root.dart} (100%) rename lib/src/store/{directory.dart => store.dart} (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f7a895c..36178c8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,13 +14,13 @@ Many thanks to my sponsors, no matter how much or how little they donated. Spons # Changelog -## [9.0.0] - "Hundreds of hours" - 2024/XX/XX +## [9.0.0] - "Just Another Rewrite" - 2024/XX/XX This update has essentially rewritten FMTC from the ground up, over hundreds of hours. It focuses on: -* improved future maintainability by modularity -* improved stability & performance across the board -* support of 'tiles across stores': reduced duplication +* improving future maintainability by improving modularity and seperation of concerns +* improving stability & performance across the board +* supporting a many-to-many relationship between tiles and stores to reduce duplication I would hugely appricate any donations - please see the documentation site, GitHub repo, or pub.dev package. @@ -58,9 +58,11 @@ And without further ado, let's get the biggest changes out of the way first: * Fixed usage of `obscuredQueryParams` * Removed support for bulk download buffering by size capacity * Removed support for custom `HttpClient`s + * Deprecated plugins * Transfered support for import/export operations to core (`RootExternal`) * Deprecated support for background bulk downloading + * Migrated to Flutter 3.19 and Dart 3.3 * Migrated to flutter_map v6 diff --git a/lib/custom_backend_api.dart b/lib/custom_backend_api.dart new file mode 100644 index 00000000..8844af85 --- /dev/null +++ b/lib/custom_backend_api.dart @@ -0,0 +1,14 @@ +// Copyright © Luka S (JaffaKetchup) under GPL-v3 +// A full license can be found at .\LICENSE + +/// Specialised sub-library of FMTC which provides access to some semi-public +/// internals necessary to create custom backends or work more directly with +/// them +/// +/// Use this import/library with caution! Assistance with non-typical usecases +/// may be limited. Always use the standard import unless necessary. +/// +/// Importing the standard library will also likely be necessary. +library flutter_map_tile_caching.custom_backend_api; + +export 'src/backend/export_internal.dart'; diff --git a/lib/flutter_map_tile_caching.dart b/lib/flutter_map_tile_caching.dart index fbee4b80..0aec5424 100644 --- a/lib/flutter_map_tile_caching.dart +++ b/lib/flutter_map_tile_caching.dart @@ -1,9 +1,7 @@ // Copyright © Luka S (JaffaKetchup) under GPL-v3 // A full license can be found at .\LICENSE -/// A plugin for flutter_map providing advanced caching functionality, with -/// ability to download map regions for offline use. Also includes useful -/// prebuilt widgets. +/// A plugin for 'flutter_map' providing advanced offline functionality /// /// * [GitHub Repository](https://github.com/JaffaKetchup/flutter_map_tile_caching) /// * [pub.dev Package](https://pub.dev/packages/flutter_map_tile_caching) @@ -28,6 +26,7 @@ import 'package:http/io_client.dart'; import 'package:latlong2/latlong.dart'; import 'package:meta/meta.dart'; +import 'src/backend/export_external.dart'; import 'src/backend/export_internal.dart'; import 'src/bulk_download/instance.dart'; import 'src/bulk_download/rate_limited_stream.dart'; @@ -54,11 +53,11 @@ part 'src/regions/downloadable_region.dart'; part 'src/regions/line.dart'; part 'src/regions/recovered_region.dart'; part 'src/regions/rectangle.dart'; -part 'src/root/directory.dart'; +part 'src/root/root.dart'; part 'src/root/external.dart'; part 'src/root/recovery.dart'; part 'src/root/statistics.dart'; -part 'src/store/directory.dart'; +part 'src/store/store.dart'; part 'src/store/download.dart'; part 'src/store/manage.dart'; part 'src/store/metadata.dart'; diff --git a/lib/src/backend/backend_access.dart b/lib/src/backend/backend_access.dart index e3ab2696..371d0757 100644 --- a/lib/src/backend/backend_access.dart +++ b/lib/src/backend/backend_access.dart @@ -3,23 +3,36 @@ import 'package:meta/meta.dart' as meta; -import 'export_internal.dart'; +import 'export_external.dart'; -/// Provides access to the backend in use throughout FMTC internals +/// Provides access to the thread-seperate backend internals +/// ([FMTCBackendInternal]) globally with some level of access control /// -/// Designed to allow a single backend to set and control the context at once. +/// {@template fmtc.backend.access} /// -/// Avoid using externally. Never set `context` externally. -@meta.internal +/// Only a single backend may set the [internal] backend at any one time, +/// essentially providing a locking mechanism preventing multiple backends from +/// being used at the same time (with a shared access). +/// +/// A [FMTCBackendInternal] implementation can access the [internal] setters, +/// and should set them both sequentially at the end of the +/// [FMTCBackend.initialise] & [FMTCBackend.uninitialise] implementations. +/// +/// The [internal] getter(s) should never be used outside of FMTC internals, as +/// it provides access to potentially uncontrolled, and unorganised, methods. +/// {@endtemplate} abstract mixin class FMTCBackendAccess { static FMTCBackendInternal? _internal; + /// Provides access to the thread-seperate backend internals + /// ([FMTCBackendInternal]) globally with some level of access control + /// + /// {@macro fmtc.backend.access} @meta.internal @meta.experimental static FMTCBackendInternal get internal => _internal ?? (throw RootUnavailable()); - @meta.internal @meta.protected static set internal(FMTCBackendInternal? newInternal) { if (newInternal != null && _internal != null) { @@ -29,16 +42,22 @@ abstract mixin class FMTCBackendAccess { } } -@meta.internal +/// Provides access to the thread-seperate backend internals +/// ([FMTCBackendInternalThreadSafe]) globally with some level of access control +/// +/// {@macro fmtc.backend.access} abstract mixin class FMTCBackendAccessThreadSafe { static FMTCBackendInternalThreadSafe? _internal; + /// Provides access to the thread-seperate backend internals + /// ([FMTCBackendInternalThreadSafe]) globally with some level of access control + /// + /// {@macro fmtc.backend.access} @meta.internal @meta.experimental static FMTCBackendInternalThreadSafe get internal => _internal ?? (throw RootUnavailable()); - @meta.internal @meta.protected static set internal(FMTCBackendInternalThreadSafe? newInternal) { if (newInternal != null && _internal != null) { diff --git a/lib/src/backend/export_internal.dart b/lib/src/backend/export_internal.dart index afa59d7a..49c138c5 100644 --- a/lib/src/backend/export_internal.dart +++ b/lib/src/backend/export_internal.dart @@ -2,5 +2,4 @@ // A full license can be found at .\LICENSE export 'backend_access.dart'; -export 'export_external.dart'; export 'interfaces/models.dart'; diff --git a/lib/src/backend/interfaces/backend/backend.dart b/lib/src/backend/interfaces/backend/backend.dart index 813dad78..43638249 100644 --- a/lib/src/backend/interfaces/backend/backend.dart +++ b/lib/src/backend/interfaces/backend/backend.dart @@ -3,18 +3,29 @@ import 'dart:async'; +import '../../export_external.dart'; import '../../export_internal.dart'; /// {@template fmtc.backend.backend} /// An abstract interface that FMTC will use to communicate with a storage /// 'backend' (usually one root) /// +/// --- +/// +/// For implementers: +/// /// See also [FMTCBackendInternal] and [FMTCBackendInternalThreadSafe], which /// have the actual method signatures. This is provided as a public means to /// initialise and uninitialise the backend. /// /// When creating a custom implementation, follow the same pattern as the /// built-in ObjectBox backend ([FMTCObjectBoxBackend]). +/// +/// [initialise] & [uninitialise]'s implementations should redirect to an +/// implementation in a [FMTCBackendInternal], where the setter of +/// [FMTCBackendAccess.internal] and [FMTCBackendAccessThreadSafe.internal] may +/// be accessed - see documentation on [FMTCBackendAccess] for more +/// information. /// {@endtemplate} abstract interface class FMTCBackend { /// {@macro fmtc.backend.backend} diff --git a/lib/src/backend/interfaces/backend/internal.dart b/lib/src/backend/interfaces/backend/internal.dart index 47ea216b..b490f464 100644 --- a/lib/src/backend/interfaces/backend/internal.dart +++ b/lib/src/backend/interfaces/backend/internal.dart @@ -19,6 +19,7 @@ import '../../export_internal.dart'; /// threads. /// /// Should be set in [FMTCBackendAccess] when ready to use, and unset when not. +/// See documentation on that class for more information. /// /// Methods with a doc template in the doc string are for 'direct' public /// invocation. diff --git a/lib/src/backend/interfaces/backend/internal_thread_safe.dart b/lib/src/backend/interfaces/backend/internal_thread_safe.dart index 646e59be..121759dc 100644 --- a/lib/src/backend/interfaces/backend/internal_thread_safe.dart +++ b/lib/src/backend/interfaces/backend/internal_thread_safe.dart @@ -4,6 +4,7 @@ import 'dart:async'; import 'dart:typed_data'; +import '../../export_external.dart'; import '../../export_internal.dart'; /// An abstract interface that FMTC will use to communicate with a storage @@ -15,14 +16,13 @@ import '../../export_internal.dart'; /// /// Should be set-up ready for intialisation, and set in the /// [FMTCBackendAccessThreadSafe], from the initialisation of -/// [FMTCBackendInternal]. +/// [FMTCBackendInternal]. See documentation on that class for more information. /// /// Methods with a doc template in the doc string are for 'direct' public /// invocation. /// /// See [FMTCBackend] for more information. -abstract interface class FMTCBackendInternalThreadSafe - with FMTCBackendAccessThreadSafe { +abstract interface class FMTCBackendInternalThreadSafe { const FMTCBackendInternalThreadSafe._(); /// Generic description/name of this backend diff --git a/lib/src/root/directory.dart b/lib/src/root/root.dart similarity index 100% rename from lib/src/root/directory.dart rename to lib/src/root/root.dart diff --git a/lib/src/store/directory.dart b/lib/src/store/store.dart similarity index 100% rename from lib/src/store/directory.dart rename to lib/src/store/store.dart From effd926b8e12ea1551715d9e3c01f57bfe0d78ee Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Wed, 20 Mar 2024 13:21:24 +0000 Subject: [PATCH 143/168] Updated README Added `ImportConflictStrategy.restart` Former-commit-id: 0e9a657d38304af2c3303ecfa0f40d20b13745a7 [formerly a8147aec17a16e8c478c6ed53c6e6ca1c5203844] Former-commit-id: 20ef3a64f4992c00063609a390a4e250f2213408 --- README.md | 40 ++++++++++++------- .../export_import/components/import.dart | 6 ++- lib/src/root/external.dart | 8 ++++ 3 files changed, 37 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 2c017d74..db850e8d 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,13 @@ -# [flutter_map](https://pub.dev/packages/flutter_map)_tile_caching +# flutter_map_tile_caching -A plugin for ['flutter_map'](https://pub.dev/packages/flutter_map) providing advanced caching functionality, with ability to download map regions for offline use. Also includes useful prebuilt widgets. +A plugin for ['flutter_map'](https://pub.dev/packages/flutter_map) providing advanced offline functionality. -[![Pub](https://img.shields.io/pub/v/flutter_map_tile_caching.svg?label=Latest+Stable+Version)](https://pub.dev/packages/flutter_map_tile_caching) [![likes](https://img.shields.io/pub/likes/flutter_map_tile_caching?label=pub.dev+Likes)](https://pub.dev/packages/flutter_map_tile_caching/score) [![pub points](https://img.shields.io/pub/points/flutter_map_tile_caching?label=pub.dev+Points)](https://pub.dev/packages/flutter_map_tile_caching/score) -[![GitHub stars](https://img.shields.io/github/stars/JaffaKetchup/flutter_map_tile_caching.svg?label=GitHub+Stars)](https://GitHub.com/JaffaKetchup/flutter_map_tile_caching/stargazers/) [![GitHub issues](https://img.shields.io/github/issues/JaffaKetchup/flutter_map_tile_caching.svg?label=Issues)](https://GitHub.com/JaffaKetchup/flutter_map_tile_caching/issues/) [![GitHub PRs](https://img.shields.io/github/issues-pr/JaffaKetchup/flutter_map_tile_caching.svg?label=Pull%20Requests)](https://GitHub.com/JaffaKetchup/flutter_map_tile_caching/pulls/) +[![pub.dev](https://img.shields.io/pub/v/flutter_map_tile_caching.svg?label=Latest+Version)](https://pub.dev/packages/flutter_map_tile_caching) +[![stars](https://badgen.net/github/stars/JaffaKetchup/flutter_map_tile_caching?label=stars&color=green&icon=github)](https://github.com/JaffaKetchup/flutter_map_tile_caching/stargazers) +[![likes](https://img.shields.io/pub/likes/flutter_map_tile_caching?logo=flutter)](https://pub.dev/packages/flutter_map_tile_caching/score) +       +[![Open Issues](https://badgen.net/github/open-issues/JaffaKetchup/flutter_map_tile_caching?label=Open+Issues&color=green)](https://github.com/JaffaKetchup/flutter_map_tile_caching/issues) +[![Open PRs](https://badgen.net/github/open-prs/JaffaKetchup/flutter_map_tile_caching?label=Open+PRs&color=green)](https://github.com/JaffaKetchup/flutter_map_tile_caching/pulls) --- @@ -14,16 +18,22 @@ Alternatively, for the API reference, look at the [auto generated 'dartdoc'](htt --- -## Supporting Me +## [Supporting Me](https://github.com/sponsors/JaffaKetchup) -I work on all of my projects in my spare time, including maintaining (along with a team) Flutter's № 1 (non-commercially maintained) mapping library 'flutter_map', bringing it back from the brink of abandonment, as well as my own plugin for it ('flutter_map_tile_caching') that extends it with advanced caching and downloading. -Additionally, I also own the Dart encoder/decoder for the QOI image format ('dqoi') - and I am slowly working on 'flutter_osrm', a wrapper for the Open Source Routing Machine. +Hi there 👋 My name is Luka, but I go by JaffaKetchup! -Sponsorships & donations allow me to continue my projects and upgrade my hardware/setup, as well as allowing me to have some starting amount for further education and such-like. -And of course, a small amount will find its way into my Jaffa Cakes fund () - why do you think my username has "Jaffa" in it? -Many thanks for any amount you can spare, it means a lot to me! +I'm currently in full-time education in the UK studying Computer Science, Geography, and Mathematics, and have been building my software development skills alongside my education for many years. I specialise in the Flutter and Dart technologies to build cross-platform applications, and have over 4 years of experience both from a hobby and commercial standpoint. -You can read more about me and what I do on my [GitHub Sponsors](https://github.com/sponsors/JaffaKetchup) page, where you can donate as well. +I've worked as a senior developer with a small team at WatchEnterprise to develop their Flutter-based WatchCrunch social media app for Android & iOS. + +I'm one of a small team of maintainers for Flutter's №1 non-commercially aimed mapping library 'flutter_map', for which we make internal contributions, regulate and collaborate with external contributors, and offer support to a large community. I also personally develop a multitude of extension libraries, such as 'flutter_map_tile_caching'. In addition, I regularly contribute to OpenStreetMap, and continously improve my skills with personal experimental projects. + +I'm eager to earn other languages, and more than happy to talk about anything software development related! + +Sponsorships & donations allow me to further my education, whilst spening even more time developing the open-source projects that you use & love. I'm grateful for any amount you can spare, all support means a lot to me :) +If you can't support me financially, please consider leaving a star and a like on projects that worked well for you. + +I'm extremely greatful for any amount you can spare! [![Sponsor Me Via GitHub Sponsors](https://github.com/JaffaKetchup/flutter_map_tile_caching/blob/main/GitHubSponsorsImage.jpg)](https://github.com/sponsors/JaffaKetchup) @@ -35,10 +45,10 @@ This project is released under GPL v3. For detailed information about this licen > Permissions of this strong copyleft license are conditioned on making available complete source code of licensed works and modifications, which include larger works using a licensed work, under the same license. Copyright and license notices must be preserved. Contributors provide an express grant of patent rights. -Essentially, whilst you can use this code within commercial projects, they must not be proprietary - they incorporate this 'licensed work' so they must be available under the same license. You must distribute your source code on request (under the same GPL v3 license) to anyone who uses your program. +Essentially, whilst you can use this code within commercial projects, they must not be proprietary - they incorporate this 'licensed work' so they must be available under the same license. You must distribute your source code (at least on request) (under the same GPL v3 license) to anyone who uses your program. -However, I am willing to sell alternative (proprietary) licenses on a case-by-case basis and on request. +I learnt (and am still learning) to code with free, open-source software due to my age and lack of money, and for that reason, I believe in promoting open-source wherever possible to give equal opportunities to everybody, no matter their age, financial position, or any other characteristic. I'm not sure it's fair for commercial & proprietary applications to use software made by people for free out of generosity without giving back to the ecosystem or maintainer(s). -I learnt (and am still learning) to code with free, open-source software due to my age and lack of money, and for that reason, I believe in promoting open-source wherever possible to give equal opportunities to everybody, no matter their age or financial position. I'm not sure it's fair for commercial proprietary applications to use software made by people for free out of generosity. On the other hand, I am also trying to make a small amount of money from my projects, by donations or by selling licenses. And I recognise that commercial businesses may want to use my projects for their own proprietary applications. +On the other hand, I recognise that commercial businesses may want to use my projects for their own proprietary applications, and are happy to support me, and I am also trying to make a small amount of money from my projects, by donations and by selling licenses! -Therefore, if you would like a license to use this software within a proprietary, I am willing to sell a (preferably yearly or usage based) license for a reasonable price. If this seems like what you want/need, please do not hesitate to get in touch via [fmtc@jaffaketchup.dev](mailto:fmtc@jaffaketchup.dev). +Therefore, if you would like a license to use this software within a proprietary application, I am willing to sell a (preferably yearly) license. If this seems like what you'd be interested in, please do not hesitate to get in touch at [fmtc@jaffaketchup.dev](mailto:fmtc@jaffaketchup.dev). Please include details of your project if you can, and the approximate scale/audience for your app; I try to find something that works for everyone, and I'm happy to negotiate! If you're a non-profit organization, I'm happy to also offer an alternative license for free*! diff --git a/example/lib/screens/export_import/components/import.dart b/example/lib/screens/export_import/components/import.dart index e514e37e..f4085b20 100644 --- a/example/lib/screens/export_import/components/import.dart +++ b/example/lib/screens/export_import/components/import.dart @@ -172,6 +172,8 @@ class _ImportState extends State { children: [ Icon( switch (e) { + ImportConflictStrategy.restart => + Icons.restart_alt_rounded, ImportConflictStrategy.merge => Icons.merge_rounded, ImportConflictStrategy.rename => @@ -185,10 +187,10 @@ class _ImportState extends State { const SizedBox(width: 8), Text( switch (e) { + ImportConflictStrategy.restart => 'Restart', ImportConflictStrategy.merge => 'Merge', ImportConflictStrategy.rename => 'Rename', - ImportConflictStrategy.replace => - 'Replace/Overwrite', + ImportConflictStrategy.replace => 'Replace', ImportConflictStrategy.skip => 'Skip', }, style: const TextStyle(color: Colors.white), diff --git a/lib/src/root/external.dart b/lib/src/root/external.dart index a28f0e31..ab07d8b9 100644 --- a/lib/src/root/external.dart +++ b/lib/src/root/external.dart @@ -75,6 +75,14 @@ class RootExternal { /// /// See documentation on individual values for more information. enum ImportConflictStrategy { + /// Deletes all existing stores, and imports all new stores + /// + /// This is significantly quicker than other options, as it only requires + /// a few filesystem operations and quicker calculations to complete, but is + /// mostly intended for situations where an archive is being shipped to new + /// users. + restart, + /// Skips the importing of the store skip, From f007704307e6b92d87e894b2546be635b94b495c Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sun, 24 Mar 2024 21:41:18 +0000 Subject: [PATCH 144/168] Minor documentation improvements Former-commit-id: f2ee9cbcfa021bf7fe1db04a354f2110d986fb18 [formerly adf13f3ea0170a7efee2dc111e9c7a2925ebfc6d] Former-commit-id: 6953d9bc238047e5b10203dea6836c3f11e75e0d --- lib/src/backend/impls/objectbox/backend/internal.dart | 4 ++++ lib/src/backend/interfaces/backend/internal.dart | 11 +++++------ lib/src/root/statistics.dart | 2 ++ lib/src/store/statistics.dart | 2 +- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/lib/src/backend/impls/objectbox/backend/internal.dart b/lib/src/backend/impls/objectbox/backend/internal.dart index 0c35c8b1..93ad96eb 100644 --- a/lib/src/backend/impls/objectbox/backend/internal.dart +++ b/lib/src/backend/impls/objectbox/backend/internal.dart @@ -5,6 +5,8 @@ part of 'backend.dart'; /// Internal implementation of [FMTCBackend] that uses ObjectBox as the storage /// database +/// +/// Actual implementation performed by `_worker` via `_ObjectBoxBackendImpl`. abstract interface class FMTCObjectBoxBackendInternal implements FMTCBackendInternal { static final _instance = _ObjectBoxBackendImpl._(); @@ -84,6 +86,8 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { try { // Not using yield* as it doesn't allow for correct error handling + // (because result must be 'evaluated' here, instead of a direct + // passthrough) await for (final evt in controller.stream) { // Listen to responses yield evt; diff --git a/lib/src/backend/interfaces/backend/internal.dart b/lib/src/backend/interfaces/backend/internal.dart index b490f464..8f609945 100644 --- a/lib/src/backend/interfaces/backend/internal.dart +++ b/lib/src/backend/interfaces/backend/internal.dart @@ -219,8 +219,8 @@ abstract interface class FMTCBackendInternal /// {@template fmtc.backend.setMetadata} /// Set a key-value pair in the metadata for the specified store /// - /// Note that this operation will override the stored value if there is already - /// a matching key present. + /// Note that this operation will overwrite any existing value for the + /// specified key. /// /// Prefer using [setBulkMetadata] when setting multiple keys. Only one backend /// operation is required to set them all at once, and so is more efficient. @@ -234,8 +234,8 @@ abstract interface class FMTCBackendInternal /// {@template fmtc.backend.setBulkMetadata} /// Set multiple key-value pairs in the metadata for the specified store /// - /// Note that this operation will override the stored value if there is already - /// a matching key present. + /// Note that this operation will overwrite any existing value for each + /// specified key. /// {@endtemplate} Future setBulkMetadata({ required String storeName, @@ -304,8 +304,7 @@ abstract interface class FMTCBackendInternal }); /// {@template fmtc.backend.watchStores} - /// Watch for changes in the specified stores, or all stores if no stores - /// are provided + /// Watch for changes in the specified stores /// /// Useful to update UI only when required, for example, in a `StreamBuilder`. /// Whenever this has an event, it is likely the other statistics will have diff --git a/lib/src/root/statistics.dart b/lib/src/root/statistics.dart index e0ca29ed..fb668bb8 100644 --- a/lib/src/root/statistics.dart +++ b/lib/src/root/statistics.dart @@ -33,6 +33,8 @@ class RootStats { } /// {@macro fmtc.backend.watchStores} + /// + /// If [storeNames] is empty, changes will be watched in all stores. Stream watchStores({ List storeNames = const [], bool triggerImmediately = false, diff --git a/lib/src/store/statistics.dart b/lib/src/store/statistics.dart index 8599b294..a8d4117d 100644 --- a/lib/src/store/statistics.dart +++ b/lib/src/store/statistics.dart @@ -41,7 +41,7 @@ class StoreStats { /// {@macro fmtc.frontend.storestats.efficiency} Future get hits => all.then((a) => a.hits); - /// Retrieve number of unsuccessful tile retrievals when browsing + /// Retrieve the number of unsuccessful tile retrievals when browsing /// /// {@macro fmtc.frontend.storestats.efficiency} Future get misses => all.then((a) => a.misses); From 2a9d71af5b09b07eca6fde4fd4e8c482bf6d2b2c Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sun, 31 Mar 2024 19:41:57 +0100 Subject: [PATCH 145/168] Minor improvements Former-commit-id: bf1ab455f75b7e2471ebbef3f4d48e0b9db70d74 [formerly 168cc47ee4fb9fd7769f9792c9c4371fde0e17c8] Former-commit-id: 353e5024190f8aba0fe30db9a6ad148f9a34dc82 --- .../export_import/components/import.dart | 3 -- lib/src/backend/errors/import_export.dart | 32 ++++++++----------- .../objectbox/backend/internal_worker.dart | 30 +++++++++-------- lib/src/root/external.dart | 18 +++-------- lib/src/store/download.dart | 3 +- 5 files changed, 38 insertions(+), 48 deletions(-) diff --git a/example/lib/screens/export_import/components/import.dart b/example/lib/screens/export_import/components/import.dart index f4085b20..20b91e44 100644 --- a/example/lib/screens/export_import/components/import.dart +++ b/example/lib/screens/export_import/components/import.dart @@ -172,8 +172,6 @@ class _ImportState extends State { children: [ Icon( switch (e) { - ImportConflictStrategy.restart => - Icons.restart_alt_rounded, ImportConflictStrategy.merge => Icons.merge_rounded, ImportConflictStrategy.rename => @@ -187,7 +185,6 @@ class _ImportState extends State { const SizedBox(width: 8), Text( switch (e) { - ImportConflictStrategy.restart => 'Restart', ImportConflictStrategy.merge => 'Merge', ImportConflictStrategy.rename => 'Rename', ImportConflictStrategy.replace => 'Replace', diff --git a/lib/src/backend/errors/import_export.dart b/lib/src/backend/errors/import_export.dart index fbbdca5b..8017d10a 100644 --- a/lib/src/backend/errors/import_export.dart +++ b/lib/src/backend/errors/import_export.dart @@ -10,8 +10,8 @@ base class ImportExportError extends FMTCBackendError {} /// Indicates that the specified path to import from or export to did exist, but /// was not a file final class ImportExportPathNotFile extends ImportExportError { - /// Indicates that the specified path to import from or export to did exist, but - /// was not a file + /// Indicates that the specified path to import from or export to did exist, + /// but was not a file ImportExportPathNotFile(); @override @@ -22,8 +22,8 @@ final class ImportExportPathNotFile extends ImportExportError { /// Indicates that the specified file to import did not exist/could not be found final class ImportPathNotExists extends ImportExportError { - /// Indicates that the specified path to import from or export to did exist, but - /// was not a file + /// Indicates that the specified file to import did not exist/could not be + /// found ImportPathNotExists({required this.path}); /// The specified path to the import file @@ -35,16 +35,14 @@ final class ImportPathNotExists extends ImportExportError { } /// Indicates that the import file was not of the expected standard, because it -/// either: -/// * did not contain the appropriate footer signature: hex "FF FF 46 4D 54 43" -/// ("**FMTC") -/// * did not contain all required header information within the file +/// did not contain the appropriate footer signature +/// +/// The last bytes of the file must be hex "FF FF 46 4D 54 43" ("**FMTC"). final class ImportFileNotFMTCStandard extends ImportExportError { /// Indicates that the import file was not of the expected standard, because it - /// either: - /// * did not contain the appropriate footer signature: hex "FF FF 46 4D 54 43" - /// ("**FMTC") - /// * did not contain all required header information within the file + /// did not contain the appropriate footer signature + /// + /// The last bytes of the file must be hex "FF FF 46 4D 54 43" ("**FMTC"). ImportFileNotFMTCStandard(); @override @@ -56,16 +54,14 @@ final class ImportFileNotFMTCStandard extends ImportExportError { /// Indicates that the import file was exported from a different FMTC backend, /// and is not compatible with the current backend /// -/// The bytes prior to the header signature (hex "FF FF 46 4D 54 43" ("**FMTC")) -/// should an identifier (eg. the name) of the exporting backend proceeded by -/// hex "FF FE". +/// The bytes prior to the footer signature should an identifier (eg. the name) +/// of the exporting backend proceeded by hex "FF FF FF FF". final class ImportFileNotBackendCompatible extends ImportExportError { /// Indicates that the import file was exported from a different FMTC backend, /// and is not compatible with the current backend /// - /// The bytes prior to the header signature (hex "FF FF 46 4D 54 43" ("**FMTC")) - /// should an identifier (eg. the name) of the exporting backend proceeded by - /// hex "FF FE". + /// The bytes prior to the footer signature should an identifier (eg. the name) + /// of the exporting backend proceeded by hex "FF FF FF FF". ImportFileNotBackendCompatible(); @override diff --git a/lib/src/backend/impls/objectbox/backend/internal_worker.dart b/lib/src/backend/impls/objectbox/backend/internal_worker.dart index dd908725..52f42fe3 100644 --- a/lib/src/backend/impls/objectbox/backend/internal_worker.dart +++ b/lib/src/backend/impls/objectbox/backend/internal_worker.dart @@ -184,12 +184,9 @@ Future _worker( /// Verify that the specified file is a valid FMTC format archive, compatible /// with this ObjectBox backend /// - /// If [truncate] is `true` (default), the FMTC information will be stripped - /// from the file. - void verifyImportableArchive( - File importFile, { - bool truncate = true, - }) { + /// Note that this method writes to the input file, converting it to a valid + /// database if possible + void verifyImportableArchive(File importFile) { final ram = importFile.openSync(mode: FileMode.append); try { int cursorPos = ram.positionSync() - 1; @@ -213,7 +210,7 @@ Future _worker( ram.setPositionSync(--cursorPos); } - if (truncate) ram.truncateSync(--cursorPos); + ram.truncateSync(--cursorPos); } catch (e) { ram.closeSync(); rethrow; @@ -996,7 +993,18 @@ Future _worker( macosApplicationGroup: input.macosApplicationGroup, ); - final storesQuery = importingRoot.box().query().build(); + switch (strategy) { + case ImportConflictStrategy.skip: + // TODO: Handle this case. + case ImportConflictStrategy.replace: + // TODO: Handle this case. + case ImportConflictStrategy.rename: + // TODO: Handle this case. + case ImportConflictStrategy.merge: + // TODO: Handle this case. + } + + /*final storesQuery = importingRoot.box().query().build(); final tilesQuery = importingRoot.box().query().build(); final specificStoresQuery = root @@ -1220,10 +1228,6 @@ Future _worker( rootDeltaSize += -existingTile.bytes.lengthInBytes + importingTile.bytes.lengthInBytes; } - - // TODO: Implement merging strategy - - // TODO: Write `storesToUpdate` to db }, ), ) @@ -1250,7 +1254,7 @@ Future _worker( }, ); }, - ); + );*/ case _WorkerCmdType.listImportableStores: final importPath = cmd.args['path']! as String; diff --git a/lib/src/root/external.dart b/lib/src/root/external.dart index ab07d8b9..2e895d86 100644 --- a/lib/src/root/external.dart +++ b/lib/src/root/external.dart @@ -24,7 +24,7 @@ typedef ImportResult = ({ /// the file will be unreadable by non-FMTC database implementations. /// /// If the specified archive (at [pathToArchive]) is not of the expected format, -/// an error from the [ImportExportError] group: +/// an error from the [ImportExportError] group will be thrown: /// /// - Doesn't exist (except [export]): [ImportPathNotExists] /// - Not a file: [ImportExportPathNotFile] @@ -54,13 +54,13 @@ class RootExternal { /// CAUTION: HIGHLY EXPERIMENTAL, INCOMPLETE, AND UNTESTED @experimental Future import({ - ImportConflictStrategy strategy = ImportConflictStrategy.skip, List? storeNames, + ImportConflictStrategy strategy = ImportConflictStrategy.skip, }) => FMTCBackendAccess.internal.importStores( path: pathToArchive, - strategy: strategy, storeNames: storeNames, + strategy: strategy, ); /// List the available store names within the archive at [pathToArchive] @@ -75,14 +75,6 @@ class RootExternal { /// /// See documentation on individual values for more information. enum ImportConflictStrategy { - /// Deletes all existing stores, and imports all new stores - /// - /// This is significantly quicker than other options, as it only requires - /// a few filesystem operations and quicker calculations to complete, but is - /// mostly intended for situations where an archive is being shipped to new - /// users. - restart, - /// Skips the importing of the store skip, @@ -92,8 +84,8 @@ enum ImportConflictStrategy { /// not belong to the importing store). replace, - /// Renames the importing store by appending it with the current time (which - /// should be unique in all reasonable usecases) + /// Renames the importing store by appending it with the current date & time + /// (which should be unique in all reasonable usecases) /// /// All tiles are retained. In the event of a conflict between two tiles, only /// the one modified most recently is retained. diff --git a/lib/src/store/download.dart b/lib/src/store/download.dart index 0889fe6e..3d5ad499 100644 --- a/lib/src/store/download.dart +++ b/lib/src/store/download.dart @@ -151,7 +151,8 @@ class DownloadManagement { final instance = DownloadInstance.registerIfAvailable(instanceId); if (instance == null) { throw StateError( - "Download instance with ID $instanceId already exists\nIf you're sure you want to start multiple downloads, use a unique `instanceId`.", + "Download instance with ID $instanceId already exists\nIf you're sure " + 'you want to start multiple downloads, use a unique `instanceId`.', ); } From 92df6c74abbbaa3ea6f16bead3394683b87749f1 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sun, 31 Mar 2024 21:42:00 +0100 Subject: [PATCH 146/168] Fixed severe bug with browse caching Former-commit-id: 323ffecf322d9cfb93abc9910423f4c370eab43f [formerly 7e0ad8ae78b4561118680c22ea503da637e65b81] Former-commit-id: 985243d7c5b29117bcf47b7a0a57db7a2e0df5c3 --- .../objectbox/backend/internal_worker.dart | 52 +++++++++++++------ 1 file changed, 35 insertions(+), 17 deletions(-) diff --git a/lib/src/backend/impls/objectbox/backend/internal_worker.dart b/lib/src/backend/impls/objectbox/backend/internal_worker.dart index 52f42fe3..54c30de7 100644 --- a/lib/src/backend/impls/objectbox/backend/internal_worker.dart +++ b/lib/src/backend/impls/objectbox/backend/internal_worker.dart @@ -504,28 +504,46 @@ Future _worker( ); } case (false, false): // Existing tile, update required - int rootDeltaSize = 0; + final storesToUpdate = {}; + + // If tile exists in this store, just update size, otherwise + // length and size + // Also update size of all related stores + bool didContainAlready = false; + + for (final relatedStore in existingTile!.stores) { + if (relatedStore.name == storeName) didContainAlready = true; + + storesToUpdate[relatedStore.name] = + (storesToUpdate[relatedStore.name] ?? relatedStore) + ..size += -existingTile.bytes.lengthInBytes + + bytes!.lengthInBytes; + } + + updateRootStatistics( + deltaSize: + -existingTile.bytes.lengthInBytes + bytes!.lengthInBytes, + ); + + if (!didContainAlready) { + storesToUpdate[storeName] = store + ..length += 1 + ..size += bytes.lengthInBytes; + } + tiles.put( - existingTile! - ..lastModified = DateTime.timestamp() - ..bytes = bytes!, - mode: PutMode.update, + ObjectBoxTile( + url: url, + lastModified: DateTime.timestamp(), + bytes: bytes, + )..stores.addAll({store, ...existingTile.stores}), ); stores.putMany( - existingTile.stores.map( - (store) { - final diff = bytes.lengthInBytes - - existingTile.bytes.lengthInBytes; - rootDeltaSize += diff; - return store..size += diff; - }, - ).toList(growable: false), + storesToUpdate.values.toList(), + mode: PutMode.update, ); - updateRootStatistics(deltaSize: rootDeltaSize); case (true, true): // FMTC internal error - throw StateError( - 'FMTC ObjectBox backend internal state error: $url', - ); + throw UnsupportedError('Unpossible.'); } }, ); From f27ac76e4c884d5301cdaafdf89451d37d1df1b3 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sun, 31 Mar 2024 22:29:07 +0100 Subject: [PATCH 147/168] Implemented import phase 1 Improved worker implementation of `deleteTiles` to support multiple stores Former-commit-id: 30debcf7d09f428acdeca245a1a4915370f58aca [formerly 522205df8f9a5eb71e8ac58d09c5e9f72592cb0e] Former-commit-id: 0f92b7d49c41405c5729e6a51987aa11354dc9f2 --- .../screens/export_import/export_import.dart | 33 ++- .../impls/objectbox/backend/internal.dart | 19 +- .../objectbox/backend/internal_worker.dart | 242 +++++++++++++++--- 3 files changed, 251 insertions(+), 43 deletions(-) diff --git a/example/lib/screens/export_import/export_import.dart b/example/lib/screens/export_import/export_import.dart index 753c9fdb..3ac2120d 100644 --- a/example/lib/screens/export_import/export_import.dart +++ b/example/lib/screens/export_import/export_import.dart @@ -23,7 +23,7 @@ class _ExportImportPopupState extends State { final selectedStores = {}; Future? typeOfPath; bool forceOverrideExisting = false; - bool isProcessingExporting = false; + bool isProcessing = false; void onPathChanged({required bool forceOverrideExisting}) => setState(() { this.forceOverrideExisting = forceOverrideExisting; @@ -71,7 +71,7 @@ class _ExportImportPopupState extends State { ), ), Expanded( - child: pathController.text != '' && !isProcessingExporting + child: pathController.text != '' && !isProcessing ? SizedBox( width: double.infinity, child: FutureBuilder( @@ -166,15 +166,15 @@ class _ExportImportPopupState extends State { return FloatingActionButton( heroTag: 'importExport', - onPressed: isProcessingExporting + onPressed: isProcessing ? null : () async { if (isExporting) { - setState(() => isProcessingExporting = true); + setState(() => isProcessing = true); final stopwatch = Stopwatch()..start(); await FMTCRoot.external( pathToArchive: pathController.text, - ).import( + ).export( storeNames: selectedStores.toList(), ); stopwatch.stop(); @@ -192,9 +192,30 @@ class _ExportImportPopupState extends State { ); Navigator.pop(context); } + } else { + setState(() => isProcessing = true); + final stopwatch = Stopwatch()..start(); + await FMTCRoot.external( + pathToArchive: pathController.text, + ).import(); + stopwatch.stop(); + if (context.mounted) { + final elapsedTime = + (stopwatch.elapsedMilliseconds / 1000) + .toStringAsFixed(1); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Successfully exported stores (in $elapsedTime ' + 'secs)', + ), + ), + ); + Navigator.pop(context); + } } }, - child: isProcessingExporting + child: isProcessing ? const SizedBox.square( dimension: 26, child: CircularProgressIndicator.adaptive(), diff --git a/lib/src/backend/impls/objectbox/backend/internal.dart b/lib/src/backend/impls/objectbox/backend/internal.dart index 93ad96eb..26bb976b 100644 --- a/lib/src/backend/impls/objectbox/backend/internal.dart +++ b/lib/src/backend/impls/objectbox/backend/internal.dart @@ -179,6 +179,8 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { } if (evt.data?['expectStream'] == true) { + // TODO: FIX worker should stop sending events if possible, + // otherwise, don't use null check (eg. import stream) _workerResStreamed[evt.id]!.add(evt.data); } else { _workerResOneShot[evt.id]!.complete(evt.data); @@ -572,7 +574,20 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { }) async { await _checkImportPathType(path); - final storesStreamController = StreamController< + final res = (await _sendCmdOneShot( + type: _WorkerCmdType.importStores, + args: {'path': path, 'strategy': strategy, 'stores': storeNames}, + ))!; + print(res); + + return ( + stores: Future.sync( + () => <({bool conflict, String importingName, String? newName})>[], + ), + complete: Future.sync(() => null), + ); + + /*final storesStreamController = StreamController< ({String importingName, bool conflict, String? newName})>(); final complete = Completer(); @@ -607,7 +622,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { return ( stores: storesStreamController.stream.toList(), complete: complete.future, - ); + );*/ } @override diff --git a/lib/src/backend/impls/objectbox/backend/internal_worker.dart b/lib/src/backend/impls/objectbox/backend/internal_worker.dart index 54c30de7..ac2f6f81 100644 --- a/lib/src/backend/impls/objectbox/backend/internal_worker.dart +++ b/lib/src/backend/impls/objectbox/backend/internal_worker.dart @@ -128,33 +128,36 @@ Future _worker( /// /// Returns the number of orphaned (deleted) tiles. int deleteTiles({ - required String storeName, + required Query storesQuery, required Query tilesQuery, }) { final stores = root.box(); final tiles = root.box(); - final storeQuery = - stores.query(ObjectBoxStore_.name.equals(storeName)).build(); - int rootDeltaLength = 0; int rootDeltaSize = 0; + final storesToUpdate = {}; root.runInTransaction( TxMode.write, () { - final queriedTiles = tilesQuery.find(); - final store = storeQuery.findUnique() ?? - (throw StoreNotExists(storeName: storeName)); + final queriedStores = storesQuery.property(ObjectBoxStore_.name).find(); + final queriedTiles = tilesQuery.find(); // TODO: Memory improvements + + if (queriedStores.isEmpty || queriedTiles.isEmpty) return; for (final tile in queriedTiles) { - // Modify current store - store - ..length -= 1 - ..size -= tile.bytes.lengthInBytes; + // For each store, remove it from the tile if requested + // For each store & if removed, update that store's stats + tile.stores.removeWhere((store) { + if (!queriedStores.contains(store.name)) return false; - // Remove the store relation from the tile - tile.stores.removeWhere((store) => store.name == storeName); + storesToUpdate[store.name] = (storesToUpdate[store.name] ?? store) + ..length -= 1 + ..size -= tile.bytes.lengthInBytes; + + return true; + }); // Delete the tile if it is now orphaned if (tile.stores.isEmpty) { @@ -172,12 +175,11 @@ Future _worker( deltaLength: rootDeltaLength, deltaSize: rootDeltaSize, ); - stores.put(store, mode: PutMode.update); + + stores.putMany(storesToUpdate.values.toList(), mode: PutMode.update); }, ); - storeQuery.close(); - return rootDeltaLength.abs(); } @@ -337,7 +339,7 @@ Future _worker( root.runInTransaction( TxMode.write, () { - deleteTiles(storeName: storeName, tilesQuery: tilesQuery); + deleteTiles(storesQuery: storeQuery, tilesQuery: tilesQuery); final store = storeQuery.findUnique() ?? (throw StoreNotExists(storeName: storeName)); @@ -393,7 +395,7 @@ Future _worker( root.runInTransaction( TxMode.write, () { - deleteTiles(storeName: storeName, tilesQuery: tilesQuery); + deleteTiles(storesQuery: storesQuery, tilesQuery: tilesQuery); storesQuery.remove(); }, ); @@ -556,7 +558,11 @@ Future _worker( final storeName = cmd.args['storeName']! as String; final url = cmd.args['url']! as String; - final query = root + final storesQuery = root + .box() + .query(ObjectBoxStore_.name.equals(storeName)) + .build(); + final tilesQuery = root .box() .query(ObjectBoxTile_.url.equals(url)) .build(); @@ -565,11 +571,13 @@ Future _worker( id: cmd.id, data: { 'wasOrphan': - deleteTiles(storeName: storeName, tilesQuery: query) == 1, + deleteTiles(storesQuery: storesQuery, tilesQuery: tilesQuery) == + 1, }, ); - query.close(); + storesQuery.close(); + tilesQuery.close(); case _WorkerCmdType.registerHitOrMiss: final storeName = cmd.args['storeName']! as String; final hit = cmd.args['hit']! as bool; @@ -628,7 +636,7 @@ Future _worker( id: cmd.id, data: { 'numOrphans': - deleteTiles(storeName: storeName, tilesQuery: tilesQuery), + deleteTiles(storesQuery: storeQuery, tilesQuery: tilesQuery), }, ); } @@ -639,6 +647,10 @@ Future _worker( final storeName = cmd.args['storeName']! as String; final expiry = cmd.args['expiry']! as DateTime; + final storesQuery = root + .box() + .query(ObjectBoxStore_.name.equals(storeName)) + .build(); final tilesQuery = (root.box().query( ObjectBoxTile_.lastModified .greaterThan(expiry.millisecondsSinceEpoch), @@ -652,7 +664,7 @@ Future _worker( id: cmd.id, data: { 'numOrphans': - deleteTiles(storeName: storeName, tilesQuery: tilesQuery), + deleteTiles(storesQuery: storesQuery, tilesQuery: tilesQuery), }, ); @@ -987,7 +999,7 @@ Future _worker( case _WorkerCmdType.importStores: final importPath = cmd.args['path']! as String; final strategy = cmd.args['strategy'] as ImportConflictStrategy; - final storesToImport = cmd.args['stores'] as List; + final storesToImport = cmd.args['stores'] as List?; final importDir = path.join(input.rootDirectory.absolute.path, 'import_tmp'); @@ -1007,20 +1019,180 @@ Future _worker( final importingRoot = Store( getObjectBoxModel(), directory: importDir, - maxDBSizeInKB: input.maxDatabaseSize, // Defaults to 10 GB + maxDBSizeInKB: input.maxDatabaseSize, macosApplicationGroup: input.macosApplicationGroup, ); - switch (strategy) { - case ImportConflictStrategy.skip: - // TODO: Handle this case. - case ImportConflictStrategy.replace: - // TODO: Handle this case. - case ImportConflictStrategy.rename: - // TODO: Handle this case. - case ImportConflictStrategy.merge: - // TODO: Handle this case. - } + final importingStoresQuery = importingRoot + .box() + .query( + (storesToImport?.isEmpty ?? true) + ? null + : ObjectBoxStore_.name.oneOf(storesToImport!), + ) + .build(); + final specificStoresQuery = root + .box() + .query(ObjectBoxStore_.name.equals('')) + .build(); + + final nameToState = {}; + // ignore: unnecessary_parenthesis + (switch (strategy) { + ImportConflictStrategy.skip => importingStoresQuery.stream().where( + (importingStore) { + final name = importingStore.name; + final hasConflict = (specificStoresQuery + ..param(ObjectBoxStore_.name).value = name) + .count() == + 1; + nameToState[name] = hasConflict ? null : name; + + if (hasConflict) return false; + + root.box().putQueued( + ObjectBoxStore( + name: name, + length: importingStore.length, + size: importingStore.size, + hits: importingStore.hits, + misses: importingStore.misses, + metadataJson: importingStore.metadataJson, + ), + mode: PutMode.insert, + ); + return true; + }, + ).toList(), + ImportConflictStrategy.rename => + importingStoresQuery.stream().map((importingStore) { + final name = importingStore.name; + + if ((specificStoresQuery + ..param(ObjectBoxStore_.name).value = name) + .count() == + 0) { + nameToState[name] = name; + root.box().putQueued( + ObjectBoxStore( + name: name, + length: importingStore.length, + size: importingStore.size, + hits: importingStore.hits, + misses: importingStore.misses, + metadataJson: importingStore.metadataJson, + ), + mode: PutMode.insert, + ); + return importingStore; + } else { + final newName = + nameToState[name] = name + DateTime.now().toString(); + final newStore = importingStore..name = newName; + importingRoot + .box() + .put(newStore, mode: PutMode.update); + root.box().putQueued( + ObjectBoxStore( + name: newName, + length: importingStore.length, + size: importingStore.size, + hits: importingStore.hits, + misses: importingStore.misses, + metadataJson: importingStore.metadataJson, + ), + mode: PutMode.insert, + ); + return newStore; + } + }).toList(), + ImportConflictStrategy.replace || + ImportConflictStrategy.merge => + importingStoresQuery.stream().map( + (importingStore) { + final name = importingStore.name; + nameToState[name] = name; + if ((specificStoresQuery + ..param(ObjectBoxStore_.name).value = name) + .count() == + 0) { + root.box().putQueued( + ObjectBoxStore( + name: name, + length: importingStore.length, + size: importingStore.size, + hits: importingStore.hits, + misses: importingStore.misses, + metadataJson: importingStore.metadataJson, + ), + mode: PutMode.insert, + ); + } + return importingStore; + }, + ).toList() + /*.then( + (importingStoreNames) { + final storesQuery = root + .box() + .query(ObjectBoxStore_.name.oneOf(importingStoreNames)) + .build(); + final tilesQuery = (root.box().query() + ..linkMany( + ObjectBoxTile_.stores, + ObjectBoxStore_.name.oneOf(importingStoreNames), + )) + .build(); + + root.runInTransaction( + TxMode.write, + () { + deleteTiles( + storesQuery: storesQuery, + tilesQuery: tilesQuery, + ); + + storesQuery.remove(); + }, + ); + + storesQuery.close(); + tilesQuery.close(); + + return importingStoreNames; + }, + )*/ + , + }) + .then( + (storesToImport) { + if (!root.awaitQueueSubmitted()) { + throw StateError('Store import queue failed unexpectedly'); + } + + sendRes( + id: cmd.id, + data: { + 'expectStream': true, + 'nameToState': nameToState, + }, + ); + + // At this point: + // * storesToImport should contain only the required IMPORT stores + // * root's stores should be set so that every import store has an + // equivalent with the same name + // It is important never to 'copy' from the import root to the + // in-use root + + if (strategy == ImportConflictStrategy.skip || + strategy == ImportConflictStrategy.rename) { + throw UnimplementedError(); + } else { + throw UnimplementedError(); + } + }, + ); /*final storesQuery = importingRoot.box().query().build(); final tilesQuery = importingRoot.box().query().build(); From 9f1b716c8b75043d0596aa3f698fea758c3ea1e0 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sun, 31 Mar 2024 22:41:11 +0100 Subject: [PATCH 148/168] Fixed bugs Former-commit-id: 7e9e68f542cf3f55f728604a403fe347c18d0c8d [formerly c48ba68a8fabfab510f5f6e2f991cdcb0a2d9f63] Former-commit-id: 18f33494123ad0d35a3827a2b592b9f833eb8e1a --- .../export_import/components/import.dart | 12 +- .../screens/export_import/export_import.dart | 9 +- .../impls/objectbox/backend/internal.dart | 5 +- .../objectbox/backend/internal_worker.dart | 161 +----------------- 4 files changed, 23 insertions(+), 164 deletions(-) diff --git a/example/lib/screens/export_import/components/import.dart b/example/lib/screens/export_import/components/import.dart index 20b91e44..03b681b4 100644 --- a/example/lib/screens/export_import/components/import.dart +++ b/example/lib/screens/export_import/components/import.dart @@ -8,12 +8,17 @@ class Import extends StatefulWidget { super.key, required this.path, required this.changeForceOverrideExisting, + required this.conflictStrategy, + required this.changeConflictStrategy, }); final String path; final void Function({required bool forceOverrideExisting}) changeForceOverrideExisting; + final ImportConflictStrategy conflictStrategy; + final void Function(ImportConflictStrategy) changeConflictStrategy; + @override State createState() => _ImportState(); } @@ -24,8 +29,6 @@ class _ImportState extends State { late Future> importableStores = FMTCRoot.external(pathToArchive: widget.path).listStores; - ImportConflictStrategy selectedConflictStrategy = ImportConflictStrategy.skip; - @override void didUpdateWidget(covariant Import oldWidget) { super.didUpdateWidget(oldWidget); @@ -163,7 +166,7 @@ class _ImportState extends State { width: double.infinity, child: DropdownButton( isExpanded: true, - value: selectedConflictStrategy, + value: widget.conflictStrategy, items: _conflictStrategies .map( (e) => DropdownMenuItem( @@ -197,8 +200,7 @@ class _ImportState extends State { ), ) .toList(growable: false), - onChanged: (choice) => - setState(() => selectedConflictStrategy = choice!), + onChanged: (choice) => widget.changeConflictStrategy(choice!), ), ), ], diff --git a/example/lib/screens/export_import/export_import.dart b/example/lib/screens/export_import/export_import.dart index 3ac2120d..72475ed5 100644 --- a/example/lib/screens/export_import/export_import.dart +++ b/example/lib/screens/export_import/export_import.dart @@ -23,6 +23,7 @@ class _ExportImportPopupState extends State { final selectedStores = {}; Future? typeOfPath; bool forceOverrideExisting = false; + ImportConflictStrategy selectedConflictStrategy = ImportConflictStrategy.skip; bool isProcessing = false; void onPathChanged({required bool forceOverrideExisting}) => setState(() { @@ -111,6 +112,10 @@ class _ExportImportPopupState extends State { child: Import( path: pathController.text, changeForceOverrideExisting: onPathChanged, + conflictStrategy: selectedConflictStrategy, + changeConflictStrategy: (c) => setState( + () => selectedConflictStrategy = c, + ), ), ); }, @@ -197,7 +202,9 @@ class _ExportImportPopupState extends State { final stopwatch = Stopwatch()..start(); await FMTCRoot.external( pathToArchive: pathController.text, - ).import(); + ).import( + strategy: selectedConflictStrategy, + ); stopwatch.stop(); if (context.mounted) { final elapsedTime = diff --git a/lib/src/backend/impls/objectbox/backend/internal.dart b/lib/src/backend/impls/objectbox/backend/internal.dart index 26bb976b..37efeb39 100644 --- a/lib/src/backend/impls/objectbox/backend/internal.dart +++ b/lib/src/backend/impls/objectbox/backend/internal.dart @@ -574,11 +574,10 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { }) async { await _checkImportPathType(path); - final res = (await _sendCmdOneShot( + _sendCmdStreamed( type: _WorkerCmdType.importStores, args: {'path': path, 'strategy': strategy, 'stores': storeNames}, - ))!; - print(res); + ).listen(print); return ( stores: Future.sync( diff --git a/lib/src/backend/impls/objectbox/backend/internal_worker.dart b/lib/src/backend/impls/objectbox/backend/internal_worker.dart index ac2f6f81..019a7ffd 100644 --- a/lib/src/backend/impls/objectbox/backend/internal_worker.dart +++ b/lib/src/backend/impls/objectbox/backend/internal_worker.dart @@ -1050,7 +1050,7 @@ Future _worker( if (hasConflict) return false; - root.box().putQueued( + root.box().put( ObjectBoxStore( name: name, length: importingStore.length, @@ -1073,7 +1073,7 @@ Future _worker( .count() == 0) { nameToState[name] = name; - root.box().putQueued( + root.box().put( ObjectBoxStore( name: name, length: importingStore.length, @@ -1087,12 +1087,12 @@ Future _worker( return importingStore; } else { final newName = - nameToState[name] = name + DateTime.now().toString(); + nameToState[name] = '$name [Imported ${DateTime.now()}]'; final newStore = importingStore..name = newName; importingRoot .box() .put(newStore, mode: PutMode.update); - root.box().putQueued( + root.box().put( ObjectBoxStore( name: newName, length: importingStore.length, @@ -1116,7 +1116,7 @@ Future _worker( ..param(ObjectBoxStore_.name).value = name) .count() == 0) { - root.box().putQueued( + root.box().put( ObjectBoxStore( name: name, length: importingStore.length, @@ -1166,10 +1166,6 @@ Future _worker( }) .then( (storesToImport) { - if (!root.awaitQueueSubmitted()) { - throw StateError('Store import queue failed unexpectedly'); - } - sendRes( id: cmd.id, data: { @@ -1194,152 +1190,7 @@ Future _worker( }, ); - /*final storesQuery = importingRoot.box().query().build(); - final tilesQuery = importingRoot.box().query().build(); - - final specificStoresQuery = root - .box() - .query(ObjectBoxStore_.name.equals('')) - .build(); - final specificTilesQuery = root - .box() - .query(ObjectBoxTile_.url.equals('')) - .build(); - - final storesToUpdate = {}; - final storesObjectsForRelations = {}; - final storesToSkipTiles = []; - int rootDeltaLength = 0; - int rootDeltaSize = 0; - - final importingStores = importingRoot.runInTransaction( - TxMode.read, - storesQuery.stream, - ); - - root - .runInTransaction( - TxMode.write, - () => importingStores.listen( - (importingStore) { - if (!storesToImport.contains(importingStore.name)) { - storesToSkipTiles.add(importingStore.name); - return; - } - - final existingStore = (specificStoresQuery - ..param(ObjectBoxStore_.name).value = - importingStore.name) - .findUnique(); - - if (existingStore == null) { - sendRes( - id: cmd.id, - data: { - 'expectStream': true, - 'storeName': importingStore.name, - 'newStoreName': null, - 'conflict': false, - }, - ); - - root.box().put( - storesObjectsForRelations[importingStore.name] = - ObjectBoxStore( - name: importingStore.name, - length: importingStore.length, - size: importingStore.size, - hits: importingStore.hits, - misses: importingStore.misses, - metadataJson: importingStore.metadataJson, - ), - mode: PutMode.insert, - ); - - return; - } - - if (strategy == ImportConflictStrategy.skip) { - storesToSkipTiles.add(importingStore.name); - sendRes( - id: cmd.id, - data: { - 'expectStream': true, - 'storeName': importingStore.name, - 'newStoreName': null, - 'conflict': true, - }, - ); - return; - } - - String? newName; - if (strategy == ImportConflictStrategy.rename) { - newName = - '${existingStore.name} (Imported ${DateTime.now()})'; - importingRoot.box().put( - importingStore..name = newName, - mode: PutMode.update, - ); - } - - sendRes( - id: cmd.id, - data: { - 'expectStream': true, - 'storeName': importingStore.name, - 'newStoreName': newName, - 'conflict': true, - }, - ); - - if (strategy == ImportConflictStrategy.replace) { - final tilesQuery = (root.box().query() - ..linkMany( - ObjectBoxTile_.stores, - ObjectBoxStore_.name.equals(existingStore.name), - )) - .build(); - - deleteTiles( - storeName: existingStore.name, - tilesQuery: tilesQuery, - ); - root.box().remove(existingStore.id); - - tilesQuery.close(); - } - - if (strategy != ImportConflictStrategy.merge) { - root.box().put( - storesObjectsForRelations[importingStore.name] = - ObjectBoxStore( - name: newName ?? importingStore.name, - length: importingStore.length, - size: importingStore.size, - hits: importingStore.hits, - misses: importingStore.misses, - metadataJson: importingStore.metadataJson, - ), - mode: PutMode.insert, - ); - } - }, - ), - ) - .asFuture() - .then( - (_) { - sendRes( - id: cmd.id, - data: {'expectStream': true, 'tiles': null}, - ); - - final importingTiles = importingRoot.runInTransaction( - TxMode.read, - tilesQuery.stream, - ); - + /* root .runInTransaction( TxMode.write, From 3437111bab89cc3ee32b0638cf14217933a21c86 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sun, 31 Mar 2024 23:36:33 +0100 Subject: [PATCH 149/168] Implemented (worker side) tile importing (for `skip` and `rename` `ImportConflictStrategy` only) Fixed race condition in exporter Fixed bugs in example application Former-commit-id: 6c50bc8abf8b076a6a13f975453316c67f3910f8 [formerly f198d3ab3be38405dcb5e18d71696e639f7d00d6] Former-commit-id: 17aa32ed67e9d30e8264098dfb0083d260c0745b --- .../screens/export_import/export_import.dart | 3 +- .../objectbox/backend/internal_worker.dart | 265 +++++++++++------- 2 files changed, 161 insertions(+), 107 deletions(-) diff --git a/example/lib/screens/export_import/export_import.dart b/example/lib/screens/export_import/export_import.dart index 72475ed5..4f1398b6 100644 --- a/example/lib/screens/export_import/export_import.dart +++ b/example/lib/screens/export_import/export_import.dart @@ -156,8 +156,7 @@ class _ExportImportPopupState extends State { late final bool isExporting; late final Icon icon; - if (snapshot.data! == FileSystemEntityType.notFound && - !forceOverrideExisting) { + if (snapshot.data! == FileSystemEntityType.notFound) { icon = const Icon(Icons.save); isExporting = true; } else if (snapshot.data! == FileSystemEntityType.file && diff --git a/lib/src/backend/impls/objectbox/backend/internal_worker.dart b/lib/src/backend/impls/objectbox/backend/internal_worker.dart index 019a7ffd..ad2bdf79 100644 --- a/lib/src/backend/impls/objectbox/backend/internal_worker.dart +++ b/lib/src/backend/impls/objectbox/backend/internal_worker.dart @@ -923,7 +923,7 @@ Future _worker( exportingRoot .runInTransaction( TxMode.write, - () => exportingStores.listen( + () => exportingStores.map( (exportingStore) { exportingRoot.box().put( storesObjectsForRelations[exportingStore.name] = @@ -940,7 +940,7 @@ Future _worker( }, ), ) - .asFuture() + .last .then( (_) { final exportingTiles = root.runInTransaction( @@ -951,7 +951,7 @@ Future _worker( exportingRoot .runInTransaction( TxMode.write, - () => exportingTiles.listen( + () => exportingTiles.map( (exportingTile) { exportingRoot.box().put( ObjectBoxTile( @@ -968,7 +968,7 @@ Future _worker( }, ), ) - .asFuture() + .last .then( (_) { storesQuery.close(); @@ -1039,31 +1039,35 @@ Future _worker( final nameToState = {}; // ignore: unnecessary_parenthesis (switch (strategy) { - ImportConflictStrategy.skip => importingStoresQuery.stream().where( - (importingStore) { - final name = importingStore.name; - final hasConflict = (specificStoresQuery - ..param(ObjectBoxStore_.name).value = name) - .count() == - 1; - nameToState[name] = hasConflict ? null : name; + ImportConflictStrategy.skip => importingStoresQuery + .stream() + .where( + (importingStore) { + final name = importingStore.name; + final hasConflict = (specificStoresQuery + ..param(ObjectBoxStore_.name).value = name) + .count() == + 1; + nameToState[name] = hasConflict ? null : name; + + if (hasConflict) return false; - if (hasConflict) return false; - - root.box().put( - ObjectBoxStore( - name: name, - length: importingStore.length, - size: importingStore.size, - hits: importingStore.hits, - misses: importingStore.misses, - metadataJson: importingStore.metadataJson, - ), - mode: PutMode.insert, - ); - return true; - }, - ).toList(), + root.box().put( + ObjectBoxStore( + name: name, + length: importingStore.length, + size: importingStore.size, + hits: importingStore.hits, + misses: importingStore.misses, + metadataJson: importingStore.metadataJson, + ), + mode: PutMode.insert, + ); + return true; + }, + ) + .map((s) => s.name) + .toList(), ImportConflictStrategy.rename => importingStoresQuery.stream().map((importingStore) { final name = importingStore.name; @@ -1084,7 +1088,7 @@ Future _worker( ), mode: PutMode.insert, ); - return importingStore; + return name; } else { final newName = nameToState[name] = '$name [Imported ${DateTime.now()}]'; @@ -1103,7 +1107,7 @@ Future _worker( ), mode: PutMode.insert, ); - return newStore; + return newName; } }).toList(), ImportConflictStrategy.replace || @@ -1128,7 +1132,7 @@ Future _worker( mode: PutMode.insert, ); } - return importingStore; + return name; }, ).toList() /*.then( @@ -1169,7 +1173,7 @@ Future _worker( sendRes( id: cmd.id, data: { - 'expectStream': true, + 'expectStream': true, // TODO: Needs subscription? 'nameToState': nameToState, }, ); @@ -1183,7 +1187,129 @@ Future _worker( if (strategy == ImportConflictStrategy.skip || strategy == ImportConflictStrategy.rename) { - throw UnimplementedError(); + final importingTilesQuery = + importingRoot.box().query().build(); + final importingTiles = importingTilesQuery.stream(); + + final existingStoresQuery = root + .box() + .query(ObjectBoxStore_.name.equals('')) + .build(); + final existingTilesQuery = root + .box() + .query(ObjectBoxTile_.url.equals('')) + .build(); + + final storesToUpdate = {}; + + int rootDeltaLength = 0; + int rootDeltaSize = 0; + + Iterable convertToExistingStores( + Iterable importingStores, + ) => + importingStores + .where( + (s) => storesToImport.contains(s.name), + ) + .map( + (s) => storesToUpdate[s.name] ??= (existingStoresQuery + ..param(ObjectBoxStore_.name).value = s.name) + .findUnique()!, + ); + + root + .runInTransaction( + TxMode.write, + () => importingTiles.map( + (importingTile) { + try { + root.box().put( + ObjectBoxTile( + url: importingTile.url, + bytes: importingTile.bytes, + lastModified: importingTile.lastModified, + )..stores.addAll( + convertToExistingStores( + importingTile.stores, + ), + ), + mode: PutMode.insert, + ); + + rootDeltaLength++; + rootDeltaSize += importingTile.bytes.lengthInBytes; + } on UniqueViolationException { + final existingTile = (existingTilesQuery + ..param(ObjectBoxTile_.url).value = + importingTile.url) + .findUnique()!; + + final newRelatedStores = + convertToExistingStores(importingTile.stores); + + if (existingTile.lastModified + .isAfter(importingTile.lastModified)) { + /*for (final newRelatedStore in newRelatedStores) { + storesToUpdate[newRelatedStore.name] = + (storesToUpdate[newRelatedStore.name] ?? + newRelatedStore) + ..size += + -importingTile.bytes.lengthInBytes + + existingTile.bytes.lengthInBytes; + }*/ + return; + } + + root.box().put( + ObjectBoxTile( + url: importingTile.url, + bytes: importingTile.bytes, + lastModified: importingTile.lastModified, + )..stores.addAll( + { + ...existingTile.stores, + ...newRelatedStores, + }, + ), + mode: PutMode.update, + ); + + for (final existingTileStore in existingTile.stores) { + storesToUpdate[existingTileStore.name] = + (storesToUpdate[existingTileStore.name] ?? + existingTileStore) + ..size += -existingTile.bytes.lengthInBytes + + importingTile.bytes.lengthInBytes; + } + + rootDeltaSize += -existingTile.bytes.lengthInBytes + + importingTile.bytes.lengthInBytes; + } + }, + ), + ) + .last + .then((_) { + updateRootStatistics( + deltaLength: rootDeltaLength, + deltaSize: rootDeltaSize, + ); + + importingTilesQuery.close(); + existingStoresQuery.close(); + existingTilesQuery.close(); + importingRoot.close(); + + importFile.deleteSync(); + File(path.join(importDir, 'lock.mdb')).deleteSync(); + importDirIO.deleteSync(); + + sendRes( + id: cmd.id, + data: {'expectStream': true, 'finished': null}, + ); + }); } else { throw UnimplementedError(); } @@ -1196,78 +1322,7 @@ Future _worker( TxMode.write, () => importingTiles.listen( (importingTile) { - if (strategy == ImportConflictStrategy.skip && - importingTile.stores.length == 1 && - storesToSkipTiles.contains( - importingTile.stores[0].name, - )) return; - - importingTile.stores.removeWhere( - (s) => storesToSkipTiles.contains(s.name), - ); - - try { - root.box().put( - ObjectBoxTile( - url: importingTile.url, - bytes: importingTile.bytes, - lastModified: importingTile.lastModified, - )..stores.addAll( - importingTile.stores.map( - (e) => storesObjectsForRelations[e.name]!, - ), - ), - mode: PutMode.insert, - ); - - rootDeltaLength++; - rootDeltaSize += importingTile.bytes.lengthInBytes; - } on UniqueViolationException { - final existingTile = (specificTilesQuery - ..param(ObjectBoxTile_.url).value = - importingTile.url) - .findUnique()!; - - final newRelatedStores = importingTile.stores.map( - (e) => storesObjectsForRelations[e.name]!, - ); - - if (existingTile.lastModified - .isAfter(importingTile.lastModified)) { - for (final newRelatedStore in newRelatedStores) { - storesToUpdate[newRelatedStore.name] = - (storesToUpdate[newRelatedStore.name] ?? - newRelatedStore) - ..size += -importingTile.bytes.lengthInBytes + - existingTile.bytes.lengthInBytes; - } - return; - } - - root.box().put( - ObjectBoxTile( - url: importingTile.url, - bytes: importingTile.bytes, - lastModified: importingTile.lastModified, - )..stores.addAll( - { - ...existingTile.stores, - ...newRelatedStores, - }, - ), - mode: PutMode.update, - ); - - for (final existingTileStore in existingTile.stores) { - storesToUpdate[existingTileStore.name] = - (storesToUpdate[existingTileStore.name] ?? - existingTileStore) - ..size += -existingTile.bytes.lengthInBytes + - importingTile.bytes.lengthInBytes; - } - - rootDeltaSize += -existingTile.bytes.lengthInBytes + - importingTile.bytes.lengthInBytes; + } }, ), From 50627c8323ad3d1968f3ea78da910308510d48f3 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Tue, 2 Apr 2024 16:46:45 +0100 Subject: [PATCH 150/168] Fixed bugs Improved `ImportResult` Former-commit-id: 22bb8cb997b24ded2fb0d4d2227e094db15dde2f [formerly 605713731fabe6dc0e6cded61e52abb0a6e13e1b] Former-commit-id: 8a93e7e49831ea7dc13c7e20f7fae15f9a90478d --- .../screens/export_import/export_import.dart | 9 +- .../stores/components/root_stats_pane.dart | 4 +- .../impls/objectbox/backend/backend.dart | 1 - .../impls/objectbox/backend/internal.dart | 84 +++--- .../objectbox/backend/internal_worker.dart | 242 +++++++++--------- .../backend/interfaces/backend/internal.dart | 4 +- lib/src/root/external.dart | 22 +- 7 files changed, 188 insertions(+), 178 deletions(-) diff --git a/example/lib/screens/export_import/export_import.dart b/example/lib/screens/export_import/export_import.dart index 4f1398b6..9db55e24 100644 --- a/example/lib/screens/export_import/export_import.dart +++ b/example/lib/screens/export_import/export_import.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; @@ -199,11 +200,13 @@ class _ExportImportPopupState extends State { } else { setState(() => isProcessing = true); final stopwatch = Stopwatch()..start(); - await FMTCRoot.external( + final importResult = FMTCRoot.external( pathToArchive: pathController.text, ).import( strategy: selectedConflictStrategy, ); + unawaited(importResult.storesToStates.then(print)); + final numImportedTiles = await importResult.complete; stopwatch.stop(); if (context.mounted) { final elapsedTime = @@ -212,8 +215,8 @@ class _ExportImportPopupState extends State { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( - 'Successfully exported stores (in $elapsedTime ' - 'secs)', + 'Successfully imported $numImportedTiles tiles ' + '(in $elapsedTime secs)', ), ), ); diff --git a/example/lib/screens/main/pages/stores/components/root_stats_pane.dart b/example/lib/screens/main/pages/stores/components/root_stats_pane.dart index 552dba6d..209ba452 100644 --- a/example/lib/screens/main/pages/stores/components/root_stats_pane.dart +++ b/example/lib/screens/main/pages/stores/components/root_stats_pane.dart @@ -13,9 +13,7 @@ class RootStatsPane extends StatefulWidget { } class _RootStatsPaneState extends State { - late final watchStream = FMTCRoot.stats.watchStores( - triggerImmediately: true, - ); + late final watchStream = FMTCRoot.stats.watchStores(triggerImmediately: true); @override Widget build(BuildContext context) => Container( diff --git a/lib/src/backend/impls/objectbox/backend/backend.dart b/lib/src/backend/impls/objectbox/backend/backend.dart index eda30734..d9157c94 100644 --- a/lib/src/backend/impls/objectbox/backend/backend.dart +++ b/lib/src/backend/impls/objectbox/backend/backend.dart @@ -7,7 +7,6 @@ import 'dart:io'; import 'dart:isolate'; import 'package:flutter/services.dart'; -import 'package:meta/meta.dart'; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; diff --git a/lib/src/backend/impls/objectbox/backend/internal.dart b/lib/src/backend/impls/objectbox/backend/internal.dart index 37efeb39..570f713b 100644 --- a/lib/src/backend/impls/objectbox/backend/internal.dart +++ b/lib/src/backend/impls/objectbox/backend/internal.dart @@ -75,8 +75,12 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { onCancel: () async { _workerResStreamed.remove(id); // Free memory // Cancel the worker stream if the worker is alive - if (!_workerComplete.isCompleted) { - await _sendCmdOneShot(type: type.streamCancel!, args: {'id': id}); + if ((type.hasInternalStreamSub ?? false) && + !_workerComplete.isCompleted) { + await _sendCmdOneShot( + type: _WorkerCmdType.cancelInternalStreamSub, + args: {'id': id}, + ); } }, ); @@ -170,7 +174,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { final err = evt.data?['error']; if (err != null) { if (evt.data?['expectStream'] == true) { - _workerResStreamed[evt.id]!.addError(err, evt.data!['stackTrace']); + _workerResStreamed[evt.id]?.addError(err, evt.data!['stackTrace']); } else { _workerResOneShot[evt.id]! .completeError(err, evt.data!['stackTrace']); @@ -179,9 +183,11 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { } if (evt.data?['expectStream'] == true) { - // TODO: FIX worker should stop sending events if possible, - // otherwise, don't use null check (eg. import stream) - _workerResStreamed[evt.id]!.add(evt.data); + // May be `null` if cmd was streamed result, but has no way to prevent + // future results even after the listener has stopped + // + // See `_WorkerCmdType.hasInternalStreamSub` for info. + _workerResStreamed[evt.id]?.add(evt.data); } else { _workerResOneShot[evt.id]!.complete(evt.data); } @@ -554,6 +560,10 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { required List storeNames, required String path, }) async { + if (storeNames.isEmpty) { + throw ArgumentError.value(storeNames, 'storeNames', 'must not be empty'); + } + final type = await FileSystemEntity.type(path); if (type == FileSystemEntityType.directory) { throw ImportExportPathNotFile(); @@ -566,62 +576,40 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { } @override - @experimental // TODO: Finish implementation - Future importStores({ + ImportResult importStores({ required String path, required ImportConflictStrategy strategy, required List? storeNames, - }) async { - await _checkImportPathType(path); - - _sendCmdStreamed( - type: _WorkerCmdType.importStores, - args: {'path': path, 'strategy': strategy, 'stores': storeNames}, - ).listen(print); - - return ( - stores: Future.sync( - () => <({bool conflict, String importingName, String? newName})>[], - ), - complete: Future.sync(() => null), - ); - - /*final storesStreamController = StreamController< - ({String importingName, bool conflict, String? newName})>(); + }) { + Stream?> checkTypeAndStartImport() async* { + await _checkImportPathType(path); + yield* _sendCmdStreamed( + type: _WorkerCmdType.importStores, + args: {'path': path, 'strategy': strategy, 'stores': storeNames}, + ); + } - final complete = Completer(); + final storesToStates = Completer(); + final complete = Completer(); late final StreamSubscription?> listener; - listener = _sendCmdStreamed( - type: _WorkerCmdType.importStores, - args: {'path': path, 'strategy': strategy, 'stores': storeNames}, - ).listen( - cancelOnError: true, + listener = checkTypeAndStartImport().listen( (evt) { - if (evt!.containsKey('finished')) { - complete.complete(); - listener.cancel(); - return; + if (evt!.containsKey('storesToStates')) { + storesToStates.complete(evt['storesToStates']); } - if (evt.containsKey('tiles')) { - storesStreamController.close(); - return; + if (evt.containsKey('complete')) { + complete.complete(evt['complete']); + listener.cancel(); } - - storesStreamController.add( - ( - importingName: evt['storeName'], - conflict: evt['conflict'], - newName: evt['newStoreName'], - ), - ); }, + cancelOnError: true, ); return ( - stores: storesStreamController.stream.toList(), + storesToStates: storesToStates.future, complete: complete.future, - );*/ + ); } @override diff --git a/lib/src/backend/impls/objectbox/backend/internal_worker.dart b/lib/src/backend/impls/objectbox/backend/internal_worker.dart index ad2bdf79..fff896ad 100644 --- a/lib/src/backend/impls/objectbox/backend/internal_worker.dart +++ b/lib/src/backend/impls/objectbox/backend/internal_worker.dart @@ -39,19 +39,27 @@ enum _WorkerCmdType { getRecoverableRegion, startRecovery, cancelRecovery, - watchRecovery(streamCancel: cancelStreamedOutputs), - watchStores(streamCancel: cancelStreamedOutputs), + watchRecovery(hasInternalStreamSub: true), + watchStores(hasInternalStreamSub: true), exportStores, - importStores(streamCancel: cancelStreamedOutputs), + importStores(hasInternalStreamSub: false), listImportableStores, - cancelStreamedOutputs; + cancelInternalStreamSub; - const _WorkerCmdType({this.streamCancel}); + const _WorkerCmdType({this.hasInternalStreamSub}); - /// Command to execute when cancelling a streamed result + /// Whether this command streams multiple results back /// - /// All streamed cmds must specify a cancel cmd. - final _WorkerCmdType? streamCancel; + /// If `true`, then this command does stream results, and it has an internal + /// [StreamSubscription] that should be cancelled (using + /// [cancelInternalStreamSub]) when it no longer needs to stream results. + /// + /// If `false`, then this command does stream results, but has no stream sub + /// to be cancelled. + /// + /// If `null`, then this command does not stream results, and just returns a + /// single result. + final bool? hasInternalStreamSub; } Future _worker( @@ -142,7 +150,7 @@ Future _worker( TxMode.write, () { final queriedStores = storesQuery.property(ObjectBoxStore_.name).find(); - final queriedTiles = tilesQuery.find(); // TODO: Memory improvements + final queriedTiles = tilesQuery.find(); if (queriedStores.isEmpty || queriedTiles.isEmpty) return; @@ -875,10 +883,16 @@ Future _worker( ) .watch(triggerImmediately: triggerImmediately) .listen((_) => sendRes(id: cmd.id, data: {'expectStream': true})); - case _WorkerCmdType.cancelStreamedOutputs: + case _WorkerCmdType.cancelInternalStreamSub: final id = cmd.args['id']! as int; - streamedOutputSubscriptions[id]?.cancel(); + if (streamedOutputSubscriptions[id] == null) { + throw StateError( + 'Cannot cancel internal streamed result because none was registered.', + ); + } + + streamedOutputSubscriptions[id]!.cancel(); streamedOutputSubscriptions.remove(id); sendRes(id: cmd.id); @@ -940,9 +954,11 @@ Future _worker( }, ), ) - .last + .length .then( - (_) { + (numExportedStores) { + if (numExportedStores == 0) throw StateError('Unpossible.'); + final exportingTiles = root.runInTransaction( TxMode.read, tilesQuery.stream, @@ -968,9 +984,17 @@ Future _worker( }, ), ) - .last + .length .then( - (_) { + (numExportedTiles) { + if (numExportedTiles == 0) { + throw ArgumentError( + 'must include at least one tile in any of the specified ' + 'stores', + 'storeNames', + ); + } + storesQuery.close(); tilesQuery.close(); exportingRoot.close(); @@ -1036,7 +1060,17 @@ Future _worker( .query(ObjectBoxStore_.name.equals('')) .build(); - final nameToState = {}; + void cleanup() { + importingStoresQuery.close(); + specificStoresQuery.close(); + importingRoot.close(); + + importFile.deleteSync(); + File(path.join(importDir, 'lock.mdb')).deleteSync(); + importDirIO.deleteSync(); + } + + final StoresToStates storesToStates = {}; // ignore: unnecessary_parenthesis (switch (strategy) { ImportConflictStrategy.skip => importingStoresQuery @@ -1048,7 +1082,10 @@ Future _worker( ..param(ObjectBoxStore_.name).value = name) .count() == 1; - nameToState[name] = hasConflict ? null : name; + storesToStates[name] = ( + name: hasConflict ? null : name, + hadConflict: hasConflict, + ); if (hasConflict) return false; @@ -1076,7 +1113,7 @@ Future _worker( ..param(ObjectBoxStore_.name).value = name) .count() == 0) { - nameToState[name] = name; + storesToStates[name] = (name: name, hadConflict: false); root.box().put( ObjectBoxStore( name: name, @@ -1090,8 +1127,8 @@ Future _worker( ); return name; } else { - final newName = - nameToState[name] = '$name [Imported ${DateTime.now()}]'; + final newName = '$name [Imported ${DateTime.now()}]'; + storesToStates[name] = (name: newName, hadConflict: true); final newStore = importingStore..name = newName; importingRoot .box() @@ -1115,11 +1152,12 @@ Future _worker( importingStoresQuery.stream().map( (importingStore) { final name = importingStore.name; - nameToState[name] = name; + if ((specificStoresQuery ..param(ObjectBoxStore_.name).value = name) .count() == 0) { + storesToStates[name] = (name: name, hadConflict: false); root.box().put( ObjectBoxStore( name: name, @@ -1131,7 +1169,10 @@ Future _worker( ), mode: PutMode.insert, ); + } else { + storesToStates[name] = (name: name, hadConflict: true); } + return name; }, ).toList() @@ -1173,10 +1214,15 @@ Future _worker( sendRes( id: cmd.id, data: { - 'expectStream': true, // TODO: Needs subscription? - 'nameToState': nameToState, + 'expectStream': true, + 'storesToStates': storesToStates, + if (storesToImport.isEmpty) 'complete': 0, }, ); + if (storesToImport.isEmpty) { + cleanup(); + return; + } // At this point: // * storesToImport should contain only the required IMPORT stores @@ -1188,7 +1234,13 @@ Future _worker( if (strategy == ImportConflictStrategy.skip || strategy == ImportConflictStrategy.rename) { final importingTilesQuery = - importingRoot.box().query().build(); + (importingRoot.box().query() + ..linkMany( + ObjectBoxTile_.stores, + ObjectBoxStore_.name.oneOf(storesToImport), + )) + .build(); + final importingTiles = importingTilesQuery.stream(); final existingStoresQuery = root @@ -1209,9 +1261,7 @@ Future _worker( Iterable importingStores, ) => importingStores - .where( - (s) => storesToImport.contains(s.name), - ) + .where((s) => storesToImport.contains(s.name)) .map( (s) => storesToUpdate[s.name] ??= (existingStoresQuery ..param(ObjectBoxStore_.name).value = s.name) @@ -1223,7 +1273,12 @@ Future _worker( TxMode.write, () => importingTiles.map( (importingTile) { - try { + final existingTile = (existingTilesQuery + ..param(ObjectBoxTile_.url).value = + importingTile.url) + .findUnique(); + + if (existingTile == null) { root.box().put( ObjectBoxTile( url: importingTile.url, @@ -1239,58 +1294,53 @@ Future _worker( rootDeltaLength++; rootDeltaSize += importingTile.bytes.lengthInBytes; - } on UniqueViolationException { - final existingTile = (existingTilesQuery - ..param(ObjectBoxTile_.url).value = - importingTile.url) - .findUnique()!; - - final newRelatedStores = - convertToExistingStores(importingTile.stores); - - if (existingTile.lastModified - .isAfter(importingTile.lastModified)) { - /*for (final newRelatedStore in newRelatedStores) { - storesToUpdate[newRelatedStore.name] = - (storesToUpdate[newRelatedStore.name] ?? - newRelatedStore) - ..size += - -importingTile.bytes.lengthInBytes + - existingTile.bytes.lengthInBytes; - }*/ - return; - } - root.box().put( - ObjectBoxTile( - url: importingTile.url, - bytes: importingTile.bytes, - lastModified: importingTile.lastModified, - )..stores.addAll( - { - ...existingTile.stores, - ...newRelatedStores, - }, - ), - mode: PutMode.update, - ); + return 1; + } + + final newRelatedStores = + convertToExistingStores(importingTile.stores); + + final existingTileIsNewer = existingTile.lastModified + .isAfter(importingTile.lastModified); + + root.box().put( + ObjectBoxTile( + url: importingTile.url, + bytes: existingTileIsNewer + ? existingTile.bytes + : importingTile.bytes, + lastModified: existingTileIsNewer + ? existingTile.lastModified + : importingTile.lastModified, + )..stores.addAll( + { + ...existingTile.stores, + ...newRelatedStores, + }, + ), + ); - for (final existingTileStore in existingTile.stores) { - storesToUpdate[existingTileStore.name] = - (storesToUpdate[existingTileStore.name] ?? - existingTileStore) - ..size += -existingTile.bytes.lengthInBytes + - importingTile.bytes.lengthInBytes; - } + if (existingTileIsNewer) return null; - rootDeltaSize += -existingTile.bytes.lengthInBytes + - importingTile.bytes.lengthInBytes; + for (final existingTileStore in existingTile.stores) { + storesToUpdate[existingTileStore.name] = + (storesToUpdate[existingTileStore.name] ?? + existingTileStore) + ..size += -existingTile.bytes.lengthInBytes + + importingTile.bytes.lengthInBytes; } + + rootDeltaSize += -existingTile.bytes.lengthInBytes + + importingTile.bytes.lengthInBytes; + + return 1; }, ), ) - .last - .then((_) { + .where((e) => e != null) + .length + .then((numImportedTiles) { updateRootStatistics( deltaLength: rootDeltaLength, deltaSize: rootDeltaSize, @@ -1299,15 +1349,11 @@ Future _worker( importingTilesQuery.close(); existingStoresQuery.close(); existingTilesQuery.close(); - importingRoot.close(); - - importFile.deleteSync(); - File(path.join(importDir, 'lock.mdb')).deleteSync(); - importDirIO.deleteSync(); + cleanup(); sendRes( id: cmd.id, - data: {'expectStream': true, 'finished': null}, + data: {'expectStream': true, 'complete': numImportedTiles}, ); }); } else { @@ -1315,42 +1361,6 @@ Future _worker( } }, ); - - /* - root - .runInTransaction( - TxMode.write, - () => importingTiles.listen( - (importingTile) { - - } - }, - ), - ) - .asFuture() - .then( - (_) { - updateRootStatistics( - deltaLength: rootDeltaLength, - deltaSize: rootDeltaSize, - ); - - storesQuery.close(); - tilesQuery.close(); - importingRoot.close(); - - importFile.deleteSync(); - File(path.join(importDir, 'lock.mdb')).deleteSync(); - importDirIO.deleteSync(); - - sendRes( - id: cmd.id, - data: {'expectStream': true, 'finished': null}, - ); - }, - ); - }, - );*/ case _WorkerCmdType.listImportableStores: final importPath = cmd.args['path']! as String; @@ -1406,7 +1416,7 @@ Future _worker( sendRes( id: cmd.id, data: { - if (cmd.type.streamCancel != null) 'expectStream': true, + if (cmd.type.hasInternalStreamSub != null) 'expectStream': true, 'error': e, 'stackTrace': s, }, diff --git a/lib/src/backend/interfaces/backend/internal.dart b/lib/src/backend/interfaces/backend/internal.dart index 8f609945..5f46a976 100644 --- a/lib/src/backend/interfaces/backend/internal.dart +++ b/lib/src/backend/interfaces/backend/internal.dart @@ -335,7 +335,9 @@ abstract interface class FMTCBackendInternal /// /// See [RootExternal] for more information about expected behaviour and /// errors. - Future importStores({ + /// + /// See [ImportResult] for information about how to handle the response. + ImportResult importStores({ required String path, required ImportConflictStrategy strategy, required List? storeNames, diff --git a/lib/src/root/external.dart b/lib/src/root/external.dart index 2e895d86..c09ace7c 100644 --- a/lib/src/root/external.dart +++ b/lib/src/root/external.dart @@ -5,15 +5,24 @@ part of '../../flutter_map_tile_caching.dart'; /// The result of [RootExternal.import] /// -/// `stores` will complete when the store names become available, and whether -/// they have conflicted with existing stores. +/// `storesToStates` will complete when the final store names become available. +/// See [StoresToStates] for more information. /// -/// `complete` will complete when the import is complete. +/// `complete` will complete when the import is complete, with the number of +/// imported/overwritten tiles. typedef ImportResult = ({ - Future> stores, - Future complete, + Future storesToStates, + Future complete, }); +/// A mapping of the original store name (as exported), to: +/// - its new store `name` (as will be used to import), or `null` if +/// [ImportConflictStrategy.skip] was set (meaning it won't be importing) +/// - whether it `hadConflict` with an existing store +/// +/// Used in [ImportResult]. +typedef StoresToStates = Map; + /// Export & import 'archives' of selected stores and tiles, outside of the /// FMTC environment /// @@ -53,7 +62,8 @@ class RootExternal { /// CAUTION: HIGHLY EXPERIMENTAL, INCOMPLETE, AND UNTESTED @experimental - Future import({ + // TODO: Above + ImportResult import({ List? storeNames, ImportConflictStrategy strategy = ImportConflictStrategy.skip, }) => From c6fd6f140dd8f7c09e964123c600dc8c8eece059 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Tue, 2 Apr 2024 17:31:36 +0100 Subject: [PATCH 151/168] Made download progress UI responsive in example app Former-commit-id: 3e01089aef4134d76788119df701513ee4d269a2 [formerly ad693fa7ca8ac632c9c8d998bc7dde075bf0bb53] Former-commit-id: 5de9c902058f6d9e154b3b7f2454521ba3e60248 --- .../components/download_layout.dart | 415 ++++++++---------- .../downloading/components/stats_table.dart | 83 ++++ lib/src/root/external.dart | 7 +- 3 files changed, 279 insertions(+), 226 deletions(-) create mode 100644 example/lib/screens/main/pages/downloading/components/stats_table.dart diff --git a/example/lib/screens/main/pages/downloading/components/download_layout.dart b/example/lib/screens/main/pages/downloading/components/download_layout.dart index 48464765..70d726ca 100644 --- a/example/lib/screens/main/pages/downloading/components/download_layout.dart +++ b/example/lib/screens/main/pages/downloading/components/download_layout.dart @@ -9,6 +9,8 @@ import 'main_statistics.dart'; import 'multi_linear_progress_indicator.dart'; import 'stat_display.dart'; +part 'stats_table.dart'; + class DownloadLayout extends StatelessWidget { const DownloadLayout({ super.key, @@ -22,192 +24,145 @@ class DownloadLayout extends StatelessWidget { final void Function() moveToMapPage; @override - Widget build(BuildContext context) => Column( - children: [ - IntrinsicHeight( - child: Row( + Widget build(BuildContext context) => LayoutBuilder( + builder: (context, constraints) { + final isWide = constraints.maxWidth > 800; + + return SingleChildScrollView( + child: Column( children: [ - RepaintBoundary( - child: ClipRRect( - borderRadius: BorderRadius.circular(16), - child: SizedBox.square( - dimension: 216, - child: download.latestTileEvent.tileImage != null - ? Image.memory( - download.latestTileEvent.tileImage!, - gaplessPlayback: true, - ) - : const Center( - child: CircularProgressIndicator.adaptive(), + IntrinsicHeight( + child: Flex( + direction: isWide ? Axis.horizontal : Axis.vertical, + children: [ + Expanded( + child: Wrap( + alignment: WrapAlignment.center, + runAlignment: WrapAlignment.center, + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 32, + runSpacing: 28, + children: [ + RepaintBoundary( + child: SizedBox.square( + dimension: isWide ? 216 : 196, + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: download.latestTileEvent.tileImage != + null + ? Image.memory( + download.latestTileEvent.tileImage!, + gaplessPlayback: true, + ) + : const Center( + child: CircularProgressIndicator + .adaptive(), + ), + ), + ), ), - ), + MainStatistics( + download: download, + storeDirectory: storeDirectory, + moveToMapPage: moveToMapPage, + ), + ], + ), + ), + const SizedBox.square(dimension: 16), + if (isWide) const VerticalDivider() else const Divider(), + const SizedBox.square(dimension: 16), + if (isWide) + Expanded(child: _StatsTable(download: download)) + else + _StatsTable(download: download), + ], ), ), - const SizedBox(width: 32), - MainStatistics( - download: download, - storeDirectory: storeDirectory, - moveToMapPage: moveToMapPage, + const SizedBox(height: 30), + MulitLinearProgressIndicator( + maxValue: download.maxTiles, + backgroundChild: Text( + '${download.remainingTiles}', + style: const TextStyle(color: Colors.white), + ), + progresses: [ + ( + value: download.cachedTiles + + download.skippedTiles + + download.failedTiles, + color: Colors.red, + child: Text( + '${download.failedTiles}', + style: const TextStyle(color: Colors.black), + ) + ), + ( + value: download.cachedTiles + download.skippedTiles, + color: Colors.yellow, + child: Text( + '${download.skippedTiles}', + style: const TextStyle(color: Colors.black), + ) + ), + ( + value: download.cachedTiles, + color: Colors.green[300]!, + child: Text( + '${download.bufferedTiles}', + style: const TextStyle(color: Colors.black), + ) + ), + ( + value: download.cachedTiles - download.bufferedTiles, + color: Colors.green, + child: Text( + '${download.cachedTiles - download.bufferedTiles}', + style: const TextStyle(color: Colors.white), + ) + ), + ], ), - const SizedBox(width: 32), - const VerticalDivider(), - const SizedBox(width: 16), - Expanded( - child: Table( - defaultVerticalAlignment: TableCellVerticalAlignment.middle, - children: [ - TableRow( - children: [ - StatDisplay( - statistic: - '${download.cachedTiles - download.bufferedTiles} + ${download.bufferedTiles}', - description: 'cached + buffered tiles', - ), - StatDisplay( - statistic: - '${((download.cachedSize - download.bufferedSize) * 1024).asReadableSize} + ${(download.bufferedSize * 1024).asReadableSize}', - description: 'cached + buffered size', - ), - ], + const SizedBox(height: 32), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RotatedBox( + quarterTurns: 3, + child: Text( + 'FAILED TILES', + style: GoogleFonts.ubuntu( + fontSize: 24, + fontWeight: FontWeight.bold, + ), ), - TableRow( - children: [ - StatDisplay( - statistic: - '${download.skippedTiles} (${download.skippedTiles == 0 ? 0 : (100 - ((download.cachedTiles - download.skippedTiles) / download.cachedTiles) * 100).clamp(double.minPositive, 100).toStringAsFixed(1)}%)', - description: 'skipped tiles (% saving)', - ), - StatDisplay( - statistic: - '${(download.skippedSize * 1024).asReadableSize} (${download.skippedTiles == 0 ? 0 : (100 - ((download.cachedSize - download.skippedSize) / download.cachedSize) * 100).clamp(double.minPositive, 100).toStringAsFixed(1)}%)', - description: 'skipped size (% saving)', - ), - ], - ), - TableRow( - children: [ - RepaintBoundary( - child: Column( - children: [ - Row( - mainAxisSize: MainAxisSize.min, + ), + Expanded( + child: RepaintBoundary( + child: Selector>( + selector: (context, provider) => provider.failedTiles, + builder: (context, failedTiles, _) { + final hasFailedTiles = failedTiles.isEmpty; + if (hasFailedTiles) { + return const Padding( + padding: EdgeInsets.symmetric(horizontal: 2), + child: Column( children: [ + Icon(Icons.task_alt, size: 48), + SizedBox(height: 10), Text( - download.failedTiles.toString(), - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - color: download.failedTiles == 0 - ? null - : Colors.red, - ), + 'Any failed tiles will appear here', + textAlign: TextAlign.center, ), - if (download.failedTiles != 0) ...[ - const SizedBox(width: 8), - const Icon( - Icons.warning_amber, - color: Colors.red, - ), - ], ], ), - Text( - 'failed tiles', - style: TextStyle( - fontSize: 16, - color: download.failedTiles == 0 - ? null - : Colors.red, - ), - ), - ], - ), - ), - const SizedBox.shrink(), - ], - ), - ], - ), - ), - ], - ), - ), - const SizedBox(height: 30), - MulitLinearProgressIndicator( - maxValue: download.maxTiles, - backgroundChild: Text( - '${download.remainingTiles}', - style: const TextStyle(color: Colors.white), - ), - progresses: [ - ( - value: download.cachedTiles + - download.skippedTiles + - download.failedTiles, - color: Colors.red, - child: Text( - '${download.failedTiles}', - style: const TextStyle(color: Colors.black), - ) - ), - ( - value: download.cachedTiles + download.skippedTiles, - color: Colors.yellow, - child: Text( - '${download.skippedTiles}', - style: const TextStyle(color: Colors.black), - ) - ), - ( - value: download.cachedTiles, - color: Colors.green[300]!, - child: Text( - '${download.bufferedTiles}', - style: const TextStyle(color: Colors.black), - ) - ), - ( - value: download.cachedTiles - download.bufferedTiles, - color: Colors.green, - child: Text( - '${download.cachedTiles - download.bufferedTiles}', - style: const TextStyle(color: Colors.white), - ) - ), - ], - ), - const SizedBox(height: 32), - Expanded( - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - RotatedBox( - quarterTurns: 3, - child: Text( - 'FAILED TILES', - style: GoogleFonts.ubuntu( - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), - ), - Expanded( - child: RepaintBoundary( - child: Selector>( - selector: (context, provider) => provider.failedTiles, - builder: (context, failedTiles, _) => failedTiles.isEmpty - ? const Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.task_alt, size: 48), - SizedBox(height: 10), - Text('Any failed tiles will appear here'), - ], - ) - : ListView.builder( + ); + } + return ListView.builder( reverse: true, addRepaintBoundaries: false, itemCount: failedTiles.length, + shrinkWrap: true, itemBuilder: (context, index) => ListTile( leading: Icon( switch (failedTiles[index].result) { @@ -233,67 +188,81 @@ class DownloadLayout extends StatelessWidget { }, ), ), - ), + ); + }, + ), + ), ), - ), - ), - const SizedBox(width: 8), - RotatedBox( - quarterTurns: 3, - child: Text( - 'SKIPPED TILES', - style: GoogleFonts.ubuntu( - fontSize: 24, - fontWeight: FontWeight.bold, + const SizedBox(width: 8), + RotatedBox( + quarterTurns: 3, + child: Text( + 'SKIPPED TILES', + style: GoogleFonts.ubuntu( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), ), - ), - ), - Expanded( - child: RepaintBoundary( - child: Selector>( - selector: (context, provider) => provider.skippedTiles, - builder: (context, skippedTiles, _) => - skippedTiles.isEmpty - ? const Column( - mainAxisAlignment: MainAxisAlignment.center, + Expanded( + child: RepaintBoundary( + child: Selector>( + selector: (context, provider) => + provider.skippedTiles, + builder: (context, skippedTiles, _) { + final hasSkippedTiles = skippedTiles.isEmpty; + if (hasSkippedTiles) { + return const Padding( + padding: EdgeInsets.symmetric(horizontal: 2), + child: Column( children: [ Icon(Icons.task_alt, size: 48), SizedBox(height: 10), - Text('Any skipped tiles will appear here'), - ], - ) - : ListView.builder( - reverse: true, - addRepaintBoundaries: false, - itemCount: skippedTiles.length, - itemBuilder: (context, index) => ListTile( - leading: Icon( - switch (skippedTiles[index].result) { - TileEventResult.alreadyExisting => - Icons.disabled_visible, - TileEventResult.isSeaTile => - Icons.water_drop, - _ => Icons.abc, - }, - ), - title: Text(skippedTiles[index].url), - subtitle: Text( - switch (skippedTiles[index].result) { - TileEventResult.alreadyExisting => - 'Tile already exists', - TileEventResult.isSeaTile => - 'Tile is a sea tile', - _ => throw Error(), - }, + Text( + 'Any skipped tiles will appear here', + textAlign: TextAlign.center, ), - ), + ], ), + ); + } + + return ListView.builder( + reverse: true, + addRepaintBoundaries: false, + itemCount: skippedTiles.length, + shrinkWrap: true, + itemBuilder: (context, index) => ListTile( + leading: Icon( + switch (skippedTiles[index].result) { + TileEventResult.alreadyExisting => + Icons.disabled_visible, + TileEventResult.isSeaTile => + Icons.water_drop, + _ => Icons.abc, + }, + ), + title: Text(skippedTiles[index].url), + subtitle: Text( + switch (skippedTiles[index].result) { + TileEventResult.alreadyExisting => + 'Tile already exists', + TileEventResult.isSeaTile => + 'Tile is a sea tile', + _ => throw Error(), + }, + ), + ), + ); + }, + ), + ), ), - ), + ], ), ], ), - ), - ], + ); + }, ); } diff --git a/example/lib/screens/main/pages/downloading/components/stats_table.dart b/example/lib/screens/main/pages/downloading/components/stats_table.dart new file mode 100644 index 00000000..7c312023 --- /dev/null +++ b/example/lib/screens/main/pages/downloading/components/stats_table.dart @@ -0,0 +1,83 @@ +part of 'download_layout.dart'; + +class _StatsTable extends StatelessWidget { + const _StatsTable({ + required this.download, + }); + + final DownloadProgress download; + + @override + Widget build(BuildContext context) => Table( + defaultVerticalAlignment: TableCellVerticalAlignment.middle, + children: [ + TableRow( + children: [ + StatDisplay( + statistic: + '${download.cachedTiles - download.bufferedTiles} + ${download.bufferedTiles}', + description: 'cached + buffered tiles', + ), + StatDisplay( + statistic: + '${((download.cachedSize - download.bufferedSize) * 1024).asReadableSize} + ${(download.bufferedSize * 1024).asReadableSize}', + description: 'cached + buffered size', + ), + ], + ), + TableRow( + children: [ + StatDisplay( + statistic: + '${download.skippedTiles} (${download.skippedTiles == 0 ? 0 : (100 - ((download.cachedTiles - download.skippedTiles) / download.cachedTiles) * 100).clamp(double.minPositive, 100).toStringAsFixed(1)}%)', + description: 'skipped tiles (% saving)', + ), + StatDisplay( + statistic: + '${(download.skippedSize * 1024).asReadableSize} (${download.skippedTiles == 0 ? 0 : (100 - ((download.cachedSize - download.skippedSize) / download.cachedSize) * 100).clamp(double.minPositive, 100).toStringAsFixed(1)}%)', + description: 'skipped size (% saving)', + ), + ], + ), + TableRow( + children: [ + RepaintBoundary( + child: Column( + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + download.failedTiles.toString(), + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: + download.failedTiles == 0 ? null : Colors.red, + ), + ), + if (download.failedTiles != 0) ...[ + const SizedBox(width: 8), + const Icon( + Icons.warning_amber, + color: Colors.red, + ), + ], + ], + ), + Text( + 'failed tiles', + style: TextStyle( + fontSize: 16, + color: download.failedTiles == 0 ? null : Colors.red, + ), + ), + ], + ), + ), + const SizedBox.shrink(), + ], + ), + ], + ); +} diff --git a/lib/src/root/external.dart b/lib/src/root/external.dart index c09ace7c..e1f0e16b 100644 --- a/lib/src/root/external.dart +++ b/lib/src/root/external.dart @@ -60,9 +60,10 @@ class RootExternal { FMTCBackendAccess.internal .exportStores(storeNames: storeNames, path: pathToArchive); - /// CAUTION: HIGHLY EXPERIMENTAL, INCOMPLETE, AND UNTESTED - @experimental - // TODO: Above + /// Imports specified stores and all necessary tiles into the current root + /// + /// See [ImportConflictStrategy] to set how conflicts between existing and + /// importing stores should be resolved. ImportResult import({ List? storeNames, ImportConflictStrategy strategy = ImportConflictStrategy.skip, From fdbbcada0592486877464ff79666b13bf59f7662 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Tue, 2 Apr 2024 18:16:44 +0100 Subject: [PATCH 152/168] Added support for `ImportConflictStrategy.replace` Former-commit-id: 3076483891e881c46e9f79e5828597f265b5cb5e [formerly 49c7c7650553dcea5ab3487140b49014aa52bb23] Former-commit-id: ff0179460771cf8b9022f867a3f12538e5866ab7 --- .../objectbox/backend/internal_worker.dart | 124 ++++++++++-------- 1 file changed, 71 insertions(+), 53 deletions(-) diff --git a/lib/src/backend/impls/objectbox/backend/internal_worker.dart b/lib/src/backend/impls/objectbox/backend/internal_worker.dart index fff896ad..d92627e2 100644 --- a/lib/src/backend/impls/objectbox/backend/internal_worker.dart +++ b/lib/src/backend/impls/objectbox/backend/internal_worker.dart @@ -1175,39 +1175,7 @@ Future _worker( return name; }, - ).toList() - /*.then( - (importingStoreNames) { - final storesQuery = root - .box() - .query(ObjectBoxStore_.name.oneOf(importingStoreNames)) - .build(); - final tilesQuery = (root.box().query() - ..linkMany( - ObjectBoxTile_.stores, - ObjectBoxStore_.name.oneOf(importingStoreNames), - )) - .build(); - - root.runInTransaction( - TxMode.write, - () { - deleteTiles( - storesQuery: storesQuery, - tilesQuery: tilesQuery, - ); - - storesQuery.remove(); - }, - ); - - storesQuery.close(); - tilesQuery.close(); - - return importingStoreNames; - }, - )*/ - , + ).toList(), }) .then( (storesToImport) { @@ -1231,8 +1199,7 @@ Future _worker( // It is important never to 'copy' from the import root to the // in-use root - if (strategy == ImportConflictStrategy.skip || - strategy == ImportConflictStrategy.rename) { + if (strategy != ImportConflictStrategy.merge) { final importingTilesQuery = (importingRoot.box().query() ..linkMany( @@ -1240,7 +1207,6 @@ Future _worker( ObjectBoxStore_.name.oneOf(storesToImport), )) .build(); - final importingTiles = importingTilesQuery.stream(); final existingStoresQuery = root @@ -1271,8 +1237,57 @@ Future _worker( root .runInTransaction( TxMode.write, - () => importingTiles.map( - (importingTile) { + () { + if (strategy == ImportConflictStrategy.replace) { + final storesQuery = root + .box() + .query( + ObjectBoxStore_.name.oneOf(storesToImport), + ) + .build(); + final tilesQuery = (root.box().query() + ..linkMany( + ObjectBoxTile_.stores, + ObjectBoxStore_.name.oneOf(storesToImport), + )) + .build(); + + deleteTiles( + storesQuery: storesQuery, + tilesQuery: tilesQuery, + ); + + final importingStoresQuery = importingRoot + .box() + .query(ObjectBoxStore_.name.oneOf(storesToImport)) + .build(); + + final importingStores = importingStoresQuery.find(); + + storesQuery.remove(); + + root.box().putMany( + List.generate( + importingStores.length, + (i) => ObjectBoxStore( + name: importingStores[i].name, + length: importingStores[i].length, + size: importingStores[i].size, + hits: importingStores[i].hits, + misses: importingStores[i].misses, + metadataJson: importingStores[i].metadataJson, + ), + growable: false, + ), + mode: PutMode.insert, + ); + + storesQuery.close(); + tilesQuery.close(); + importingStoresQuery.close(); + } + + return importingTiles.map((importingTile) { final existingTile = (existingTilesQuery ..param(ObjectBoxTile_.url).value = importingTile.url) @@ -1335,27 +1350,30 @@ Future _worker( importingTile.bytes.lengthInBytes; return 1; - }, - ), + }); + }, ) .where((e) => e != null) .length .then((numImportedTiles) { - updateRootStatistics( - deltaLength: rootDeltaLength, - deltaSize: rootDeltaSize, - ); + updateRootStatistics( + deltaLength: rootDeltaLength, + deltaSize: rootDeltaSize, + ); - importingTilesQuery.close(); - existingStoresQuery.close(); - existingTilesQuery.close(); - cleanup(); + importingTilesQuery.close(); + existingStoresQuery.close(); + existingTilesQuery.close(); + cleanup(); - sendRes( - id: cmd.id, - data: {'expectStream': true, 'complete': numImportedTiles}, - ); - }); + sendRes( + id: cmd.id, + data: { + 'expectStream': true, + 'complete': numImportedTiles, + }, + ); + }); } else { throw UnimplementedError(); } From 22ccf0167f2ccfb4fda8c0e51dabdadb35399335 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Tue, 2 Apr 2024 18:18:24 +0100 Subject: [PATCH 153/168] Fixed GitHub workflow Former-commit-id: a8f2636dccc292e7dcd1e7b7efd86fac4ca4e4a9 [formerly 92a2f1948ea08d6e79889bf0bc525041ea6389d8] Former-commit-id: 2cc2efc39fa7334a982c4600f00e06687353f6f8 --- .github/workflows/main.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9f51b020..773d019f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -37,7 +37,7 @@ jobs: - name: Checkout Repository uses: actions/checkout@master - name: Setup Flutter Environment - uses: subosito/flutter-action@master + uses: subosito/flutter-action@main with: channel: "beta" - name: Get Package Dependencies @@ -58,7 +58,7 @@ jobs: - name: Checkout Repository uses: actions/checkout@master - name: Setup Flutter Environment - uses: subosito/flutter-action@master + uses: subosito/flutter-action@main with: channel: "beta" - name: Get Dependencies @@ -82,7 +82,7 @@ jobs: distribution: "temurin" java-version: "17" - name: Setup Flutter Environment - uses: subosito/flutter-action@master + uses: subosito/flutter-action@main with: channel: "beta" - name: Build @@ -105,7 +105,7 @@ jobs: - name: Checkout Repository uses: actions/checkout@master - name: Setup Flutter Environment - uses: subosito/flutter-action@master + uses: subosito/flutter-action@main with: channel: "beta" - name: Build From 2c3514655c4b2030566506440d809f6e7c32f256 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Tue, 2 Apr 2024 23:18:27 +0100 Subject: [PATCH 154/168] Partially added (buggy) support for `ImportConflictStrategy.merge` Fixed bugs Former-commit-id: 191a0d466550f8d3530e09cb3f278dee2ab30a9c [formerly bd9fc2144cfa585dee32d73a68366a226b8247c2] Former-commit-id: 37314074a70a4eeba4bb1807782b2b7d15bb44fb --- .../impls/objectbox/backend/backend.dart | 1 + .../objectbox/backend/internal_worker.dart | 400 ++++++++++-------- .../impls/objectbox/models/src/store.dart | 14 + lib/src/root/external.dart | 7 +- 4 files changed, 247 insertions(+), 175 deletions(-) diff --git a/lib/src/backend/impls/objectbox/backend/backend.dart b/lib/src/backend/impls/objectbox/backend/backend.dart index d9157c94..a346fccf 100644 --- a/lib/src/backend/impls/objectbox/backend/backend.dart +++ b/lib/src/backend/impls/objectbox/backend/backend.dart @@ -6,6 +6,7 @@ import 'dart:convert'; import 'dart:io'; import 'dart:isolate'; +import 'package:collection/collection.dart'; import 'package:flutter/services.dart'; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; diff --git a/lib/src/backend/impls/objectbox/backend/internal_worker.dart b/lib/src/backend/impls/objectbox/backend/internal_worker.dart index d92627e2..0a46a5b6 100644 --- a/lib/src/backend/impls/objectbox/backend/internal_worker.dart +++ b/lib/src/backend/impls/objectbox/backend/internal_worker.dart @@ -975,9 +975,11 @@ Future _worker( bytes: exportingTile.bytes, lastModified: exportingTile.lastModified, )..stores.addAll( - exportingTile.stores.map( - (s) => storesObjectsForRelations[s.name]!, - ), + exportingTile.stores + .map( + (s) => storesObjectsForRelations[s.name], + ) + .whereNotNull(), ), mode: PutMode.insert, ); @@ -1094,8 +1096,8 @@ Future _worker( name: name, length: importingStore.length, size: importingStore.size, - hits: importingStore.hits, - misses: importingStore.misses, + hits: 0, + misses: 0, metadataJson: importingStore.metadataJson, ), mode: PutMode.insert, @@ -1119,8 +1121,8 @@ Future _worker( name: name, length: importingStore.length, size: importingStore.size, - hits: importingStore.hits, - misses: importingStore.misses, + hits: 0, + misses: 0, metadataJson: importingStore.metadataJson, ), mode: PutMode.insert, @@ -1138,8 +1140,8 @@ Future _worker( name: newName, length: importingStore.length, size: importingStore.size, - hits: importingStore.hits, - misses: importingStore.misses, + hits: 0, + misses: 0, metadataJson: importingStore.metadataJson, ), mode: PutMode.insert, @@ -1153,24 +1155,38 @@ Future _worker( (importingStore) { final name = importingStore.name; - if ((specificStoresQuery - ..param(ObjectBoxStore_.name).value = name) - .count() == - 0) { + final existingStore = (specificStoresQuery + ..param(ObjectBoxStore_.name).value = name) + .findUnique(); + if (existingStore == null) { storesToStates[name] = (name: name, hadConflict: false); root.box().put( ObjectBoxStore( name: name, - length: importingStore.length, - size: importingStore.size, - hits: importingStore.hits, - misses: importingStore.misses, + length: 0, // Will be set when writing tiles + size: 0, // Will be set when writing tiles + hits: 0, + misses: 0, metadataJson: importingStore.metadataJson, ), mode: PutMode.insert, ); } else { storesToStates[name] = (name: name, hadConflict: true); + if (strategy == ImportConflictStrategy.merge) { + root.box().put( + existingStore + ..metadataJson = jsonEncode( + (jsonDecode(existingStore.metadataJson) + as Map) + ..addAll( + jsonDecode(importingStore.metadataJson) + as Map, + ), + ), + mode: PutMode.update, + ); + } } return name; @@ -1199,184 +1215,226 @@ Future _worker( // It is important never to 'copy' from the import root to the // in-use root - if (strategy != ImportConflictStrategy.merge) { - final importingTilesQuery = - (importingRoot.box().query() - ..linkMany( - ObjectBoxTile_.stores, - ObjectBoxStore_.name.oneOf(storesToImport), - )) - .build(); - final importingTiles = importingTilesQuery.stream(); - - final existingStoresQuery = root - .box() - .query(ObjectBoxStore_.name.equals('')) - .build(); - final existingTilesQuery = root - .box() - .query(ObjectBoxTile_.url.equals('')) - .build(); - - final storesToUpdate = {}; - - int rootDeltaLength = 0; - int rootDeltaSize = 0; - - Iterable convertToExistingStores( - Iterable importingStores, - ) => - importingStores - .where((s) => storesToImport.contains(s.name)) - .map( - (s) => storesToUpdate[s.name] ??= (existingStoresQuery - ..param(ObjectBoxStore_.name).value = s.name) - .findUnique()!, - ); + final importingTilesQuery = + (importingRoot.box().query() + ..linkMany( + ObjectBoxTile_.stores, + ObjectBoxStore_.name.oneOf(storesToImport), + )) + .build(); + final importingTiles = importingTilesQuery.stream(); + + final existingStoresQuery = root + .box() + .query(ObjectBoxStore_.name.equals('')) + .build(); + final existingTilesQuery = root + .box() + .query(ObjectBoxTile_.url.equals('')) + .build(); + + final storesToUpdate = {}; - root - .runInTransaction( - TxMode.write, - () { - if (strategy == ImportConflictStrategy.replace) { - final storesQuery = root - .box() - .query( + int rootDeltaLength = 0; + int rootDeltaSize = 0; + + Iterable convertToExistingStores( + Iterable importingStores, + ) => + importingStores + .where((s) => storesToImport.contains(s.name)) + .map( + (s) => storesToUpdate[s.name] ??= (existingStoresQuery + ..param(ObjectBoxStore_.name).value = s.name) + .findUnique()!, + ); + + root + .runInTransaction( + TxMode.write, + () { + if (strategy == ImportConflictStrategy.replace) { + final storesQuery = root + .box() + .query(ObjectBoxStore_.name.oneOf(storesToImport)) + .build(); + final tilesQuery = (root.box().query() + ..linkMany( + ObjectBoxTile_.stores, ObjectBoxStore_.name.oneOf(storesToImport), - ) - .build(); - final tilesQuery = (root.box().query() - ..linkMany( - ObjectBoxTile_.stores, - ObjectBoxStore_.name.oneOf(storesToImport), - )) - .build(); - - deleteTiles( - storesQuery: storesQuery, - tilesQuery: tilesQuery, - ); + )) + .build(); - final importingStoresQuery = importingRoot - .box() - .query(ObjectBoxStore_.name.oneOf(storesToImport)) - .build(); - - final importingStores = importingStoresQuery.find(); - - storesQuery.remove(); - - root.box().putMany( - List.generate( - importingStores.length, - (i) => ObjectBoxStore( - name: importingStores[i].name, - length: importingStores[i].length, - size: importingStores[i].size, - hits: importingStores[i].hits, - misses: importingStores[i].misses, - metadataJson: importingStores[i].metadataJson, - ), - growable: false, - ), - mode: PutMode.insert, - ); + deleteTiles( + storesQuery: storesQuery, + tilesQuery: tilesQuery, + ); - storesQuery.close(); - tilesQuery.close(); - importingStoresQuery.close(); - } + final importingStoresQuery = importingRoot + .box() + .query(ObjectBoxStore_.name.oneOf(storesToImport)) + .build(); + + final importingStores = importingStoresQuery.find(); + + storesQuery.remove(); + + root.box().putMany( + List.generate( + importingStores.length, + (i) => ObjectBoxStore( + name: importingStores[i].name, + length: importingStores[i].length, + size: importingStores[i].size, + hits: importingStores[i].hits, + misses: importingStores[i].misses, + metadataJson: importingStores[i].metadataJson, + ), + growable: false, + ), + mode: PutMode.insert, + ); - return importingTiles.map((importingTile) { - final existingTile = (existingTilesQuery - ..param(ObjectBoxTile_.url).value = - importingTile.url) - .findUnique(); - - if (existingTile == null) { - root.box().put( - ObjectBoxTile( - url: importingTile.url, - bytes: importingTile.bytes, - lastModified: importingTile.lastModified, - )..stores.addAll( - convertToExistingStores( - importingTile.stores, - ), - ), - mode: PutMode.insert, - ); - - rootDeltaLength++; - rootDeltaSize += importingTile.bytes.lengthInBytes; - - return 1; - } + storesQuery.close(); + tilesQuery.close(); + importingStoresQuery.close(); + } - final newRelatedStores = - convertToExistingStores(importingTile.stores); + return importingTiles.map((importingTile) { + final convertedRelatedStores = + convertToExistingStores(importingTile.stores); - final existingTileIsNewer = existingTile.lastModified - .isAfter(importingTile.lastModified); + final existingTile = (existingTilesQuery + ..param(ObjectBoxTile_.url).value = + importingTile.url) + .findUnique(); + if (existingTile == null) { root.box().put( ObjectBoxTile( url: importingTile.url, - bytes: existingTileIsNewer - ? existingTile.bytes - : importingTile.bytes, - lastModified: existingTileIsNewer - ? existingTile.lastModified - : importingTile.lastModified, - )..stores.addAll( - { - ...existingTile.stores, - ...newRelatedStores, - }, - ), + bytes: importingTile.bytes, + lastModified: importingTile.lastModified, + )..stores.addAll(convertedRelatedStores), + mode: PutMode.insert, ); - if (existingTileIsNewer) return null; + // No need to modify store stats, because if tile didn't + // already exist, then was not present in an existing + // store that needs changing, and all importing stores + // are brand new and already contain accurate stats. + // EXCEPT in merge mode - importing stores may not be + // new. + if (strategy == ImportConflictStrategy.merge) { + // No need to worry if it was brand new, we use the + // same logic, treating it as an existing related + // store, because when we created it, we made it + // empty. + for (final convertedRelatedStore + in convertedRelatedStores) { + storesToUpdate[convertedRelatedStore.name] = + (storesToUpdate[convertedRelatedStore.name] ?? + convertedRelatedStore) + ..length += 1 + ..size += importingTile.bytes.lengthInBytes; + } + } + + rootDeltaLength++; + rootDeltaSize += importingTile.bytes.lengthInBytes; + return 1; + } + + final existingTileIsNewer = existingTile.lastModified + .isAfter(importingTile.lastModified); + + final relations = { + ...existingTile.stores, + ...convertedRelatedStores, + }; + + root.box().put( + ObjectBoxTile( + url: importingTile.url, + bytes: existingTileIsNewer + ? existingTile.bytes + : importingTile.bytes, + lastModified: existingTileIsNewer + ? existingTile.lastModified + : importingTile.lastModified, + )..stores.addAll(relations), + ); + + if (existingTileIsNewer) return null; + + if (strategy == ImportConflictStrategy.merge) { + print(relations); + print(convertedRelatedStores.toSet()); + print(existingTile.stores.toSet()); + print(convertedRelatedStores + .toSet() + .difference(existingTile.stores.toSet())); for (final existingTileStore in existingTile.stores) { storesToUpdate[existingTileStore.name] = (storesToUpdate[existingTileStore.name] ?? existingTileStore) + ..length ..size += -existingTile.bytes.lengthInBytes + importingTile.bytes.lengthInBytes; } + for (final newConvertedRelatedStore + in convertedRelatedStores + .toSet() + .difference(existingTile.stores.toSet())) { + storesToUpdate[newConvertedRelatedStore.name] = + (storesToUpdate[newConvertedRelatedStore.name] ?? + newConvertedRelatedStore) + ..length += 1 + ..size += importingTile.bytes.lengthInBytes; + } + } else { + for (final existingTileStore in existingTile.stores) { + storesToUpdate[existingTileStore.name] = + (storesToUpdate[existingTileStore.name] ?? + existingTileStore) + ..size += -existingTile.bytes.lengthInBytes + + importingTile.bytes.lengthInBytes; + } + } - rootDeltaSize += -existingTile.bytes.lengthInBytes + - importingTile.bytes.lengthInBytes; + rootDeltaSize += -existingTile.bytes.lengthInBytes + + importingTile.bytes.lengthInBytes; - return 1; - }); - }, - ) - .where((e) => e != null) - .length - .then((numImportedTiles) { - updateRootStatistics( - deltaLength: rootDeltaLength, - deltaSize: rootDeltaSize, - ); + return 1; + }); + }, + ) + .where((e) => e != null) + .length + .then((numImportedTiles) { + root.box().putMany( + storesToUpdate.values.toList(), + mode: PutMode.update, + ); + updateRootStatistics( + deltaLength: rootDeltaLength, + deltaSize: rootDeltaSize, + ); - importingTilesQuery.close(); - existingStoresQuery.close(); - existingTilesQuery.close(); - cleanup(); - - sendRes( - id: cmd.id, - data: { - 'expectStream': true, - 'complete': numImportedTiles, - }, - ); - }); - } else { - throw UnimplementedError(); - } + importingTilesQuery.close(); + existingStoresQuery.close(); + existingTilesQuery.close(); + cleanup(); + + sendRes( + id: cmd.id, + data: { + 'expectStream': true, + 'complete': numImportedTiles, + }, + ); + }); }, ); case _WorkerCmdType.listImportableStores: diff --git a/lib/src/backend/impls/objectbox/models/src/store.dart b/lib/src/backend/impls/objectbox/models/src/store.dart index 4a32f71f..fe9bc77c 100644 --- a/lib/src/backend/impls/objectbox/models/src/store.dart +++ b/lib/src/backend/impls/objectbox/models/src/store.dart @@ -7,6 +7,8 @@ import 'tile.dart'; /// Cache for store-level statistics & storage for metadata, referenced by /// unique name, in ObjectBox +/// +/// Only [name] is used for equality. @Entity() class ObjectBoxStore { /// Create a cache for store-level statistics & storage for metadata, @@ -25,6 +27,8 @@ class ObjectBoxStore { int id = 0; /// Human-readable name of the store + /// + /// Only this property is used for equality. @Index() @Unique() String name; @@ -50,4 +54,14 @@ class ObjectBoxStore { /// /// Only supports string-string key-value pairs. String metadataJson; + + @override + bool operator ==(Object other) => + identical(this, other) || (other is ObjectBoxStore && name == other.name); + + @override + int get hashCode => name.hashCode; + + @override + String toString() => 'ObjectBoxStore(name: $name)'; } diff --git a/lib/src/root/external.dart b/lib/src/root/external.dart index e1f0e16b..3765a7c8 100644 --- a/lib/src/root/external.dart +++ b/lib/src/root/external.dart @@ -63,10 +63,11 @@ class RootExternal { /// Imports specified stores and all necessary tiles into the current root /// /// See [ImportConflictStrategy] to set how conflicts between existing and - /// importing stores should be resolved. + /// importing stores should be resolved. Defaults to + /// [ImportConflictStrategy.rename]. ImportResult import({ List? storeNames, - ImportConflictStrategy strategy = ImportConflictStrategy.skip, + ImportConflictStrategy strategy = ImportConflictStrategy.rename, }) => FMTCBackendAccess.internal.importStores( path: pathToArchive, @@ -82,8 +83,6 @@ class RootExternal { /// Determines what action should be taken when an importing store conflicts /// with an existing store of the same name /// -/// If speed is a necessity, prefer using [skip] or [replace]. -/// /// See documentation on individual values for more information. enum ImportConflictStrategy { /// Skips the importing of the store From 5887d277fca3a1f0c2c3e8d80088855c5aadb277 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Wed, 3 Apr 2024 12:52:59 +0100 Subject: [PATCH 155/168] Fixed bugs Former-commit-id: e8fdbf329a228f49ce3fe28ad6fa4190b9c5b1d9 [formerly b6f589bea2953a82a48029938f0a669f5228baac] Former-commit-id: 03d84cc1c8d86af42ffd6cbefd196bf88ac09e45 --- .../screens/export_import/export_import.dart | 3 +- .../main/pages/downloading/downloading.dart | 1 - .../main/pages/map/components/map_view.dart | 5 +- .../objectbox/backend/internal_worker.dart | 59 +++++++++---------- .../impls/objectbox/models/src/store.dart | 4 +- lib/src/providers/image_provider.dart | 1 - lib/src/providers/tile_provider_settings.dart | 4 +- 7 files changed, 36 insertions(+), 41 deletions(-) diff --git a/example/lib/screens/export_import/export_import.dart b/example/lib/screens/export_import/export_import.dart index 9db55e24..d0302f36 100644 --- a/example/lib/screens/export_import/export_import.dart +++ b/example/lib/screens/export_import/export_import.dart @@ -131,7 +131,7 @@ class _ExportImportPopupState extends State { CircularProgressIndicator.adaptive(), SizedBox(height: 12), Text( - 'Exporting your stores, tiles, and metadata', + 'Exporting/importing your stores, tiles, and metadata', textAlign: TextAlign.center, ), Text( @@ -205,7 +205,6 @@ class _ExportImportPopupState extends State { ).import( strategy: selectedConflictStrategy, ); - unawaited(importResult.storesToStates.then(print)); final numImportedTiles = await importResult.complete; stopwatch.stop(); if (context.mounted) { diff --git a/example/lib/screens/main/pages/downloading/downloading.dart b/example/lib/screens/main/pages/downloading/downloading.dart index 9a15b526..f113d800 100644 --- a/example/lib/screens/main/pages/downloading/downloading.dart +++ b/example/lib/screens/main/pages/downloading/downloading.dart @@ -108,7 +108,6 @@ class _DownloadingPageState extends State ); } - // TODO: Make responsive return DownloadLayout( storeDirectory: context.select( diff --git a/example/lib/screens/main/pages/map/components/map_view.dart b/example/lib/screens/main/pages/map/components/map_view.dart index d3a58deb..fb7b5628 100644 --- a/example/lib/screens/main/pages/map/components/map_view.dart +++ b/example/lib/screens/main/pages/map/components/map_view.dart @@ -73,9 +73,8 @@ class MapView extends StatelessWidget { metadata.data!['validDuration']!, ), ), - maxStoreLength: int.parse( - metadata.data!['maxLength']!, - ), + maxStoreLength: + int.parse(metadata.data!['maxLength']!), ), ) : NetworkTileProvider(), diff --git a/lib/src/backend/impls/objectbox/backend/internal_worker.dart b/lib/src/backend/impls/objectbox/backend/internal_worker.dart index 0a46a5b6..f7210e71 100644 --- a/lib/src/backend/impls/objectbox/backend/internal_worker.dart +++ b/lib/src/backend/impls/objectbox/backend/internal_worker.dart @@ -433,9 +433,10 @@ Future _worker( final stores = root.box(); + final queryPart = stores.query(ObjectBoxTile_.url.equals(url)); final query = storeName == null - ? stores.query(ObjectBoxTile_.url.equals(url)).build() - : (stores.query(ObjectBoxTile_.url.equals(url)) + ? queryPart.build() + : (queryPart ..linkMany( ObjectBoxTile_.stores, ObjectBoxStore_.name.equals(storeName), @@ -1347,7 +1348,9 @@ Future _worker( } final existingTileIsNewer = existingTile.lastModified - .isAfter(importingTile.lastModified); + .isAfter(importingTile.lastModified) || + existingTile.lastModified == + importingTile.lastModified; final relations = { ...existingTile.stores, @@ -1366,43 +1369,37 @@ Future _worker( )..stores.addAll(relations), ); - if (existingTileIsNewer) return null; - if (strategy == ImportConflictStrategy.merge) { - print(relations); - print(convertedRelatedStores.toSet()); - print(existingTile.stores.toSet()); - print(convertedRelatedStores - .toSet() - .difference(existingTile.stores.toSet())); - for (final existingTileStore in existingTile.stores) { - storesToUpdate[existingTileStore.name] = - (storesToUpdate[existingTileStore.name] ?? - existingTileStore) - ..length - ..size += -existingTile.bytes.lengthInBytes + - importingTile.bytes.lengthInBytes; - } for (final newConvertedRelatedStore - in convertedRelatedStores - .toSet() - .difference(existingTile.stores.toSet())) { + in convertedRelatedStores) { + if (existingTile.stores + .map((e) => e.name) + .contains(newConvertedRelatedStore.name)) { + continue; + } + storesToUpdate[newConvertedRelatedStore.name] = (storesToUpdate[newConvertedRelatedStore.name] ?? newConvertedRelatedStore) ..length += 1 - ..size += importingTile.bytes.lengthInBytes; - } - } else { - for (final existingTileStore in existingTile.stores) { - storesToUpdate[existingTileStore.name] = - (storesToUpdate[existingTileStore.name] ?? - existingTileStore) - ..size += -existingTile.bytes.lengthInBytes + - importingTile.bytes.lengthInBytes; + ..size += (existingTileIsNewer + ? existingTile + : importingTile) + .bytes + .lengthInBytes; } } + if (existingTileIsNewer) return null; + + for (final existingTileStore in existingTile.stores) { + storesToUpdate[existingTileStore.name] = + (storesToUpdate[existingTileStore.name] ?? + existingTileStore) + ..size += -existingTile.bytes.lengthInBytes + + importingTile.bytes.lengthInBytes; + } + rootDeltaSize += -existingTile.bytes.lengthInBytes + importingTile.bytes.lengthInBytes; diff --git a/lib/src/backend/impls/objectbox/models/src/store.dart b/lib/src/backend/impls/objectbox/models/src/store.dart index fe9bc77c..690f6472 100644 --- a/lib/src/backend/impls/objectbox/models/src/store.dart +++ b/lib/src/backend/impls/objectbox/models/src/store.dart @@ -55,7 +55,7 @@ class ObjectBoxStore { /// Only supports string-string key-value pairs. String metadataJson; - @override + /*@override bool operator ==(Object other) => identical(this, other) || (other is ObjectBoxStore && name == other.name); @@ -63,5 +63,5 @@ class ObjectBoxStore { int get hashCode => name.hashCode; @override - String toString() => 'ObjectBoxStore(name: $name)'; + String toString() => 'ObjectBoxStore(name: $name)';*/ } diff --git a/lib/src/providers/image_provider.dart b/lib/src/providers/image_provider.dart index 3ad38733..17065e71 100644 --- a/lib/src/providers/image_provider.dart +++ b/lib/src/providers/image_provider.dart @@ -85,7 +85,6 @@ class FMTCImageProvider extends ImageProvider { return decode(await ImmutableBuffer.fromUint8List(bytes)); } - // TODO: Test Future attemptFinishViaAltStore(String matcherUrl) async { if (provider.settings.fallbackToAlternativeStore) { final existingTileAltStore = diff --git a/lib/src/providers/tile_provider_settings.dart b/lib/src/providers/tile_provider_settings.dart index 92ad677e..e482a52a 100644 --- a/lib/src/providers/tile_provider_settings.dart +++ b/lib/src/providers/tile_provider_settings.dart @@ -50,7 +50,7 @@ class FMTCTileProviderSettings { /// To access the existing settings, if any, get [instance]. factory FMTCTileProviderSettings({ CacheBehavior behavior = CacheBehavior.cacheFirst, - bool fallbackToAlternativeStore = false, + bool fallbackToAlternativeStore = true, Duration cachedValidDuration = const Duration(days: 16), int maxStoreLength = 0, List obscuredQueryParams = const [], @@ -103,6 +103,8 @@ class FMTCTileProviderSettings { /// /// See details on [CacheBehavior] for information. Fallback to an alternative /// store is always the last-resort option before throwing an error. + /// + /// Defaults to `true`. final bool fallbackToAlternativeStore; /// The duration until a tile expires and needs to be fetched again when From 96c25a340b576ea256054202c1b3a3a7639ef604 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Wed, 3 Apr 2024 12:56:53 +0100 Subject: [PATCH 156/168] Prepared for final prerelease Former-commit-id: 4585a7207fb4ffb111445384e24a2be7e1eb8bcf [formerly 8d2ccdac9c172f8167551a1afccfd071e767a9cf] Former-commit-id: b81e33f492d480ea77a7b5366597520d99f98720 --- CHANGELOG.md | 2 +- example/pubspec.yaml | 6 +++--- pubspec.yaml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 36178c8d..24600ca5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ Many thanks to my sponsors, no matter how much or how little they donated. Spons # Changelog -## [9.0.0] - "Just Another Rewrite" - 2024/XX/XX +## [9.0.0] - "Just Another Rewrite" - 2024/04/XX This update has essentially rewritten FMTC from the ground up, over hundreds of hours. It focuses on: diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 09ff84c5..f1f7825b 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -15,18 +15,18 @@ dependencies: better_open_file: ^3.6.5 collection: ^1.18.0 dart_earcut: ^1.1.0 - file_picker: ^6.2.0 + file_picker: ^8.0.0+1 flutter: sdk: flutter flutter_foreground_task: ^6.1.3 flutter_map: ^6.1.0 - flutter_map_animations: ^0.5.3 + flutter_map_animations: ^0.6.0 flutter_map_tile_caching: google_fonts: ^6.2.1 gpx: ^2.2.2 http: ^1.2.1 intl: ^0.19.0 - latlong2: ^0.9.0 + latlong2: ^0.9.1 osm_nominatim: ^3.0.0 path: ^1.9.0 path_provider: ^2.1.2 diff --git a/pubspec.yaml b/pubspec.yaml index 681a7374..af3b4465 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_map_tile_caching description: Plugin for 'flutter_map' providing advanced caching functionality, with ability to download map regions for offline use. -version: 9.0.0-dev.7 +version: 9.0.0-dev.8 repository: https://github.com/JaffaKetchup/flutter_map_tile_caching issue_tracker: https://github.com/JaffaKetchup/flutter_map_tile_caching/issues @@ -35,7 +35,7 @@ dependencies: sdk: flutter flutter_map: ^6.1.0 http: ^1.2.1 - latlong2: ^0.9.0 + latlong2: ^0.9.1 meta: ^1.11.0 objectbox: ^2.5.1 objectbox_flutter_libs: ^2.5.1 From 0d38b5b6a7edce9a4f2162a082c2064ea40fd723 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Wed, 3 Apr 2024 13:22:57 +0100 Subject: [PATCH 157/168] Empty Commit From 75cf594e5f7710def1ecebf6218a186f81b43cb0 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Wed, 3 Apr 2024 23:52:42 +0100 Subject: [PATCH 158/168] Improved automated tests Fixed bugs Added support for in-memory database (without import/export) --- .../impls/objectbox/backend/backend.dart | 5 + .../impls/objectbox/backend/internal.dart | 28 +- .../objectbox/backend/internal_worker.dart | 57 ++- .../backend/interfaces/backend/internal.dart | 14 +- lib/src/bulk_download/manager.dart | 2 - lib/src/bulk_download/thread.dart | 1 - lib/src/store/download.dart | 1 - lib/src/store/metadata.dart | 2 +- lib/src/store/store.dart | 3 + test/general_test.dart | 469 ++++++++++++++++++ test/lib/objectbox.dll | Bin 0 -> 1009664 bytes test/lib/objectbox.lib | Bin 0 -> 101570 bytes .../{fmtc_test.dart => region_tile_test.dart} | 0 13 files changed, 529 insertions(+), 53 deletions(-) create mode 100644 test/general_test.dart create mode 100644 test/lib/objectbox.dll create mode 100644 test/lib/objectbox.lib rename test/{fmtc_test.dart => region_tile_test.dart} (100%) diff --git a/lib/src/backend/impls/objectbox/backend/backend.dart b/lib/src/backend/impls/objectbox/backend/backend.dart index a346fccf..b758954b 100644 --- a/lib/src/backend/impls/objectbox/backend/backend.dart +++ b/lib/src/backend/impls/objectbox/backend/backend.dart @@ -8,6 +8,7 @@ import 'dart:isolate'; import 'package:collection/collection.dart'; import 'package:flutter/services.dart'; +import 'package:meta/meta.dart'; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; @@ -39,16 +40,20 @@ final class FMTCObjectBoxBackend implements FMTCBackend { /// specify the application group (of less than 20 chars). See /// [the ObjectBox docs](https://docs.objectbox.io/getting-started) for /// details. + /// + /// Avoid using [useInMemoryDatabase] outside of testing purposes. @override Future initialise({ String? rootDirectory, int maxDatabaseSize = 10000000, String? macosApplicationGroup, + @visibleForTesting bool useInMemoryDatabase = false, }) => FMTCObjectBoxBackendInternal._instance.initialise( rootDirectory: rootDirectory, maxDatabaseSize: maxDatabaseSize, macosApplicationGroup: macosApplicationGroup, + useInMemoryDatabase: useInMemoryDatabase, ); /// {@macro fmtc.backend.uninitialise} diff --git a/lib/src/backend/impls/objectbox/backend/internal.dart b/lib/src/backend/impls/objectbox/backend/internal.dart index 570f713b..31b0f0a2 100644 --- a/lib/src/backend/impls/objectbox/backend/internal.dart +++ b/lib/src/backend/impls/objectbox/backend/internal.dart @@ -18,11 +18,10 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { @override String get friendlyIdentifier => 'ObjectBox'; - @override - Directory? rootDirectory; - void get expectInitialised => _sendPort ?? (throw RootUnavailable()); + late String rootDirectory; + // Worker communication protocol storage SendPort? _sendPort; @@ -117,16 +116,21 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { required String? rootDirectory, required int maxDatabaseSize, required String? macosApplicationGroup, + required bool useInMemoryDatabase, }) async { if (_sendPort != null) throw RootAlreadyInitialised(); - this.rootDirectory = await Directory( - path.join( - rootDirectory ?? - (await getApplicationDocumentsDirectory()).absolute.path, - 'fmtc', - ), - ).create(recursive: true); + if (useInMemoryDatabase) { + this.rootDirectory = Store.inMemoryPrefix + (rootDirectory ?? 'fmtc'); + } else { + await Directory( + this.rootDirectory = path.join( + rootDirectory ?? + (await getApplicationDocumentsDirectory()).absolute.path, + 'fmtc', + ), + ).create(recursive: true); + } // Prepare to recieve `SendPort` from worker _workerResOneShot[0] = Completer(); @@ -200,7 +204,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { _worker, ( sendPort: receivePort.sendPort, - rootDirectory: this.rootDirectory!, + rootDirectory: this.rootDirectory, maxDatabaseSize: maxDatabaseSize, macosApplicationGroup: macosApplicationGroup, rootIsolateToken: ServicesBinding.rootIsolateToken!, @@ -389,7 +393,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { required String url, }) async => (await _sendCmdOneShot( - type: _WorkerCmdType.deleteStore, + type: _WorkerCmdType.deleteTile, args: {'storeName': storeName, 'url': url}, ))!['wasOrphan']; diff --git a/lib/src/backend/impls/objectbox/backend/internal_worker.dart b/lib/src/backend/impls/objectbox/backend/internal_worker.dart index f7210e71..272427ad 100644 --- a/lib/src/backend/impls/objectbox/backend/internal_worker.dart +++ b/lib/src/backend/impls/objectbox/backend/internal_worker.dart @@ -65,7 +65,7 @@ enum _WorkerCmdType { Future _worker( ({ SendPort sendPort, - Directory rootDirectory, + String rootDirectory, int maxDatabaseSize, String? macosApplicationGroup, RootIsolateToken rootIsolateToken, @@ -85,7 +85,7 @@ Future _worker( late final Store root; try { root = await openStore( - directory: input.rootDirectory.absolute.path, + directory: input.rootDirectory, maxDBSizeInKB: input.maxDatabaseSize, // Defaults to 10 GB macosApplicationGroup: input.macosApplicationGroup, ); @@ -238,7 +238,11 @@ Future _worker( root.close(); if (cmd.args['deleteRoot'] == true) { - input.rootDirectory.deleteSync(recursive: true); + if (input.rootDirectory.startsWith(Store.inMemoryPrefix)) { + Store.removeDbFiles(input.rootDirectory); + } else { + Directory(input.rootDirectory).deleteSync(recursive: true); + } } sendRes(id: cmd.id); @@ -248,8 +252,8 @@ Future _worker( sendRes( id: cmd.id, data: { - 'size': Store.dbFileSize(input.rootDirectory.absolute.path) / - 1024, // Convert to KiB + 'size': + Store.dbFileSize(input.rootDirectory) / 1024, // Convert to KiB }, ); case _WorkerCmdType.rootSize: @@ -318,7 +322,7 @@ Future _worker( size: 0, hits: 0, misses: 0, - metadataJson: '', + metadataJson: '{}', ), mode: PutMode.insert, ); @@ -465,6 +469,8 @@ Future _worker( case _WorkerCmdType.writeTile: final storeName = cmd.args['storeName']! as String; final url = cmd.args['url']! as String; + + // TODO: `null` `bytes` is never actually used. Do we need to keep it? final bytes = cmd.args['bytes'] as Uint8List?; final tiles = root.box(); @@ -509,10 +515,6 @@ Future _worker( ..length += 1 ..size += existingTile.bytes.lengthInBytes, ); - updateRootStatistics( - deltaLength: 1, - deltaSize: existingTile.bytes.lengthInBytes, - ); } case (false, false): // Existing tile, update required final storesToUpdate = {}; @@ -715,12 +717,12 @@ Future _worker( (throw StoreNotExists(storeName: storeName)); query.close(); - final Map json = - store.metadataJson == '' ? {} : jsonDecode(store.metadataJson); - json[key] = value; - stores.put( - store..metadataJson = jsonEncode(json), + store + ..metadataJson = jsonEncode( + (jsonDecode(store.metadataJson) as Map) + ..[key] = value, + ), mode: PutMode.update, ); }, @@ -743,13 +745,12 @@ Future _worker( (throw StoreNotExists(storeName: storeName)); query.close(); - final Map json = - store.metadataJson == '' ? {} : jsonDecode(store.metadataJson); - // ignore: cascade_invocations - json.addAll(kvs); - stores.put( - store..metadataJson = jsonEncode(json), + store + ..metadataJson = jsonEncode( + (jsonDecode(store.metadataJson) as Map) + ..addAll(kvs), + ), mode: PutMode.update, ); }, @@ -776,8 +777,8 @@ Future _worker( query.close(); final metadata = - jsonDecode(store.metadataJson) as Map; - final removedVal = metadata.remove(key); + jsonDecode(store.metadataJson) as Map; + final removedVal = metadata.remove(key) as String?; stores.put( store..metadataJson = jsonEncode(metadata), @@ -805,7 +806,7 @@ Future _worker( query.close(); stores.put( - store..metadataJson = jsonEncode({}), + store..metadataJson = '{}', mode: PutMode.update, ); }, @@ -903,7 +904,7 @@ Future _worker( final outputDir = path.dirname(outputPath); - if (outputDir == input.rootDirectory.absolute.path) { + if (path.equals(outputDir, input.rootDirectory)) { throw ExportInRootDirectoryForbidden(); } @@ -1028,8 +1029,7 @@ Future _worker( final strategy = cmd.args['strategy'] as ImportConflictStrategy; final storesToImport = cmd.args['stores'] as List?; - final importDir = - path.join(input.rootDirectory.absolute.path, 'import_tmp'); + final importDir = path.join(input.rootDirectory, 'import_tmp'); final importDirIO = Directory(importDir)..createSync(); final importFile = @@ -1437,8 +1437,7 @@ Future _worker( case _WorkerCmdType.listImportableStores: final importPath = cmd.args['path']! as String; - final importDir = - path.join(input.rootDirectory.absolute.path, 'import_tmp'); + final importDir = path.join(input.rootDirectory, 'import_tmp'); final importDirIO = Directory(importDir)..createSync(); final importFile = diff --git a/lib/src/backend/interfaces/backend/internal.dart b/lib/src/backend/interfaces/backend/internal.dart index 5f46a976..55400632 100644 --- a/lib/src/backend/interfaces/backend/internal.dart +++ b/lib/src/backend/interfaces/backend/internal.dart @@ -2,9 +2,10 @@ // A full license can be found at .\LICENSE import 'dart:async'; -import 'dart:io'; import 'dart:typed_data'; +import 'package:meta/meta.dart'; + import '../../../../flutter_map_tile_caching.dart'; import '../../export_internal.dart'; @@ -32,12 +33,6 @@ abstract interface class FMTCBackendInternal /// Generic description/name of this backend abstract final String friendlyIdentifier; - /// The filesystem directory in use - /// - /// May also be used as an indicator as to whether the root has been - /// initialised. - Directory? get rootDirectory; - /// {@template fmtc.backend.realSize} /// Retrieve the actual total size of the database in KiBs /// @@ -86,6 +81,8 @@ abstract interface class FMTCBackendInternal /// /// This operation cannot be undone! Ensure you confirm with the user that /// this action is expected. + /// + /// Does nothing if the store does not already exist. /// {@endtemplate} Future deleteStore({ required String storeName, @@ -99,6 +96,8 @@ abstract interface class FMTCBackendInternal /// /// This operation cannot be undone! Ensure you confirm with the user that /// this action is expected. + /// + /// Does nothing if the store does not already exist. /// {@endtemplate} Future resetStore({ required String storeName, @@ -173,6 +172,7 @@ abstract interface class FMTCBackendInternal /// * `null` : if there was no existing tile /// * `true` : if the tile itself could be deleted (it was orphaned) /// * `false`: if the tile still belonged to at least one other store + @visibleForTesting Future deleteTile({ required String storeName, required String url, diff --git a/lib/src/bulk_download/manager.dart b/lib/src/bulk_download/manager.dart index fb2c5f91..c27e9a46 100644 --- a/lib/src/bulk_download/manager.dart +++ b/lib/src/bulk_download/manager.dart @@ -6,7 +6,6 @@ part of '../../flutter_map_tile_caching.dart'; Future _downloadManager( ({ SendPort sendPort, - String rootDirectory, DownloadableRegion region, String storeName, int parallelThreads, @@ -189,7 +188,6 @@ Future _downloadManager( ( sendPort: downloadThreadReceivePort.sendPort, storeName: input.storeName, - rootDirectory: input.rootDirectory, options: input.region.options, maxBufferLength: threadBufferLength, skipExistingTiles: input.skipExistingTiles, diff --git a/lib/src/bulk_download/thread.dart b/lib/src/bulk_download/thread.dart index cd26a1d3..6f512a7e 100644 --- a/lib/src/bulk_download/thread.dart +++ b/lib/src/bulk_download/thread.dart @@ -7,7 +7,6 @@ Future _singleDownloadThread( ({ SendPort sendPort, String storeName, - String rootDirectory, TileLayer options, int maxBufferLength, bool skipExistingTiles, diff --git a/lib/src/store/download.dart b/lib/src/store/download.dart index 3d5ad499..5c3674c0 100644 --- a/lib/src/store/download.dart +++ b/lib/src/store/download.dart @@ -173,7 +173,6 @@ class DownloadManagement { _downloadManager, ( sendPort: receivePort.sendPort, - rootDirectory: FMTCBackendAccess.internal.rootDirectory!.absolute.path, region: region, storeName: _storeDirectory.storeName, parallelThreads: parallelThreads, diff --git a/lib/src/store/metadata.dart b/lib/src/store/metadata.dart index ec96216a..7a7a1a4f 100644 --- a/lib/src/store/metadata.dart +++ b/lib/src/store/metadata.dart @@ -33,7 +33,7 @@ class StoreMetadata { .setBulkMetadata(storeName: _storeName, kvs: kvs); /// {@macro fmtc.backend.removeMetadata} - Future remove({ + Future remove({ required String key, }) => FMTCBackendAccess.internal diff --git a/lib/src/store/store.dart b/lib/src/store/store.dart index 95a8af23..0031c624 100644 --- a/lib/src/store/store.dart +++ b/lib/src/store/store.dart @@ -82,4 +82,7 @@ class FMTCStore { @override int get hashCode => storeName.hashCode; + + @override + String toString() => 'FMTCStore(storeName: $storeName)'; } diff --git a/test/general_test.dart b/test/general_test.dart new file mode 100644 index 00000000..8bdf89ab --- /dev/null +++ b/test/general_test.dart @@ -0,0 +1,469 @@ +// Copyright © Luka S (JaffaKetchup) under GPL-v3 +// A full license can be found at .\LICENSE + +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:flutter_map_tile_caching/custom_backend_api.dart'; +import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; + +// bash <(curl -s https://raw.githubusercontent.com/objectbox/objectbox-dart/main/install.sh) + +void main() { + setUpAll(() { + // Necessary to locate the ObjectBox libs + Directory.current = + Directory(p.join(Directory.current.absolute.path, 'test')); + }); + + group( + 'Basic store usage & root stats consistency', + () { + setUpAll( + () => FMTCObjectBoxBackend().initialise(useInMemoryDatabase: true), + ); + + test( + 'Initially zero/empty', + () async { + expect(await FMTCRoot.stats.length, 0); + expect(await FMTCRoot.stats.size, 0); + expect(await FMTCRoot.stats.storesAvailable, []); + }, + ); + + test( + '"store1" creation', + () async { + await const FMTCStore('store1').manage.create(); + + expect(await FMTCRoot.stats.length, 0); + expect(await FMTCRoot.stats.size, 0); + expect( + await FMTCRoot.stats.storesAvailable, + [const FMTCStore('store1')], + ); + }, + ); + + test( + 'Duplicate creation allowed', + () async { + await const FMTCStore('store1').manage.create(); + + expect(await FMTCRoot.stats.length, 0); + expect(await FMTCRoot.stats.size, 0); + expect( + await FMTCRoot.stats.storesAvailable, + [const FMTCStore('store1')], + ); + }, + ); + + test( + '"store2" creation', + () async { + await const FMTCStore('store2').manage.create(); + + expect(await FMTCRoot.stats.length, 0); + expect(await FMTCRoot.stats.size, 0); + expect( + await FMTCRoot.stats.storesAvailable, + [const FMTCStore('store1'), const FMTCStore('store2')], + ); + }, + ); + + test( + '"store2" deletion', + () async { + await const FMTCStore('store2').manage.delete(); + + expect(await FMTCRoot.stats.length, 0); + expect(await FMTCRoot.stats.size, 0); + expect( + await FMTCRoot.stats.storesAvailable, + [const FMTCStore('store1')], + ); + }, + ); + + test( + 'Duplicate deletion allowed', + () async { + await const FMTCStore('store2').manage.delete(); + + expect(await FMTCRoot.stats.length, 0); + expect(await FMTCRoot.stats.size, 0); + expect( + await FMTCRoot.stats.storesAvailable, + [const FMTCStore('store1')], + ); + }, + ); + + test( + 'Cannot reset/rename "store2"', + () async { + expect( + () => const FMTCStore('store2').manage.reset(), + throwsA(const TypeMatcher()), + ); + expect( + () => const FMTCStore('store2').manage.rename('store0'), + throwsA(const TypeMatcher()), + ); + + expect(await FMTCRoot.stats.length, 0); + expect(await FMTCRoot.stats.size, 0); + expect( + await FMTCRoot.stats.storesAvailable, + [const FMTCStore('store1')], + ); + }, + ); + + test( + '"store1" reset', + () async { + await const FMTCStore('store1').manage.reset(); + + expect(await FMTCRoot.stats.length, 0); + expect(await FMTCRoot.stats.size, 0); + expect( + await FMTCRoot.stats.storesAvailable, + [const FMTCStore('store1')], + ); + }, + ); + + test( + '"store1" rename to "store3"', + () async { + await const FMTCStore('store1').manage.rename('store3'); + + expect(await FMTCRoot.stats.length, 0); + expect(await FMTCRoot.stats.size, 0); + expect( + await FMTCRoot.stats.storesAvailable, + [const FMTCStore('store3')], + ); + }, + ); + + tearDownAll( + () => FMTCObjectBoxBackend() + .uninitialise(deleteRoot: true, immediate: true), + ); + }, + timeout: const Timeout(Duration(seconds: 1)), + ); + + group( + 'Metadata', + () { + setUpAll(() async { + await FMTCObjectBoxBackend().initialise(useInMemoryDatabase: true); + await const FMTCStore('store').manage.create(); + }); + + test( + 'Initially empty', + () async { + expect(await const FMTCStore('store').metadata.read, {}); + }, + ); + + test( + 'Write', + () async { + await const FMTCStore('store') + .metadata + .set(key: 'key', value: 'value'); + expect( + await const FMTCStore('store').metadata.read, + {'key': 'value'}, + ); + }, + ); + + test( + 'Overwrite', + () async { + await const FMTCStore('store') + .metadata + .set(key: 'key', value: 'value2'); + expect( + await const FMTCStore('store').metadata.read, + {'key': 'value2'}, + ); + }, + ); + + test( + 'Bulk (over)write', + () async { + await const FMTCStore('store') + .metadata + .setBulk(kvs: {'key': 'value3', 'key2': 'value4'}); + expect( + await const FMTCStore('store').metadata.read, + {'key': 'value3', 'key2': 'value4'}, + ); + }, + ); + + test( + 'Remove existing', + () async { + expect( + await const FMTCStore('store').metadata.remove(key: 'key2'), + 'value4', + ); + expect( + await const FMTCStore('store').metadata.read, + {'key': 'value3'}, + ); + }, + ); + + test( + 'Remove non-existent', + () async { + expect( + await const FMTCStore('store').metadata.remove(key: 'key3'), + null, + ); + expect( + await const FMTCStore('store').metadata.read, + {'key': 'value3'}, + ); + }, + ); + + test( + 'Reset', + () async { + await const FMTCStore('store').metadata.reset(); + expect(await const FMTCStore('store').metadata.read, {}); + }, + ); + + tearDownAll( + () => FMTCObjectBoxBackend() + .uninitialise(deleteRoot: true, immediate: true), + ); + }, + timeout: const Timeout(Duration(seconds: 1)), + ); + + group( + 'Tile operations & stats consistency', + () { + setUpAll(() async { + await FMTCObjectBoxBackend().initialise(useInMemoryDatabase: true); + await const FMTCStore('store1').manage.create(); + await const FMTCStore('store2').manage.create(); + }); + + final tileA64 = + (url: 'https://example.com/0/0/0.png', bytes: Uint8List(64)); + final tileA128 = + (url: 'https://example.com/0/0/0.png', bytes: Uint8List(128)); + final tileB64 = + (url: 'https://example.com/1/1/1.png', bytes: Uint8List(64)); + final tileB128 = + (url: 'https://example.com/1/1/1.png', bytes: Uint8List(128)); + + test( + 'Initially semi-zero/empty', + () async { + expect( + await const FMTCStore('store1').stats.all, + (length: 0, size: 0, hits: 0, misses: 0), + ); + expect( + await const FMTCStore('store2').stats.all, + (length: 0, size: 0, hits: 0, misses: 0), + ); + expect(await FMTCRoot.stats.length, 0); + expect(await FMTCRoot.stats.size, 0); + expect( + await FMTCRoot.stats.storesAvailable, + [const FMTCStore('store1'), const FMTCStore('store2')], + ); + }, + ); + + test( + 'Write tile (A64) to "store1"', + () async { + await FMTCBackendAccess.internal.writeTile( + storeName: 'store1', + url: tileA64.url, + bytes: tileA64.bytes, + ); + expect( + await const FMTCStore('store1').stats.all, + (length: 1, size: 0.0625, hits: 0, misses: 0), + ); + expect(await FMTCRoot.stats.length, 1); + expect(await FMTCRoot.stats.size, 0.0625); + }, + ); + + test( + 'Write tile (A64) again to "store1"', + () async { + await FMTCBackendAccess.internal.writeTile( + storeName: 'store1', + url: tileA64.url, + bytes: tileA64.bytes, + ); + expect( + await const FMTCStore('store1').stats.all, + (length: 1, size: 0.0625, hits: 0, misses: 0), + ); + expect(await FMTCRoot.stats.length, 1); + expect(await FMTCRoot.stats.size, 0.0625); + }, + ); + + test( + 'Write tile (A128) to "store1"', + () async { + await FMTCBackendAccess.internal.writeTile( + storeName: 'store1', + url: tileA128.url, + bytes: tileA128.bytes, + ); + expect( + await const FMTCStore('store1').stats.all, + (length: 1, size: 0.125, hits: 0, misses: 0), + ); + expect(await FMTCRoot.stats.length, 1); + expect(await FMTCRoot.stats.size, 0.125); + }, + ); + + test( + 'Write tile (B64) to "store1"', + () async { + await FMTCBackendAccess.internal.writeTile( + storeName: 'store1', + url: tileB64.url, + bytes: tileB64.bytes, + ); + expect( + await const FMTCStore('store1').stats.all, + (length: 2, size: 0.1875, hits: 0, misses: 0), + ); + expect(await FMTCRoot.stats.length, 2); + expect(await FMTCRoot.stats.size, 0.1875); + }, + ); + + test( + 'Write tile (B128) to "store1"', + () async { + await FMTCBackendAccess.internal.writeTile( + storeName: 'store1', + url: tileB128.url, + bytes: tileB128.bytes, + ); + expect( + await const FMTCStore('store1').stats.all, + (length: 2, size: 0.25, hits: 0, misses: 0), + ); + expect(await FMTCRoot.stats.length, 2); + expect(await FMTCRoot.stats.size, 0.25); + }, + ); + + test( + 'Write tile (B64) again to "store1"', + () async { + await FMTCBackendAccess.internal.writeTile( + storeName: 'store1', + url: tileB64.url, + bytes: tileB64.bytes, + ); + expect( + await const FMTCStore('store1').stats.all, + (length: 2, size: 0.1875, hits: 0, misses: 0), + ); + expect(await FMTCRoot.stats.length, 2); + expect(await FMTCRoot.stats.size, 0.1875); + }, + ); + + test( + 'Delete tile (B(64)) from "store1"', + () async { + await FMTCBackendAccess.internal.deleteTile( + storeName: 'store1', + url: tileB128.url, + ); + expect( + await const FMTCStore('store1').stats.all, + (length: 1, size: 0.125, hits: 0, misses: 0), + ); + expect(await FMTCRoot.stats.length, 1); + expect(await FMTCRoot.stats.size, 0.125); + }, + ); + + test( + 'Semi-write tile (A(128)) to "store2"', + () async { + await FMTCBackendAccess.internal.writeTile( + storeName: 'store2', + url: tileA128.url, + bytes: null, + ); + expect( + await const FMTCStore('store1').stats.all, + (length: 1, size: 0.125, hits: 0, misses: 0), + ); + expect( + await const FMTCStore('store2').stats.all, + (length: 1, size: 0.125, hits: 0, misses: 0), + ); + expect(await FMTCRoot.stats.length, 1); + expect(await FMTCRoot.stats.size, 0.125); + }, + ); + + test( + 'Semi-delete tile (A(128)) from "store2"', + () async { + await FMTCBackendAccess.internal.deleteTile( + storeName: 'store2', + url: tileA128.url, + ); + expect( + await const FMTCStore('store1').stats.all, + (length: 1, size: 0.125, hits: 0, misses: 0), + ); + expect( + await const FMTCStore('store2').stats.all, + (length: 0, size: 0, hits: 0, misses: 0), + ); + expect(await FMTCRoot.stats.length, 1); + expect(await FMTCRoot.stats.size, 0.125); + }, + ); + // then real semi-write to store2 + // then delete as above + // then other sizes + + tearDownAll( + () => FMTCObjectBoxBackend() + .uninitialise(deleteRoot: true, immediate: true), + ); + }, + timeout: const Timeout(Duration(seconds: 1)), + ); +} diff --git a/test/lib/objectbox.dll b/test/lib/objectbox.dll new file mode 100644 index 0000000000000000000000000000000000000000..cbf26108515c6104ef6d7bd6c751f35e8c416c1a GIT binary patch literal 1009664 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 z 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⩔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

FRibemT`d2;wb^>6dP5$ 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 VK=HO8PxmOq`&gM=&i|AD{4ak{fCc~n literal 0 HcmV?d00001 diff --git a/test/lib/objectbox.lib b/test/lib/objectbox.lib new file mode 100644 index 0000000000000000000000000000000000000000..711c8f3f09835e518527f2fb4302ba5da589f76f GIT binary patch literal 101570 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 literal 0 HcmV?d00001 diff --git a/test/fmtc_test.dart b/test/region_tile_test.dart similarity index 100% rename from test/fmtc_test.dart rename to test/region_tile_test.dart From bc54ce6eca98e3ed57c3df5e510c0edbafa27f4a Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 4 Apr 2024 11:02:19 +0100 Subject: [PATCH 159/168] Fixed GitHub Workflow --- .github/workflows/main.yml | 2 ++ .gitignore | 1 + test/general_test.dart | 5 ++++- test/lib/objectbox.dll | Bin 1009664 -> 0 bytes test/lib/objectbox.lib | Bin 101570 -> 0 bytes 5 files changed, 7 insertions(+), 1 deletion(-) delete mode 100644 test/lib/objectbox.dll delete mode 100644 test/lib/objectbox.lib diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 773d019f..e3ea1f2d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -63,6 +63,8 @@ jobs: channel: "beta" - name: Get Dependencies run: flutter pub get + - name: Install ObjectBox Libs For Testing + run: cd test && bash <(curl -s https://raw.githubusercontent.com/objectbox/objectbox-dart/main/install.sh) --quiet - name: Run Tests run: flutter test -r expanded diff --git a/.gitignore b/.gitignore index eb120e3c..a4fb7a9d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Custom local/ +test/lib/ # Miscellaneous *.class diff --git a/test/general_test.dart b/test/general_test.dart index 8bdf89ab..1788770c 100644 --- a/test/general_test.dart +++ b/test/general_test.dart @@ -9,7 +9,10 @@ import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; import 'package:path/path.dart' as p; import 'package:test/test.dart'; -// bash <(curl -s https://raw.githubusercontent.com/objectbox/objectbox-dart/main/install.sh) +// To install ObjectBox dependencies: +// * use bash terminal +// * cd to test/ +// * run `bash <(curl -s https://raw.githubusercontent.com/objectbox/objectbox-dart/main/install.sh) --quiet` void main() { setUpAll(() { diff --git a/test/lib/objectbox.dll b/test/lib/objectbox.dll deleted file mode 100644 index cbf26108515c6104ef6d7bd6c751f35e8c416c1a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1009664 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 z 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⩔RnsKA7PqgJA+L8bHmIJj(yc`7@Sx;;UjVWs5810$P&vvv?C2 zyd*`_=`%{lncZbPGxO)~uo6wG8Q52F~W8mgV7M6?%+)@RXJ zP!|9GjTx&?X1<5knvZhUM@_tg(YgJulL`nB(Qj3O+P zCuX|&?Cjw+h`6fI^-NV4;4T*iFLhoyQs%C_KM+Qxt{^#Mf>=G4yTymYwKFc@ZqLF+ zUwBssM)9@dlG{{oVO7MP2TPs!Uu342T2CpJTF)QliEQ*wl(v9eFizMb1FVj1=i2Wy<$;s76 z5IvJ+?9vk8DslcYk5rjs6}$ZEyg{MFDkfnbi4AA;jU=+eXl6uX<+mhN!mL|Ri(-$J zHBrtxBqksyvL%WKVkk%kVMV_=t*g)V+%oxv6BCn)fvXP?m%BnxDC5cL=q=4(N%LC} zAFq_*&Pc)*emHTg5;aYsrX2cqoK&N%*0 z^#zZgWz(2&@8zRpR9A@OZu11gyMF+sUV6pQ3}%rUIZM^5ewPG>D3gbr2!{~yR8TCrW9b)gwB9Rm4m*NGQbI&}=SaW{xlzgG&)BIJU z=CE9^I!^q%fRdJ{a3nv8AIw|1PR2~}Yx-4K%)H^ueGVnSP<9}1xsuX+NzZ7!Ykoq_ z)Dri$-GO?i_UDOIVw%_`<#PCYq^;a}!6I{4ELg8hpJb*10%(1+$MD-Oyzu_C{Mwd%WXH?Wo2Ik-XNB{e*AT6L zZQ{3;^I3OFwrpsrYTcD^U;h=agIllkZ~r1ne_eMr0%ZZ>7(%hPBS# z=XWK{nvY`)E$%wb^sqdxcUah13!7}p(4qoK-$Y=*UaO#;mo}jf^7185v4$`|IG`;s zU~#aZl~tknxC$E<_?zDQhVQMu=i17$>-u!CgtUNLbGak?O6pr11`zq*(CTEFcF|4h zpHlF?1ia3}B!*M1K<+)|Tu=AhavY0N66Bs3EwM`&#A}=z*wXGRB9SqxO6<(8kyK2C zF*k9-FvrSHw0w7U#D6WlU+`P=*TWhzf-@(~QWD2S5R8}1++eJ>lVM@e>|>VzlvV27 zL!#OskqM=8T3K|2#n{fbDlZsiEuvoeL)TmpsFX}I7mOk?;b{Nh8c{a6Xc}AeO}B?q zROx}8(o5|5QEXo=aT4(_5`n1Lfpq#)k?MxL+qsk_v2@CI2-D1?X$cd6jXWL-*h^VH z(ezzgnGA3l$!JTno0u(e;#6N)!~rtVv#f}p1+fANW3F%per=pD z$w`%1b^KWI#MK#6YbQTlsnsc$u74{TKcq?68Sa?+apxJ3%`_sMDU;U$CjI`^MX?!- zy?V|ItXGZc-%lx|;yBYN;7A;lu7Cev8&zApFhA{L7kkN@ztH^MmgXAs5*A2 zWB%$04qEK2YXt9+5j6})MjZ1|h4Ba}K~JVuFJHV?>>SG-m!^lxW#*sDZ>kSTB(8XN zTyqX0%T~C%u`NVPjoko|Zz`)GWo7hESnXb7qzR^99>Pb7Lh`z7DXXvAHGI58K9Wek zJ<%S$-NNNEW^Qe8S#1Nnf^w=l-Ayu&N?@LVoJpXr_=@2N1q|>3+GwnwG*-RFJ2ZMk z6-+3h{@J0zMWd7~Cq(m-9}PgGnkmv;ajwNm?Iy6*&OBK2?rcdU4?*j#F6J>^pwa$sK>;9_?jhSN)g$R7z* z;?1@CRd~a0gveXJLx`mPv4;{PK$KLM3XFfckB^ql?-n#Ouv^)9&1W=e_i4^k0bCH5g-6PaJk=b2y_{SZ#Z9MGsx~AxHH| zG1)C&&x=F?^ugx^!&<&tzCQ8IIa$iv-vrvW6F2r}-JcVfmB=c}%9>n>$L_cQw7jP` zFF7l&6n4TB_POv?+U9FH_3II^T)Dopq563^ei!&>izQQi+A z2QkDK@4ybvJ5VC)|6RNT)2hTXFysp9>Ny&{$e9kgR!(~$I-Ij;`?3N1{35w8oLZAM z4`Zdz{3qkYx%|t|*{nN7KGXdzDbb|aWzD>p_t;XEPHD1Rb~{JfN|9KuG*?Z@U&46} z<@|^EeuQZcr~ESH?-n*A%pKl{nERzxMBwXP(@FR)v~t!GA%#Yz#t;I}vqUolmda1} zyobYNss_Win+aK#a4?XM`*%>RqZeSN$pGyfCproPz*A(*R5teR~L9 zg_6!}t)29__fv7^X3KRD(TF|t#!Sg)AV}3`_G=zVA59e^Hbh~sBQ$WplkeoQvL>lBLK0g^a^1XGXQT)9t8n8d(BwSQ7RzLHuX!}hJbF}e3A!ts%Z`;^|5#erXy|+H7P-kJ&d~a?wC{ZJQ@U!-(@4!) z@i90=Xt~_9+}tFv6%Lb@|MH~UvNVu9uQr)7YvK*=EN*gD>G1)d=z=J_gX*egG=}nI zZw($dNx%Du0|*P10WT5Ol4+z4GN2HiqQYX|Lnyy)lVQ*cjjKqPhu&~12SPuB&sY=4!&lUlZaeYOFh)R|%cc&mq}G<|@zhWz)2(!<7YspGN6@Z07P z8`lHy6l2KwOgYL7Pln2CF<~%KzD{5u!S6L!3ctV2xI+W`yM7gJM?N}NJdw`S!XfDb zUgbv@`-dG9vAh9)6^_V;U#uvaF_J%e1JV}_92uOMfB<645{#80vN2U7r;u^QHSl)$yA-s2O*UrYT005fV zAp909ZoFNaIZ2wC_w!^k-f%SY5%DFuJtQd~hmX&YkH&!aUC+lm^~^%yhWgJSsDfE|Eta^2q(`f=_cu1vNfxiY-*5yYOf!_J9-@m^Q&K@2N5^Mn({N9Hf9 z9g-Efy`o3>L`*l56sfjQ8Wg{$XE4tU4lrrDNgadWco1bN$*?tP{tjI$MCY!yO2R(s z#SG-6*^2?A^S4cR4I$(RJt7#JQ7S8h0C;>JPjboahQ`n0Qit=%FLx#8)(#P{Bt=`5 zgpZ6B-CgP_pq}^`ei;OsK`4`i2!*tLV_TWIOHo4W8S|V`urnHN6f@KwR^pcx%^V4P zQv(VFM%qf84al?>do{=ZWOLl3&GVWPYX~tl54Dl8qqDDWP}9SK>N=t$E22|4D<{YI z++5N8TIPE)V%;Y!vf7S{04SvOK#1Uv8^O;Y=Rnb`mKMs4W~Ie#bCu+=$@ z9<}&mGf}_lx2j`4*02MA^lqHrq@BK`Uoduci4_YU7MEk)1di~TrtCatH)eyBgd|zUO+*Wv@F$a?7~5JhFzhbMeps zUxg?{RRZa#AXncF#yIE4iiOH9kKWlRdpm7RbF4+6l8tj>sEgg$gD)6&Sbm^#cxgb$ zCVx$V0Ys5-Gdf&?$A_0|)QG=gc)5g(`0Z?ebU3HG_2BC?p63uqS_N=?4e4b2{Z9MN zM$4YU%rFIK@XpYXrn{=L!tgcQLJn}=71c}}{f97x#5KQuhDeHWczDPwdN2|tYSBud`HS`!N%&dQ%rmCh41$AbB4=TSPDtZE)riM^B? zs*h~{QtG<%5x1(bLdUVrd2UtHohG-cMuA+bIzPRtOWdk@g#4IeTGghka3M@N4`!O@ zWNSqY3!EygCpKN(Eb;RyMuC{JWi92xG#Rc8FO39BZ6q zAja*|8s}AuF6n5&fw0oqOJbeQ+@!oZr|I?~8f$=T_-Ah<34|xU=UXKhGe>M>iL;g= z-*kIbRv7zZgDmrpVP{~A`a&Non@%54NEChtFk?CIOh1RwTIpFr(t!?#)RBfwPHO-*6MDYfS;-B!U zc-W+`NtIsM>saw`l(NW=7u1p6EnbDr-5GU>1f@SoUvU;P%d>E7e+6Qb8GeSe=(#A# zUq{Q%aR&?E?(Qs%?CudhLt}X(?Uqmv3w)*X)IsvN-Lj2eGie*uAP8NL@|8={*0!2! zOQ|?^F{5*(gQ}9%xs+K7Ayqu_Iit9gI;ZU+jz3oX_!mZTDRq7|T8%XFOS2_p)<-Qm zl=6mNkN=aF zEFo220qRUr9TGyc`XjA9>JeKKD|qCAvREN!?mlfI)ICG>PYg?lA$Vg`JsQWoe^wpl z$NE+H^j2oB{tt=95i5RI?};4x{VpZLA)k9>I2!MgtIFtC&BYx1VcctUbdP0U4r&9c zIhO~SQZ=_NX`02|e^{V;Q@F_4%>3U)LE!=Y2)qRE%HK>vZSG>ch=Smp@KX&aZ5{wA zSx&-*P2$GIrOtJFKU6M(AgDlsXqJ7$y(LgDzEe;pCsbXvMF!0U?4;A5CvmE)7GSXz zeZ}mG(X_j5rA{j(K=)`~NMXD1!&}nB3~-2y5;bh}U!Rhyi8|2ip-pEWEl~&8fLw_- zAd0tPz$rfr z<6YxQQo3U>se3FnUtmw5o_YD5lJQ93ch8Oc;P)kAc7xwR!sk@r z$rRUgw!S`s*A*p`!0*)=4F87n#E&!a%W!c6Y&cBbFkS|J57KTzEHiY&Cvyl%DRa3h z>XlofBPu$-t7H6zjWXoNB0j z0n_For@n7#nrZY67<{J8RR=_7Vm1wMG2WHcJ)(?%M2MnvV3v?YhHs@u%D-cz=xD$> z!itJ8ZRC7imbZ|PhfZVj(Pc?V7;@qtbLKpyH-Id?cRjDfcn-tYd|?~tJ6m| zngf#S_el;y)pt+@-sB`h%B*x&J|G=QrsZVrER|U?88H+j(8rcnJK|eq1hAhSQEaAI zmj7(dZ9??-XJ1eFVCNl%C8EEN+NLmP`4SZ;2lY-86upSdsOZ6yB>$>D1bqTU zauwbw&yo3_T2z^O)6^sLQ`+));YBHZAtfY`B*EqxrM}sIonzoJOc?O^bLS$*jQS$kO%0 zf@{7FAHmcN4Oejg2E$PJ=#x-?wy5lnr5|`$IKlMM_Z(d91`u5C2h3SZn*iEBhG~KE zL0py$r&GAq$=>$jA3W{pbyxjU6^d6nJ%!@c^D`6*vG0*j8RKEIa8t0l1{Ln%c2crN z+VY+8Z^}fTBf&bg`pmW4^E5FW278*B(DAi&PY&2QJ`k(SnWea6E>hy@uBr|5Y2>xBPmJRDcE-|_GeV!G>i*nMud<3Zvz zUw1B6fV$J4x*ZRvCOP@xCAh&wRXlZFB4N@L<*9sPe5A?#$d@pY8bJR_^xpuz(#OY; zbN=P{xaI%6@p1M?IzE2oc;n;u7*S~=vD@(>QZ{J!2oCY*>kttSSH5%Y+AjU6O&=70 z60=(S!=lr_GN3VCRsYkufE9yo28Iix`scd1=324WYLt`mVnLVR>MP9gEm&;EQhP=; zqsUoV-{1)HxU68LrQH8hXaNMMN;{u#e(1~W>TE?wTN6zQq zN1UJURKM{J)?809=tL!h)))^GadWJ`8Oo`7K3iWLyoSZ-*<}9uHA*e9)}q=Go+}=x z(ar=Lks!`63BU~u_$nE8J6QS^Ynbmu?(YOXW)Fo0T*P3-46K5 zDv)5?1^8Ha<&wEUyP*ZIYhb|AU;ze3Lkq#S^^f?Qwo0&Vs~c?F$`M|VXt1q&z}LKn z0jnB_T#DhOX%RCoRzHtg=PFz_aK(RxAlBF$P$V|h8!>~WzF^tkK3vs!4cTW&DKKCa zAuK-e=c$lo+WKn1z(jpS&@*ePP(R(Pk{1@T}$d_Dv8lGtdQ=Y+SG^FC&^; z%J4@JFgxeD8NKY^4Fax^Am9pN2)crSzRLOXC5ZvXT}c<6f6((5!(s1pDKWq)&QD=> z9*ysd>(0X)>{mHbMcsx3G&zk7c#DGzqvz*)L}hEJXs^(&$Y~=Qp%W98wvD&UH@ks} zUlc|mFMdnc5_t;lh!5rwGxv$ti)6xi_si`nnC|$S2qu0PV>`(L&!8uDqIvOW@>Mut zEfdYAh;buq(tEsny(wGcmEDY8(Bd4}Y66L)-fK1HwZI2%6VLKmn&{I@CR!l{GcQwZ ze0594WXc;so2?_s0Qum64tbA$#Wyexc<#-&;=JGNs?kpo{f$~Ln&e~tUj`S%5hD@ZyL%NX7e3LRDoyzLmcN7M976suHOV9>9N*zDd&3Z zAL~lJ@DlbQ!EV;Kyc&p%DB97$^2Diz8T`Js;mRq;=p4@3v#I;$0AKe$2lxsCnk61i zoLktz^j&D6)EG@{E)#k*b(p?fc544w zV@0w{Y~H;0bPDMGj?qmkoCE#otgAe%V)>URChxoQ&9moZr-cOJ|3zeeG~0O`bXc~5 zOJl@P%ohGVxq`9AX+4=2Vj~-!%2#AQVhLq9m9yfd5%P}+v^Xh#B5z5DGNW09KC?d3 zPTP404NI9iZiJ$+Ec=ORPZ&TPBCbh$kBsD0RM$V(2@pcQ$G0j9dcN(c=LggzyQ=+9 zUtgK$-?v2^D>QnZ6Ecm4;8UoZTynSUA~st|mwp)Cm6)GRKo8d=xE`E>&S3^{4Q|#| zV#TA9;C=J1f_ME}9(cE6GAUf%M>>32`-Sfu=QZ+*2{Rqcc+JdRgHLQZPpoJ`I>x@+ z1*R8K1Psc)XJFc}#e-=J_K*ioT6~ktR3)13t5oNV-DY!SVibW( z+kAyn+O_#K`SL8jeQ!0@YZ}%%+*vN`=AL9Z(KIG9NrvvjuX zv(x)zb}XAnShIU+j1_%My_L>GbP~@lZh|$=Xqi8W>!~sKa{rXw*<7~ ze8(GJ57rLp+Jj8|UsL_<_^(Uizw9H?dkluxf?<)9^YXh#@>7FS0Ghbz1Ck)VxLH9Q zm;~{pOc1-`_rP+&pFm8kkAp<|1c z!*;M)B4DQ6C|N7e0;-%jGsHp6c6OH8^(d!OC#1JF$pYBr9PJGMs26C1(+OeJ#4mNS zZg;KyD(8#aq=x$xC{e$PMGrEWP~m$OL!bW0=v||n@=Z#UM_s*Zl=JAB^cdUwd3~k0 z=6-pzFy0CyUlq!a-OYFBANpR!C~J_EAu?KGupI8na7LH>F-Fz_FS%gBd zs&gPJ(OmM#i_hJ>mK<6;%cb(i(udYHB0ewe5I{vS%ay#3vErLIcw7<2yCbm-mC0A$ zdqZD2FZs$v@=BLK!56l%n-9mGST>&NcFLPrh;xvGv%~>z@?5QPa*n}4G;O6!)$rgO8XWb!X?Ko(d2wTh5LL0LMxcP-kD?mJ%u46(Qn_(oS(7|~scVkIy&p3> zOEdD3CrjT+Y1c;E-Ji>}eI5?K8H&ZuHsI5?SHGrhpP4Luu9QyyOU<|IPfzFUBu^ji z+`tZ{B&PzUc!t;zO|GVR@yZ0`sAZSUCb+dJHe65o}i7ug@vF`1Wa=}+qwlSd|7 zD$|zI_4U@u^d2GWl1wqgnf;!pP^@$;&5P6FRG53YsOde@)ZfDl2r_C^@XuP}VfPdR z!y~Ib?9Ri=O2=+Geg|hjlLX~(r?;NB;99m%%a~7bPI0oWCF``mCnnoEP1{QG%lg6T zy-lk>ny%jlGwc7$Vy`dbO#Q1~_4@K!h4h8}qG_=ceE|ffD`rdQ*@&4NLnK=R|$Y zt-w>#=Nzj@5{iuB=xI?{m+Jh!(s_~ZPSW@lZ`tnkEV@=P_vtFHXM3?{Wa?Rvd}X3} zWli#xjq-~8rSntcsVNL&n9^_Ln+v{YU!A=_f5A8b4C%hoIZQ!y>HQ*e5A>F-)5AzIfyf4VdH zCLx?Wo>SZCI(KTs?6<_y*WI3Hs^U{z>gqqwi~e&XGXxcVB|wtj#5`IpkNomTdzH2y z_ycm{~t{7X45;-NNx z?ltt;If+F01QrMdHyQxYX>jwoLBOlefqje!`uyA1s#l4o`^-x{R|jU9loc zi{4>ZtT zCuF~QDqPDUyDOcXVu=MXE(7draD*L zAic~@>*a2EfnI)ps=L3r$km6t^e=RW*SFB*B-$I&`jzTZMT_&<_7wgLOpk^UzSoOg z%7*jZtzA`YE_3Ok5MOK<^D6x+JY67uYLY;F(ZMOY3^x$J98S{3B@goA5*K6;aFP|?Bh8j6;Y8N(~tG5aM`o;bjZXsH2;6r zUpnCRx8sBT^!L`uUVrnt?eG0O|5yDzf}X)v4^1}x{rq0-@7MZO_{KBzH!z_~f4%Yl zpVHR|yd-^7G_6ST;@j^_S3@TofpD1+^^)2)1`uXG2x~?Nj#}jLdvzoMBd?630 zWrqZQ1~DY_tW0&uRqEuMM0vVzcEoozCaV?x(S>EtXlAskT#5%{>NauC!X1J)3h!De zGQ&B-C;ereu*&&gQKM)tYn&^&5<#El-1lUO0Obj0&n2q-D=r_A67JqrO;as$ z(Kma!^fEbP8C;tg%QyQ$#XwSCij4H+r8#g%q@~s3^xh^k&6Jk6iQzl{R*!v$ACN}8 z)!yW_mgoCQ%SC#ZwmfNR31wwnvQ%82jM<90O2{GlRd~=JA!PG)VB9$ct;CzU2k;c) zKXxm`Py0(B&t2v>BAYU>@vr9Ze0keQte)@fJA!ro{{1Z;3TlCEU;cfS=ldPObhUnt zcJ+MyDm)G^ob!`wrK@!e7q6>TJf*A8+(K8Sf8O}rLuyy`vY-^KbiRSHh(DMW-fJTnXwdqm11c=f~EX)-3JThl9<^!{>J2R(Drq`_y1*n z6Y1dc+oSL9hu>;$_K^KXK^n4?{5F>7`|{g)diUG-t+iQ+uw1_ipLiM~6ps}myo{&Y zBf_6}3K32u0wayx3_iNor~XrZQvqrC?MLtIhu?2I4nHuZE$_F-8r|%W}3Vh>G8I`y@1#!dC%_ z;qniKEa0P9K6yK=Rrk36(9KS4XuTtl7j6yL`FCgCC z+k@-s5@m>Xxfuk@SYz4C;4<@V$&2g@q}b&NJtylA?-Svc;u;Pj2F!KwaXtHec+ zcs!%yS0Yr5k?G}iXwvTI-EwRS!wg-A@JA71^_84Hy3?BT{YJK&`ew5`Ez22q!R|yg z+qIjYq9`#OMK!ZO4y*ku>obD3P@xFv8(;e}5x<)fM0BXIC?#&RW-d8FrNAi`DYIrj zE{_o*FEFgoq?;vb(Gw%S*|+kGAWkl^G+MB5id1!t zR3$sHv3WUc%{!h8dFP!%x!>cta()TYomog_m~8q&Aj!JNn%;StO=lS43`J&8;Oma zLZ-F%&2FVMfm|svNWph%eh*ifJlIFcUHNnQN&WR{B3aiRnEw~jLTdhJxPy&!i?won zi!ft_^W^%J#DOaqiFGk6S6t!7f@*PkyeV%OgZs$H?n6R{8YOQyp@aN#EsqNaFIPn^ zX|K*bK;%)4QQR0U)u5K4SEpZvr~d|4e(DPPf@N|Tna^`Uf=Ay6+yQbWreie@X75!T|+7pEe;QtPv7*^^5`u@g$ z4d2&yDtuEX82H}cC-A+iUxf>v1inF;@X2`cyng=y4t=xd09FPbMb}a|m%kx!h5}}| z-097Eq0t$y%j~bO|3qKUaBLkR<~&Mrg=hY7i9wWZ3-vP{jg2=Q{rC>;=uZ7AT!Soh zhFz8#Wq;>Bqil@4o%!d{CpB8|RVJMcOv?)6&e#7n>hCsfd^yfQbftl4yn$$_Ks5i- z4Aw~JkAK@AD08cVG;UvzGW>yCbbJ+mpkIYAA)uWKY2SI18a(N$k0$ic7*KKaOEM2v9M~Mo z(v3vGE}U+;)-Kk;iPjK(DLjf{P^GjGk+W=p3anH9;t8xXO9Uyd;mBssSaEIgC#E9ByKOn}l@43Z2R1l(L9%I?}na8ZUu0WHOwek{CK4vYIdL#@*l0RLr ziEfJ1P@RHuZ>&?T_cn=!oMQb=)@9L4DTyz z0v4y|D2Z$k73jXQd2-`d6VWEv_s4#!-f*HL;e!doTg_zbx!Xgc#zcr1j5`AlOzd+3Wd~|PHWFL`8P|P zT#6YVeIu|+&#%C(ZUju%8dSeLw>sL zyepUHFTH*O^&$S|L`xIT@z)$qbTnOY?PVi;t;D>oJXK^3Ze7+3?J6VZHS!`E3o`GJ zbp2zqGrv|r~O4oPK{H z;5GU=RzI)P&++;>Q9mc?=lAsUCjGocKO6OPvVMm2bEEeg| zo{8d#Ki0KNI?ytrh3!XC9v!1ePOl1}$&k9Ms6R zt)ZcI-pj=~S#3_&36rI_ ztu#=*BHZbZyx0Gg-O~b*_jZJio9Or5($Ns>*L$L`ae3BfzFV^=`lhx`^lfaV%qf&1 zne&S9c1g@#G_j>_NH*zZB5k?+4ztYHwuFj4^S#(=w^})EOSq7)6v%brR8^Cy6xuUy$(!}4oJHnrgz{>e_Lk{zl_h5 zCI;vV2Rzc1o0gmX3#LC+$9o+j3xFh;UJ?E~y$bnSf^L7<4)87gNerC*L!t9M`Z-rW z@72%y^z#q;xmG{d^XcsyVsvn%8S?S_2g%@<=;)QsR*ucIX+IVk?ru3GlpwkK1S16ROnE$@>yxD?g;g(Y~5k`o++$2Atx*R4p}J8ykbG2Lx!gGkfkC8F>R@?= z8qf~?dLH7sjrpj#Yg41V|p#ii{kNWS{#vpPC=N z!O(UtJ3mZz(C9o(L+4X6i(T2*NA_e-`=0YFkPe#5o415H!K3geJauLjX;%fxC*=3{ z^>^rGuv#g{V*fsjK#O!F*v6H2120(7yvcmYPx<_S#3BBY@OL>`!T0=y z{n*J=!U*&|@9)X!s7=9G-xXH0ALrMs3zgXC^$k|HP42@!pq#c~b?fAO-Y)Na7kV|X zSI+1cxvebgPT`S0@L^BBC2)lenr>dKdpA4xV~JO;2QLM)w}g9HbBZS)r_`b!9v-jf zPS(AfL%FlphxuJ=nyto;?)_;J2xbHhMaw7a&@DxWjPZyOjDLS zq!Qtw2HOuSu0W(Y3^bGK+0-QP)!QZLD*nj(#CYa7jIlfSV?>W9BTX%3qG6#2a==GKHqiBcTcm11~ zJU`p2F1*}_I4H8(+KMLb;+*$z_7baYc~Sh=>H@KB&UzTlXq?1gB#ljUy4gO+BFyD< z=Fp*3N7k`x5Z4ueqBRn>xMA(IY8km*}z`sDebC%t&gG1?iWQR^1wk)>? zM{gW;I*Tu)svXW(=Ru-oz(73{#b-Pqqu-pn7B|h(jvT3hjl*f!D1h;MfgYOguiufq zIiXzGcamLLo({@O^MNEtN{)bi_Q?|VI>PSHS<};xwZ?tM_QD=k?6h4HBxwNoeTx9{ zk9^*k0U%_9Ve{gFqAE^6E(M}Tp3I3*oiCXS(YR#A%80Ra>?CC+`=8SGx7fw8G6sqL ziB1Be0nGnb0nf6}W{=B}=g?TV&kL<`C+FSHL6PdC!zW2*&@AU;ghC}ArcKVKOi@j)g(vhY1y#fC&a%np^y z$03ou}EyKP9*67Q_<`W3OXO}P9*N#M7GBoysMF+r{Z1D@-@H0qxQUG_#v$6RM9M|=mpN9 zu)EqePFxI}Sn=UPBH}{%a*kHf96sK8ojP@;F;=|ze)F6!XE%l0^H$+qMm|d700huk zBy_+_8$Q?hvsz4>EY2Y#h=B=pj)ECN@Ge^<1?0$D*(g}}qaQ%3#JWV@I;v6}u^Xxj zhP|P&5ZyM*nVm-<5yLw6m@nT4jX5T$}+9Xq0V(|O=*AQQi0rZh72 zZ~;18>1?NG&YPI~P5^Kt1AA~kOXMi9R<5tPca0O{O2YmHulAD8UmE~&1b`NIyp#r` zH6_te@S>IPo$o8t_FhSxw?6C}QoQ?^oUEC>aN7R^=wrhY`Cq@o|@f*)1gH2SXx)l@%Aoa9f^MP2?GS}K zt4mwC+SW22M5?pG$3_Ygo%MLV7;hiqP4**fXniA>iQ)%%sdICrIuUv&*);NBURxjl zh}gID-p}WEG*>jFr^FxacJn6FG_A{D+I$!Jh~$C&NZ5dm*-*JfGQO4s2d!=J4cjmw zXdgMS2v%yS-Z15miT+vJvdG-{)2tmNu~?QPcf&h^Hf13Ps?5?N(G934rKaLks9f%Q zqRwusuiR2+ulLW|aZ6~zP5xP*-f+u=K%KqP-XXJ0ZFYUZTHh1?S)YuUa7#FvTUKAN zj(T$KJrWbiX0pCvLi7sz)3wCF9p@%|R(oDAeq=Rq7H(_L?5t4v(6K#xWQC3y+9+R# z=gr^q6+@|XTA9UChWrCPzF)RY=~rjxJ$kevHoGpG_X93FdvULsiv_)Si&cm(t9RV- z9RV9a>TF@wTn`N2qW61G=~G|OM%jxf(w;ZHml2$G(ZVp7zD3*n`6A4!S=<=QjTd0s ztmsjfn`cK%nZX-JPMJdW*=wz~ZKYPhTG=gEDv_6^$1XW*N%%55ZxC-sI`Z4|4(Eby zW<~Q#xWY&)4OK-tO55|Y`H7(6L82#+XJzHAtsAhZj(m?OP1IDj_3W+T!bss(p4LQ< zdV?<+H02`yBpwE0;iQUOOIGVx-q!CivHaSw?N%3Ng`?*P6P(SGm$gXbNX zg9B$(e7StCV$FU?-b1v2$+ILfaOhD$yhO+HC06X|vKr(*K8jYrwKp%s=>{wRdbX|Ziym2u{9`LhV>XeJp%H@gT1GMVq4sADrSn~`@6zriQ;V$5p^6iymJ*wx8=xSIvsX;PZZ4aHNrNEEl+ zEk8RJ_0G*|^~QOfydvxh@v2;GS7}S3UM%orWxaYll41Cf%zFPPU4vPwu!>HCXaaDR z>1*CgynskQL~cj(<{TlvG=D{&&6D=LYq=xMw&z{Rm2|v4Z-RRz%a2*sjjdEzc(s&l z&&`&WQ}^w{FLI>fU~Z{*6P7D1&2?{5Ff<9DZ9*^-Sy}}zYTSC{qZ;v-6_2?`Apg^D zDy>Yw36hazEb~I5_;h&!N05GSymEw_S$k}X86|LpJDT)5)=*-8KO)cS$uH!E0gR3xd6Rx#s-KpA zo~55B>gUn=S(JAFD=?85_;q?yZ2$EON{Dkn;648HG!)mT{2&2Rx-T5r5L( z1)gMm-*fAG_C0|IYatV!E>-Re&k1h%$y&bYUzD#ATu9rC%H>VFp~m@Iww$B(DD3;z zzhb<>*Fn_ZvQKaI{fnj2aw;WoQUS0pVf;6gJLlgZ%QQL@kn5jjWAfNJGxj-?7xq>o zI9|Q+V z&kx6${tYH|MzlVaO-q=e3-SSz`Yoyd1Bv37-(G!m*zng+-1@eWS0ZZAfxcn>jODoE zpUWPAZu|S&>kR%rC6`3Bc|I>^G=Z#5hZy`-NqtN2+8;fxaO-Z>1{QCYY2D3$s` zUVZNPFZ1Bvhdz6+b;m#TXY#XvnxtnEe=FSg58CH_!5;lldGq<5N&Xup@I2}MPK`GK z6D}U@#3WUQ$6J&iE$`3v%6sr8+dt23|G~HS#YZ>vvkg46J$#totU{E;OMgnwU3ka; zF$u52e_EH{pZ~->Pr`54mqxZ`qbd|eCX{}oqkAmxBs<~~nfhy_KYw=1b8f8)8WKLn zv!wmx?~-^Eecqq^z!`+BJ^@OOjs6@pt%GX{E!tj)WCm z$8#5EOy_6XF!JEdF{b>nT0V2UbjyFzzjz({_yh$>d@U;Fwxa2ANu`sdw|#G8Ht&z8 z6?s1izqBu^8Der__&9{*Wbz0^o2T>gw+(@e@`9#?Q>E}1|5oAd+pkzSU8?w$h4=rw z{i=o2q;U9uXuoD*uj8fv|9Sfcf7ZgDQhxA%TYl8SE!ux}?Ei=TH|>vJxJ}AG``?xy zyYPJ}Kl#5cKWX71QhxA%TfTAOLE3-P5&T#EpT6)w?f-vS{^2GD&cZ{bid+A66&d`m z_k%%uF zTTV=9u-4Mr+C-N{yYdA{P%e}tuBd23n_d95b!liDP$7k23Me!sfe@;;E)CF7wN3f$ z(gJD;5H2CV@AsTD^IUeVHQ0re}?&A z$Syo`M)@CO{->q>XOw@I`3o%n+c?dC`t^U3`EO$W51mo|Ip)7v>VHQ0E6hL0{Hx9= z|2*?QF7?Ox{?n;HPD?$@%>PX0kDj{ly5rBg=^pv`mxyBIG|SiiznJ+)rTxz+e-rbs zXaD;)?gTiU`fK@1nLoq)A3CG_0rOwY{3B1>f1lKUf%*OX^XNq<;OO<<{^L!M#;&7_o_uuCIVA{0m~cUufWIOM_gey% zgZae4a$HU6bA5a%6VH9olN%pe!oMai{O!Zwa`zXX31NJZM_+W#-4iv|TQklZ)9mv< z*u!bys`l)T*n?6ZSim?Vi0i8LhFL7B&K=it9B{M28phNJK2kh9vu#huQOsK>mMkST zk3;9hr#0)MW%}`o_HyN&hOrK^bBXNM8e~Ze+T+Wp{hX=%UXR+(|IWrs_($r@S0?Vs zvi^6zGig6l3{n5C%8j_pCu%=;5&nXIHGUnH`FPiD50CTngnf?km+(lxi|OyFNq6(D zK-fJoNnq@eJP&Vkf&ojOvB}0~c}*6u`uZyTHk|nJz;oX*;NLZ1z=!jJURR%quSDw; zFTWGb!aW&0rsZhpe%jD(3ekr3bMw#j-_`kq#UH%N#{*XU&M@A~_yusm5DDgMl725- zD-w^6E?c zjz1Ykp+AAYD_!|KI2p3q`~kqXxH|tDKE^BhNbt`M0sq83Pz0#|JzAd-f7~B10dHS1 zhFMz?`B^`~KdJaFj`_dhCu!At?W}=sPxdcN{Rc3oJq$I_TiJQUy1{F3@1b`Z0)N2# zk9?xO{H81CWmSH6eqn*~Chhmk-M;-Y#NTiD!}^P>*7KK*s{FlaLzKTB{y5Q~B0*&W z7nk`42-BxDeO+ZJn%GBb)^64T}pq5FC+A# zya!_Vy#EdUFYv&|d+A@}C?nRs=%1O&4ck;+|LAiI(YL<*gny1befl9_a*ikMj*0P$ z<$YtTmbY5UJC#26x&!t|1ruxYwxuvTiQFKuQGrAy_)(qS8lTMKm1tIaG|%Y zro41zTcW%)+vAas`SKVS{?2})w!C$fA6}#N-L$asvX!TJQo^wj?MHd5_Jrkm`RlDy zUb=FRmG=*i)~jDA4~wgl$3d_8$IEX|$lFJKoQ$vbgz0xN-eK_?eyyuql%QYa_g;tZ zsQBZBUsEXG=dY2c=1MQ}ct$vRnz(0%<*$96=YPQXG8cFD2u|QIi^uQQpR$!(@UD?Z z8}AQCX%8jLq1XN6_jK`S&3{0TIsLz=-H*SMEdN-y(G%5&GXDJ#DYE|eS@y4N%s!~~ z?}qd~1hmlK*~$}q#>C5m>91#g^gSoHH5ZRN%r~P_G zn(GzWO0W!}$c zqA)(m_%mBH-P2?12NU-pwQ|E3oybAn#|gi7Wn?|h&wnxC7rBDab##JSq4GuO9(;oV zC0d!m&usjyKD{*okFM&Bp*;CLLFuYI#l^fR z%bk9^_mW2WvA!hjb+}RlOe}L$f7FzBKg%0Rmgo6d11}7EE(!1S$+2sUzh+;az$zR? z#`iDq_!~Spf^oKa2VO?ya`qp7En-;_+TeZr037+WH$Pcv`g6O|02{8BeCJk>|8sx) z%=6>7bm7e?!FMh3#kzT||G?=B(o^}=pQ(pnV#x`j`}89*bm!EdtJQb0`h6I?j9{>K zZMXV8{8VlIoc(P#IXU@!FkFA^Lc?HuX|ASl90>hE;KbO3KDlC77>Fc9+a}i0HKFjoPHqJlC^p-mLGs=&} z)!9B?26K z-rv#lpyyA(@{fgaq343X^63&Uf7r(ber=d8ai6aCRQ18PczwSP_#yv6HPeduinq7Y z)usoc?VTkUyW7xXj`^h5!)L0K9~XHseA#ODqG)`enu)~CAJ&3TQg8QnZ=!L2>0j%=)3E%z&LRCfRBT-JJil&x znDShGVx^i{b#MrYSE_^5z>1ZFNf(Oe~bQC2|>E_?-PIK^Ut!O7Jb6!4;Vl3KOTOP@$#oM zJ?!t?C=Gc#TzQU{=({l*^=E#_>*DI%M?}6XzO*{~2K4Vp`eZB}{v-5}*!TgfXrOZ5 zxzFlO<#YuH;DBJ;y2>lRY8`Y0Z(+weXe_^Ma1JVuxoqG5ie+2pvwcWZoUb?ye#cVfwvE*uTw{)CcP7Pu8dU^!-|q#`&|A8Nm30OyU~DXDYMk z97%Y}C(FBcg?1dD|2Xx5G~N?`6R8j2n+gBmo&<%Uxy#XgSGWR zdGkWwzj^teW=CE7ke7d<=ftRQH>kHODvs5(8tURCp z_#X6c@T0l%$eplP27Y9j z-Vw&f8NV~EzwrCS8$5pwGkuxuNA>g`pTlR2|FZ}x-@rLeWdrc@OZCKciT3sUbNj&# zSLUV@%mV|ReDs9nKSX?Mzv1y$7VvlB#j`blK&z`$b!3Zip(8KZ0*FUD8Ftv*L zKi`K9ZDP3J2bYUdg8XUbf9A4hoxYcNhl_jt1^u&J>;tBYKFVHBYyO@h^Y@ob9{m2f zbTx}_NT)miK3mPi;>Bu!HgoB={?!7Mu4-C8%YWO$kFl5~jnfx%SbEOL_BcZPT}*$# zmNDz3PcnU?ar*s|-WY#{>DwCT=h4Y(OXKvVlHM5q9;Pp8oPHnEpTOd#uN(hB`%aQ^ zkG$#R{bK?%?!gP8WH(1x1pL{^#hs?O~CZ z8ylyKyxiCrzsSo)jq{7VJcq?XUpD^;%NKchZgs=@i@ZG5I9=rBJxTn&y%YY3iHRvR zQ{@&oXJR7a;g8R-8EHIi`|Fd*$GS>OG#&EEjMZg8%=&qJQvM?NuQ2|le+0cc2e8U> zSZ1tWzQ8Xw`0oQgh5lH1A_?#15sq7KU+Aj*94okB%QGE~wmuMJo7GxmRam$v=&w5|zd9rjj}M zfnUU5Eq_}1h4=vD{b4-Ac#p+>eU5gn_2d0oj`!;-(_G(-8WY5yn9qv8u2~5CpSN)` zE6%#RPz1geD-*B#t}<`S7j#4k>$|v`C4OK3bam#fGF}*ex~f4ezNgw0iw{(LHNJ<1 z(f&;-K8s^~jHP!~XWt_DEqzaQCKex1{5HQ|`(Dq#S@P@HU_$Iz={r1=N zfy#~1bkuj8_)fgXe;K00xTA9bCJ8QqJ1)OGa6pzG*MD(~Guvl5F=cb_Yz&B7HEr!d)nW!m8DDOgZX z(op%!PA<|!u-#sRP4o@>Q+3fRynK!`-j7vL;m5>1Q;he7aaO6?5ysas{$MEIX~w<0 zt9@C$x7F|+Reg5im#w^`_w)i@`u_~Rp?_ICs7_xLjjyXt#p2oO?90P+jMpso0V6}& zQs_5)w>>P~uH_Ofwodv|+IQb492#jr0fW zzZn1Mzm%O{l@=^p#zZ^b%N&VZ) zLo+fCz3a=Oe+T@Iqd6G+q6R((500sUWdDErIItrJ&npF$cW(bK{n2bXdpQCPPl(FB z4%aOqE}<_$kHZJ^3HW~UBuT1=ufySMIURi48`Ad-3}vb7Q}EVAR37-52L08J3w--$ z8Nc9e)AwqBRM)=B^ShbpC+@Ly^G|qqlfPv8y?1$d$zQhoUcQdASTLoxvh6P;@)@S@ zap^75bm%kEFZ!+NLg)|I9~^zQpQF6O)1Lu+^#Ye}@XKoG(eftl2Xy85v?D2`$LNLs z%O^(V|G4F+bXMN%K|tR)%YV9A=?U>2hJHl-4_CH%;21tDudC8hKi&A#RasI$eG

    FRibemT`d2;wb^>6dP5$ z#r3&QelWtXFnvJN(f;+*Eq}UlOZ{|f&pnkJ>!(|L?y0P=pKk4m{cq89>krjqzoz{y zq?dw>`k?(Eh}6gRmlFl;{~_xSwy z?#4&fuky_oqf19kQV(zYi?n~f%%cp`r=+}z48`b&f8nYO`eu<(eR{y_yK%-(4153G z0OR*2^x>Uh{2`|IfH2YT&>twF+F|jqeC^LV?S6B#{3n@zZ4%zs-;MXZmCU9@XNl?q zf#-Nhc+Bs)|7Gu~;F}Eo+X6rT##(sDk2I9|hn@fktLcBfeo}tM@O@hFP1VPjVSVp3 zL~;9{tdyT`W0c>EFV!9gZ|lX+g!~SWF8V$=U-Z4jmsXoLqCPsFNqUj=B*tHCaCi2= zn>PKJ^YPFgn1#(PW8}x-VR81r(R(s1&+Olptv`jX`DX8^oxE=-Qu2~ zVg4qH(Q{vFn7)VUPc=@TV){4#DUsi|*YRnTul@f6t}}p&u)W#es@txqt)Iz*)UTy+ zy3}v|ZzjqQ@k{+Kszblwm-;=Iq(96r^;^<7Jp)xyd%=19w{JqS7%exxV zKh5-(*CqHq&bdtNEKN z7bWrs0`Kl4G5fCEt%N)Md~cxmtbh3V^#pJB;57H~%EeflcMrsz|2s3d))2RGUoCfX zKQ8+fpRBb0n(Rlyjh%9<)RLEe4$5}L6gI+g>7@IPXV2G+c!LCZxo7$1xUC|-qwTHwhWw<5RF0HYt|r9MCvg)}hLMiuffsHFa^(I$Mk4?C zOZ@R^EIOthJ(riHbxmBexa(1LYW(@=#>En%bt-oP?7tSR`RY-4HMV7;=(=!2ZDrBO zkDt$c@6kng9f-JI51=@k)5_*VdnAt9to8dDLNur! z_RAo(x^|s!-wfkDdqew+@e@~Bdpmm}mDIOcfOXMr|4lZtvne4ztbBFt-+6f(VEj&p zFJ?c$+t+;lNALOphJwjEKUZGcrQ2Xb6?M^V5f!!P(_qLi{eDbE{g>|iy#7JTKW-6= z>OL>8{*7JuJ8x-z{vER@4@J#L4V*Uo@VFKAc*jwNm{{^kmiPf&%_0w0J%8axuVBW< zaNj8YIQotgz;mU*-n!!akG?~zF*9~iWxwKrUR*{D2Y`Wh4S)NlAJ^}tq*Q<4UqF8K z+~&s@i60xcxL=Q7T%EZ+T#tv^s?H)0#%F1mJ09Lsox>T2Fg{S7zncS);8%N&@Docm zK7LNB8vMCF$%6C@%Rgr4BS6nO#ur^4mzQTQ^KqeX?Tuc3o|XJ~>5Mf({yB*|eCGe? zcj*y&viKI#x2_ueiO^5{pf_6`2MuBRKsAWe$NZ&B46Sif>@m_W^_|B9XvlBzb=8?; zg3sdFYM}TnK2V*xOZX*m!~XzKc-&S$^1FKM$~yY7^2S?DraISk9?iTQ%BSi5DwFpaWH%v|OdFptd;${U zf8)B(Zf=DPKVDf4yciSLRqpwuc{L zGY#F4XupvDCZ!MOe`566e1!V}CT|yc$qfyzrzf#KD*oOzDBmes!-)Qs=0xR@&-waifEe^FtSKXE@ z5BX&xai`_4Eg${^uFvZD{oLvV388$U<#+(KH=GZi^IUvBIJ3d#gCBs0#pE+mUShqV ztJ0f#u3miu)UVp}Lcjl6#+Uv!uaA0}zU=bkc%}Qbu|~Z94NIEu{{zx{CtyvV+|T&EX@(%gGk=o43 zdslV-SEF%^HyW4zcE`h+YVX@5zs0lF8SFksev3oDemmMf$4TY`_~iSF@WbG3|Mf(j z^ei$$-ZRztTctdUW52hSXYqk*R?CyP_0RQ<@L72m)k)8|^2W71i)X8I6R6*Wo$tf- z<)lC4H=6ue$r^LilNg@2yNE;F|?L+^-lge!<-dd9U$*bya3>0e@=s zhvZ*`x(L5*ypP<^mnQzFk=N2C{>UHrc!uc@x_HdKL*E%ks?1hxMep|uq7?#F6BA6X zeE-(xbljQzvsht{ zF~%eNE5$Lg3@5>?zBAOX7liR%!XJBw&oA)husj)P`d!@fL&o2UcM}@x>;&SFyA^$8 z5@M0*x^gkzK!;PHt!rX}K$VMuqH6+Ic}`4_#LB;w*nKcAK3=)pZQ!4$_+G{r*+Mh$ zQIM;5!VEWjQ;e70`3=)Q_x{AE>-ZAJX9YftkCVR-y7Q+xe?Wi6{>~qg0WzPE@oaph z#3zwo(u^#YC(q0GKd# zeG>cQ#6sI|Xe58Qf24EIHo5<`7tqw_)tj`W`}{aLc{DD1r@ z(RAqVE@Z5hm-zOTc)uMysmXu1^2EIn{AkaB`_CVFtXW93B zeX^|I1@SogYg4reeGc$UN4&W@i8Wp8AKst3u>DH=FRs5|>07@@724VC_A3QZQmM{r z_8Us?^(J6!ztRg>NEF*Yz!~E!F4$!LnPGkVZ}RK^jH7;C)tL@o-)cvgZuRe~rX%SO z7JPn5H~ZxlfLKWyn0-~f)8IpS7-z9;=JoH2$Y+>-zoz$AE{diDUoR4>$HMYO@Ti}A zl|GB({Ld>R=ksdzw+vKfH`mISFTVzUPi3kOJoHDJ^qlyP?~fVAm%aJq_Rj;|D|07bZv^}zUGL+5V$AfXPYz!*KN>$d ze2gAC>awSV59uL%a7uXYFTi?f8ZT`DexB=v3Hvh6A6qX?e_P;ZP6mH`9&82}?tgw~ zJ$X&Y3;9>9gAei|jLGr(faakwbs=go{V~TjRX1#K(C)HH_c(@Eo(cI1NFp(*Gqt{GOfSK8Qqh3P-G9DpD6Fde7>lwP7lL9 zy9By^4f_VBK63hG67ZF$u1YjP#GWAqnBQf9bX{c;7Sbnq&CZWH z$%JR+ELM|%X-fW-TFrkeTQPa^FhV6y+|t_VL+UYhtX z0IbS~&OamZfa%YC*Qb+F)u+Qa1*duL%=6 z5)m%GP31-0bSpyXe<5Y>6>r5uC_sJ~DU%CsG5uJ1qhf%NzLnYc)?`n3^CS8LBafe- z6nVc;S0x4FV}mpKALD17{<;1L{ky&c0={-@a(Fm7{qI!_pudON-&d2phJLh1FDK5N z&rGfdy*`fn0KO8Pzh%I}uA>tJG&}z6?XG5_{Jp)16lK9n?)Vc%xa)Hcxhe;USgn*A zc$%;V4=Z+Jitty2@Q~L%_*CrxEffO8so1K=&?zjQVf@}-G4mS z)Qs-6{6C32%D|^A3&)v2TH-XKZfA7ObAC6wGcMtq^ zOcB|qfPQm7)TCajBUiYMuY3qLE+9yFbh?<=yAv^6=7Fhsvv`cqGsgbVho6?T z5E6A@`EC4rq2-U?=F0#2{bBk18|6RBfaU+%2+N;+X`S*XmUIKEI(w_<_s_F^4!gMU z=fdUqCW3MW-pxxf0%1A&u;b6majTxlOV_LMRPc<*%fX+1{=zr1#LB;*t@v1aa7jvD zMxXu7bMPek$MLUF=i7=baq_zL&!h6X?u_NN7iFJBUO$Pq6k|;SuDu?FRS)0d`SE4O z2P|%HzD_K89IaIOJ$&stdeO(E^}brZoR(Pn)6e5~^wIhAAI5vl&%R*nmuyR46TZCV zMSK(YXCj;YkYVf57v4w=6Bk}5!IB*aw&CIN$~&*)itUoC5ts8Dm3KjGLE^WxVS_hR z|N4#b4U;NQ7ycD!miB^XSu)AUDLVHu3iBFZXG0wdw4n|l<7Z!(#lPB6&tEu!i{E4X za&O>X^lFlhN3_q72dY4%zyA;A@xg!8@#yG(v+%bqe5-{|TRzur ztKXyfUuWU4g>SX+*Dd@D3ui3+l7-*0@W&Ru?7fPw)xu2{_E@;j!eI-?Exg~tCoTM* zg)e`f;%m2X)WVNi`0p03n^L$*3qNJyH!N)WLrq_5biUHU0h8ljw{Y6RFIl+g4>aE^ zEnID3#=>3;Z@2LM7JlBs|F$q?g|58267gsLSj$fxv(Ny4&f={WF0=4u3AleTIKh@MRV*wQ}by?(%;l5$~}6`9=%V7CQK;_n$)tZ_0%; zfBCZ#pG$-eewlrDahUM}A6PhU;gp3l7S36i`gw)VS{PV3Y2mbmsh1esi#43J@3R)~ zwe)cdr!1VYaL&Sc3q8H|N5<$0EIqhD%Spl|jvUWHgVkKUdC4yzpj5Q*CXRt#I*RLqOk7s%=f1 zd@J;qQX^yKy@k@={7|usyQEVR?;9B#E;E83(@|78R~Rn2`2NCZ3Ay?uzVD`dU%8a( z9xmrchjW8LespwX^ff_otQ?dEM#cvFr2?b*(%4`*C=3S|=LQFN+#Kv584ZR;`tt#i z;CB#M@Toj95)6(EA4rW2kBs)`NAvxAhjPVLCDh+134CGrU~aI`9}MJ51HrzrzMBCN zp?j|Y;h&y?e30uOLc#ms1ui%IB08)oWSiG_+b?sPw^axltR4A_+ z*}tC+m_x-V%%!nnab&a%Uhg~N$YlLXeFOQSTtHArwFz|j@NlYcY_v2osugNqy=HZL zpkKgNpK>5y&W?^0^P?)psa<8VDC{Kr(b^~xZLN5gmx4ER?%cU!=Z(SU+%U-+$PM>H z8p;EsvHrOdGzZ1<=y{rNVZf1o&hCJ$k@B`EDP`0cRo&dOG25-ZCRpAt|1Y%%%S%?? zY2`znx%*WP)27c|IQs$p?$W0%K4YOv@6Yegp{J(UHwQuY6<4Y}qh$-_BiiZLrB|dZ zK1MBYM=-NYUuD`WmJg2kzD|1Uz#l4<2Lh|X z@;8qK&{L{d1I8L)6TYm|!H9hndb6!Pz<_!2`d~0uDtF81ZG7LBD?|5|cIb$k3SJ$^ zxOt#(Fh6X%mIEy{Vfjc=?245bbgB3AfpR#1xJWxd(gW0k<8CZJc+cU6`I+vdqXh`Q{kFr(8HKEkK|hS{9~?PkBbxh;hKPQ4*?h?D0ns1%u@>q@hLx5L&Rj~} zoENi4Uf)?Jtzdh2g&Pr68bG&chmQW`$s;1bytwrTj2DXyG6RffcPFf%Z!dZIC-4;(+$1 z1&~LM{-A#(U!vBqeilKl`CyF|hQ@|Sj^G)}9gf9INUPHi9EmUuXoHn%M4vP6u=ZPfkvZ@3Q{G8i5k91Myh1scOa^Vo0!{8v$cJ{Jp@(pE&40u?=e zua!QgUxJ~rQdzKab^vQ-bvo!90L4kTL+FcUvDfC$LqtSnz?6dU4g{C3!qb}@Ayza+ z(2x6rkv^zZ*w(?mJo;ol=pPfK7Ul?K;6Q-!2P3Qs>@JiKK15^Z*p=&)X4}BLdH^LK#*yW1R+F zN;0~h=rD0iA1m(N4=Yg8y9WpJ2QXdHiiuY0?tqR#|JYB|ru;cV8r$}z(m(e^JD3^<+_EoFAZPQv9S;cwqDq*OJnN1k1 zLM(q*eiWMJ{~IdQ-hqND7ci{9IUjW7M#}+BbdFxbuzwDsqq7pwi)t#CUn!cv|_zqL|H4U z3e&vML6G^9_N#vKPivFv8yo?dioy70DY5x-xt!}8fO6hnfPVr_Dmh{ipz!gQ4J-n- zn!55cTCRuZtfY) z_2qrNJ>kYE!4H1a5SU1BR6)tlK`nOxeaTU6bf}PS866qQQqy42^8P!?7p7m2VI&6Q z=n6kNiI23`E;v^_;yURaa6OgtbwM2dc)=kKN!R>!K_q>*sIkt&>SEkQx0%Ch9hHu} z5UFcl#akC7f;W;LD?%6`aFqK*JMDs*6-u?WgZl_kaq}z552GeOx~mVmLh0^2TqtWa z)mzp6Ja3`*@4~A~%kh%XD1<5%`%)ai>ivu9Ip-G?N;g`JPBwTP19}Y)VYV@N1injn zW?}H%EY=gfg|Fcx4>l}JcEn_+wGGA<^T5-{k*iOAWFP_K+`hsvY$CBS!?A=8Bb4m( z2`VKXxeh?zaWsUY%)e<2b{C0YJ8m8+9@!SEd9#O^#1R7KI&=!?gtaXr4b4-@w=$4w zrY3)){BwRTbs~b{n5Yn9#?n)Z&Q<^Z^9mnWd^d)oUOKCFzs|m6k<2J&g${Z;lmPo{h#}eC=5j%XTZ3IAFonpp_MmM=P}md%u}fUO$S} zhB`&ik25xksm?+8v(#w_D-DevO1z(WOHV+P5nsp}n1q94Xy7`90NQ8$yI5Y}heLMr^?VuuuY@GRt0toTReA)cq zespv5!O`;JAh!=gcYS|!t*K-4NS_ZA@^(0BA)trpgD7Lp>*$5&yEN9UGDw=#EH0vV|t$`n$3P)AbXYhZ#QZeWQr|} zASHa{ANVbY|MB{bJGaCCxa;OZu?Qp>2wDd72lIo07|`rE>SYa@mzP!sJ$r&zEFTM& zmsZf9;`9~7Til(dcA&RMN5_g~=d1E8SQ&7w4PG_X|IYSz*aYueUC6|XAYfXlCL;tH1teLfg*i@AO-En>NgbH@BA%5T5CZEbttyc^x6 zZmxr%HL++Re>w>U+~x4wGF3mp78AL@ybbe_$am0m`I^uVUMM-wA-}Kl{zwe+7#Y<% zx%{%)^^;wq^>q9s)a5qkL{S0j4gZK&SX75TV*1beH=7YTMCU}*7x;F?8Vp!Ug{*0? zGWfv*1{unC_hVp1+vrF9%W=?+q;N!X8CX=Xs$}gDuE!PJq%&M?_VO#7V!3eacq0e+ zQ6CvSRkBhtuES764;)ryx>ANK@yuau43nr>O0UcI&@y}G4sjr+cnTHRJo)uG~TF1Yi8 zLj%w|x$9=+K4VAxPQFgBR}bl9DL(!emdMMiy&gx)Bli_e^CvK|aJ z!Nmv3FAqrP6r}~UCLbYqJ4R+5=heX`O0fVps(>izEGdGHxJYmdM>8Xp0>^H~@zP9D zNr7*1lM4~})w zAIB$KAKNL*YZx5CGFSx|`l7yP%mK=ma=>)5p+-q$eA_4^E`~4oSjOPgb|k?JG; zQ499$!2%&1>~OD^29)dwe|KENyj(UwJvnm{$|*;O!YW>T_P_HkzBuUS(9AVN*#R^J z$-}simj#l3$VwlKw00wg6GFG>vAlHg#mIivzw=W4)Z0OjqLBbQDqJsdhK*J}{BBG( zvGIWoGW3KEsnqnq&^s4zb_{fSS1l4uZe_HhKik`>J}GKAg!`(j zLN{?dknZkGBEe)z17;l()iG()Nlx;cp@wa(fvOr)VaNDLKi=u`QDMuPZ)ELF`jwE7 z8)Pqu^rspUIS1rY1ebl4l=D*XT1MxV)Y+;MfQuFAec2-Mo4EaLonk+>2e6y4+6%p| zo3?C)qq&6bU~Xfbuy~_i&bAE%e&tV+{d%;R?ewBRG&ul(MA0ztTyaLh_2GI2a+nZA zN|)L`6k+a4<1Q5LC4&y~Tf^vU!X`ysWi*m}qXSdD^xYAFdg;+B#nQ!UY)lRL8-q7` zxaGi;Y$ECGfUU_i5JwJ3m&PI#X8Yp)6}iCo;Z?bm_%QG{e@>XV1rQom-F}#&h-1Jv zo#YlK74qi(f`Q&bixTdi*90ifFMD$-*d|WORVjzAYHr2@sQkKY`-p7Ia{M>Gif_i? zWS%(EA}O%{8X0*m*S=;Q{9^@m<{^&Z)OArw zcMb@$skXZzYKv7gLUv1LQS5`4K^Fpu|I7xi4r`DO?YN(2+W!w(`2 zFHQ->oxh{IKS_fKgObpGSsHx9w5Gg6cNc@)Qn|mq9lmHf==RFiJ_++!qp&qc1f<6iR(M@b!8HxTC zuAoFD;$-;fSrAZ+k+KBslsxEYzl{6)V9%EF{TTTD`kxZxu%eFUBWk~FE!sojHyzmy zg0)g=&4loPgIqbd-Cj`foqilCZ_wmbz4=`=pc zcnoIHe)ZZ}YpX2{eXK08zMq;HL6Zeq$D9Yuhortk3%+KK{EFSx5)qCE~;V$yM zeI3;O#X9A1T8Bdv@D`nE5~VEs9xrkEn-8bbSYKK`hW`c!@vr{UZL8O;z3lQU)?K-A z)8>xOE&STEb9bjD%Y4cFPn$7&6TgM-(c#u?QLva&NsDv0wcoponT|LmF1I5KKEV)Q zD|_PM(gxq@HNhGUEMO>ZN~mrO6~P2@=l{w&U|kLksc1hh+#-&3G4xxVD@OMr4UUY! z--q4=w-=U}{J`Jd#&OYRay++TJ*9YTpoQ_!-nI_;WqkJQ53C}-L;pv< zTfMVz_P|dBuWEl-YN#aO2NxI8$jvQdvL888e@K_^kJqha$&Kx@bCqqNmA3;j%4ct$ z>SW6pY>+{7d_G|lZPg2P%9rD9bvGBIjuChnW~myIlu|Hcf9LA(JWKo9)o3?WL1Y!W zKtCGkdL4K+bMv6=BV!tWEXVwKTV&TalpDAGQhZcb!@!`Rn`IEX8Y1120o9=o0^xjEl@pw*ni{H~r_NaOPTz*&CLIL}=#wy?>< zr4|MjHe0yT!nGD&WnqVfTP@sU;edr@3&$;-v~bG8X$xm8oV9Sy!g&i*V@hw6g@J`> z3o{mGE$p?hXyLeplNL@{sNAI=ReVFKO(P?+^tcJT(U`VtK(~Z$#u^Xf*gl%t%7ef7 zZNWZst^jw8jLE*HU2@)od+anXVcfi!fAhz*?U-=!?=6FQSOWUnnTJUakM^2Gd~G6r zSt5RUB7Q|8zAh2JG7)dH{rs%=b#n4U0a*WNT=)?AKz>A`*|Pm&O+SC0$o<@f z8U{ajp~N@CNhV`t{J2Oec#Ebt%IDJ4hBtf3PYFJU_q<@^_KqOs@k|eXHmKzwp>`Rp1^jl9(&$ONEXcW5q^M*hD3x?PD(I}t$?()xA{+X9*{_zVmba0Kr zqVaRy@xk&33(VhSd}xGc#?l*=@4jbVtn|9gVRDL}ewqX7PvjBuU3+in+_?ir09T5c z(`j_1Ri3A|X*`LyX|sKAfM?!D(T_8PE33{gUTAt%`>OlSSll*b*G~FA|pV_~# z{PWg6**=BuwQ{ExnBSG}`nRKherRFkHyK=Ho{%^SdI@YYP?uz!^ z?ULoCOI{<6k>VhZ`~)5Y|FFwRFyrmdW90^PHnHKQZh4s0b#hnynH9;}b5Fi5 zkzw$@6#HjNmkL`iwT<^RKt|!#8(C^EDa1)Qc5g_PYzi*JSu+U#s~XzS&P|{$i7Te`aC$<~tRB zI)QK8@Xh_4rWY58Z`SaQC-9{WUv`P&ODz!JwBgGp@Xh_T)^Fx#72o7{7FOR$!xtp* zO&Y%Rd5SMsAikpEo40mx?UA+iZMs15%`C8e(}ri>=yUQsW%$xBReb3M%3sm&rA_{N zO`gYRlwb3|toX9eE{uP$>$x9C{84tLj2^Z&z2optTK}8>CB;9!!2VaO2lH?x(a~s{ z4xSD~~ya#0#=STR&6MI3Yvhc}eb2=1H1w~+j7J6~Z8jp23?4F2vy7a*jQ zjd)=-1aCn2R{VD(Kj7#0NuUiOuX3f-uE;#Cx^hO`W6tB>2q2 z`eACb!q0SQn8cSk*U}$Z7`{vcd`*9(`NtP2zSQRzhHu*N%_it`_~wkh-bWXPuh;5|3dR;e#ZFsa%Yv15MTH8sH3uzwgn*FWa| zHI8qtwSIdv>K~KSn&0Vz%u{iE(+%)7(gzNo(^pepiQ^k@fG=hJBdM>_Q<~r5%Pvq~ zoeH0;@BC*Ke~tVm{9cXfJO9U;-?eXgf%>3P`#OBCeJ4M+F!~y`@ARK)e%C*Wf4?w% zjrxbf=laL&7Z-+au7UoW{-EY}`Y&zra;o-dr2iZ~r(Y)jd13T9d``bO{ha+ooWAM* z)%cm}KZnoJ*L0ft)Y0ek%j`#$9@oC})i`~ptN$E6*S^_=K0RIimwiy_as6ZZ={S9- ztN$E6*FTyP`t)@5-|Sy1Jx(7K6Z-UY^`FD%^wsRY#Or&y`fv7qn&07@`x}j)s(&=n zf75>$ukZL->c67dSFU|$9$y%Jjr8B7=~LIf!CC6R^aqq4*FREcssFO3PhJ1$Jxl$U zcH^(ve=}cRApaWJS6S1iPQL_asQ;XPDJJBvc$V^)O~_x&Qe_~F3N@j=xPdo;&?K^>1 zPT%R;f7yim&3`qH?{w|Iro{N0I?M5Q-saP;zfJwE(swF*veEo^`k3Z-`ep7c{eQFO zPjvbvbC%<8F){uIXF2{(CdS|KvmAemiSc*tEd76ziSajchU2fZ|E3fE=gH43OkNtz zKPD6Mmp)7R%O>Qnc$V^4OvvBtqYJBVBm1u@A%D|ZAy&4<7YVjI{R-jA%F9y>90$!&twzwmpx1QYf8vp z`Yh#d*6cMm|Cs#Gc>A92{9`g9fAa}{-RatY*@XOM&r<%H67n~Fmg^t0=Ff5SkESz} zKR5o){+aTB>Zf#FC;g8aKb5_fwfRRXv7T47^~R~6R($g(7KU%i@HH7eM_(~P-^@QO z3|}MqniBMlpCx^>55@U6{a+e)`hKdx{H+mvvmZBn#=qh>`lc4x-x{@VHqpMb--yf0 z>B?U=(Y}-a9mjXN^4FB0ulFqJYf8`;oFRSb+pHb!zTV*1H9kA3;nbZPy88l~ZjHl_ z|C+|f4L*~AFWwS|?|r+*iw)qX?uf%@e?#L<@6s@3`5ir}1bq6P2G;<7Izj)`I~0Dh zL4D`nrs?in_1xVSZvdZ7z|Y*J@KX)oC&%OXQ%5yE(*S-Z(Ozk5uQ`KH@~8LqIR5m6 z#^((_*q4R=Qcn8{M+8fKlyg}^Ur@<{${+L*KY9MHhH7~FG=8_dMl3g zxbwce>(cM(D)o%Gb3kstnVg^F)d)QA4t#RnhzEM~6p(Cyj+`sUerdVRC1tmA@Lm;n z@?Y-26H{tdv|{)rWoU`NQrR#FXB)cnC+SWcYjDzy{VWMz_5q7GnvnkvpJg zcU3$TON!@wDJ^!!H%t?F^dB#wmZ45N;U1tm6*vhzx^``aPXZ78R=?V8+p&i^SML~! z2|_)3QGP>wBKf>(P5Zv>=8hj)alB)A8vVy@+>Qyp?rU~j-rHIz?KYK<4cFKiEjzC9 zps1yXS@-j%ouDS5{ap(JY$<+nhgno>xm)E`i0{hW%Z4U3t zx{eR&%GC{00B?qbj^-WTaYT~(tRJtHnt6-QzfFd74DK$}bU*ly(-MgO>^cblY25x= zYGId!cD<5V68yB-zAv)yrz~{eEt-1v!+Ku4`6C+cvG6VnXDnR&pr&_OShR4;!dVNO z{z~C`Eu6M+-oo@pHGSN|$1F^JOuuI=oV4&s3!5I&^ko({TbQxX)hBsB%xk;4Uw6&c zZQFNbuid$;XZLm2?|JeVf;xoZ7}t21xh+xwQc-f}CDZ|m5!_nOYvckb!l)zcnalRvTw*Ro*f&lPwV45cA_ zJBzdse*2D|EjxB^#}%a8al59wcnG&#Me@q!Mh2hV_}b1$rkyyphU=a&5a5v@eQMc3 zEB?(pcJADr?de1TTjdF9gQO*JNZ!F$k91`>ZtvcVv$uM+K9sE7{3(Ul75W%$ z^TP)Vz#%VC^Nqp%ygLpTgz59gQorjuckb%mv3>8h?p@n9_H6EI4{#b?UjvKgcEd=l zgOZN!?bmJG$~z9G8#_>y`gz#2Jk*%T=Qv9GdT;_->FbdTqFZnYuRTS` zhC_qHfZ=bG@7}p<$4)~DRULWQ9hq@wD4w{5qQ`BtwY6k^kxQ*$O>U7IGpOZSo>Gy| zX89lUv;B5vB9`@3p9D;HAdiHF`P2rK|kt;ssWs{Y1=P6Ec4&X^-ApoLn@4`uYuv|z! z84^DMulLdKr^KuR(VU`*st_dFqdRtIw{-96?2wCVz!67J$EI%314~RV^ejMY;1ej3 zN6^D-O;#BXV7$5+jNG|TJlUN)z|n1_$)(Z$!l|ht3fW;i2%x9nA`iH%O7*95sWSd+ zO%37ieb!v7QiG|HRA1`m#P=5eT>-bJdQu&!t@vN7h4#ISn{?Ly7 zyKvwP(*d83CMxIM$JY*y?8^=QDypIByoou|hIYFb<~MC=Jj&jEIB3z4)yJ22%hgK( zens3g&!1iQE`J^#F7z`8@!Ey(rWy3!FfH}jt2N~PCoqz5yCUu@!7b1md2UaVdT@#N zE?j7t$2|zU>}^LTbmaGq9oRY|{dQwNEG2yF!8@Rm3I7ll`JhYn%k}wvyKvQao^Lhs zw|NlvRU@$rZ*)fCUOSpA4qUs{-VaEnwhr}66RBTdS3Wn|H^BUSl3j0hVJwxBqsyF8 z#}0+bj5x1H!Sx{Y7Bs>bZuh+udH6mk^A1`W{M)^UC?^-M>RB0V-@sFJ@yDS7S2>r^ z_}P12Q|GAPq_}zUIPc$aKX<=3uFA*#Jvdqh^U^)HMd`q;6k?t7p(ETH%_fA<;^d~g zf>+LhQ!Bt)L^Y{3{lvWAO?Z=8v)+pqk-4E#L*d{j=u*o0` zhpxpf?ii2;^TP-D)(JlA8%U84=<=8)jTQNtE1KQOU&4Rl@k3V4u4=|8TZY?bImU${ z9O}pT%~}GVlWV?vqA&cpcMDX;WN1JYaBUnW$&hFEO&+ThBTnvDph23t0k6~XdS&Ul zd?OI_iStB_J>HorjKG^b0<+^9N#=LClDu9!+gld`h%1>e-ygMy$xt(}Os{{$1LM~k z*T^(ei}mpB;)4=oGzJ&G7{=bztn8JXHanL~66@vP$T!TQne~1zxgT$+&^I~)A0^W5 z=~be1y{^6-V*<-ZKk(X_PP(Dk`96nT8+LNE_2`5D6&(OimH=rDX(1`n4)yZG@rG+T z$rZ~zwh9k;VN4hqT_s%u3JZ7Y54t-mRkHQpdib)+X4D91pf6dYRncmR!28w{nRlZa z&PC(RuQ71cUSmO_EXi(~XBG8fVD?~?AZBuepnCM+c0}0Vq9YRR1c~mG$JSth$^0r} z;o~v06?ol+_x1TP38dl{@s%73$~lb~e_3u)Uu=fL=P7F$6%N6J+n1M@(b)yvm7J%Y z&+2+Q`S?r(A6L@%-;~o)eM^UDOqE6Wsqe${-h4XoO;T4nk3B|K&Th1=s7gXlQnQRv zG7n(`Vn*s()IF6YrPOP$g74R^XM|m9qs@KYovzGpZ_c7sf| z*(kRiNqB|FB|HCCPTt5-ig}nHcLvfkEFOM1lJrGHeQz)_8A6Tf)n_A-v3n`Ipu*dY zMljw@<_rCUdC1aChuR;#TQr>S)NtD38H>C1v0j5uS^A8nPgy*bfOk#n-i@7lo8JGE zxm(N0-lpM{tryN%Jae1DjcYh#p<6dA47cbjI97neKY6>tXAS?P;hXz4{XThz;k`@4 zd4o?|`uN-RyCQF=9I>P76X0wY8bGQ>eJ_=PLoeav`(Z*6RKL6|drY2w%}7`5+|q>3ObyNpFNByQT|i7voe%pA>QLe!;fOb1K4j zKHcA}V{JJw5MZbH_bq{jO=H@inXXk{&%ptK{ZNTp;Z4z6e7G?@Ppbxa#qPu$oz|MX ze<`jE(Im`g`rqmsGn*U|nnvC#9vMDQ=(l|4S~tp}kuc=8*xWCoWCB)S9)q+xEr-R$ zl}FDSSJ_LKmo7~V|5Bdm%MSMhC|;9{YBcl zp6@I!wYz(6viPOaHW!MAV4}Va4%uHI9$f%rSO-Ph{W^P=6diO z>WaG}cg>sPqr-VVj*9w@$^%1Ko|D&}@LK8{WM*?C=SgyJjHs0w=7DjJ{Ce#mT~~XU zj5^R|kYvHw_#IM#!D=I}edn8cc;7~P&x#bTtFyldg>~I#W_7Q+ zGcGld52gwP&W%$MYu_f`l8pPC_%2Cf{we((`$Eb!pZZ|sQ6orbF~?Wl>DR?Nrk}TS zb;NDK*ec5c)IoUKO^idniw=F#mR^}or*+#0-!PLE8J83(vLP{)=d-7F_SMCf*Ypx>D*W0gZUuOBMz6> za}o~w_JYJ$6^)QZ+%H!2TCr(HV%3_}MVVUzJx%0?-$ z_JU?UYENp#KZF(@C>)@Zh9vSHN|sP?>pnnPq8tzRdErOC`A_ORqxfkJF@DQ5t?_#} zL9@Q+s`r)Qo_~EaomHVxF8)@IgK}jaW?FL98|Gc=!C%v+`Pf0SxU>xoFZ8>8P1>(x zL#vF;WFT5J@os^Qj4&`jHw)G5Zf#2}X&G#2g2>Okciyo{y?a=4QQt!1g>%OTwjFya zXWhBTOGU|aBo1?_5BRhk$ZPN*{~quRzf-H~Pc zpm@KU@R;_d_8r;xV9AtI_;wuv*|Wf223;sO|H-wYwqpZSm|q7YXyiV)RyXCaT2Oy! z8BnT<87s5OKV2tm7=k@sYF>*Fa{EJdT^eqH4z`n*%=`i{2S+NkaC40e6@kCid~E@KT80P?HX z4lUpWp3m7s%A=!42rhcn&5OMM*~CFb0WCdx1W!Dnv1A}rofz@)E0}Fbh}K)LHq?Yp zeUd4{$xwWB;L(OGe#`Wgg_9OeS(tn$7M1oIDI#{M$#Z{Zoa;-Qc|8;x*bcV+)%ZHQ{^7n8Q^y5a$O92htz116})y{FDW0j zM7M5c4d>JoaAMIzmagi;LC?1EN=?zTv{D-2Nq!eQ#l7i7%r2icnkzp1?%p&H2Wu!g z1r|Y2>Oy#TKP45TD2hZ)TzM`TxL~GZM=0&KSgnHes@33cTN?H`5;*X1qgq?=*0)CE zYm@?c=W3_BbklJrq-z1?wXFg!Dp!4@FH+->LSJWBkJ1tNVul*k7Ug5@@c=i8=zF_5 zXrjSSj&Do+!kCEXLS=r&O0 zbgxq9Tcx4`=IO=yv@bW&wrmMjg!+_Anv7rL#@kn$y(f-+Di6F5327ZxQi7RW>FL&fz{OcGxX^I*ImOB)NE zlHFc-kaAys)X2ho4HyxT&;*;9VBmq!@WXb=1yl8iXh^SuWWOgL?Iw5XIQ=yrws#4@ zzC4F+@4-Pih8LNg(kC*grWV<|Lh$mT0-kLLgvp$czPpIm+~s*LB*e!)@i)|Y3>sXZ zeI-wbQqR|Wzq#hY=v^wuuxt79C_Ex|C%uy!bR0TAyE}*+zf-}hit*r_e_0pu?&c^S zG5VxJjsn}O#Jx2uQLJl6`mxRRdlGyhmy07s*{_Z%u>2z99IkE`#-gO~eaJ|!Td;ID`A-brDTtv622Od3LY(s`^R!nqoSZ(iY_<|7>r4cs;( zN{kVKfb&oD=WbDDin$GSD|Ee6s*2ss0kCfY+n4dkUd*wmc-;&$=DT*yD25TVPr%Z{ zejr4%N1Z&K9UubHZ5!a~=l1)V96`X2ci;G&nmp?*Ui?^zwfGWKHY2>f?MnC~rB|yp zv@0(T1vM?c_0pR@@q3sX1VM1`VNRMt_G`=6PYM~WzqgM7@X z)C!r9Kg3wm%@}Z!*hnC?zyuWS1!Mw9P@$Z0+g{hJiB^7_JteY!t}`ciq(qN% zgph(;gX^Skq91S=#<)j7Nphxpv*Hs!C&!iH1G1qMydz&xeZQ^>q@U^F73ZE|3FR^X32?nXYpXD zxFK8NY10%vYETE7b{_rFrvtwR@TN~%O{Ke1K!NI~E~w^wY2$GJ^?2JdzgY(-q0b38 zXCvsD`!L;4R+Bm-wcGaHC?mz&sQD9O9&2!NQ=p*lYw{0Bl8TdXQt_cp&6^`V&9bLD zHeHH6$y_`Odo0t!?{1_FBun_+Th-{Vw5zxtpg0I9$^{l0zC)p6wN?6`ppbud;XN~0 z7YAg!C6tUBQ=-!S04!?7OPefLa6*q~w6T>`J!k9;GQMD!m*DM|u?XT15ZkTGnKEc} zG$@>t<0B|!3@$+e`5Hk`Ayb0U`IU9!FQIZFoI?r7>#&iU9y0sH?m=3mLcRw{DU=A0 z!l+Iq^B;*giLifOP)Deuo`#bqUMAf)C>0a&I`Ex`iv`=3#7!R@w(HOqp`N;fl*0U^ ze{VN5C-!H7?ItIZ*xdm`P3jQxQxfVhRrmP03ZEK2R$cfhYt0_s!K4G8O8CRj*z}JMs{IhD4ZOh|pD;pr=9_x-MTT~Tz)_ijPh5@CCpE;= z&%$NviCZ98fZI#UHGJllldZ02ic$3Y8Tj{H`#8Bb{+N1_t}ZNh>7qI!DkApXcc?I5 zr`qY1)Zvi;&zM_*_8s2@qvyB-AfJsEDW3w&U5REm_yV>jRGD>e3{}zG&~IbKK^~0F zlLoYV)_J~+GFNW$yQA2D2OUyhaG5ITi|}!wde~W@HqQ7=ulML|$s!#ZnEc zsCxYE=)hji{SY{@f7FbSK@aPaYcM`yHLg=SMv7WW)3pLgxoKnvgyXX3zZ8!76r0gJ ztY|%a4Hq&U>}%Z0BIZr90V4I9;K;}r=t9$D(Z{m0@K}Bb4?LEmBmHCW3}D3qi;7hL zSWB{Uu_6PF8_(nlC1RvGbIkWHmZwGEI>Kx`DGhEWQi_l#%G?W z0ir&oL^vl_2U|SCIPjTl!{DV`aAhR76|^mKpb5&=>g$4@u~C_9M;l0nP1lcQm`CHI z4c*X6v((fE*C4UE%8e!w{Yu*W8AjcFFE`Z+ zG>Y{pu{3Ng3EIjrvVQu(LQZH)f+gwqn!Wd`bH};vGyjn8XPy0+hK4@1D?hlu2THXC zAJpR36t4fW*P&<96$G=jk48S$OZ5~4^e$)>Q(b24=+C&{0AG0;2D@K$Hl3H9F~Lv? z>O=7&7uKxh^pNb|3}nZ%!Vw}wuM!i&oB!NYK>*V#yqu&J_BRNQ<<}2xX9yLHZmwo2 zu*Y$F)76`Co>&ddlQ1^=Ju=&;pkXC%MW;&OHmI5_aL3idr7-^L#_kuKhiw@{QOZ za$aix$Vh5mZZx`X>C@G`)z+&fsZtcR}-yedE(JjLptx`u2ENWJ{ms-B4f z)`#O>$hHN*!TTJ68puOyEBouxe=Q^y8+Rf(>emwoaxg~5z`}m;Ivje-kA`wDa^{8K zFG<9uKPtDBH0?Cp1Qy&xeElNpBwgb456BL&RBHOkxW9YypEd6M-RVSnI+319r1vJ$ zXDseRgU@_b%So;u&l$W6XAM5v0KUo6U6}o{;w?6SpEvw2OdEW!!OuUG8mGQ;=M4I} zg$I{$$a*l|WY^s@)Yr0{M`b8e1h?WzD+1ec{J`$uY8J@Y82GvL*v1{7J7O1@;UL39 z#BhM$LUss3chZ7UH9f!*VkcrPn?|DJpITl^D!YP7@WELI$qmDpLj zWDE^0#+MW>>;3k|A~#o(L2{*6%ZAi?tVLdo|E|LKs}OELxWU#Z$!_c~N*UYbZwx=^ z;n<5gd_+Q=@sxcFJVb0>l$7n7=HMaapof)Ofsloyu;10oTTj zjHX~)=7?rATOO5sbi$~Z!zKlup%UJCm90pEH(a;Ez=1&m4&?&&)WgZZ-43ag+)L3K z9`vU@5xdb>Ipfb9B^}BbC8F zqGt-F?O3SW%bh}n(l*Q}aj}&HSYY_jPX@1i#j$HX_0|{Mw(|QIKk)LeofxX#G5I@R z9nBm&_OXu~_?5?xe0}>DX8!Z=?KjNc@`dL|FMeq8mh$&EFMH?QWs_gOt^Ji-uWH`; ziFf@?>hZf~^GAPaSL+|YslV%Y{%FY$wy*oeZ*Td)*Z$(k`+s=w>i_tIwO9Y(Uq12Z zqd&XjqI<5LyXJ`pU*GoF`)>W*mk*cvzwx(!amU(!{I9?H+uNp||KUse{_7W(Jaz5I z=D*VYk1yT*$`vOD-uvp8ernGxul>Ux|HiWq{r#I~+rHTOc>8a>`=yJTKlIN(`ur!Z z$=v#bBUillT?ejv>GFTQ@0as0|M2@xRc-9QhkoX|s1Cf4J*=Fya<|U^W$#_UqpHq5 z?swMg43o))1e1_JzyzZP1Wt@0C#H%6f&~jS(rCS)4j3rlrDKd1t=dSz;DFMU9U^4uG-M{uPU*U)=YPG{h+3aQmaha zRraCrhkDyxG=4-&`u*?RKM&dcsB_Lwss&Yme!TCHcnIWpZ4%Ku{a{OP?u_AVsb@9XEiIPDbE+R1#4s>Mm@QB+hq?o0Nbf!4!eu*Wo9|Ida6d-o6Y3%;tFlPN(yXv*}+R94MmMe(Kk zdlodR_Aitl+4vrP#wm->{>pM@X~BtVI}7l0SPy5`2iv*nY4_n!raf^jH8fH6E*IVX z6v5F80zc(cw!5-JJqY)o(7DDA;@!nh?`#{I#u zb}ySBUpo7&G+Uqb)V!u4RDxYDPwF)fg1(2I&7No0%0evdjS?<9+!V4J0E z!FWo1{~-L~&y@duaEFr@oZD^~#+P}^z1R5ZyiWd<8-C44&K;c7mcRJ!o$lE%=$@H> z_|Lz&XUYG#XWD%*$PixCTn%^sS<)@j@yB&N9!}vpc zc-aA@13v#R)Lyh{rxhY}dMlb@4pC)$q+|Tez-Zsx?Q)KaeFXM)D9yFA^aM{xu2u)z zA7)$subyJJA$8aT__nRj_Nl=}so}LizkJs+xpRcN^kU~^u2GZV6e*+l;jFZ>{^ft9 z*=A&R%&!}2t>zV7Ut)eVy1xZ5(06mSgKdL_7v z;EYq2XMGa-YKmr(<4ACqQYQzCzLur1;)jL5;*4cn8$>Py!4nP(dsDhrO1(0~|Coy00mC;W*uC5}kHy5!KinVxGBl)pBn5ApmyQd^J10XI)@7hil=P zd&c~w9OJj!{{04mDM9uIeEs&_Gp9=ao7s%<0P>`j|kn?PEPSz#iVYU2I*-N!eO|G#~veR;iK z@a6i?0pEXMj`Qye==^zJkj|t}pPcLHLha>j+pcnpmo6Oz(?5SZZT32x@O=Po7UEL+ ze?^Wq-mQ%kyACAu#Mc5k2=vo-MBrzn)#KRrdhG)DE;7DV6?W@EY`?74Hb!@Toy@)x z^w~pBv9}5=^j{+@7(YY8+|c#)dY9kgGtSbEQzxH%9>Ek5g%$m^j--c=lLwFR^V!c| zKbkTy&-n-CGw?;{PR)1ELH}9eKl|F8f5xXv`tH;M=l?I}tG)6!l!|Q8{Ga5{_NPPb zx*MNq)`qhyNcV6$cfcVOqii`+KAh_d_*!+laoDZb6@=Z>a@~5sp(|9Lt<9`|Xrx^6 zhx!*=@`&=g@mx0aDvZlR;rRDm1d?ga7wSDZy00cje(S%}9)$R;UhFf&cPVXcA)X<= z;q|H4uQx$^mCc`~CNeBk!GnBkYRRqm_}k^w?0Y@Xu`AzKxv*sEY+ZMCgssyK-0eLT zmhNyptMBOSIjf;%0PSqhXX=g0Og43B9~`}A4#9Qcek^GJ(tiE_j{iK{J$uh}&w=xN zcc72za&GbIGT(6S%=zw_>~znbbKG;lryKm5bGP}=o(p~Y3*58Mf7bZ^gZ{JIm*4Bh zv*b*l-hcM`;U&*<{u%$-v&y-}kAJ_PzSc%9t`%po8fRM$f0@nQui}~;erCjOy(a6l z;<){|p)bYVV`C3qPy^jGo_?_rMPlu+7Lxmm{o7&mg`KC_!2)s_hB1F&q|Pp8)vpB; zfKwRQAzKSgRo!m9@6Q3VS|Ez=n=XCn%l+r2?pf30o;_E)XU2cucZu)zpXrUxoxa9@ zu656z%iOcumnT;`_n7ZJ6gL%jvX5ITG_Ae_P*Eu4S%-X);Q>&#>3aI*|QqDMZoX&vI?}Im!n=VY-cYU zb02OEskb*~(mDEiTb=bS=BBX4rw&cG!t%d#>WXw)#|}Ds@h=q9JE*hsC|A?{bn6|C zQ*7Qba`rWEdcjvnb`XDGU-y~vwZ93c?VpL~d!iwCana6*@tX!0>OXWI=uSR{s)0Rs z8yvay$GE=EZTIJHgLxF=V|)CzX7|iFM|L$H=#8k5L-F&+Ejr9rMf2`kj-2?r5jyK) zIda@W&*5it4UIh~I8?bby@aR!b_VaZ=s5c9xXE89`phV3^kpHl4sHeWwIA-ha-a;> zw#QX9w`|c41HB^I$x^r}zJF_YeB3Vf{K;W{=0{G*&MOq@cC)$o)<= zKZ8I2cUZEI`l_OCoxk6>+2y0$>Yl6oXTaC%(*=6vff;UpPURTKKipsE+y30z&bK%2 zfo|~0VjF(XFWfU}mBT@(|Mxxnw>bI3C9az0erIK!{~Z2a{vV3ZF5&nd)ni?L4u9Y6 z$FIkKZuFnM{_}SKseG5M{iPi(7Gdv=ic}s@B$cJ=`rrR$rn>mt+Tfns{qnlK+2xy) z*p`z`>$Vk3TUC)zxAqvIX6y3M`&A*kGO3K6C9)mso>6*;3q4#O*!^2iyl@-Vihp+O(e*um9 zCE@AkJ>OWEB*#uQkQYPmKf=DJI4Gma)Hv&3I35~K@K=e&bery`u|;y%Nb+@3k+k6V zbeqU;+C|<3C!&Ks6ZC-1ppHE>2Z4uA6!{hSrt&e_hLdu!}{Sz{lXS z`$g^qkARmz+XIY~ek4-xAp38?YVh|F5r*~=d4FC+YUfAfAaKWmh`a#a2GJ9F5AFmH zg9A>E$gyB0xO+)NUIQP3O{YZUhv4(eBGLse2ERNlA|HXo=@Gg593$Jn3*fk~895bP zy2{9}!CT-b=NtJYcG=c}NFp>c~!56MFaw0f< zy^;0c4)EzMMn>FfWF@!=#BVcl6BzqFBe&gUvpkr#hv zWMe-vwj1gBrIA@r8Hqk`q~Zl5kA6xTzy~1qcO%!gM&-w!i;8zhR6c5p%0)*-Wdj(0 zOjLf=5tTQ==g*AFmd>b5Jtry;o*R|*Uyn-tH=~jSAA{mGQR!U^4d}ZpDv>Lra_G0C zaunzX&w`V#j>_4f?HcGdL}eTJ$+c1WHJE-K{@~#oqA~yu`wqO|`P-v%?Hy6M6FhNe zRDJ{g1`2;bxOYV*dUsU*4a@*L?up8$AoU+n=>{$LM&*DxQeSk=vuP2t4tt zsJsMj`%P4S0Omaxl}>OmsDC~xhkze|pMe8jKt6coKcn*BVDn2+xli$OR2~P#uSBKt z)u>DcM}kGb%aEpaPZpf)$!c&j_!)Qs9Jj=iQ$h4Ao*V>bgT*U6x%gC1HiGU>PreU+ z3bvi?$y?xWV8%I~91Xg`dNBJ!PnLicZ+SB5kDeUXl_zK4m?vNRVocruMJJ7r+FM4* z;h+s{yLE*88pv%UWIyoaJtO2dpl8bn`3{)$qY=^w5)Y1$a*zZ^gXh5S!T2Xe$V6}? zI2xS&J9xo$;PqFL`}+|x0z``Aaylp}iAxQrD2+=4Xa+ODT=2|3arr%%QWux|z@uQ) zzHzx_zqmXCZm5sTZQwER3GgPyWhLka4}eF&HZTC51HS_!ClMZ~1N(zX;2_WqZUo;4 zPk=`bic4cME=Pb4a1J!uj#wI(CClRyT@jaOPmRkI{EaCxYU9-z=z=4Zt?{D8r;8{^nlCPkpCCQU~g<8uBZ=nXSqnCT$H5Pq^W_EbHYophNby)`9!MufPW&ae2Pf-b}o}aksz+HiG-W6W}HA zi{FuN;PhAXNafEGG83HJpO6PY(^hD~1>k0ICwKrn z@n}L8KSsL2I`B8}caV5IAvGWkl20UL707HyHu&a~#Pb&kc@C7nn2^a}4mcT{366Uy zA*;bPVD!rgSqiQMz2F30+|V(2EPYyG#AL|e=m^n zpu43&E(7bqP2h3xB6tIAI=Dc72o7p3kbOT_AjcnCAS=L!(+ed3F#JFZI1+ptJOH+X z!)F%A=EDo*=2-<2Iif&1=fDFRk1UX{&o7XJzgQrDT38^z`$~a~SYE(5qd?9AJ5MF9 zrxnOFa1^*1JbZeA{05YrL3+SR;2dzuR}18+Rm2s1`RfI8Ik*{oZZ+?~6Cm}?0$Budqsi# zo8n5+0;YevK<0r{dkW;Ps|uukJ$&FN;018|)dliP@RMr_M|Ot`B+7T#STYrq%pK|aU;*-{`$&;tfQ z!w<;^&<9HH!w!1EAZWWEJCFyU1AUbwyC->-W0BwaDc| zXV)opb7$E2^yl~;@6s3cYj<=5_)&F{32(cjLs#kix=yp3dlR~1X8%60X!QEA>@j~`Fp z?;!P)tBb?+w@JNLZ7i16nn#Wosr>b!Kh*s3vs-m##U^S)PIy9*^ zM;M7Dj3gRkZB-ifLerqUP0(208CZ@|1{miKF&=65GgS#s63kH(3u9^JS*|>1V_X2P z0I_&+31giWBgR{cn?Q98^l-e{O!p=jZBzX8vK){nMNQ>W6d5H*v+;irmX^mbeg$3y zv3R2410$)V3VZ<%1nMB`kKUtR+E2$q(oy6gYcDC<>Eb1g$SK080Q&)7PSVO5j|Xye zkoKZ?tB1~aIDZlf%Re8>iAqlJM-Q>R;B8hmm-nx`xOXX!=KV!juChF0E^$0M7EE~y z%mPin&BK`Hf7+J$YQHS?sbxQtXF)^?mhjZFSAVSw`cz)#`pR2d=}F~|xa2j(q@RP= zJHc}VSDR-fuYH7df9^1J`|3xy`hLjGfB;^BPKNVC8ebny* zeQ9GXWy_%}CMi;JF6e?>%OUKJl*Oft<5FehZ&_o>U~#6f-|?ZVW7Bw?GP~913%l)i z+v&dBcX{9PvBqUuk&FV=bq&MS^#mRyP9wQG)?zBW6j>xy<>=tL_LB0BQBvMiD&^0X zX0$9?pl+Fs(GHe^cs!9DAC;a)`xWxVd9g)!BT=U^`O(D1h-{8^dl#EzJLcI>*VR>*nM8F zVYN!X3EHww+JKQBWmY01x41I%ntfTNSZaZasM058P4(Ild%(K~5#e~ZD35-_v#}g) zdBn_jJR0xgiT5$tcr!ojbn#9r-vt;KgX;kCu3^;c%HT>o{4%)7#7{Egy;5VQYTUzN z-v^Zqdn=Y_l!ZKd(y?mTCwWuN@?ku02Rt8Q$@_Q5^XU-JvBdYBVLa84*>EOeIoRhp zP@k@YSneHZbnq08TU)?$B9_yAo~1)P%iyu$*zz38mvzdg`LY4a_bi|BZgPBDC6}7` z60+lM%?(!@x%eFoz#@K=!V^R!WGcY38Yd+ER8f8r&P*U5BwG;xxt_XgrC zZ3b6W)8TbhNp(l1R5y*5YWkY;!M!r2{Unh3Uf)6((=d(%CjdW;o<^o;I*2A-`J zJvIrc;<-j{G|Rme#*`Mw9B*l`c({G-#dZh1SOTZY&Q2ErJD*f&2>Beu%GL{>BAXEl@V|H z@N}sgTibG8hn)JWFm3`~VK-eus##F*F_;;1 z!`)$?Qd>{?Zuw@l4#W>7{8EfcU|jg-QX{qHG5UViCW*J^ch7MmH-jMGa@~v-p$*20 z;dhZS2^rJHdJFHzAZrY=#xz!B!hOf(1a0g3^;quEcu)^F>C?42(@l*VqirT}#QEWX z{z)t^`Si~?dexD3nT&Dc7n*yp zSl(Egab+2kk}q&}0Ic%mxn5!E>d}`)#zX>Qg%%v;W?c$T;r**O`CVlYy5(Hz~TsM5fXsXKTr`MYX&1>5y zse6lQP#t9-VJ{?4>mS0>4@{x~cgnj_8j&PbRXV12V??FTgz4YG;k{|(5XRoNE;8n) zPK;@%PBu{|7g85%*+tWYn{au+s%8l~bQAk|i4Mfml8d;>Ah6%&FaK^kAH) z(dbTGZqAEsGyBEwA~F%3XRc@NLf=u#I$@iJU4&S@4PryCTR$VQCeNdc&bFI+eb2{ zDwgWb%E5|E*}$m&65HmSKZ7=BEyjA_B~r^sN^7^8vC^nH)brT45?PdxMIW#}%QsTg z#JXprn-5l1v1cl>m;ENY3D?$BtxMk&`5AEeMgJ3WM+%J;E{r!+?XTb57{0kGDd*dE zB~1xZ)@3t^4)W^9VVMZLgoHHZ7@nu6s}py`^XPnWL0dm9Z^6}^bg*Ndg7 zb7;Q4NO^U>xdO|1me+V^I$k>j&x>8?ePFi2zQ^!;Rff*9?#A+nmBIMO$WlMJvjstTTf~8)CocPR?majzGj^b=e{{Mc zRc^;rF9-bZVEM@Be_x-G?q0sJ82kSoFf`QsmHPTL{btIJpj{e=Q3qJ7tE5FTqv$ur zdNOvQlOGwSeMN%4B40)|77uD)VcVb`?6G2yP;eDPR@k*&&vldG!)U5XBT~8=Lc;EW z$L7;bSRT^wDWBVTdZS0_zU7oIsvd!w=I1<2ldzK|F&oNo9EhU@N4G9FAB-c7qes`BXW+OUN3SkG&&BaJjy_$9J_<+u1MFnhCF$dE zq;L%A+Vo-^8*yZGk$MG=K@vNttJP=Xc#$^|qwCh^;)?&MSZa-~U|)o54z3wS*Rn6c zwGP)}qpR9i;o6StBBSfu*DK$H#j?rhO7|w^!?o4un)e;Jx^cZ`boKjQT>ZErQC$ar z2-hI4+NiFGZ^c#r5aC62ZTv}GX<~Kuf>(Zwbj!#`H8sdDZ|%1U7bG=m)4|6p03j$?3H%wTKxqkw!pkd zM)EbdIemh5FJDG3WKLh9eQl}jYqd&Mp=*vm>c+!u>fYDL=x0mXbj+3t+*_R6zGv;N zC`@0+{^VyzX6fzNmHyr8LIb^VQ%nXoa~H-f^vhIP{f@$Jrvq}jas6(#Lz8+M1Fb|b z7U;Q^J><6yjXlHeaNFb^$@fu;F%h(a!gz5X-`8Xfu%|hLb*iv$;X$@$Cl2I2ZKd{| zXXhDmCbykye7mXkeqlb!X=9&)!iN14mUon#w&OL&reV)Cb^f}{knT*r z;PVt=83&Z7T%V=gI)_??ZfGKYRlA9E60&TZ=i@lZ^0WTo_%*7JMB}fTF?)Vajxwoa-KKJ9X-4zwVMx!?{CX10v)~Qj z=U1=IuixU~=hx|8{94oNwVUgsu=rz*k@Ts>^r`f5>s_B(IM0t$6_$xW^P*OtE-#*o zuQxO0N^_>>ht*#W<>Q=y_h>9jecls?c-O_}nL}iivER=yotf6(b&xK<3gbHEDQ?Eg zv}rhd7?(W=S<7P>PlHzh>uJhl*VCTIO`J6!BgQ+)ba;MzOS1a&k07!6pEtiq%7CV| zNS~2z-KP^fUY<4c2tPEgn#^mSf0gsMHZ^OZNXyp#m+w=!? zPh)u*Xnda6Cu1Da6JKRE%1K70QWqk^O3={S;6m00X^`^Bm%gbH`3)F^Rr4?GcHchw zSk~ioolC#JL%vufjnLK4#`0wlE7Yc{tw}Xk$>xE_Q7gU>+s~hqyLzX5U64X2^%r1S z1B~VC@oqJb~%XERRUJy2nOjtUm4cXV=eM8qkLf#v+o(`saO! zwf#ldZPSSkk@|cxuzEAX%W?ZOHPMJ11_GL}JCg57erH}Lnlf#*hb82&4?LN|yttZq zahcAu$)n`th^zpU+gvB6GHf=qsYux6Y=Zp(WZe*q&F+CE~C@*hZQpZa$^ z^+RW^=HnsPIm3u#%UBDNYhp88PRy3 zjnot^9v3e^T`f@AHL2NHj!_oMb-rWO)zOBz5Coa!;Te(#^Rc%_d#ZPw_8i4Uo4SiZKtI z2(sm&tJo=99_@JhaER>b<%jA1_AW=6AA7u(}Ha(<4%cRJ#=t;X9=9?AfI||1VpYOOK zKE_~eme1;^-I!6s><)OZz;Z44KJdq|Nt^DQ@$dtm8;$p~06yImxw=?Aw2w(;Jq2Ya z@vna#%j>|iQ-|J0t07Ok%@|-Zu{hn9fvr=yLm9l4@9(fz2^-p8G0%FC3j6J5-Jb}@GQFyso7h7h| zlY5P++cnN|tRLqhEaQO2xm=%a{CqKCAGw=2w*-8%u^jF5&3Akn=Ud@hu6%a9#`o)w zJJSK*1z4`|`PMo!?DumUp&Omw7uWV4EWiLv_59nwbq4sH{kf5 z&wG>O)q3%yiQmq5cb{ox>|2nhYyFwL{%A_u^IFw^GN!C}u271*&@rHw%sfv2{RHE9 zL}*{0>t;-7$Dpk1Krh4SODmakv){7v+0wz0nWA)Is-R8V{>dlsolV0y1eioG!>G3S za(TgA;YknQ&1dbMSgyQhVq6HkL{B8b-U(~!-ihn&-igoRzdhi;7fb9z&uof%9GxaF zJSkvptDtd2ruGC+PC##z$5=D7cSQaS9){eeKje1nQst~mm9Z|hmsD@bU6(3bL|n&V zG=Ny5kGQ5=mk!VY@|a-Auv0Q|smmEaw2$lGv`< z)2Ll#rf>)I@`F8d<_H!sCP3!PJ7ACU`oi)a!eK?+;Tb(siwHND8zKFL5gAi7G=Ir) z(|_ePF&5|jZ$mU;cd6E|NHya|SHHCE>rW(44#YSTxNmI0wmm&7wHaaCA7)#km9axZ ztU6991nFrYJ^Sr8f4UJhAwvFKg5^5k@~7A4&kR$h8FXAe^TNNI{E=R>nIK!dnbb3@ zpG03cl60_tAreWZJyLp&uej))1E~3 zGP2js!iGq>k@B6z4H3p|V|iDmv}MfiSkJM0gSM2U?cVA~@gEhDQLA~9mqq$i_hz2K z+CFtNXKkN(So(yBoUte(?I*c-hTT=19Uk2=Rz|ZvIhyuvG-Ef_<7->j2R))$|HkqH zNZ2)}?nb*pxzd`=-P>vMdNC4zH|NKu#@iC4X4ii2BiL!KB8kX5gM?ZO}qu2JS)!UDL5*|Wh{XVVwnIvvkCyS)*jEJPcJXo}B!tSctn2hT1 z*WGtI{lbmVbwX#yGq>T0zbYLJ+}09qsa<8twhuGh#QB}r&Q3VRFKCN^eH0;i!x}2Y(7_}isWj-ZrK;hRB#wzJfs=y#%0ZT zkWqSY?R}Hj=3PjiJ>IN_XZRk34k+1vAuVTOIS*V0{4riCsU6H}Jp2wO-(%8nEqcmV zNae0&b{CYk3?9Prq_R;4TOGTOuP%(uM90XOV@d)03#*vgRIm!TFSK!tzld&~&zPXlgr|MqFv8>&Xp?zadB>5BUh17c30!uc3X5%l&vGSZ)Wg(D*fD>vnhHD!DUO z!7`88&s(MWW7qL3`MyT8j?etn?$~f;_%b50X?zoh_mo)!PiSvTw%w4=@y&vFA=A1U zc87J=lb87UuX|e$#^FsOzk@O#8Xq0YtL*q_DUNf1=FjQ+bYqF7(RiD=fkoyW@W=RO z7P|Rv4i6Xv9Bm_vJAo`NJ>&tAguN6Txx*Bq}-oz8V(9%;ke z%^aZc6vEayKoyorK;vAiPdD_>&(py$V}A`l6z5sWr}HM=D|!?T;%M7~5Y6&{=4>o$ z0Yc0L`gG&$^WbS4CiljG=MF3n`8@Xy@vMf&uK$L@83=e@!SbHZ^X3rG`S8$F<;36X z@Z(>Kr50%1s`TmNwhkV)7&@L#S57Sf&ulD5`#kd=l>2SZCO)sYv$f z^jjI)s#VIbdt%pOxz_Rd7$)AMzjSG%d(Z!53r7e2%iTc+U|?^DOCdVx1hd>wUr zjkyN?x)Jn8s($7FBzqLp(-NaqU! zE7_xbs*$&0tsO;wyaPSWb2^sh{v7<5fDiN?=^yS6^H$lpS|xL}(ahESHkl7w`;GKh z{5JVqEVqIZe+sI~w{&xg>Nd4ojUJ49a-G$mn2S)q#OA~oC3?Im(ZV^!i%NRD`}54r zj6Le4w353xD6dM!ONE1pjK6E!?SCMs`u8yY20XXk(4^zlWa1dP)Fizw^8v~7*S)jj z%5-TRTR^(@mAZ~PsiV)TV@#Z#DQIKGcbr^~?70}rK*H+Lwwmb!oM4}t!MHTT8bojN zAl8y?GRIsP8xwyFbzA%rWBN5rr$b@wo3$8~*Mk_>{$sft=y!3OKHYlM7S=_tgzsO8 z+sm-oxV?!b8~i2>{&WrgUy)I>!j~}-OUZUW3EWtcjGt>|)Uw;4XHTd`O`=KRQ%Kz{+dag9gVLw=0RDIsFT7(=>+Bp%Zp`v$9^)Nu*NfXusf=> z9rVN5uHk4oJcFg=QD4*sjpqe6p8tZ3!j-;^ax7UH_p6MhDkDzteES*LS{*KpEHjcf+tH4t^b4N?TF-&d}inN(;ECx;)}9Cv7~ zW#AMVxQ^U#)IA4m_ru9`=Z2HRPdH(BBp#LcOqNO-7~g9G3?51U@}9q`tC#mE=IT^fe2 zK@%s9kNRx@5Bg!}y8W7ZD1F#P~outiDiw_+4d+!KYtJ2^3V3n_Q7KJ%szxAmT%0#o_7ghR+XU}b>{+u zGb?s{kbx{aK6p=gZqot5HVrFc_vaR&2VtLXQ$ng6v#nb?vZPp+u;=&0t~@!G;l!7q zSAH=}{B=CgR#1w2;jTku%_%W>rvJX}HI6Ry|}k zAE#lN1+<(G)n}wz?<{fStef!Nvy9UM8RueI=gYXr$YI&4xPU2pXu*v z(tiFZ|3n;xuaiGjdFEj5CClc|XXyXJ{3`RquVX^tE;oqVBngx+; z7P$C-M*3kHN`Jt=)iaC##q^&UPCpKteoFLTO@EkwMZjO4XZFqi*U}HmQ2Lc$(@*~S zRp6hdAC{r?2mFr>EB`&`e@$m7{E0Yh_#b;_nyZ5UEc_HK+3;5c{Lc&vf6w)&7nYrT zH@5!Xq5N9^X5^U_|62Xag!$hI`017X{CDj?ZECxIm&4kBJs&ueK0^0bb$Sg}&)($g z*@Hcr?72yP%SZ>PJInR$VRxz3nbI$zr`7Xr+Fqxjt3MZG4e*M4pYn(M-aJ|Fk=F2; zxEq!BHjI0fwouh?spdg0gffqN;jz;06dT9MVZIHuzGnyc=;myEe_lOhTT*s$-#_E*b zo=@4|wzJE5#TFOGb2g5(N(;{gjz`zazUOT-v+;9t&r0T=qpAC4*}3Okkgi8={ry;; z0!|&-);vfKPS&hRH`j1KR{S}(hWsjiGo(^gX(WGro*j3*3rjY=ALB@z?a~|3r%Ue& z6I+u_uifv-8F|LFdQM*Ziv~#bvYz^bu`B|GcK(*O`sbw8(IuPvDIXgjS5%3gN{x9c z_G}@GbXB2qDXU<&dAJtK2B7(Ig+5(eCYkEp-mlQ7e2-wf0R8|9eZ6dRLy!@@%_*$? zx>@&C2oR{3hb@lArlL38#xO4&4>`w=M-i5BK;u!aPZy77v&6VEsiI6M7v`6NJ|GF% zE#ybbd@Rer*FZKt!!p9g=L`rCtQVTSA1!>)`(ENcGmldYb^f|-1^Xh(vil->p|*O~ zyRr0xXFwue+!kbUvbh^8OK-9Gc3vS_uS>h%#Cy#R@m5u3h?|esGauid`S^sKwNv>T zJla3zW7!7;>pHeN*>#;-(`uJ@N_KD}>1-os#LhMG{bJWMzz^=QaC27|&Q!vgLOAs~ z;VegVtA=wXmi54`TcoY%O)dmKAb(A%&@~S-8rCfhV`z!?6+FBDzrC8&rH?g;azK1 z50l-3w+(+bmZO2np07_QyV0w0vUjt8rz_yS2uq+VyJU#>RSjzrb4+ybmFVCr(ZQ=; zgI&rtjt@d=>)m5mo(8W1^mQ53CT{;PZsJiwWq267Y-VYihV7X?U+Hv>_iL zB41ZNF&`@*I%48sCb4fM)HatvV(DwK1a0#~<#ReH8^6$ZHY=b%3d>@pL;i7&UUk2d zyh`^igyOU+;L$j(#o;xuM<+;k?tZsh1KNABYy~R+A$_`U?Z+HlIZcwLD)b${%xgMD zmyUOEeC+eR@Ay=geYx3^a~DnF`F^^quuKFRPOUy&U$B{atdn%R_TjGsB$a1A#xdY4 zfVI6r_8=#dS~w@+0kif$`Npd@&ED?Hbq!Q4S7Ce?^a1wf*tJI6r!7Y@8~Z-KzMv`5 z*fwu!EJK-v);+dEV9V@zEJ2&{isRBU_%O=%W9ue&6*1P+{YbT=JQ>B=+C212eJ>jM z8+aR*TGAMH+i)7)x<|tWoU@sRp?mK_bzlx&HvFTpEY)_j$Ui>QFZM_@cCA}evuoXz ztaXoOt($#+p|x)B8~%4vie(Hq08obqC|g&DC*Xll{b24kg_2P zl|PoH%A)1(ShfCjdvi3a{k$I9`_~=tU4i9hpKpWX)4N`|FKfD)%RLl%x^IO$vVLKt z69jwn!)_geRCG)xod=N4{c_UzE+Xw1{9~2fP1yp{S@TPNw-+ z_KE~c**xFLFYcooIy%pMkGVh{_YG(|?6)*@CPM4nJS4OnhjA8I12{*PM8M<>f%GoX zsP;DYVfJm;!RgnbAY*m6@Y-l%jyWyX6hF`;IHNUH*Wdq*^>^l`jHjv@PleatAB6EB z`sfx-%VRips3el`G|!wQyk9_#<(Axo3yR++30G+nu4P=f(kmUqod=w$8p9rnG0)mN za@sTvB^S}R=^n2tEb-IW+%=mYj415MvOY$n{vNeKds4#ciQ}^O za~}5}4DC^qJB&;P?XYT_lj}w|3%x)e>`@E9|7W_Jmyx*>k#_IZn>b2zHxas@?d&Fc zDUUkb?CXyXAb)qif2(G7NXA4Q%>O&Q6`pz1ZP^O9i*tCLnvY>d`EvI>uU{3Gdr=_w zUbcdT_i=HT!Snpi3kd3Z*zMB4=eWNd4&%)rj3=#bPt|9Ueez@ws>a2nc<0cG0NtylU9Uao#eTSvJKegK z_C0lL{W6+@r3tuvPscuzcXd!#yLa$i7qR!WkTn-%{mID9Ao#9x-NAQFHGy7+(N|62 zo|4FD$sZhwpT^@Zg5FFV>+i?%u*RcMyT9((MN!kvNV;_IeA&BwwCv4TVsH95?$Jt@ zr(9jw0eSs<7$0W&YNN4Z%p?2+ldTV<;VW#(S z<9O^y-BQ7)Af=@-&K;FX>6W2=vhB)uBF1SUzSfdYX%&t4uBm}7XdiVCuRW7dt9P}0 zz<8{S^(@vH8~(v>Nr5gz*uL+%LhC+KqOwN)kfHCx?l4bee4&g-)_7!%XT5Sfy1Vg> z_I|PcQoCMRsCS`c?>38ni#V5JR0FTLhD0{EVZ>(`SwyFI2$7Cd#+Z-N544Zawa@Hb zR1IEKz5wcB)p+H&wcqaIt|C%D9t7t)!|qZ$FXT5N{59}vG%hz`+@WzvWVqTa+1&69 zCklHQL6he(G#;#N=y%?)v^s}-5=*RAD$IUTYWrsNR?PK+J09N-=-yYld5pWJL#OvZ z4$%~@LsmIPEkHL(h?DHrgxC_otv98*FJdRB%XAe*Z(1Y#r1H$gI03|pQvnYLFZe3c z@eUMfH^$11mbnejeOs3IeC1t>ah>HI7|wevyo+Lg^W3=y_MUQ&P8snnWB;j!{jlPXfBUOa1VUky6_hjM}9wsE4HfK#W6yS1iwF>&AH; zQ<#H}kb6Mrs|p6W|0bL3G*IOGa#(D>pNZu{<+pv;V5{bSZ{bExTAwdX-U80(h+X%~ zy?fBsmD^#nd2JI8P zv-9Kp%eeOqsLWD*M!NNP6wtg>AW5os#tH>i#v4oLbn;-o1=NN2DbHDC$GSAjmon3`X&<>%RStr^O310{EsfoxaPD|gLMA;IlUnv~Rd?yP#CU;rVeqi1j7di2R-&Qp zaE@EsVcPDJnNe8)?3hyhSesGzGO)wv8}82G3HF#UZp<7Sl~L29Id@5``vfrg3Kxf5 zx4*}P{zLC`%;6`#VRxy$qm3yk_a2R5j4%eB#hA{DjMnS>kX`=>#x`IQgC?JxzFaz0 z;axh8-|LB#OT}PWX4F7Qe{s4fRoLe0&b!L@Ax8d{eDC&t)s$_fBW@Bs5WB;~HjVJ! zCYq&H38{*V&22{;m`)RZ6YTYeVypzk_S$ju)-?BSr8;Gmr8CS*H$R_V^E}%h2PxND zQ}LHrYy9!V^up#uWie+}?}~9T@|fc0ghX^4v5-CJ18$AJR{O9IiY2iPwPriN-!L~S z-v?_5(vHh>+(Z~!fmVjmy0weFS-W6AV`wkqC&+nJzq`C|v&aeDJ!id*vWC+fvEw{* zgxh0nzg_lQ4ck|z-)<7N%_?Iqmdn7H;%=IasXc1+sWG6&pc*C5VKk`Grbdq%y%-m! z)z*h`%m7B&2i#Iqa(-ER#fhe}q2lH-lPj*2c3PpTJ>1!+<*|*RQ%_=;JH?HAIL1i5 z8Q;$Mhk7%9p&R#P@6cvERL1wt9k1ys&QT`lFQaL9=U^Y{DYav#Qe>1MVD|cLKjR3Fg6~EcXD{Ur<*U!WcWAkjB$$`T9KW=;0gIdpwh`FFtk-+#@? zQJDf>)Oe^L-=ey220PRbxx?LI9=E2;7^JCC%GSGa$gHc#kNFsOA8&CR^_{Vz?v*QG ztjMzoJz1mEpMO?6db!HkfDvv-8~AXNl)vp?S$o?}|GFI(TMwVX@)B@!lnBe2!fuvk zlH~6GW}aH>p(tyjU*4BAgY4b7A$O#LvvdpH9lS2Retl_l>1|rC<&V)0;%@&|bLJ%! zJ^IC`Pa4D;Z{qh8-iyX>rfi#~_u}Vf94i9FT(D`gM)4?icV|u71Q<--q3)FpqY6 zw4Y(WZ2vGo|1g8!D&u_GWcr8k^bZw&{~)*WTdQE3A4aa5J`f&(N8qu0@u>GXG+JMX zYwr)E@>Af(^2B8qKdNb%W)Jb{+r)jzn}_0)>)w5Qa%lYcln~a`Ve!fJ6UlMwcxDH8 za8mah)<d+oHiQ-`y;~cs#?8UaDuPv z9lBd^<277qgv|O)SRMlLMBA18Y;I#Wdc=r1L+|;yS&foq+-~M|#3JnLj2FfVy^6TI z52r*p!7Eci&0Lp;u-lEt_TFuqUbeyimd0V32t2C_?ru*0j!L~qmYJE}O5$eM)R-@G zA7RcJ(KcA>kHc6AID_>Ht-D5bVTSLxEq+s8>=(>xCL244K;NP5=^Ev^0^=HB+r;0D- z9)`MDXMEa-*d<1er=PX+LtSGUx(_IREpgEEP^DO6bu!sZkgA3P^a;h0y;*qN-TBTW zq#Q+~|HE^DT6mtBRI<`I9iQrdcfY@_pCA(v!haIWCpP@FhHsbeZ5C`Y zt(pfJ>!^*o1^4LY1{#yLXVvVj{;_HGX2v$e;vYwgOHF*DDXBH_OzcEd5Aljvb7BH> zhkf);^h!N%ufIDH>@T(DFzY(M9Oh$L3Rv3wtse*1VRc`s-Bx$vQUa=vPl=mj$ucRe zh@q!14BA;cuDTrd`VAQ00j78WIc8t&RB}f^C3>04E%ZD6CAME3gw*a|`4~&#MjZocR<+sON!$Gn>BO-VeSPfi zm@JA_P1ZYtZ(;1YnLQ0RxIO0s_7+b&uS+80=XAs^^RbM(o=py5q3sXS&D}5PFh`AE zjAQ!MHlRjEjX^aU=qIKoF)l3mPj#x%mi22>XIhOOHTu*TP-9SyRF);=m=B-W{AaZ%o#S}9X@-_AfoV(@ByS38=%&8Tm(C82e7hFNuY zw1hysnX`_VOuCMox8NWiOtaGX^62A{?3BjXr1+hT*lw&mzxt?HeedLlqD_Bqp4@U| zYv&==t)H6d_q1*@$qBP&nJJGfrpqL6L43C1R{=B(+kRKm zepj$oH`IO~3&&>O*DuC$I`C}V+M3hahEDO!R2sl5`3CkblD#{|p|`B2of(VXlKlol zHzeCRd@tiG;SIDci2ih_1BigB#Qw{8bInt9;M%ZZf7KUSn>JHJf7-lg;0R z>sR=(S$|(yu!m`AUzztEfBs#BWp5C(dd_sC?QxcA(_MTL4d`>RoiZ<0$HFt|K7enL z^T}20OQ}NFYFp+3y#dwfJB~%nYMXv6rvuOSU&-kzEq5uU#j~4e8r;CGzae!Rad2(@(3u{U|AFPB4t%P7 zR~qd{swVPV5u6{gXTx(^IhUr%H~MLshUIg>6!&VILw=)A=eHNA10Iulo!j>yuXaI< z{qo!$-&HOidw2%!5my zn?_c+yBTP6SpO_1U-Wku{NFc8)#_LSzf$Hv`u()s5S5dF*GrtWF7Dv>z;WLK3e}Hv z4R`18SRLa~8RWdPW1yk1wCi~tWaov1rS7_KM`e{C)^K+Y&#KmG8u1d;^Zpu5Xg>+72Zi?Q0iU@+LSRR*hfST~*JxocNWYH$o4S(_;nkvZ~!vRf#>acTgsxa3v7{~8RInBFq0gS%vh?p~e+ zS!yl@Yj1n_ZH>XwYCU66$6RL2E@ySLtz4(UU4^kxq)&ix=`NnILzK2dr=m9T*ORVK5I_75phePso#X>&chRdC?HDQXjtUu^}%o_OEWxcgdmib@RKSt*WCBV$VW3doH+dDEoUe8(~sq0kSOkZgR&e@BcsM3#+_Ch zJZcFlnM+AdHTP`SNmJsAV64yi5%eab=&ytG3WJDRt#UucQu=+>E7_^JRSWblQG5T* zJ%}DfrEG{FBHTa_J4343V^d9*)83({-f7pM?EOAz&7-~8(}j*Ej*`BIF@4qNDRD$Q zJyf}MFTw8+^-~$x!|pJTTO(u5W4+UZ_ri0e#`g{^eZbX&w%CL0Fhi^B|K?dHk2-0` zb*$y--iey|p3DUgYMA;R@r|py2W(S6WDa+Sc_N8CNi2yCxY~g1>7GpQU>#-$zZ*Tv zlP`dh+kM#|#H@S%Z1zs6AF}m**d69^I)%f~DNIMF(7cOI;TmMz!gtlO35(pp218I^ zJfPoZMvXx=2MQ#a+i z^c+ljT1ZdBF6rq&?pd0iGqL;*{1Hs%w+M(+i5fL(G^o+0Mp}&?HTu*TP-9SylGiaB z)M!&9jZt6RgW24VaZ(0jwWco;onE4!ND_k+3f1+K-XKJhmd0b!fq9{%Ej8n3do^oE zj9)%s()cN+u3`Ko9ygyZ89&$T*HeGqr0QtUG!kM>PS1^TE8^raQ+vwF;wwlhi1!+P{uU8T%dN*H?v2%OZ3MdWCxw}N;x>s8v_gXJ0xho$4K4IoZl6+ zy0(wts*n7DyWatNuv=6QrsjY(yW{tmzj(`HN5^UA?EM9dg>}qUv%e=_0(L%07-9GD zb*vPkbx(i&3M^*>uQ){vZ8hwU6&dzWvAI{rlDT)Q>3Cox$kx3f6KA=AYraih8wzlqhOa<9ku9`F(wn%7ioH*h!OUO&S=ydc z8Jf0NK<;tn>3sYE zyf)AEKBQMP9iKQ|U}Mf0+3Es632609zut|&pBod}-}Fh>YCW?$1jc-)GUhvt-_^Uv z#Vzc1>nRghPucYxKnL`XBDel@ELVWS;sL&asTnl})sQzaN-*qq(OaAsWz*N9c&$mW zrL`?l!#Llbn`c2WGHI8wv$hf25j&GMw*DC`pMsL&8Wq~0MpBJ7HBxHyU@YwWCTsk* zwVV;HEO+5eizc>43zsKW6)%n^8swNnX9?R9_S7~FXt>G$2(?Y~aioAYNk{9`HAyvZ z*(Pa|Hc3_deiC;u4#qN`9b+ZQm)gDfO}yt4+~GM&y!H}Fp1~cSU^yaO-7B!}tMNNu z{ov8}VRsIXFxN(&-7}6%87@=j$ohUa?;;i2PO~P#T&SXPsGS}@E@|l_zqL$fV3`Bl zxFi{GW1Mt%RA%xy{Ij*2^Oa4@Y%P{PP-xFLN8DKTqG)Ss8Iv{BsgD`aJN5R_8Rm8O z7`2YOoc|Kzs;R$uuX&03!iSiW(@vM_9bp|K(aq9_pkJXMv1=te5MTcu#>c=b?xPA= z)qgs+fZwyb*!w>yI~(|@imU(6y}8+JHp{XJ0g~|21OZnHG{lesMO+XlTC^)gixhF8 z!6HRnVz6kzE{F;gY@}+%q776nTIxUYs70%GRcz6!T`XGhQJ-khs?~PoiG9?n{eREQ z-Q?~j5xSqxZ+2$xyx+O=a^{>fhW1%wbWP^$Bn_P>a!((1m(+J0mMOpq#dxi{zt-43 zs|1}DsOMCb)uL;>Ty=5on)pkJaC6m@{48*6iK6F?~-%9nuHzb?83U5nx{}x~}}(+v%ux zz(I;4{VCTi>vicLMi-6uS?B(dG&CJS)&_^oyr@X;I1t%m(#&?q#^E&oM3JHpFRq1_$$-6np7mFHb<8ZSQ{VZ7;7 z4q==Z9w7L;B)?Y4uNqxI9RpOw;ejgLJe2+45Y@JcIstVr`G{VGeHGdr-wYi_{D>#d zyWHeW+?;rlw>KTPJ!&cQX&~DkUbpD5g zHgR7E_KKf`J@D=dVZHO>HQ%4=qlcc z|8N*OrlW2K_4m2$1&_VB9{{t(k2G`LQkJ}Ugz=_h?%fNqw+*VYCbMU@`V^@?i@g#9 z?%2VaB)-#8Yk+B2c^~lm zvVg~tB^ck;;S}L)S-^LW&2i7z+-%0?*fvL91`m?y!yfVqRS*8;BaY4ny}FeFhuKvtN1nh^`Tz`% zK@mGnH&nmPDz8?Is2C|RG^1rqZ0SyCTx>}U%`uQ%*orlxXXL5&fCILFb#Pi}b=m0h zGb)x=eNZyAzG{uFHnWBsF1j(hpCiwv2wSB*A_sLA4&STl3`{FilY#e**YRSlN0z|r(*_=(Wl9`iqHJH>vvopk|ILFw=yw9iF-T7FM%CueTVPo zY^v7vU2^t*q@NNBsBa#6Bt{6&ldR>l^AL8I@2O4V zJb>|Ai4&^+8+pxA!}Wzdnr^?ejB}RWb?$c%P3zS96wCOBafITeF}6jF6o&r#9xd1; zw6sSH-Z6igLcGZAP25*kHpI4HDL~8dXy4lfp&P@umaQoaKER5M{+TCVDBsj_vTBlk zuoBBPz^C~qbZ&7&nGc+A-wLDNF!am_4I9}4;a9I!`mA|3Q67`{_lX6Veq?yJS_0?8 z<1$z-1tN&M_v&3X_Cul{k+iHwpsxb2-a5+;-Dx|!>?aBwv=FX=x5gQE3~A0Lna(uN z!eLTbTOcFX2__Y@C+uA=xW${sHXLQTB*g^|N7oIM>7S-Br9=G4vkXu}I5!w_q)T@D zX`)1)$9Nf-`#IwNCiYdA6{?$fF-O~zhJQ;mi*~8Ray;Nh)$7#^ol>cUU6VwOAIL)(+<|ef=o$Z_CF2mBjzhd6T+)-ud%E z39sp2SmdsdM++Rbl}Dw$1yjTm+FO4tas~al=+=}mz9s@CKQL$Gq&?D;B#?78aT zVsqshD;`C~{YZ>aVgW5azdtO*ojM#O(k6*KXjhg%7o2A9pO$emCc(4u7D_z(Qkuc6 zLF*mW#>;;cwAGsRo!4#pXYcy>+DJ~1T@PUSJuv;V&Al(gd~sU}oRyA!Phrk?+dB`f z^O)P`Wi04}BXp0n{}{&^b~po96>`s~_{ReiZD*u^XYe*PPx|`tMd;m_uUz^Mt0|NAQj&X& z=aCgyHiLj!-!`=TgOi&S#?={&yQ?!CKB28y^aa&9Z*h?f|Go}2#jf>N*b_u)b!`Ak z0JXP9$8x~*S=Q2W4mGs#i_|Yd{f2d`-@9b1$F>i!RPD>JU({2-T2K8T#t!#C6}-dL z?vQZn z%4_Zw%B3wyKEwADXAH&!0PUNxszHpv0?6m1)I|=Fjl61KiinV?fYyGONMqag1 z;$Mhy2_R2~IhOg&pPT1R5=Zjf0Rq>n+0Jwov6H=)aep@a0SSL8sn;XgT<7jTy7RC} zy*`mRg-`ZTC4hYkRr1L~dY!aIxyTi>f8xBZoc&jJA5}}x$V7~jb(|Kd%WQQvtS8wx zgr_fOi0A}k5|g>Bg~!wI)8{JNa5(e1kSI6Iz1}bn zOPDyfs+0GaGBpPJPGkdmZC0U^erb^!1YgP^_)-SJBRA+s*S@QRXpv7bPJD_@Vkkjp zYlvYi6ys_!?iRln#Q02%(`_+-rOiGCVjrDR^PCOpMi#=`{hMH}-GtyrN{tPq)Z*|# zH8ylm%`bmejV*gt)!U_!kcv>YVU%q!WgAG@%Fw-3#=c+n@6tvaC`#lyjO~EFOdbs_ z7y+hMXA?)->uHBx&R6ymWvjH?0W7cR_!-{I=+yTLRU9U|IS3p`yY?2}{|8U}QXKwd zXB^!RSda9jFKE4SO{TmP2o{-&@m&xOwUJ*#8e^f@t`^(fVtYYspNZ{sTdcznvr zMFMle{D;Vio;}CDNX_w2QFDB!QO?hk{ce+dAHeu6pwE(~Z!vv#!BOJ9m5uj479C?* zk7G*sY0|I6_yT~oc>wmgNwA3ZH2zGMurn~ero)E+i?HqsWmnfgu()&E9Udk+m}u687yxC!_$?M-p!)SEg4Fw z%OusO{b$o}t7TuKY0h1I{n8J^&q)0-h65+0=z^Mu(04WpU|FA z>9%|B7`cHUk?Syi0_>1tEQZ~s);8a4SvGyGD@ex5hX}SVoBk6x0vB=B+B@*m?CLT7 zw+Iq>AH$^I&UkzcB;F(HE|mA_ zN>kR)i@Vty_Xdf(SKa1~D?CzjkvHOOsq#qI*}SyHeu8)M86Cjk+iV{w(yT>2wnYa) zBA;Rur=^cq)7E;dZ1GJ|44S9gxpS9kUlHbM{`cASKjYd%fbO3q13m&*M& z{Z;U}uC`i0u&(;!@U_8gOTBstzu6o91_^(c2`}rArtZ8udkLb?$e+M+5U>WYpOJqn z+4?S%!oxO1m=%n2^t%Dbz{4YeOn+8}IIh<}*))ougE$K$jZ*oyl3_`Mt2k1kB}~%j zr5sU$8y+t#*>ap^(#ViTZ}C=T<6VX2dI?Y7TTHx|TmE+Y7G3_*Q6V)7UjGruwT^&~ za0KTJBevFLzR*4-Jt$SF*AM%N8aaURDlqr!#`XQWak+p6a?(A>+Z{tX;Xk?QRATY% zaFC{S9+0`Bj}ILc#i~yob6NsLP>lJLCF~50IRF_y_N0bh(CUm*cafi_Wx&sCeL)Ey zhQvuo9JxpKGAw~vo!7
      T-JdppJ+0KGRS{r#lhLz&VN=Mcu5I*#mKWW$WZLiK(pZBiq5UmefgS3`Te`)Yy2S%UF1P!f@cJ74ebuk3mhpcwgb>j z;eBBpITRh9L-*v;C0~@hU&Htm1VX9zq3p}?Rk9(2b)gt{32#xn>~8xaXPAGE^B8jZ z(|o@w2;3$e-pKbu3Z0WH-(UMY=}yEr8Q7r|<+E9*sLE2>wyx`5!g)M4-^FsqKM6a% zlNTkn$fv#oviu=lx0i=B%Tr*^mZ~^gD(^g7l4mXQU5-Q_9qarGc-<=u54}Fadzmbo zcpiBJOT`O%P4Y+9GU>lidojjlD7xmRze2OrSO2g0@!$$E#>Q|4&-JN09p9Z!@btp+ z*IA@~PJXk=Lg#lDmaU*d>#{uN{1TGAWO{`dA#UI?Ip67&9dat()tMVUG|%}z*({bi zK8fXzz}9Cu_Kg(O@JZ0^gipdB?!d>#p{y)3touybRfZ=CrbJnTdy3QQX z^<@twbOxE*CJ;o=gHy4b0nD9x?fOo=Bn0~V=ns{0X4rLq{c?iovWiT{N{O@H?Au^+ z^y=+w;ZbB8#%^HN#%WEvFe}MWhP-i3>7`tRP;3JT7rHP{7!67W|z|^C)d=AGS zFQ}{0B{sYhwA<(18CdZm<&ZlAYp`%l;H~a8fm+6vaZq#>Rt`fR7XIpTPrsCM%p=NH zc`u8w8~`Ij8F{^`qeRV@3jw3ocDi5E?jg2#G)RCaZ0&PWL8s3f1={Car_-mfQ2VSY z?DQGo(>^!&&P>YaH`3drZJJ4{^Bt|h5xyJFk!JOZV^?uxD(ErZZg0H%B;M|B@eIFcK(&7p zy|(j;)aI`nnwi&~zfN;r@~l((;PB6;vs9>43%mEPS{`?neIgFsu^#Kzu`1zp4$_wJ zx^~ukIl<>kpAbE&Ds_?3Co#ooEf=s-n3t^Et#9=Xr8Q;$W+w`{q7)Z zfo9}rpe2Nt`P6(cm#9M5%yn~uMOZ~xl^3>hXpsuP-pT9Lx%YUQbUN$%JdW@JbxwiZ zs5lHV9c!{}cs}xUW4b&a!I$0rnv75VafB~Y*A>ta9<}YI3Uz2zYKBewm~qYMDwTIM zouH%Sy`GL`F0i%!kErvMeFxsk8spb=spFviltKF`vP#W72n{IFP^9*P1w@lJ@VYH{ z{=TpQJw+^FoY&p+GuGLylZw>y;CeTX*RAkUAmf;gjlCI|cYlJU@KAF78r}lyU zIzEdS{8cx9`kyD9nM>f6sMoTRT2Ad~XW0v0anA`ZSit;`L+j2)zUBm@<67(g*=p8= z>vVG`%s2GQ?Yae|-*s;WqdgGs(jnDmXOT71^^`3vy8-HUq{*YtN zKjn@8B#tuC&yJ{R3LWngea=XGn0k;dezbP(|OO7 z=RZj2CT}`BaAf=7l4I)^FM8v@mW|K7cgGmN;x$iu)ZozhKk3-FA6x$Oyzv)h<3DrE z@o)0R-;s^ay?Ji_W(^k9a-vocKIEPLv@cN@7y4ukbDVRyAEJ{*>SXW`(an9Z0c2E5 zwcj`Ke_H%C4|kao_XBqosdn+>8_su2I-PNNo|87~ghF@?V|$9!)gUXQ;B{-=15N0S zMjvyH@cSVLRnEPteTf&)a&>)F6n?KQApW|PA%F}59hWe|$7u9-V=}0q6#L zjRx&FzO6|84J^=UG1BMzNxLHJFz{x=fTWvezT2CQ*6V@J439pcj9C+Mm%_>-u6HX^ z``vVM-Q?e#8^-i8&R`8z zK$;Kn~v9QbQ=B;U1P~^yiB^E z$~PVr;! zt&EIubhEAxsP!G_Ek#zU|2}x-$y4+o?E2q==%YoMb!D?Ir0xF#w?MyrR@WJ8zS@8#@OfkUXj{I+VY;fZfkx}%_r}y znu^rVK{iZ|Tf)?H9TI)`i(~yp7q86U%}Tq!jNd9ZZF%l>d*f7&6Fqt}(4#jUJ$k3m zwHMIp4lGgw*Y(Skru%Ac6yY(}b)ub8ef+2=V4WlRN_hIBxX%F2 zJEorb?rw3IvomiMsTq8OtEQMZUbjcTgx>2LSu=f zb`0W~B24aczO!y|kj?j`p~q5W>NGVsU6RV)Vd2VYXA0AX?x5=oYg6?HMe1kZ6P>@L zpXaCh7rH;1&7Cg?2wVA2^v(Uv)Hl~n`42PRl1BEjt8yFduC|nFXk@3`3lxI+4-w- zOffwAC&N$LptQ#K6elWf8CqpbmwC5i3S;?6D)uY%bKecEz|Aw)E%O+cRpy72Henqh zZIV8>NF4&2-&Xo_v!mVrz%T8lDbKxbZydu5)?NR*d{es^M~uwLW!%$rsq!UEJL+}c z0CY3$6wRyDN6XPONAoV@=b`Hw%zckGl=86d5ce-YJnP3dC+_`VRo0JrLEQffHfQ|~ z`n7vN9G%qSM|tGA*PRoG=Q-)(&dD2hPWhDR0OWh4Jc_)GOfYNn{CX(q--KbTRn#Z{ zxz(4o{yNtG@}6sH54kg`gEjZTQuY9SX@ii8E@b}z;(vGhmd9=QG;(!x?IoO+t$P%U zz0-ETdZVK^~iay`wsM{s9)U&vU(W3 z?jHCURg9Dv z?P918FsdW{Z!Hb|8!iUTnaxQm5@;!R*g%o;S2g$7?OypB6<1>BKfKXJDh>MXF zqg9NQ8135cpJ?w@7-Or&h>8)%2=p)1`F&KN=E(g7WJ?Dx;I2uQ|VN>N>wRI=W6k?ks}b+41P;s?HhDB>i@h zJR<48jiulFw4+YHwNA9iChO!1rs$3Fr(h=0%-jvtpOmN(w)U=g&F5Qya#R0Xb~I2Q zYzmFzns)BkSd!25WkCj4X89%3Ut!;BaOX*ng*O^c@nyPdZq2 zaa0%@_j-aoCuQ7%k_}7{JgVw`S&-$RbuZn%cuYL!vCp$BahO~R>T)*0t^*iWS;=UB5#E-Vg zcS{@-McoIV7VOp3S;s>z4vOE8QYbr47ks18+`?%Uq_wLYNf?-j%Irl2{(J3MAdc+O zSU7>DI7a4iUFozY2sS4z7+jj3^#rRl3v5l)aSUMpCI=IG$Lg!T6e$LW@p* z_^N*46!sSE_YK0%Rio-(CF)`9a_wjW`}nHro$8>SL=f&nC*$82xW(suf*e7xE^w;j z|Df1uQfCA>a4`Dw%2dAYNBQnwB-?579f-{5Yf^@ts1TMi^mmxnyLG(= zm9`3gz}ZeE#z^oL;14A}e&iN<( zR^G`R&b^Q$sw@|o_FVqq1S#nAqcvErljJyax~ePL+K)<57`wsNd(cLs>s$BldiHT& zr>}zR2`+uv>(1UStn1J7Z}5D=8(7{0{r^c9^OwniSzPk_06*VA-Tu+&|8eGSI$SJ` zeO+m?nyStB&EfE8FlS%6a#6J;b;2Z!X`sb1+?H{t-kymJRh}%=f^69Hb=Xn%WZFM~ zj4SjXY46q9=T~D%*i{tGPZ{Ly;>8-7ZQ=Rur+L1mU6KURd=wk8 zJP67{?S$7?0xWD5TbmduG13@Xp?`F~v|XE?T;k!|(Ck^~+aBB(x+(l@*=@W-t-}aj zw|bT~dHingl7)VUA7F8wWjv)^dH2GVf8@^LL$DaR^A=P;vuBn&GW((>)u`pGuBf!Z4nse!v_G zI`2Ybyb<@GL5A$4&*Zzkam@F}w+GE+FuX22*rVecec0`iirg!QP%FBoG<}Y8>;3Pgesw3v>OXGry0h(q-Sj6--j=xtpRO072A#!MWadK0^z# zQjvpZB126hq_l~PMMz2D}r?i8tn`*E)ZT2*I7kXb++2g)yHo?|O8PCG9 z5(I<7!pNN$pD}tR#b^1KvQ^&8dssdK=<|lGp|220AY3%s zp02h!6Fln<o$dTCicktZ^`$)#O45F~OP#!UdeJ#JHsItzt6ityR<2y}o**n1`H!x)cC z*ieje`KD2m*-{>%HNo@Px2Z()D7O6 z`RVe>8my}=B1F|~J`Rg>oo%P&{dCva6q9i0V8nqPl)Gh;s9d>R($(^s@NoBKt=9Ls z63t7wo?wf47TJPj7YKwhf1nemDAw4t7#WN?(t(qK5e}E_ELIbIR|HJEA5Xs0w*T8vsTqGH4`v>uc;y|Bq>bCDg~;RMz|<_H`NMnc2GQV0F5q0<{#3$I@9{Um)Q z&*RTvv2SwkaUZC%a=}8YO-OwG-UjBpH>V(^cJeIJ2TM6H-(K9;!fmQ^S(nv_%q{oW zc=Y;6dPzyovv$n-85&D}ReQj%7J>;RSwq-dH+`x(FO1(-e*D`0PzJMZVCVA~YanpYEt}V^`TO9}6M2PfyJh30p5?slk6rO{ z+){qlB++O5Y7NMy?R9saUDW5EqbbrI2bLmlr@zU$KJA{2-zxE=?DE{}_Qo+h^ZoSw zq1iHvtkpoq<$=h*4y3>6dyQxd`Bpz9P3bR7ubbV6LovS9JM78=bp@@fbQwA06?lRCbIVoBM}GAYcqm(Lk6V|8bt;-^g?mBMLnIyDXY@H&f7S0Gx)bNpcRKv) zkAQON`g`5_p1M6~he?#hOJm_$8RO!Fi(G(lF_6zQsO&Zx+VIQd^82rr;+v`~WlcnQ4xcRm}92;YHkaE@_UzG86biNqH4Q%zM<)_k(Sc$*IOE`_#7A z6gm~YOYNs1&Mx)8ZQE78qou#zoBkFYWgqmE%u&nvv^SmSC7q9YPN#x3^AWR`NYK{i z+wKQxtXK^yC|2(hRrm-~ApuZcW4DTbJ|s?02pF=#&KW zB)r~}ac+wHc+lvkk?;1#G52w~G&Q5sI%U$PPZk%e2-w}3wsAM$mvYmV=U%rrPUl`U zS68-P_eP58$Kxv!n&>P^|~Z zF;VrXJ*E&DYgxN-?(MEWw@bL&F@7oGLJ9eHn3CLfWB=|HtLMPGi)60@&EDhgji!_# zDZL*26ib$-R2U$A?z|_1&i6Ec_XKj@6Yk?dkl>P^Joma4?ra>hHq5;@cscPRD>1GD zzEHG)aV;iB9K%^}!yc|mON&+M;vB7ntgy(8FL<}azYpU{iLc+mRC`K0kr$`?7jwO6 z;6v1eE#w2-&f1i`*X4fEg>E_>lFp|Xr6t*PcGy!|v+1n01LM)OiORr-|9n|#uceqT7?JtI^|v|_BE_!%Ad>aKN7c|;0*UB?~^3% zg(YY~(ez{e-k~3*Cl;%}f~e9&!%30ArcMXA>5q zT8vr@-x%Au)^{z#VCj$m`teHC5YDiNu!kE`SD6{uZgin%B%O~i`h{e#Rb42twIQpF z&h@x+mV?k2zx+FsYKaz^i17^_KPtJ$#E6TL5TgZyoh0Ykg92(0yo-a__YA7*l+V=r z(p8eqT8y9SblN0hN{qA^?P4g3WuN14&ed0si8)#-&6#BHcLKajveRVVegn&gz{n19 zm&BKg<&KmXi4UDxpw8khPTx{zjIGudLF5?@lJ39Qm8NYs8QS*fzLZ(^=;N`3%Ghy_ zW#AS>ki+DT_xjPQf;~|F8Shep>wVA)ENg%h$k2b|=04EdZM)b}@51zDba;IUdDgM$ z@M2$^mDc7vmA=q!z` zjB0x~b=3REF8!wEVZsi<7!FL>gnuSsr=wO!`b6G-bGn2-9b;~1__%K*Z8@0lxEK1! zYJ#ql@8~Kln>*u0{c-XcW!B=R9=T^hZ3NZt-~lYoa=v!TqxlMHV`%^ic0Z!Wv->DPpOZSA zisfuz@-X9LTi|ZF3t=(ppw6Jtj1H5Y$D<8I-6QE-hviPdsagkglPPkFzF2NCjOyc# zSomHjU6Wz+-|idcw|u^_@Ez+tDEmC-Ht8b=iN00R5*qF+IGm55Byj*gu;=}dUdM44 zYxTLs>L55w6tnNo_k%~x6Gq>2ceq$pHN(flcvc0Cq^fQ}#+=IzQ005NkDZA!ckC3M z5SQT?-l#Upg=X{ZVH(5v_hK;5AV7Dmp{MkCzP6vFUYGCU0G675D4+_p`WZmlFtofW ztAvzzq%k&$py*U*N>V&pIz3$aP+^lS=B*<0=X+gTYkdpR-QYxgmvhy`<>6Nf%N{MT z=as!z7(4^Y`YQ!h+!9w7DIZ*Ri*G;!8l&qf#+D5$uPxhIW&Xu3IIEy4SvI?>xvZ+Z zp)BG4kFnjSk#f$o;roWx@0IS8ce;xzXH=@m|6&)Q;*5X#g!R>c`s(3;`buN5s-G*ovk+W`zhCd&!`vTNtiE&yYxz@M+BrOc zxqcA#Jz>mV?8F}TVhhUozU7@P!IA|2Kw?#~GP<8r`i`X7PH8d@(S;j zwZNpE?xJtVcclh~)SoEggg#gngB74kpVh+E!U(+9o+NiVKS~F{RpX8kKHVLxqVcv2 z%_)6VdQuP2t+j6y=q~NMT>f6O$J-{pT~=1UjKpm&l`3M~8DZP!J2&|z`zQHkJCj27 z;dAV=l?CO~%T_p{NYzWBTUm}S^v4ihzo??2Y)-hr=$T#IrDt|8sh#i`#vec%d>AHQ zl>TgTK5wq+Aow2ICGshj(h6A@NzAye(^PX_*B(5xK%{*qsfTSR<}-JW^$|K2`r=tr z3mr@5%gIF7`lx4M@i(yTTHA}n|-|C~*NdA2s`(3%&_~`L=XXD)`@i-^? zyK}0)hwdQH<{}eKHIKx<-{kf z*WJ_JdOvAK4q*Hh_(Netcl5S;VTLJglpIASIJH{kl=&9=JLw#Aa_4Ayr{OBnsl*rw zviHKpD0tL|I`D@Jgzji~$!~|3{5E*We-1DC_HMkF3yA&_lTKs_mUb{aAbfx=4bec* z@HHkh-E3{pd`-cR^`{+NV(b4t)8ub$pR)?g|MU9a3K{Rv6OQ%{%_!9WfthJiT`tUp ziJQg8DtSSCbed~RqWkPeZRdQnA^Bs$Cw=xyp=o7@3hWolr&Ziz+s{?)9DIINO^H3Y zYEN}r)osK6UbRg-h9BaZma5@)&=;%<4h>cfAIf_$-xhk=Gw^A6#$FjuVuM`T?KxP^ z2gqXcZ6(a&EwEXQaO^$KI7bPul$Mv7iM-4Vt!dTDEsNzn))WL^Km~(kD^>%Ka{HcR?h_E1 zxLlb6>AO#pMC5sl{|0uboo~Q@n)=v&mzy}Oq5@Sk&CFTmJB6OV5813k#p+IQm?(N2 zVg3v>BQu12h4>G7i8)-JQSDqebv{ge^V1-lH_g(1qDu{VimjeqA!S`Pgt7|#Vl5Wu zSGGNsd(iu{{-gc8GJsOA+a#>;BK!)ApL+++67Jstto{A$t^0J_TOTBtrXjtCB?Dx< zdRzWcTTY|c^XSLrJ?j+<4|U@X!7>_1+!68*&lz!rU2dO!1Ue6Uj~?#sie=-Ui)FbR ze~F2okodPt{A=B_VEF%`VfEc(&VpMA#!H9(kL3>F1gqu75q%&S_rGYr42u(Y@ESUc zPt_I$)Rn;8dBPc6j+=tRx90T|JtG5~M{piAob#Y+RT49MCjS>g>SM{b6w3qgw;BYM(INM-hnKcvd!KL9lOnZr_gz->I15(iM^NB$1@nY zXwGW1<`q-Fw@7BEr1?IUK()*da_5Ck)2)oDS<=7GPB|C(A1<&hpRIi8r|3H4r@y$o zG4&)7ISJzwV24r+IOuBJgWgZ!g{Wa&`31&{!JX^Mr3BOUUxDRH;0tQitPL&1)TK!I z7uoi7waIB>7WDLuaA)7xO*Gv%Wbb&Nq_K>xBDy3s&D`9DBi~AzN0CDq?Z7tqID1ts z&F$e==&FmVA$&20(F@s1iCldNBiSbm?s1GlXl%jTJ`8P)qYG~ z8^g5xwJsC0UC&QbbG(L)D6n@qdwmmaKPp#t2KELg+M%m#`vW`hW_WMeM7w-kfgKG8 zonV%xCOUolt3l7@($t=&D3Rwe{sNp}(zav;tJ&s!an;{;o@Ott=~P2lZx86U-VP6Q z`$;91QNX9uXl+OYSxqD{{IA$*p6^e;%KPfIQjBPN>^T?9azMk|OXQ#NgB0Xm zs0~cVybfk*c-x>Ml++bv^f{}n;h-gG-o%jsbs>AdRYaAsH|24gJ}L4*@CG#*ey#n4 ziyXlCv*Z;@gc#=HUdI#}M3 zrm^>h#vX#k9#lh`x@hckC5dw}maGx}tSD7;X_Vdd1}7!#MvR{Ue<(v+j+PFYYN(cG zsg-7la$+*qS?{lve2;Q}a$3^6YHA;K2#eg|Qtw<E2- zS(oPmNxN9?@%cc?W8_RZhG6~DugS=n_C)V()$t$dqwbXWlhGzV%N?JGFg~NNAp^%z zI*{_tcP|oj9eG4v!}5Q?%y(mJ`^u&W<8m2^Q=z-;+0Od~z9Y^9wjK3vx6M1{U9z?u z!q_roPS-n)98W#QVd(KCC>*W-D~WjwuMiO~t#$?MlO)=ZW#Gce3pQ;oQwvi#`xtuwj=YR>hm zbN3aiQ#;TT2Hml+o;yXCF|LEzM3Xkjb@St@l?0))HBa7|I9osWjci6^6vAt zU40Jb4mE2=iJHMFMwIpaSD+U(K)3b}b24^2V+-dn#RnDp0!xK2MjrrpSC^A~@%Q?f z_auDuma067t1J$mz_|HE#?4XCEA{;==};<|D2aE`$n+g=yxRcpR^n;BIMheZ`^`Op zgILQ|L4zze=bi5Qbk_+ko%0?nuY#~%zcr+&oLN5UMMv_H3?6S*hh5*~8asx);w4rf z&SKxQ{s!MO1?qquTo$SiUaMXWSJVDI&86j|Xu}~GCjhgDjWr}m$~R37U_deFKhTe% z9m}{mUBX+749~cvsrg59@10nh4YNYh&;eQbmt<`Nmz`-e-WBy z7Zs})Sts>3>tD$$!lP-Dmz=4L!vW9n1P;!;VP@lfnuL*a?z2po`Sx8oVG`ajmzgkU zqbASPue_5-O!C~$<8_kW?O5)UupxQ7V_SI*dLZ*Xi`K(M4$Wh9J|R?+`C&jgn*&CV z1pZ#P{x0FG(|3c)8FWXXif#$0S>Q0q*NE&xGqmIS0_}Gd{u%Kn9SP%gd*f-nHI3|9 z8SnZVogT5$ktJ#*{GcPz3!FnubLZLS(Y)g|7&icas9id2+am5f)kN2{Hd{jz;a{?y zWxk>Qw;h;i{nPByYWNYFt5h}jZ&q_YU(I)-HR8DGJ}v1!kMS2^&f-{(3+2}Kx7u-N zlT*#QVibB(>W49ZRP;E1RE!CkyG3iToCN%u4~>G+!xq`gWK+t%N&Xhu6*r48O2MbyApjWgJol?wGh0Ig(d?UDC!tlQG-QG0x z-LrGhnuV?xogVX6IrCQEgNUfIpDe70j&)-|tpxA7>3Q8AJuB`0l&i<7BcKx70%|Q7 zJvQ5y3=Kv6?gf*@pR(n#yS;Hl_r;*OSK5Q>llCBMHB_puIUuw#`JPevj4K$UZoivq>);{Vlq-)0bQub18llPdtR5RH` z)?lm$wwB4y*e^5Y_P{HUhCdZI`>O}GwA zD|j0C4d1FgA$oRJN_-dIzJX;TGM&j#?qatxmR;QsEiq1MGS2^11<-fdDcr11b1M9L z3}LL0qPz~WvVb{mnX8+b%h~r8vX99;#d`G*0d-rQoLjnQB|KBzo-x08!fL*0!i5PJ zZVD#!-k!BiQDc_ofPRJ^+HZZGJJxQ&@?&6!+Ng-{FlY4kHeV;LFM0?)DIhC<>UA6b zkG{~IIKSNLeo?GTUM9K78yFt}&beL;Ab+S$t*P6x{jb|ooO0idw0~((bQW`;p2%*; z;fy2x;1h{I0;3in|5Hxyk776+hVGHQn2v*}m-{_QoS4KBIpK38&U*VT=|5VApu67j zBtf3zc|`O@Y?SbC*&_Rq*Ou*?UfpcQz}PE{v!I3GGM;+fdVR^f(08t(HR$(GooZhT zsPBONZXB=M$XFptHaAzlN(F7HWm9UfFeNP~uWWy+t`-5RF4se zT|f<-V`PB!`j@DY(=omQ>`;qTYAzHaGoB5iUsgd69neV+T}7~+q!(F>ri zNsb2CB;V_7RqHfEVz$lO{}RwT%CdTAyl$cCmBS|~df43^p4?EO`GF3TydEFl$1bw%mI^gJg`OMi^G?0XRG)FGh-9@_?I`&BF zQ=LP{ZX?{_DRQ(}J)Y$M__wsJq2^M)9;~zSFnDD;kYB+pUCZDePq%bCQ zCMj#hm)-qV4VDSOACfkzlV*vT=8~hvq;HAsJgd&|SB?}t_{Zz=>Kh>3I>(8s%X?d&BMOvRjB2EpRVZwT6?zENh3^^9*r|%N%gnwdu283ro*0V|9So<_*0m-P+a3S-n zmdSw9VDhb=K>NwQ=L9UyX^3%=?s38&+fUEued8fb*PG4b`9#tB-Os||Ye$X8dZp_y z&7`#)@AwJ~yE?~zz4&hw|A^5M!#=VgK} zlX!1nc^BA$cn~#&{__<=b8POO>e}PU8Y4FZ+Uxvd1JnGcN}Zw6S6V&m zl#i2)uKNX8z6VU(arhyxqCK$EcHSsLYGJ&s7BZcp>#0}`Kek3^=~kDt>NoR=eVI?5k5zUnCH2x zqb!rvTU!Ia-*ImGfkv~JOZvDe4lJc;^3HPIp7aSXI)lB>-SpehftIl2lwZz=J7~*& zU3*1Uz=|I1chk>xlRn{f`B_6q`rhZ7-dSe1n{@?yQ}+3ngUDpcItcxZyoW;GgZTXf z)QUgnwSzmLsU5Vm-=p|X7k@o(7b$VyeLSkJ#E<&syCt2@I6Tiu8@IO%W}cVx*Ib#p zw@CXj!baZ5@)c*+K{ z>*^f08zy-&7Y%`I7_Yl0oha9z6jGN+y|!VopLV~mp7|$I1es$|uP3p*0^o6n+L5Vi zWu?0V9udkP3qND4_ieex>Tcg?VFrHJ8AkesMhES&zU%BXIEC+Du|}2G;bhM zD~hWAjIX;xjArniQkslb@W-unv|*l}CMOy)Ivup0CZ0=Pu8yYa>@%&SiT5S*4t&9K z<|%86D9$pxuhk0?1u+xcR&2v-1Oynmzx()+?;rnA;;}WzwO(^Wv=z6 zFYe<&_!LwAe0R4v$jP>SlgnR$SLb4`ALn(RFoswCL}=8J9%8 zq<&i8a2Q>@rO+4qYyU@h=AUlrF5$fHoVbN%Zyu!$&jr~z(CfBheib_m4XV*SFRCj^ z=y9V1U9e};&U4U{;pXLa>+`octbbFSze+x=Rbq(ZCBeNCUiO3VCH9409)KoN$a+Se z{TQU&^m5&-bqKErFLy2}cm0!-CT-+(8@bS)?>wJ3gKu#W#zla=E>l2U4_r~_mICJ_ zsI|NUO&6+Qy@K8*Lv_;o&zp#{j^~jbSoQ#4Fr1LNthS0zIEul(W4ALI_7Y8(Fm#6Q zdNJCw>Aa2Q1K{YnH`*Xcgy+JCGsH$}a}|mN#=^eky-zOI`K@W$kbx?_l65ce&ouAd z>(=vMBQhFC49!&cB^?jF1DDc2K-7(w<95@IeXm6Q0A$n7al7eccs_JhS2|vIPkKFG zCcmB1hHqdw0?c`kIbw?XibYsE%5yV!>$2y`wwJTmWUUN7(Pih2HRt=0fH#bkl`$!r zEKyg2S%s#pa^0kp7p79LnMD4U?|}X<^Xg4s4H^9uJFuWQ(X17O&ck}a0Q>0NXE0xT!*ciV zzz)(l%v}SUO4L)}-p9@QKG#jn2`BvcIsSTHfF?N?UF$zY)(-5OV&dnziQkAm?EG+^ z{4<+N)TAw4`RBU3^3U=2&Yj_A2~Cx+Y>)U+>#C5=Yw_sos zI-We!ceFIKm*E?!oXcLOp0m}u{_RFzSl75Cx|9qbI_LSkv;KtZUsu{Lc~Da|>y8rj zYcPa7fJbVv2O*ZyY8Q2zr*uf+`sTVFRp^V!`V4J z&h6!ie(`L5Um?unyGqm|(vg3cA1kKss4Jt+dR(P$^_*{)l_aeGh1BOkWGwe;-3NM% zarKewdstXQ^g+*^w3qy{|6)&a*e}26>73c;{R(i$@!tU2dU!6uqyJ^%{hOb-D?p}B zY+b)F)(eQ_FVRDOFZK|w9XaFtU+4QjvwJ&9xnlUQ0-x7blIC0Y z9ou_pd+^x8Wy_W7rCe_vW4R*#;df9wr+YiFLsD`4H-Mv!3(^+yi#~E}^%OPu^2?5E zNBd6Gy_66ABq{=q{Q5s@{}}1!|D=Aa@IRLEYgb;o_x8TC<3*~6{Petme^29t z+~X|2Y=4sRD2lqD{9m^Hs?X|P9~mX0_|@JM?ro4?wjFzWPucvIl5YM_#*LfsPaR{P5?`)bkl)er-G7YX zvgPxCvwQiZ{f~7_%cc|SAswk_694~X{7F5YThHFcpX_(AzlVI~J7~whr|+P4UvB#H z9VFo?$^T{R6`9v_`^4}+mi{Sse#q}=?=73(O+Dl%?YJNR|D+w0dHMF%j@f*}=X9^9 z&Z!QJp2;e^~Ei5BbY? zcdYq#_WvHzk#YJh{6BBa7|H7&M_ZrPeye+X$#?d7dp#a^_WO6f3tu)5SkHjWRP5962PvRV!5^f6 zn$8*yw1EtW%^)0T2emVmY60z__N(}V6i^KMF^~e`Q%M^n1>0?v2Cdi?b%;ux77*rK z$_V5?0n|d`UqktU+CUy41u~#^BW{oZu}%1c@DC{?NP!H9Z6+?rfan&&gLY7REoA^{ zQ2isGffkSfwYS(R31TgzbBCRS@Ge``deHt7;r^R?fHcT}YWR}lparBr8);bINj_X^(dP#ZST{~Cogpi<-mszDUQK?1aZHjoDG zAp9s{KorD50;EAZsC^87kN`>03fe#lME6lX5C;j60_q9;K@7w}0wh5TXazCaC!VmB zT4AYH5O2bMiKSXVtQlFXm6nPEbtyu3pyhk`ufqQ_OQk^aa!a*=jQoEeO(&}@RlUYi z?V$Av()$5nlf(rrYl*v#=U4I^w64c}70<8cIcT|tyf%>6M(m(^6aFCiL;N@6zlA(O z{95ck!XLDOM2n?ruj9X#^!RV%KLe`A;RY=r1u~$PgS#Y10d*o_KpZ4N3S>Yv@?mk% z2Gn@c0IeVcq78%vZ6E`pUnYH!2GtWN14scik$9jLWI%Khc_*Z;>Eoasgwd@W1qsjw z!f5+RfHbJRguVq(OKkZqOni%@j!R4qHGQNQ2s+5C$Yb8%P6n6aF9x(jWuE z+Xw?%KpRMd3W=W1Qzn7p?q@TmHpQ>;kc8(TZKh9P#&#J=y2tFJa<4hy&U{2Gs84eSidG zPzz`UDZ%ZO7bxOH!H3LO_Hy`OKs7iK%mj`8x&p0krJo|wO}fk1D1hR z;2N+U>;aF1gWzTG9w@j7S~C~|#(7N;82B@I2dEX46I6o}!KvU| z;9~Fta4onM+yj0OUIp(1rwO)lFd9q)bHI1Om0%mV5Bwf{1S&3pKLy0WX7C7j1q7Q> z#RQ^YK3ERcfSbYH;CJ9<@G%&;GNir?&IYT%4d6lW3ivzt1e9IM8V{TT8o^~?Be)Ul z0uO`dz+b_?K=^y8ZUSEcXMpd3%fWhZ1K16o1%Cnm0)bT_bsYE#I2&95)`45WgWwr( z2)qxR%cvh14rYL*;2LlT_yfp*s>?&_tDq6A1Gj?TfY*WgK6M6P1Sf&7g9V@&Yyx+I z{oobw5$Lx%q>cyU!Kt7TtOvJ%{|6oi{{#L7s@CwX!4z;RSPZ@oZUlFO$3YtW1B9<2 zU2rn^23P_v2b;kT@LTXI_!w0FfONqb;C!$aYz03D_klEc7ZfHr+XNFp3@ia_z*cZ4 zco3w)+u$?MZ!PT(z62(NuY-%g2JjQG1N;|w9Q+CV1$+#`>ktV5)4(}kIk+0!0PX@0 zgFk`4fDTY{CGQJN0^a~l;977O_&xX=C|nP(KR6jI0GERn@C)z+I0Q1F=qkQN&;VwD zdEjEO9&7{mfM>xwpzvzGKkyZBHb{WY;Ah}|@FZvl{|f@wAnE|V2u=nu&b+AO-#cjsX8g#sW|c#)Em_e6SAu6x1KYs;;LqR_FyM#OAH={4@FVaG@N4i4_&abmlOLE2z74JfyTHTXdGJpV+`>2k zP66kFCa?i)2fqPNfkWV3@GW!f-Ax8;MX7p-T~H+=p&#SG=N3m8gM&! z7(5T&1_dp&IT#PV4lV#!gKgkG@H_A_cn5q40@v~Fg5$w>Fcq8wR)F>3$6yzD2*^pC ztqK&JWy+_x4^|bc0DSFmH7jbO!m3o2DdZ7Uf5mMdoUK)=Dm6$ARzuWK^#xU}YSb`w zoEol1sFCV;6;UUsQR<6ov>KzvsxPTpH4Yib6IHz$uNu^s)dV$>dx^iICaaUw6m_zS zs;O$4Iz>%aGt^A=RklH=vd#ILnypS(XQ;2Mm^u>~g>R_2>MV7(`lgzv&OsL9TWWzi zS1nZMskmCC7OQWoMzutpufC&}stb@!_^w*6E>st(i&a9cfREx5)vQ*kOV#((Ds>q; zQogTNt2OEh^#heuYvBjFQmt24sjJmBY@If$P3nhgv)ZDrRXNexc zPt;9n8+uWHsrXE-O)D!A=>i25DdQv^5{-B;#&!}hB zA5~f%K#uKCs7QETy`cWAUQ~zFOX|PX%jy;Ns`?+*u3l5GBjtEly`kPze^qa(x79o9 zZ(Psxp8C7`hsvn;k(2pf)uBF6|5X2{K2#s6kJZ1_C+bu6nff=c-{L5{fMeZ4%V!l? zeyi9DSS41_>SKkhzE;>OwaP5GEG;y}TLY~MtJ12n23doxA=Xgq3s$vNV-2&8vxZwE ztdZ97R>V5N8fAUa8f}fS##&#pYOQfropqvBZ;iJatS?&=tclhn>nqk|>m+N6b+Q$; zrdrdiQ>^LM3~Q$KRcn@Ys&$(6HEXtYx^;&2bt`6_Y0a^|Va>J9vd*@?Y0b0FvF2Of zvKCn9S_`f7thlwvT5NsWYP6PE=Ud;gmRc8B%dGEO%dHEoi>!;SgtfwIvM#Zjt(DfL z*7vMc)@9b^*7vQ|)*9;y>jzfST5GMduC&%$S6NqE*H{~@jn*dXht_6mi*>E_Bdf)_ z&f04I*t*`j!Mf4v_a_r;0kT-SMvFSw{N&+GiAC3$hqTYm9(&TCq- zV&#%cnlH%ezbrx^5kC>xR$DDcoi@XsyPlimnTDu3>`ZSxK9N4Ndd2 zOVAb$WQxRowo*d3zp1g+OJRBhqy-jcMcYhE$Vzj9x{$`Zy{6M-KKs1>x%G98bKP}j zS7sJ;$+Irt4mCBqGc}epr0*H)Y&t7Ri#ym5ZtW~u_WB*|B`NvPwGx?tZqiJWtX&B1!9vpZ+KG7GcoDzWTx$>*#+qVw$7L7gAFw4`34YZW@XysXsP;%1bRqH}+C zhTP5G#yPqNNZPLZ-#x#srLMKh=5(ZU4=rpBwz$2`&0f>gouAzunXbgpY-wt?`kS^r8@pRvcUcqcoSrg)rc8&;4sLv+RL z!i4IYTXdf{$-0)?>K&O;U{ZAJ-kE4;Z_Y%rJG6;42HU%AEz_6GVm|85+4mLOE5S$yMXf9Z8dARrnVMH z19Xdb_U!CzI-^}YnrBt1Wv z>4iPlm{Zr%+UsKazq_S1GspJaU~6;ToH-m>*l<&0BZpg4jgHOejdSD;WXfm~W)4wV zd7892E1@gcOYY6V+?D9aQ1)go+Yfk~nKCj5rtI#em$bHe{k81VMsI!Hd8VDLC4pdX zOX?bB77Eu{rM1#F`K+S!2)JA78t2pp-K`75dQ!7eb0X2q#F9Dn=oWK_TlLPg;w`Nl zl^e2-)jBH2?|Rv>jqDtMZO5dUYiY+xP_Bhd4dMErIqK(J-nrLvEDqGQWcQ04DL8d9 zr)0e?bceik^_=5$HlDS!I%;Qja%)|KN#djFXww#UmtH66YnvnIXk9B07Jot#|h*`+1>XqS7D?nu^lLiJ5vdVw5jIy0>Gaw@=CRHiyir=16* z%!kh8b&ahhmEH3oSXP=NSXrJUSlvBV?`>&y2b-HYNAcGOy}Bo52b%)A)v{t{HnFOe z6%IFryN5U%ZE9pk>Z5B`+RSmZf$a6UxaHUft##(KCp+K{)qCgYV_}!Dm1DTMDa?82 z!pu}^Qs_Emik}_KF29a<9LV)YT~MniT{E>7Fb;!q7H3s9YXy!b)|qzKMVZoNrOt$U z)T(1aj!kqd%WNhq!!DuhxyAS%(~619KF?h7WUh7O;y`BSfHg`w+ue#=BaJgL&g|W- z?e0KZbLX{1t^~4UtQ30Rbxp#w))sWv2OH;@>zJ;K$v@oGRNpynvliDi(qEf{jO%(i z+u;O1b0XABBDv&hscov)JFPR39?5Qfif84-?4C7O>>{PiC+^lJw_nzIMNOK|hzJdQ?j)Yl4-b(9sSZBT5#_Wks%xtay*iBr3atOF6H#_ocVZ z^>t+3vD}wysa$*eIZKuLbgZ(g?5t%uvyvrpr5whJ{xB6aT4I}(wefCyQ#6hv&F4Q3q7yF+mSuj^zLwTaDH7=TMI*? zp`lKne#tJF&*_R>8R)Jx^~VO8zke9Wp$qOd>!Y$V$D^w zwr*~HP%Taro2iMl85VS0S7v6i9t$|JUSpYmbctS>OL&2drLisgca^MxnuJf5$&B`GYV!Eg8vh(g%2>Hn2HFIM|Z^sIDEs_;#Ypf$d)|Tt* zGRFYDVpAxTIrGjAaB0{zo!+BeXI-n@W&s^7yUZrqWprjGw|YjmIlSlMoO$Y1v!>M^ z>tzqS9t-7E!Rk>R7k|3iy1+W~jzQC7ZI*P*N4XYujFlb>vxi(S3wyR-?ppRzzg^cX zSDkvP4dGn%=%ofZb}?69Sv^i>F}HWVIqK`Wn~==!-BV^AvpdsvAGKDV+&XBjYcw~M z%wCj?GjUG(x(t15C2LgZ7VOgAvZ5YYa#fyt5v47u>~&GMj?s}}&*sctRGmA&+^yPi zA2+MEa!Q(`88csFWvA4WeD?-#X=}(4>Xv7ZiLG-+GxzHQ`lgDmj&;SrWry7VvNF#O zWG3@2F-~ISBs4qm0b(Nb;nLC1J&8!41Z9e9!R&%dLFPGCx?*GdbnOu!z zW!-s@&?!6noc`SX%0x%?ortcbH?ho-y6Zx7LqM-x$9YND3g-C8oh0iBnCqNfXTuVv zDt(#Raiy*oWgjTJM7YGzcR#u03VGY=^_bI17BqYHVAVaY`^_L}ecf6ruAzWY_doyvr@fuFK`-ySY7>BiHWtCara4Oq-Ma z7Fvb7LUnU$&0drJkd>u7`xHp8+_}BXLb-j@x$PWl%be-=X78iyL@iw7bgtgi);g!j zx*j!OA#_g8*7g|-txbMz)9{r-OUH0{kyQ8;s z7DzW?_s>~h<(swTI}Ypn^tzCI1J0Fk=Iin<2|7oWl-cYbF8<`Z3#wx#xa?)QhJjhH zmSYsRMXb9Q*6ktornzyEz0rUJgcXr%r%w6SX zosjp~22sw^LzU&dEXg`a=(06ywYHqAnUjUC$umzlWJlYYvt|axU5a-FXOoR;%)Ss``TUNrptHc zMJ=}34aZ!Kl{IWXhbJiW>E>VWc?lx<_u#_`be=S#X-1_y-=j->n z#!!>hBH0;ADwc;jhi&(zUCXYo$m9lEt~fVTJGYRv+0L(4tlYDgn8SIGc*iu-V@by~ zMvo;OU*}m%y6l#$Yf)>VbvL!+%Z`qaY*lV1%XM1T-sdEvbC${^mT4=iV%$si`ZCY- z$erb^{VeyHGj~~a0}+v1iya?Y+WdZI{*KQJnkj(wDSN^58D`QwV)EEz_`cy6hTj<$+^W~N8QKj?4V{J)4bL<@*DzujHN4+& zo#9J{?;FOt3!%GZrHe6-6&Ty0A$A&u%H$I@Z zyTp{M-S80;UuGCF3>c0xJj!rC!ynmS^6x{#7YrXXyxDM(;Wo2gX1|r*q1Qjfu+eal z;Znmjh6%%W48Jk_+3N)03ccPC!%{=1p~vtN!}|?iHT=p@neB}; z<;b*uEpvqY8)rDo@BqW#8WtG-Xx2Z+d`_FspBg3&*BRbvnAzTy-9GQQS8rdfG&Bi+ zYd#-qIN0!jZt1p~att@|i6*~oCcZS2zsYyAiD$|)!z_Q=&|x^<`lF{=H-JX*KaoIljr1E3+NLTElY;GwClepVu0`YBk;7z0i|A7 zqpfd##&J_)CVw`cG-4-k($w5weYZYp;ds6`T(XMwu3U<${4y<}M7)L`q_#=vPI{rM#MEY`(kpqzWCHwvdW%Ar;-FDsfUrv%oA ztJG+@o-)OmE8CyQA%CoV!==RO>JkjHlyk!53Y~|9$ zM>Dsug-Jc5W7~?kXz@3x_q@JZ8MA2WqI$K%;CdH^RQnJem-II7A z#X7^hO8Yiz5U4iRWwbUeaE}g$yRAK*RhLTMKEA#WwQ={Fb93wIgpLi&I#aFaUe;+% zqj>N|w(p*7Ds)@-G*g0H4>m2x6jCaEootEwHSVU6eoVAuBV=OLVzDl{^*o|nTh_I< zsf%@tFQlZUtxHY`t3MY?wF2&Q8TLv&W$-f7 ze6LgIpUb$27P?Q@UmXOSL;7lpN$UD8Q7#s^Gv4T5sMeY7x6}uNVf82TiBS?*Aop3h zSf-Ejv@}W8MY?o4K&vj`ZjejYu}ce1)iEg;xkxRi=txW59G((Y)Ac87(W%;ZG!U02 z-S_Os7Fn;Oe`b30&E3;UzwgH<#ojb*)8Pt_p+)?3)VV9k_B6xWaK_oYPT18 z$jsEGXnJdza+H)h-IFEl|MExFH9AKO_H#SLKfl%Ko;hZ^dy>z(v(oBxW-32wJmvD?Cq9H|>3O6ro` zq@KhcbF#z!ohNPP=Q$pyZq4jHHJwDWWUt;ysxiS`q>{bi9w)nlM|xU<&N9jSue5Ps zca2dx!(ldA>f~rOY3hv8u99+xKGN`)Qr)B5+sE;BxH$|qmvdYv%3RFsVNR16qG877 zVLjqzI^x%ol8rSZcZ?3jZy6hc4c0UKCy8s}XB_;(5$BiOPOw}EZh|xeYfs6R{pr;OI|j8srNaew9h--7VpuUv4+MYpoM9j*p`h(<=WJzrNHp z(cjz1x|s=7ZufHTHhRkR$t&aZ23YP5chC)>_S!9y+_J>9MHe7f}8&ae2)lFYezm0D)JJIJqurHuk@VZ|>Oj%jE)WE=y{&7kAHrZ*{`ETF!v4Ysya0Eq}_z`EGWC?pvVUxz1?KPSP!p zw${3O&SiIzf;|~*vCi|e6RDs2Olhqj?G4K_`=fctP=3VpLjkdgO-(#NJIr`uqtr{%L$yFiM7$e9))bs-a`a=*(B1>bcC7($pz4 zCvyG8^v2NQ=RJ(-k^XsYJV@JfNzqL2+~COidWsq#_ol`f-sU>pkM1g*DrZ4GmXL|M zHh)6P7Uo)3j>Z}6ml+MK&om}M7KBMIUZ#J}3u3Gff*EBle$QHS-5vPvG8&lUJ6=Uk> zVs>JkubJN+jAnSYsCKJ0%~i}qrrYsyi(CKWufOE{_y1LV@YsI*r6%6ble1L(t*LRl zUf$b!u_=1Je3S3^nY+%Xm(4RX>wi*4`6t{}d51osx8pE$8csBvWf(B5H*7b&(r~$9 z%rI`a-Y{Xf(Xh96$W?A>lO5mba!Q>t|M=;BUUvFE1$s%I{3zcf623kn^C`1t@65E{ zvg7skUI4we6c6@0e6!@?)9hEwzUSh;>}%VY0BK|x#X7vqnnzW+rq86 z*Ui12Oyj#;E$8+)U|7HWn#TF~q`DT)JN&h|x0<`W69OaasX>wDe&O+ZS{=dCGr#74`@|Pc4cVC8F z^`A^LH`n>)o;5$;T=0K;`&NeMro z_EuC@1spXeag@(h-^}{{($boWa(~d_tqfI{di|jazoVquTNCn^2TCg|g1cS6veFl< zs0!9pQh?G5M^$BMnXjfMse6leqwGlbm=C!_OHH7?~*KMKaejM_};Ed!d&@f`b)5)B;Y9Vmj?Zz@<3&%qP!|p z5v-x}R8>`n*yX*|SH^MB`YLo6CkAFh+|~ALD*e?Je326J`W(SfX((9g50q4tRtKx; z2GzapujwNOn@7Lpkuh!&I%nvwQLS=iPtMr`|No#oJ|Cmc=W{StN-Mk}Uqv7ga0G)j z)itGzDgSQ!zduw{Q&CY_Ug0gPs1Eq6LV-$$qrzKNRmEtis_eD@WcG*8?+;Yc41tR3 zQb)kys1B7@Iw~ux1C@cQ@{$sNZ~G&&exm-|ZnRi#xmen+*pGFV*}tn~TI zYASZOerZj>>tIV>zu#9~TIwh%^Hx`tR+d&dyrDAh?#Evlo2f1j`u*ODa&JXdNkyQt z%;%`C;C8%&{khxrE2*R-R+UwFnQ+P}xwp#ebp*=-p-^?uUlQ7F`9uDSV9-$(3RTgK z164I;6{RK2gXPs#p^^&v`R?i;3Nh_EN=r&Bf+f|Vk|1+Xg}*dZ6)0s2DKFdY{KdRm z?F%}ps~n+HM=4F=uP9@`msa^$P`>-^JA4&Be?_&UoVlx-9#i8hm2Ow!@Rl+W_1^#J z_k0P@*PYpY|No%BmzR4>L)Dc*e|1e&MU}tW?<=cvlvI|MlsL-x)yVGW+ltDv(qMUY zb*Q4OvZA`O#$Q=R>(}^ds=d{rkau_O?d8DcEpb%(OUueDy`>dQK&3u^!0Ypsan<76 zZF@5`{0z8ISw&@l?&+(ns_>OmRhD~enEG+M+rEz@W3|7$yd+o>qRPJVU{yt+!b`{V z2kASz?XMLaQ_5;e%d1MND{8zBM`?L!X;n1`aSj$7M0Pt~%N@ZIufs<-E3fj)R8!)u zt{}@`Df`1w+H3pC^BTRK6YXlgr6Y#|?3|KvDL4JHqB>Mw?e|yIl=}jKs$RDz_dBfT zQ2A3lnJG2bc1C#v)7pDERm+vWx z7%^TLGhXy6ja9J{@`nyI7%zMY4J)M#@JF;=%CMhO zx1g^`6NBHP0BO>&@%Ow6lz0TjkmN7iWV~%4?@K`vcf#AydPxKSVdC;$kAwE7eTmy) z8%p3K@O88WFYo&};s70&_kLV}HcA@!p^2y9@w@{hNnG9s(tspSc`wL|Nb*d=lLzTG zalz+}Pr|%|X#eB;D;wN)F#8XmhAoHiitHM;1)C33svq$PyaCO^3%^H6yzp1F4bNA} z>Les-gg)fK3m2nSd=&m24JZE;Tr-$>Reu$Sdmq8J@OHQW^_4QfmLWt3&$~e7S^uK|Bc`D&;*MQfBxSlD;JG5gk@$`ZK)2_ypXioYyYUKONAGB)tbN zM6GOB_!f%bg?m>hwG3~Ei;$#`!E=u1dt}P&fun|#hIV$r8<6B5gYTp9#D!~5WGvu? zPoR3d@NKjhFZ{cSr(oZc*cWV9=tJxA!Y9xsd;K~_7%(oytF)^);#NXBCXK4E+U z{$Mn7k2NY>?#N~`He^7O!mjhFZGe2yfIyq{<6SUqOtJv|p7 z8Oss)wuz_UG2`?+An)(F1j!he_xOBZ;wgCa$+RcilK1*7Kri4U@ChW_O28kCm-qdQ zMUqC|`?D5FUF7{gdrr`0miGXih@?DDxDZ)w0+*x5ST_d$i=;gAexMp8Y2-aYmm$eN z3ZFzfm?2q_nR?O3Z17UFm3R~u zpGI5ZZSY7J$BW^#Kb(UkA9)||w`d3PG@N!i+rrEHb1z1cCJO&(d;$)cqT}+C_3Mm} z!jq@!{9W*QBuqQNJyzu+Zj zvLDG)cqyvJ3zwkr_$Yh<$y}6#d!1$a4?G=7+ym#Ccoe>Fd=l<8+oXq6jrYKNj8DKl z&(`Bt-f0y?(r@LxRu>{Er!bC&;f0?fCtmmqnuZtt4+Zf2ad=gOtg!$mBgsej3R;90 zzKWuF;ondUFYk`}21$Bhotv?S7oLkY;)V0j7QDQ7>K!EWjJ$j58)U5u=XyAnkVaUK z6#G%wf^4!+;Sw|$FU<2&N4&5Pa^i)1AQxWV-BW>D$Ha&D7=S1h1jqsbGV)6RJd-UE+ZsN)XkI*)RWVO{vRc(#>*Th3Q%0C}e1 z@r(G(cAc>J0==#cEb-z&ezooMHIZCG1Ch3f5ez<4$-#N|H|;K7(XG3jd2 zNI@NoB#lFO8T*BJ9Im~bd4~KG@RTbUQ^e(+bvGaxJM!+jACVjfC@ z0RN2Scq;G4`x;r>g5#E&eBez;<|uh*-q&cOlm}K^qvK9^HInmzD16t%Q}EbpO_^aE zl6)fYMdOq3fa`R5)U0L96%*+HaLG-y1M5cN zU8n#r@7^oA+4Ki^mGSZ(zNxq9Jmp<{e?bw_C*Z!z>GOCylxJ7unk51sMBAlau;4cO z{3-NdxZ!rj%c-mjzd({E4F|1Y8ENeBZ|Ez03eLKd`Iht^xc^@IS~& zdKFXZC?t70;DvY7pGgyepWefqN!`J(~v?dQcxP z~_t$X#fS30m9`FcrINlD&Bk6lCc$x80q4D-d$uCY@kzU@Bc-tR! zKJu=__t)xt^n&hf3uE#NBi60;b_n#+8K^E-UGdlbB#tk49Bde z9Z2tjkDxGK-o^L|l6gSh$++*6dQ8c?8OI>2{P54nKAC<37j0l%lV=2OMOP9}!Bd~2 zAK+auh~!!_44*`@?}P)NW}0JNVE`?cvcWT+Va&2_1m26b%eLS@kQ}GN&vL$pU@FbL!ZNarj=A{Ju0ZH9?g^a2};uG*EB-`S} zGHN=KapZv)q4?STExv?L;Dzs?&G;1j?aQP&lXn0?_eRp-g+Y|U3+JLVJ`Df*3jQqG zA3nK>euGcIkKdqe@o89`q-=5y4^R0UbK@-9875G8Ch6guZ!!jFPzLxtlJva(M(weg zzH|ob!ohE|U#HMdpyM4mKgPo&-zCj7JUki6x-K}!_%JNmLOIFb29HFN#sOEN!IUQs z&wP)3N#lVpplZsNgcrQe*kfA}cngwsW6=F~`Y_w|z$B8o2tPxTCJhr?*^jI%eEtLG z4eFAF&wR-INnH}~vya%`>7<88rD!*)BRtpmFkJW#-3}4B5v?ap68`;T<}vm~3V!?v z{ha*Muy~s;gAKN!uP9pt-uqADQ>ia}>r?GhaM5R+4-=2T#oL*OsaF(Me9rhK?u5-> zFb1R@;Mrf&Z`rN~K8t44hDo^iE9ymGio&PTwBc;(2>1Mf`Iop2-t`N0Bp!p$|4Mr4 zKk!MFr|hSZ9$uH1r)J}mu(eN~HIGH$R^wA}|NJ~_Y}(-nB-d8Lb5UOx=jw0~8ibF) zs)9W0_~nFdBc&xBKU*E%Y&A z1=@xezJwJ0L3q#sd1?S&_!Jt97rudc?tn`V(q)Lk@IB*iDom7ggXY)Z}7rBj-V~@!sF0#yl^;r4DW=^ zNb(V`M=S8cZ9_!>{CMb?}TZ$-n13mcElQw3~Gcpe&x z7oIRIPgP49U>%b5!m-B^#|uwKv++Vds>g@n#Ynalhi{=Y#~tC`$K|P?@xn4>qwl%k z4M@_*;6d^pU5>?07(z=)Bk$S0AIbeRdDrfz#>+c*2k?$vIiAY9cPAk^2D#u>NRB&E z_>_suJ9)o0J`MlCJ9#CIyrZ`bS?9g*X(V~dJA40Uyy88*1Cg~Ycm|SnJ+R%xBk&0m zPr$E{b*>H%D%W1#^?L@g)`gFv71UAQ{rd~rNS^Wz;7LflybJhNw1zbDPT+qcDW|*} zxS~>fc}MX1XeDXnUBQnc*{-}Z_;V!Nl6MDJ@XlaaSKc9fnep;2;a8BPk#`F3%e#an zjl5g90ZAF;9m9{KW#ljK8vY4MdU@yYSS0D?-NSbvNiXjp{@QqX7jeb$X1nkjBx&Sb z!cQUVoB{rTtUOPkpP;Y2^b2MZ$Kc~g)=j{7jZeXSN9k?Z;dJ9Yuy(Z0 zCk#IwLpeRPKRjS;o^{@Chjl1H*~0J%;}h`Eam)jxcfd2zcG}7VS0SlO9DaiaQMNR! zIGK5hxD$>auea-hPfcKK(k2Pm_Y}RZ4bDbON$-ITr_!IvCk)pk$v*)LC+a+H@C+p7 z^uRljY&QmNlM(wxI2&1c!iLjmH#d303tT$?2>crwN_kT7>(g~S4M$GVaTlC5RhQob z8>X4E!6%TEAp!TAPX2fsJQB%%alng_aA zGjv-yVD*_gy%S!KB+nRp8_BvUxc^!78|r9>lhG>j3B$(Od1@Nl6+Vw@@j}Ph9PjbM zN6(?HX}37M-@|cMJUrFM97vunI3hrMO8Md6gF2pqGv@H#Z~BG@{#2{usxD8hK9{nw zZXBLkZ}NfT8g>3I7)BFizrfW^)E6&&1%>fRI6cgEN02|>C%%Adp_a97L-t+z%+DV@B zzW1vZ==Ad5_X+LV%lqH=SZMNr@*eoUvL@vG0*Y27SYR+RM7~e*2oGdR=+X{SU^=`|h7WlBc}){;X>m zM|cnX$#_nH)jTv@@_`Q{>sSN5QO>o9hv6Tt*ZDZ$DM81;aP7ts2_=<~;QY+F8%M2j?%RUp7((IOw)K<-*(HGq-b$z$f5=cW~Sx zy&ax>C+7+clmV_oLy0HglDiqF#G|lrC4CehhNnEhxv``duI3nxPr-p}7$cG={OiNy zKZmx3=Rc}_1P=KVWv*kq!=lG18+Ek79qTB6E&J`lxn%97ZaLlSVk@cwGh$Jbr}E z-wB6~Aw6|*z4Sl2be@n|D`SU5P?N2Tz>8ngp1bfbc~ zTo=Q7WXFf$JxH#Pgxisa^uo78lpinr0xiW0ZFBml6?ox6Xg%HzPeW_SQ+Sia@xnK1 z`=}&dm_pm}!lF9b886%i4U{Uf9wgb?9$#34Rz}cncaXZ4L)C_OY&~9Pl0_=O=MEv`Kphyx;gZ91=Eh z=t0{VJHlI$oRi1kd&Z|=!Mr}!d~Sm$BANf4FpOkf;c6u7#$jPIZCgs4!=UkD_@ePi zctnd%NGBR6_Wg;@D<~eaE~?}x4{XHiBT zKRj>&<7FZH5w1kD+bKWXubuj~5Qi5o?4y^tsh*x*<+lyzNj4w5usc!lv%xOh;J>Pxh|jX44K zk5Fd34IYlH`_*t8+D@6%aOlPCU&`iywJ1&cF#H86%EO=JQdMXSb$7!5pe>X^T}s;^ zS=R|`E~C#g4>;lE%jwVL?}E>vrT8SQyh8USCk&v$#D#xE4!rOcG#;OVmtIAGr94si zyK5)|+p@!oD030@b;8%KCx6P5gy%1#UX(copGVTSlkoVPXcPKa7%o9_z7d5FpaEZ0=OGy*5%?OCb(8Q5B>AV|p0{dmgAGW^5Qa-kJPKDCABV3RpM;+w zX@@lIdz;Az9**MdcLyA8ybJn`55r51kHY^VxhJD;@1ssdt60|sUqvZ=5{6c=t@9`w zJP*meod|plZIpV!WA0#H#XH~yNa7Lrfr+Q!@9ylQ%Fm}iz!%Xn@=U^ip#tLSu0HBe zB;EnvK@&-nf`J(2Bp!zA&}QNZc;MaaZ^;u*L(*qFupLP|N8n21SX-{}9s=kPE2S+_j-SIAX4XVC?I4pjI zIfwK%_=iW?zY*F5o)f445cj}^f7I~^e0nWy&bkTs$e-9x_&6+DPkT`Y8@zo3$6e|Y zgDua}R>ULF_adHn7%qO9vB$Qe@U2&vmq?R>vtBiQ05&7JcP)&Hm%6}5(RzFm{twBV zqyE}Q9fHK$VGEMJ6oGFbJLOEl?~v>ZwW*J4LXtiVhrK~RAx{U4zDfH~cj0|+^O64-Jg?&`OpI|MzSxWa66JVPs745 zNW*q*a4H&(kH9y+WPC|^;PGG4=gHp*i@xR?_z7yg@$WWAG3E)9D@XEF|eY@M`0u z@Q=nP;7%lSi~5zZj3m7So`6~}=ecNjipp1u@xq5t3@=Qe$MC{m&G;s$!6%D1#!naU0Ubq=e#HZktqI|WK{@{UE6_YecaTRIcv;q05FWv)R+BaWqV%;R1 zvmfn=55q6f2GXSAyx-@mZTJX0bYQ*;;~j7*a^a)!Q?x?Ti$5@54PJ_es}9Ono~tP* z+=8S}reNv8x=oz$bhL?bdf>GvNqXTHd%oI=7Zw~!eeuHIp#kJ?ho>Xyw;p&UijgJ? z`yH0A?06eI6UlZx@ERoPqwrZI{UHf`hf_ysH+TnjU5$sdOegxi$pEaPlJbY)%O;+L>#NuX?UR5*s&yI%tTR3gZ$$EZxbP(u zA-(Vev`qFLJh(=$YloAKcfkvdkHANaPr$Q=Q+~?nffpkwTNJ*Dto{#=I8n!)@NKk> z`ljICCo$e=6X98C*fsqvP8&fV#tZAvEWGeKRErm`K#TEl_&I8q^1}n1lm{>TWhDC) zFWhq!+rr!6WF+Sv!Vua-=K#PeB zPdJ74#0xJ)alG&ml)ww0GVugleJcAeO8Mb&6Lr6JzzZjlKW!U<<&$Y2+RX*$BWdRd zT#KZg6L8vT)C=!{E0Cl~z+aK1Q7-z%6vh{2aKdMh^!X%oP1Rm_z_fg|k@DE#HAu!n z6z(zI^h0 za%R5TPTk}1hO_e3&-fVp5KSbF@W|QuYBpXt4TbR@_}jDT+jtw?*Ui3=eFTp|TZjwq zMBDJfZ_rM>@MqMIb{1aXVISio@Ns0dC;Y_tG(6a=<90aNco#gv$6U!dqyzT#>$Y_U z^3^G5Fnv$B9M#|5-{P(K#rPOpi?-o~wjjqWyzqCZ;2!D%|2&6d7Cr&rLsB;3v9;8f z^ultq2JeJtAnEhM=g|h@!dK8{d=fr4m-@%>@VQ3TmpJ?nqS+l^1d+9f6qUqjO8lW^44+PmOVB^`ks#xc@S<@8D@Sv%hbp&Eaim5NU*cZ=wEp;YDZ+J^~*_R-cC@%e8mHk+*R^ zKtFfEb5H?oF1#1@!wdJgoiupiWoS5FcsZJg7hZ#A;}a|L)wf8trS6cj(1$34@H(^! zFC20g=PG#NC{#fI5!RvscwrkFj*q~#NcM&BC$xgNaPZxXYrL=qZNv-RXbWCgk9Oe0 z@J?j41I)XJb7uO3@SS_A^uU2wiha|o5tos>bcwsHtffrti3P>M? z`>kRu+{Sze3sy51-A@06uRp}`kJ4B1G59EwV_zJ;6sPaemy+<` zNRDf^KeB)R#2iHaHh3(OdI?=DrRLR*N3M?cN{c{BOL>t7%* zd<=eqr0(iP>V@Q3EL?{+(GJ2tyhOS24*1>6j96@r8{2Ix+X}EL~X=tk` z{2E!u_t!Y?yiPkXjyy1yB!Af#F!3hG7xEDm&LDd3xYwNb-row~>uLW=k-2GW>o@vX9D@&`X%BOK4UhbmeDK1DQ3Nl15iP?D_exV2ybWH8 zr0!Ai|6<-C9)(~2o4$lk!{7Wz*U=(=5b;)c5pv*#mm{aF3x7efkJJvX1<;r^v@?7M zZO5nJo8QyV#lwSsqCO5&Ht7uR7%-#iR_-fi5{s&p-)%z5v_fdrOYCwT%M^fepEZSG6vB5GV zaSyy;V1XL+AZg(H`|C7m=suvpdd|uNmm$eN28SM4V12*rfFB`=r{Hyiw2#6!4lYp1 z`G(t7FKov+^!8|k& zZ-Yl7Nh3_5WyFR5K&$X+SbT&|BTS&7#D&kI;rJx{3`su1*pLFAPoci>ZnPL5hxQ}c z7qpvjDw5;7a1FB4=E8wP3skl2ci8_Ax=rkG)KR+KT+n&6&PRCkFw!%|9q?=<`6rGk zP_AQ%vtNXd9!Hzt<8ZGM?QQVx(gGDBo-8lmxmLZdswhwoR28V!`$!L$jVMqyd<@PW zU7)s*-Z7>?or-MqCE=v8lpimA2vy^SPojx<;b&+zK02;I?SC?D$bJ<1&~Uu)CNvE% zJaBx0s>KVxLhbl8JZu8{k^UgOej@FK7p_6&c;QEAB3`)fB({sU!wG0T`3R%v1-$Sn zv=uM>7NzmR?~$U+YI1>E=_*icq! z*i6G|XP9vc_nS?>d4zJpL%j4+@o*86F&KdlB3U;M5Ar*@RD{fq3CF z$bnD5p$)n{9WaKhIRw@;nsEWoLbZ=_-UV+&i}1p4&~m(RTodKT3!Bjfyl^qvgcmMD zU*U!8kfce#x$~I!#KS|IS!Xfh0{*V0K=q|A9=H<8eviYdHuAiVYYI4VA^nPWb3w-< z`VU@s&jpMl){Vn|UC6fBr|Kg1JKBWzz&#?&^UO0gcrKFjlQ4YQ_$2%cS#uGLT}TmI4`~Z9uK7b^R&~s^l8cba1Mb-E)yaGuY;Yo|B3vuBH zG#l@P^N`F-5qK|>z9H;$8OI~i2oFaQyaS$sB#khJ7ClD!;r%FzkHb&JlU}$3t;7pA zT+SHB3x7nL@WO+xpdIkSYLv!1;l(IMp2F+Vdc5#fl*9|yUR9vB;f0k;7i5dk@mj*cZ-+ z=%;I#=bvG_aM>dT{H~NXhll==ex6`kuj1KhF_|fp_TtqzS{D-ly(tD+ZtYh;0#1!hJs`J#jm{ zW*c=U9)-`NcFLcW*ca?yyzp5h?IZjV1t^d3SJaMIU(&~r+;fh>qOWvY+2F#j3)HYD zs0+OITgrovK~GxGpTfESrvLqgGQ;nXjk#3)r$G64a4e>dVR!|y`WOs+$Jmzi@PzM) z#NpXL(*K{NFTnvn(N?6f!x|)Yal$i<_rQzM8pDDOAJRmIK~`mQjW{{4Bpv&Bogb3e{R9$A|>{8p$|H!{ZBCw}$e- z1|;z?eA2`da8Oa9H3sc)9+LDC_$pdVy^`>NV(sm4J+ih1$Mh{!J1CnAZbo*}r(pj* z^tNnpHj+F&@EQ}3!Y`1F{WLsmPo3TY&ofyU*$&faBVKsx0P>+f#9(MY^7sA6~*yyl2PwH+UY6OSxZn_eUii~dlkmf~&j-J@8B zPr#t7;HF}^e<2bSmh{GwRk5SR7#qcC_ntUjCJvbLL=<2~?J<704>@kzMH2|B$E4maKj+l-IE z)yBu+su6{1*lUyrt{QDTTy?VXaMh{C!&Rpl4_8e!9^6 z;hH(d!!>h_ho2+q4{3OBeW9|kALG#9pnVt~-l)9;eqwwY-qTd5wk1gqePQjx@aTDk zYSvqfQ@9OT-;KhR&DzJ|*p@=Ia}#|4{?qt0{8uYw8_Bq9qrV`Ti_)-sKKWjE}+Bj8DRo7w9xDxZd~#JhWZMqwwv8Y>U2;f&F=!7j82?4F@kW;~EBxkHdFSgnla=bOGZIFPwu`;lr@_LdG)fX@d_KABQJj zRH(ip?t-5p+h*DdUJ=ppC>(OJjyvFbG??@WIQAgL3)M|X;xSmUSbHa2 zj%5Cc!JWqQN>()sS#ug}y_~Uj68jD=yF&XI{L7WvC*ZqRX`g~WEzw?GU8sJ)RC@KeU|9Prp{jfZ2d(`j5V5Y^s(eW6--1LdTxB5*qzOutIQSvP9$fwOKU{VUWLzI|Jv z^1RA^ha>M~*~`o!&(Uru`8snH{N8x=Jo^GkJPhA8J_S#HLC0P2apM#4$QO0o0dFxr z28&YPtS*P*9jm9To^+p|c!d1q{;lNjP+zziaJ_>gluU=)oL{|CX z7UNTJ{9kq41=ky&fI~OwxC1UXJ_d_k({US|Z+rxPYJ3{bcwMLQz?Y0q!pb*v+zIbD zJ`VRw>bMjsZSI6z}O5>w&r}1hF?T=);VYtQk6deDa zj=SJ`;}dY``#SD`%Z-o0;=k*9+28`>Bk(ih({ScioyG%SHa-cfKG1O|e8BiP{QZYI zZiiPGABEo=uRfyxBdh)4yT+&B_>_*j;CkZ|aOgjD+yR#xAA@~A)^QtbH$DQl8=r== zKGA7BaHH`_SiMchop6=$aX9dwI&O!T8y|(=8LvL2{~)XU@Ezk*aNK7)?t<%#PrxDD zb=(1OHa-T6KG$&@oNs&terkLg&iF#7@xYgiPr}MCb=(Q>H$D#c`%1^{@Jb~8Bno#L zufC@LAglktEykzd_-{=Af$NP=z@guo{sWgAAA^0V;+yyrnpMb-@({TrkA*s92 z_PzGPi1ETnI}6p;H#iQ$gMQH74$nYxZsUPVket^=;T=B~szn>g6R!V>^C5f!w*Aby z_z3KmSER;sZeW8q_bKA<43Qq5lwYLAaL(g|9~z&67Z&Jv1Rh+dy&XPm&XJ~sFqO5l^Qc`)0M_JqMBid0{G7=DKqaV%Ctiqu^w z`d5y{&~s#w8X(7LxC6=l;t$BE`_V?y#NpqLE>eReJ$!#yk=icDaoBVW`-^qM@cd(o z)GFc;_yCf8;_zMLQ?U3r9k;Md>Gzrd<;Hgd;)%CdDsXdXD*#_4lX~P6O`2^CCzYD&Aws78( zgky$l?}Fc>mBiJF^dBet4)1_hqMfu`6z)H=h(EH$euU2<$v+9dMY7#A+-sC613b=n z2RzMq7i=;<46jCVPdEyHMN8=eYBc=_NuEwvZ+sX&Y}vtRHw_$!h*M+Mkcu!!INvMqQ- zNXI?!U9^<+DcC+o$0P8JTIxf34=kuN?GKC3rJobG!86gczxTIz+T0=)zze6N2wr#= zisC)+%zE+{56^5c9)5$qdY^p_FKpC40$)HIxAwQVN4Q9(@WQv|(GGawcgXeu_gdkq zW*v{i&GRWI+e*Pz3yahmd>rmPkL{9PonNFTEYjWumtUZL41Rl|_G!5CBJw|*b)hf9 z+?!-v!{1(3q|zzc0X}+Fk#hWlG;s2@I_`qs+@O6LF1n?N@Bf(x;3>BkS-&H8!J1f+ z8cN&=Us_4sDO(cWwo0doLFdD)Oa8*4YiWm%*w=97x*|2}L$(Wt|3&BTgnK@t*R{d; z^Lkz3#T%(7>7&r`nvM&Tf74!g+uLTl@Z9&b55wsnG9K8j2OjmYjyvF8pBAZd${d5k zzSL!Kz+b;*9$;Jifi3mV9jx2L{)JEO)axeTlApDY!h`aPRh)LP!(R)F)pE9_ii*`W zd+K-;7VK4Q)z=1p*r(WPH#MMGy)}?H=~M8+gNm*F5`iZiR;*S~HYfaONU^n#Qn3E$ zVryT7;qk{6Tl>NZ=QxV3Xa2+Rgpy)4jdACMi;!I7$6#wIBF_jsri`)?cfd9@jQR@S zK;?MhZ_A6-c)SgMgoe>?Q*dd8UN;H@l{(KbT#sgxMp#xwIq|~T)x~Nt-UIvB=yh%I z6ST6FwuLK>*YOy98!aQf&~*Z7@WSTdYy%&Gzdw=sHqz$sA0vwSJ9*>-lOtINFPt)p zGH+qK@P!G*sv4h!ubj&Mcn=RRn^ep-2K@@YFd50ZaQ9_N|0eo&Q?Tk;t!|T}xrAFUz&TPx*x=<+;-rtli?1(M z!`WX^_-C}{%l;P6ys=nqzzgT1&G;l-w2XWxr|`O4s5@R5N9B0ojOBXYdEnGr8SiY% z1HVMGNhAFIZOnIg;cOJa3m2~-JzlsRt-=c*LhJEy_(P2PQx|o2v6_zL_qHCm7}-8! zU3e`TgctsN4`ss(@4uJ!!3*C(v+%-ypn7~7p0|?v$Z_pH`WNcQxD!5fKm7qOd={1C zldygj{RgjB7poCy1M3QBqD^?=6Au=v6ka&?|7!1iz^oegzJDXxGkY3altI`M2L0Kn zOqsGH83{Wl%puyz)I@*wX)1H*u#pVPS)61z#hRHtGkg9obQpvcsnH*{gz=|iAsJ4x zq8QZa{rvX2xg3w@yyv+*?{&TJ^B&jr@%g&D*S+p_-}k!rTGPyEU_Fq$$GK>3~EWQbnWbsjtF+Su7UI6mF8DjtA%rEVTPlpIOinl(= zxtMnsKi#UY_bKk*rx_F43E>gXc>7xfJF5>+TUZ0)Zy|agzqb%Ktl_@?m@&amLWpeR z;cFQO#xsnQApH@KTF1UYx%evBMvmfE*i9C9+r&M{nh~D}1IZ!$2&6yn@N$?(eeoG@ zGEZdjOlTy>@Jny;S$r*Xgx9~#J-n5SXTC$GpW^Nxvi`{8ILs!C{U7o9m(Trzzxjk~ z|BU(VI6!R&xtCph&Zq33!f)Rbp`Pwu$%LWUxO^>OMKYZtXs0!g@I&ok8fBH zWbrW&C5!t*oE*Y~5AdmNoHvX&X82S&{dDlbexEX^6T}B*`BaSaim&eG<7Z^~-Yq-@ zOtQEXY_fPhY$DsZZ?=!0we6Pnh2$=ji(>(w${~wygT7?(638c;cqdrwb>deK@~Jkm z_;cuxJrEDi@u`ulpD;e~Ffw~c01t=s&*|eu5aqrQUwWiZO(Tn60fU^v8AtilapEQ#Zy4m zQVhQW@_oW7{C8NzXS6tS8po2wm%>i6csz9Zg8LVLD&^cSF5YJ#=cUa6UI_9WXyA_q z(Kn8Dao%8G`g@GSI0;hU!XJSAen=NTk;hv4hIN9sLzhm<@q{7FBkzV$+&R?8-?wvb zp5ar+45L2%3E_RhK9wQ|@Pm;4dl~!}ke}TUyU>R^;=Uv3lN^h$JDc`h#snuo{vO=I zZ$SZd#N9`7{bcc#5F@K|d}L4KY>E#kZ&IgCex%uxi-0Qugn7`{Q5$MIb{8~90(p9QvY{Eyy#E?#vn zGnXJZ>p#fPNbI0F%9uIssf%L0jY)egLFzCSC_J z<|*8#l5xA9F~P&DnD@P03myxtv?-nkHd$N_?PRf~vv?hBquj-l5?C-`Sb@5uVs&tIyk3}{*#0F16U~a@r-(}ehhyD>!nVEPu&9YwMQE-Yhq1S z(kA{2z&z`0KWEWreJZq*4{lxp)-hLIpOCWDQ`iuv!^gb)Yco(doT)n_P2Ajy@ zTcCq1PQY%mg%5bq>q7u<207NjkFMgn$ToiDC61*(CZ4_8E05#NApLjneQUg9O}rP< zUn_27U4ZPd;te3>DSZ3OUVQ`aUdvo?jcOfZ4q4J%s@i}RtBES?4n$>K6F$>J4Yk!_s$s@J~w z3J6jzu7H7L3r|dW*BHYmz2?nV2w$bM_(K>&KgIiRWF3;lxez0V@CP9C;^L~;y=>vX zf~*1YHJiBK$>KYpoGh+~B-z3xZ}1GmSQ*%ato@l&eA%1KDOp?zd1MO@d&|3z!g$cT zteXR}RTvL?UuQhI7a$XCoojhBU#jk=Qi@*4qYb1+L`G#vG zhw;aqJRg!>Y^A|1-ck}u$9&%uY^2xFIVvsQr-wzRgcG^qG*N{#8KjPkr+w*ffBHdr1TO%2mN4*(r*OU9=o{{L zT81*nA-oEDWmAri8qD~RgLvNQv_qXZw(>I4?{VUHLB`X?2Ml4}xv#}GOrySdCB)@i z_!E#iVsj|#pK@^pj3--o;u-YeK>Cx+)Kj8E`8|A8fp) zfc9sxx8M^;dpU&Hov+LBA1+{ycn6Q;dXQr+e9y&Rn+Cr9Qm>AICtcxPZw%**XB_BT z5FdSIhFWnD8E=9uWbs>9vA)UT?;+4VJ8l1j4AqA$J_DrO#B-vISAem_LnpDGIW~+R zgWZ(d_>LmRgmMGtPUaq=JcK`i@njd9Q`oP_;!X&WRdI%T?yc<@ejH>y zr?7JeYqTfl#Z&H~&*T`s=05hcL&*53#TjaT59SMB_#pcg9DuV8-3;)kJ-EM5cC$l|=`yz(%f zXM1hNarc$nQ?xIB>;?88viNy$$ToibCFb#9GG4TraU&b}>X&JUH4wuEYncmj1pf(S zu1&lVWFtjlq}9z&-IYS17JQ`oDU{BjPH_i=E%V3y~6QFaxT2_Ro2W= zj1_)2#a=+UgI8_z_Q@1}bd#5D?0=KJv={FV_#sI56L;twdW-!Zq&$l6*4e<%=p1fm zzXGX~!Y94$t-~m;gDtc#KJp#Lhb(>{eDqCx{JX3fviLL@NEQ!)G2}RY@I7zOG4Z1y z>)giAywAKJ!Cc_&A9C;0zKaXDFh?94#ZPRdoNQy~8`j+6j0xVUGSzI3Rr_SB?y#FI zuGu$JWzmkf7W$IKO^{EvaB#oO^!+Fv3DTb!epTlb?$^aD58>BA?r{fq?aFzXqcENh z>0|K;2V|zlFobUd`B@wBl|JfFUt9{S$l_+$M7HpljLh`Uu14_3S(&PXau@gN=CvQh zr|KNWk7j48f*@mpFFG(&tsqD6f&lf8W_)nBgS=w{IHa@K0QtJ5*wtA)q`Ox?jQ2m7 z>*c*TfJcB_V+213@}6$v;34#r^NQm-i;w9+zbOyl*K~Gppl4=!&5NH0L;8kKkL{bOwvnUQ>c=(m`7VBb0P}u4ZQ`+~WvVU5 zQ3roNi1DOc4bD_E^Js_i7|t7#Ngr7!xa-hN_#>TN{BeZw=Nesn+E}kn9KQ}SRt|n}9QPGsT;WU6B78~CuL$<#QG$wn={o= zkYmI62A$*h5uHsuG|qfcKa8J*$Vu!uc|^9GZf>Lx%!P$dZ|2%%Uho-DFou%xa*#O^mp#dI4r3^O z8QRGyd|)ekAX!`vCRw~4mXlqqp3YR$$l@r-@B9(J{0w77c?y3BavzC%uAu!>veWKG z-bN1MZZ=~W;#%-XkacU~t}7W+zSbAOvtFRD)QRIEFR|Byma1ylV}6CRw%7 zUl>mv@zHB(N6v+9ka8F2t)m^zCAMHUb;NCub82?l9Xg8-UGJ5Puhv;yrn7jx&f>uv zy!zs?I*aG(EKcYwwsjW2rL)-8S$x_n>|OLNjI&?ORO842e9=b6j%?sfZ!jNZ2dg*P zPX;n3cpNMyi;JL<9K(ME`8!JSo3Mg%@m5HYU3_0V_bfSu^WN5bBc2QL_j=-|VJYVo zw}C}Y;b-3A+Q>Hk8_0Ta@e%KOIf%!B%zG3s*4e~wfz)^K=*?W$Y235;7BI--Pr)RM zyZn{UFLD5X3wdPqKF>}Le@2es6Fy+B$ss%i+Q{O8AF@8l;)`G#If6?*Vvpn6<9G$6 z`-3mv!hOg05{Ro|>7eYi*N`o83cDa{OnuD0y_NfpatDw4gnOAB!FxfTT~$Y>>J9Su zo*{e=q(3Y1^C0&^3hw~vgZh;H>T||c+QgkOK9B3&&K?NqcL)3^NIN#}>+(5xI@gY; zfwU9D)jC^v?GEmJ+DYNjU$Q?8rk}WU7h_J2<3^A+E&Mg4`|~yTS|{s}Izc=iq|XLk z4(UGNuHRCh=b`|<^E*BtIhTo7z-+REyMFK058^zX!?*~_>3<9d{?4-p^~IO%<@z~q z6u$-XJu?pet@5XT*KQQw0n3N*wPO6{zJ9(&$U4Vg!4|UG&#(GJ+Zowuujt}e?PT!_ z&`B1zfiIu&!Fxe|55IV7SHH@kj(8^Ik;QYMfE>roP)rsN_W4yQS)32$Wbs*$BuDTq z8UFNhpn=zA`qfVQnZo`oF9+~tAkPdj?C-|0%u4{D2{Ptkd^5=X701u(Y~!6et8BkI z7G$l4@O>cdnE1H>*UNcrd}nvYjWIXyaR)OdR0nP*1|c5 z`Bf|BVSM%Be!l+7JmLYp=o@vy_>V!xf^q|&a*Wr07#D(EOBA1foS%0x#s^<@JY&nT zQG5gBGB4r>AVe0ggpp)%^9g=6o-DpG*RN)i#fzYfEWY|g`a>3XIf-!~i&sG_IfYA4 z_Iqm^uK^j`6yB|~>dV~qW35o%!0$l%9_jB_mxA1zQT&9?Hhy`4Uo{S=eSFrbocm0! z5vL&CK0f9&)-e06xCpX_Wv5+24w7yB&>;FpHt|Z3^NPQL5ar@MFp_KxW(-be9gySr znLOH|4>mr0h<9udcfvf%)llYfxL+-$eh@dq7|Ja?>r8L0#&JE!dBqNlqrP}66q8+i zYuFnH10OtscDddlz7b@6;<(3IeifE-JO$)?!(zDqIexW1%o@Y1pp-f({4vO$EFL_H z^HMIJ1;ykzHX*&XaR($QckvPD>g&Z5LB>3W6FOV?Rghy-_=N($>PsI|_Q@(ojGKw~o5(#vJK}@EB8#tpHnP};_sHV! zU>jLH>T1S^>y6-Tke*XKc#@twd?m=-iT?l@Xc>{RjwSKjjatr&e^UfQ_=T2u_&Y_<;4$C>O_)lPy#mO0dwTUc# z9(Iz&A3zt574LyuvYP2v`^LDJ$w9mf;^(sd@&3Q_?w0^gK>B;_aNaC0hw;52zgN)2 z150!{-lVgGXV3P^kGy^+PYK!|MM=O9cL z2d`((A&bw3VsZrE4APGHMJOeUx57fQi;pVx+7TDQQp&|;V37@c|L+-FvWa(s>{03l z_CwfBx%fs1FmB?9p$}QS67t9?ylEcoFwPD>^ACD2!qqxkxW`Ric@W>Ovw^?R*~KL{ z>+!_zgN&hr&y4Ff@ogaA3n5-FnLdaQyM=q7EY62%`_qpCx4RU|2yGkA-$}6u$!LzTqS8@cJgc3!=0sUIwLPaVr>P8~e+= z`r;eFq+GlRTFK%hq{tTD=T7fh#OHuRxp)rjB#X;*7FU7qe2&HKAp01GoD40)3&^>|jgX{Vya8;o*nt#Te1zexs}P=dANMlnjpG+U{y)p& z{(s^*gfS7H2Yt!n@sLjzU#-i<^L4p_r!Mx|H}LWId-EQ`t94G{7nXSIHibt#;MIxX zcXW2}wdG#9i7$GPwZZp%MsVmM#*IFRmw>E)6SwQ^;EVsv`lL=2KMhjH#s@6*asbZ& zxxXCzH;~V)j)&PN9`VlG7Z-t)7vrTmiyzf_Iet%P@eZAL;&G<8e#G-Z+7XxQEZ(T| zCOo0SJ8u+sLM#0j_p0>D#pmc;fN#}#KK?UE`%Cc}UEYR2*5w`e>?+png*+qSo4}#{ zINl3e$ST2SAM7TV;v=iQ@*r*n`Ao5KpvF6w`1o4>dyM?uI(>vOWbxZc?o;x6_^>+G z49^L@@FgJoKp|eK^D3NQ@0CaJ!;s=w6OUCC^Y(sjT;N3suM_H>7;8+K5g&eYa%&!K)mQ&b! z@hs>?Ht;aKaraiQogls# zr2QCP1nK(t4PEZw?oWAjg7^}hqxe3ZP5g$=4({=^u8%L#Ig0Ps*~D+??BE{H==%5) zoul}EolV@Xvx9S1==!)&=O`}M*~INSJ2>ZAT^|?f9L422o48$P2j@Je>*GS5qqtmW z6SwQ^;GE}meO#z>6yLA2iQ9E{aE`6(<3gRIxLjuwx9jZSoRzvhF4Q@S%XKz!yUq?? z{DNMG*!Loz*WCMr*jnlL1z(uWu}b-9bXZScxN_)?t>+@!OG9gud^D}Hq(&)!D)+ogEx}&0Aw3d;v(CQGBP)CSI*`3V*A! z+Q>UHNc{+&1#&HMyb02K6z=`H9yffk&ekT@D#)=Z{JGA-H+U9AAa%m{N}XePk8W^D{C7h#~h9Ysc+(Eb+++logJ(`@ah{M z`qfh)^=-UaX9w@mIq(tv1gR6oQ+1AE17r^oH|cTV)v6I>&GYNIMpOLuUt{-NEz2 zO2!c13;AT{Q||H4ymkV3h|Xa=S?3sjP-hdrtaA!~t#j~iel-lFKT*6uX9GW>vyDI1 zIk3&IMuS{e1TWFq#9MWC@!-$BIyT-6(!PWD=p5P3I|oR)c#+Nqwsf}fW}O|pM`!f~ z&xat#2JtYRBY3*bF}z4;13#s+jX%}d#fQ1N9eknA5j3UM0x= zh3D+?+6?bypMfAhLl?&;NIT-SI;SvSwfCMea6gbXL-+!nBY3*bFsr`57yaIwpQrXB%(U*}+}2ygET#0CFw^e+9Cqscue*>EdqLx(|4= z&M~}1XB%(T*~L8%^y-K3r8-CP0-X)~gw8hJsuT)tO9JOlE{|NiHH&Is^-SmB?2R1W`! zxcry=^F#T*hp!&a4?j zOC}W;O`llOZ~D|pvu2jeoIJbVq?yx)PAr+;f9@&0gVQI@m^!(rWcH;+vr4AUoH4BT zDg8sehj+^i2J>gnno~0Sycv^c{)?@h<{ih8B}J3w%$hp;_vvByL(xkb~0)8ya7dQU7lZ^qo2*A>m`9h@_D#H2|@ zC0yI&iPK7odJoSZ@MGKmc<_(y|D?u$ID7tpf9PxefFCcK!}ACHFrLFBBO)V4jy&(e za|;V~_%;8nLRo4)^H*9OuP&=Ls>`dB)veWbbz60+y1m+|?x=RFJF8WVuO?8FQxmMo ztqIlS)r4ybY9cj-HL;q~ns`mJ#;R$pagrU$SVL(;yrHbYXee(m8}YfwI~!G#uPM-!(^T9PYbtGuHLPW8 zb#LlUA(TW&ZsM|GwYIdR$XhIT^DT1ZHhJ*H^-aHnvLf2=6Fk4i_ucvVzwk( ztd`am8jWPBFp5`}RT`D$m1bqK(yDB&v@6>x1GPD|!P?x~P;FjqxVfM?(p=aqt;L#4 z(=FO9Z7nLw9?tQd^{T^x^tu|HLUhC9$)Vj5?WN9*b$ z;)WmAS$UJ$lx(t^TAS>qwx(25dy~`D(d0IDHmPP`bD%k=IoO=r9BR&M{-yafSTSaE zve{~GZMK`+np4f~%}#Siv)kO+tXh06ftH+>U`uXGs3or@+)~gIX(?=pwiLI-T1s2u zKU--wYbDjv-r}@$w74yuEh^5l;eIR}(`N?E95ZOX_`sXGFwgCY%^14yXlx6rfcR@1S@hYLKS%x;fjKaNJThNkccD-6VXI*B9R)?zds>9U<)sgDL>S%Rwb*#Gd z$9vx7uDAXR_x%rRG+y(6a+P-0s-!O&NaiGi$=qZpnU@SF3zCs!VKSO5PX13>!-n1~ zt%lYHyP>Ti)zIGHG;}n$4V?|D(bpJg%xMfZ<~D{J^BTjA1&xu$!p3N0abv8pv@zaT z)@U@AH=2#fMys*4(f&{N+aUX7s41^0+*HsMX)0`r{=*LZfAL+Uc(GDGd8YVP_fe;* zDtr}zil5(4g%#0?;)+;BX+^xEtiq@$uP`f;6;?%Sgd%VKDwXgh0*RbN zFp--GCGryg-U6@qfFTUIS@HF>J&vHM!oBWzL4aO8Q@FiSnM@UgKk4%XrK6@s1GZZPrcZ@>K(y_s}qJn-=et0p22`d~G7t z5a21=;42dWo`%bKe{AP{G0Z!^-5lU8&)`jtU#uWs?@|HY)(qar0=#`0JP|6M{NjA| VK=HO8PxmOq`&gM=&i|AD{4ak{fCc~n diff --git a/test/lib/objectbox.lib b/test/lib/objectbox.lib deleted file mode 100644 index 711c8f3f09835e518527f2fb4302ba5da589f76f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 101570 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 From 210b92fef38fe430ab2b8d4a70ccb7b7ad537b36 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 4 Apr 2024 12:38:05 +0100 Subject: [PATCH 160/168] Added more tests Fixed bugs Removed unused functionality Reduced code duplication & minor internal restructuring --- .../impls/objectbox/backend/backend.dart | 7 +- .../impls/objectbox/backend/internal.dart | 74 +++--- .../backend/internal_workers/shared.dart | 76 ++++++ .../internal_workers/standard/cmd_type.dart | 57 +++++ .../standard/incoming_cmd.dart | 6 + .../standard/worker.dart} | 229 ++++-------------- .../thread_safe.dart} | 111 ++------- .../backend/interfaces/backend/internal.dart | 9 +- .../backend/internal_thread_safe.dart | 12 +- lib/src/bulk_download/thread.dart | 6 +- test/general_test.dart | 218 +++++++++++++++-- 11 files changed, 458 insertions(+), 347 deletions(-) create mode 100644 lib/src/backend/impls/objectbox/backend/internal_workers/shared.dart create mode 100644 lib/src/backend/impls/objectbox/backend/internal_workers/standard/cmd_type.dart create mode 100644 lib/src/backend/impls/objectbox/backend/internal_workers/standard/incoming_cmd.dart rename lib/src/backend/impls/objectbox/backend/{internal_worker.dart => internal_workers/standard/worker.dart} (87%) rename lib/src/backend/impls/objectbox/backend/{internal_thread_safe.dart => internal_workers/thread_safe.dart} (54%) diff --git a/lib/src/backend/impls/objectbox/backend/backend.dart b/lib/src/backend/impls/objectbox/backend/backend.dart index b758954b..6fe31a25 100644 --- a/lib/src/backend/impls/objectbox/backend/backend.dart +++ b/lib/src/backend/impls/objectbox/backend/backend.dart @@ -22,10 +22,13 @@ import '../models/src/tile.dart'; export 'package:objectbox/objectbox.dart' show StorageException; +part 'internal_workers/standard/cmd_type.dart'; +part 'internal_workers/standard/incoming_cmd.dart'; +part 'internal_workers/standard/worker.dart'; +part 'internal_workers/shared.dart'; +part 'internal_workers/thread_safe.dart'; part 'errors.dart'; -part 'internal_thread_safe.dart'; part 'internal.dart'; -part 'internal_worker.dart'; /// Implementation of [FMTCBackend] that uses ObjectBox as the storage database final class FMTCObjectBoxBackend implements FMTCBackend { diff --git a/lib/src/backend/impls/objectbox/backend/internal.dart b/lib/src/backend/impls/objectbox/backend/internal.dart index 31b0f0a2..b2e8e8dc 100644 --- a/lib/src/backend/impls/objectbox/backend/internal.dart +++ b/lib/src/backend/impls/objectbox/backend/internal.dart @@ -39,7 +39,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { // Define communicators Future?> _sendCmdOneShot({ - required _WorkerCmdType type, + required _CmdType type, Map args = const {}, }) async { expectInitialised; @@ -64,7 +64,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { } Stream?> _sendCmdStreamed({ - required _WorkerCmdType type, + required _CmdType type, Map args = const {}, }) async* { expectInitialised; @@ -77,7 +77,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { if ((type.hasInternalStreamSub ?? false) && !_workerComplete.isCompleted) { await _sendCmdOneShot( - type: _WorkerCmdType.cancelInternalStreamSub, + type: _CmdType.cancelInternalStreamSub, args: {'id': id}, ); } @@ -241,7 +241,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { // Send self-destruct cmd to worker, and wait for response and exit await _sendCmdOneShot( - type: _WorkerCmdType.destroy, + type: _CmdType.destroy, args: {'deleteRoot': deleteRoot}, ); await _workerComplete.future; @@ -275,26 +275,26 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { @override Future realSize() async => - (await _sendCmdOneShot(type: _WorkerCmdType.realSize))!['size']; + (await _sendCmdOneShot(type: _CmdType.realSize))!['size']; @override Future rootSize() async => - (await _sendCmdOneShot(type: _WorkerCmdType.rootSize))!['size']; + (await _sendCmdOneShot(type: _CmdType.rootSize))!['size']; @override Future rootLength() async => - (await _sendCmdOneShot(type: _WorkerCmdType.rootLength))!['length']; + (await _sendCmdOneShot(type: _CmdType.rootLength))!['length']; @override Future> listStores() async => - (await _sendCmdOneShot(type: _WorkerCmdType.listStores))!['stores']; + (await _sendCmdOneShot(type: _CmdType.listStores))!['stores']; @override Future storeExists({ required String storeName, }) async => (await _sendCmdOneShot( - type: _WorkerCmdType.storeExists, + type: _CmdType.storeExists, args: {'storeName': storeName}, ))!['exists']; @@ -303,7 +303,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { required String storeName, }) => _sendCmdOneShot( - type: _WorkerCmdType.createStore, + type: _CmdType.createStore, args: {'storeName': storeName}, ); @@ -312,7 +312,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { required String storeName, }) => _sendCmdOneShot( - type: _WorkerCmdType.resetStore, + type: _CmdType.resetStore, args: {'storeName': storeName}, ); @@ -322,7 +322,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { required String newStoreName, }) => _sendCmdOneShot( - type: _WorkerCmdType.renameStore, + type: _CmdType.renameStore, args: { 'currentStoreName': currentStoreName, 'newStoreName': newStoreName, @@ -334,7 +334,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { required String storeName, }) => _sendCmdOneShot( - type: _WorkerCmdType.deleteStore, + type: _CmdType.deleteStore, args: {'storeName': storeName}, ); @@ -343,7 +343,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { required String storeName, }) async => (await _sendCmdOneShot( - type: _WorkerCmdType.getStoreStats, + type: _CmdType.getStoreStats, args: {'storeName': storeName}, ))!['stats']; @@ -353,7 +353,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { required String url, }) async => (await _sendCmdOneShot( - type: _WorkerCmdType.tileExistsInStore, + type: _CmdType.tileExistsInStore, args: {'storeName': storeName, 'url': url}, ))!['exists']; @@ -363,7 +363,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { String? storeName, }) async => (await _sendCmdOneShot( - type: _WorkerCmdType.readTile, + type: _CmdType.readTile, args: {'url': url, 'storeName': storeName}, ))!['tile']; @@ -372,7 +372,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { required String storeName, }) async => (await _sendCmdOneShot( - type: _WorkerCmdType.readLatestTile, + type: _CmdType.readLatestTile, args: {'storeName': storeName}, ))!['tile']; @@ -380,10 +380,10 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { Future writeTile({ required String storeName, required String url, - required Uint8List? bytes, + required Uint8List bytes, }) => _sendCmdOneShot( - type: _WorkerCmdType.writeTile, + type: _CmdType.writeTile, args: {'storeName': storeName, 'url': url, 'bytes': bytes}, ); @@ -393,7 +393,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { required String url, }) async => (await _sendCmdOneShot( - type: _WorkerCmdType.deleteTile, + type: _CmdType.deleteTile, args: {'storeName': storeName, 'url': url}, ))!['wasOrphan']; @@ -403,7 +403,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { required bool hit, }) => _sendCmdOneShot( - type: _WorkerCmdType.registerHitOrMiss, + type: _CmdType.registerHitOrMiss, args: {'storeName': storeName, 'hit': hit}, ); @@ -412,7 +412,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { required String storeName, required int tilesLimit, }) async { - const type = _WorkerCmdType.removeOldestTilesAboveLimit; + const type = _CmdType.removeOldestTilesAboveLimit; final args = {'storeName': storeName, 'tilesLimit': tilesLimit}; // Attempts to avoid flooding worker with requests to delete oldest tile, @@ -453,7 +453,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { required DateTime expiry, }) async => (await _sendCmdOneShot( - type: _WorkerCmdType.removeTilesOlderThan, + type: _CmdType.removeTilesOlderThan, args: {'storeName': storeName, 'expiry': expiry}, ))!['numOrphans']; @@ -462,7 +462,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { required String storeName, }) async => (await _sendCmdOneShot( - type: _WorkerCmdType.readMetadata, + type: _CmdType.readMetadata, args: {'storeName': storeName}, ))!['metadata']; @@ -473,7 +473,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { required String value, }) => _sendCmdOneShot( - type: _WorkerCmdType.setMetadata, + type: _CmdType.setMetadata, args: {'storeName': storeName, 'key': key, 'value': value}, ); @@ -483,7 +483,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { required Map kvs, }) => _sendCmdOneShot( - type: _WorkerCmdType.setBulkMetadata, + type: _CmdType.setBulkMetadata, args: {'storeName': storeName, 'kvs': kvs}, ); @@ -493,7 +493,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { required String key, }) async => (await _sendCmdOneShot( - type: _WorkerCmdType.removeMetadata, + type: _CmdType.removeMetadata, args: {'storeName': storeName, 'key': key}, ))!['removedValue']; @@ -502,14 +502,14 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { required String storeName, }) => _sendCmdOneShot( - type: _WorkerCmdType.resetMetadata, + type: _CmdType.resetMetadata, args: {'storeName': storeName}, ); @override Future> listRecoverableRegions() async => (await _sendCmdOneShot( - type: _WorkerCmdType.listRecoverableRegions, + type: _CmdType.listRecoverableRegions, ))!['recoverableRegions']; @override @@ -517,7 +517,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { required int id, }) async => (await _sendCmdOneShot( - type: _WorkerCmdType.getRecoverableRegion, + type: _CmdType.getRecoverableRegion, ))!['recoverableRegion']; @override @@ -527,7 +527,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { required DownloadableRegion region, }) => _sendCmdOneShot( - type: _WorkerCmdType.startRecovery, + type: _CmdType.startRecovery, args: {'id': id, 'storeName': storeName, 'region': region}, ); @@ -535,14 +535,14 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { Future cancelRecovery({ required int id, }) => - _sendCmdOneShot(type: _WorkerCmdType.cancelRecovery, args: {'id': id}); + _sendCmdOneShot(type: _CmdType.cancelRecovery, args: {'id': id}); @override Stream watchRecovery({ required bool triggerImmediately, }) => _sendCmdStreamed( - type: _WorkerCmdType.watchRecovery, + type: _CmdType.watchRecovery, args: {'triggerImmediately': triggerImmediately}, ); @@ -552,7 +552,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { required bool triggerImmediately, }) => _sendCmdStreamed( - type: _WorkerCmdType.watchStores, + type: _CmdType.watchStores, args: { 'storeNames': storeNames, 'triggerImmediately': triggerImmediately, @@ -574,7 +574,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { } await _sendCmdOneShot( - type: _WorkerCmdType.exportStores, + type: _CmdType.exportStores, args: {'storeNames': storeNames, 'outputPath': path}, ); } @@ -588,7 +588,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { Stream?> checkTypeAndStartImport() async* { await _checkImportPathType(path); yield* _sendCmdStreamed( - type: _WorkerCmdType.importStores, + type: _CmdType.importStores, args: {'path': path, 'strategy': strategy, 'stores': storeNames}, ); } @@ -623,7 +623,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { await _checkImportPathType(path); return (await _sendCmdOneShot( - type: _WorkerCmdType.listImportableStores, + type: _CmdType.listImportableStores, args: {'path': path}, ))!['stores']; } diff --git a/lib/src/backend/impls/objectbox/backend/internal_workers/shared.dart b/lib/src/backend/impls/objectbox/backend/internal_workers/shared.dart new file mode 100644 index 00000000..926d7484 --- /dev/null +++ b/lib/src/backend/impls/objectbox/backend/internal_workers/shared.dart @@ -0,0 +1,76 @@ +// Copyright © Luka S (JaffaKetchup) under GPL-v3 +// A full license can be found at .\LICENSE + +part of '../backend.dart'; + +void _sharedWriteSingleTile({ + required Store root, + required String storeName, + required String url, + required Uint8List bytes, +}) { + final tiles = root.box(); + final stores = root.box(); + final rootBox = root.box(); + + final tilesQuery = tiles.query(ObjectBoxTile_.url.equals(url)).build(); + final storeQuery = + stores.query(ObjectBoxStore_.name.equals(storeName)).build(); + + final storesToUpdate = {}; + // If tile exists in this store, just update size, otherwise + // length and size + // Also update size of all related stores + bool didContainAlready = false; + + root.runInTransaction( + TxMode.write, + () { + final existingTile = tilesQuery.findUnique(); + final store = storeQuery.findUnique() ?? + (throw StoreNotExists(storeName: storeName)); + + if (existingTile != null) { + for (final relatedStore in existingTile.stores) { + if (relatedStore.name == storeName) didContainAlready = true; + + storesToUpdate[relatedStore.name] = + (storesToUpdate[relatedStore.name] ?? relatedStore) + ..size += + -existingTile.bytes.lengthInBytes + bytes.lengthInBytes; + } + + rootBox.put( + rootBox.get(1)! + ..size += -existingTile.bytes.lengthInBytes + bytes.lengthInBytes, + mode: PutMode.update, + ); + } else { + rootBox.put( + rootBox.get(1)! + ..length += 1 + ..size += bytes.lengthInBytes, + mode: PutMode.update, + ); + } + + if (!didContainAlready || existingTile == null) { + storesToUpdate[storeName] = store + ..length += 1 + ..size += bytes.lengthInBytes; + } + + tiles.put( + ObjectBoxTile( + url: url, + lastModified: DateTime.timestamp(), + bytes: bytes, + )..stores.addAll({store, ...?existingTile?.stores}), + ); + stores.putMany(storesToUpdate.values.toList(), mode: PutMode.update); + }, + ); + + tilesQuery.close(); + storeQuery.close(); +} diff --git a/lib/src/backend/impls/objectbox/backend/internal_workers/standard/cmd_type.dart b/lib/src/backend/impls/objectbox/backend/internal_workers/standard/cmd_type.dart new file mode 100644 index 00000000..f9bed693 --- /dev/null +++ b/lib/src/backend/impls/objectbox/backend/internal_workers/standard/cmd_type.dart @@ -0,0 +1,57 @@ +// Copyright © Luka S (JaffaKetchup) under GPL-v3 +// A full license can be found at .\LICENSE + +part of '../../backend.dart'; + +enum _CmdType { + initialise_, // Only valid as a request + destroy, + realSize, + rootSize, + rootLength, + listStores, + storeExists, + createStore, + resetStore, + renameStore, + deleteStore, + getStoreStats, + tileExistsInStore, + readTile, + readLatestTile, + writeTile, + deleteTile, + registerHitOrMiss, + removeOldestTilesAboveLimit, + removeTilesOlderThan, + readMetadata, + setMetadata, + setBulkMetadata, + removeMetadata, + resetMetadata, + listRecoverableRegions, + getRecoverableRegion, + startRecovery, + cancelRecovery, + watchRecovery(hasInternalStreamSub: true), + watchStores(hasInternalStreamSub: true), + exportStores, + importStores(hasInternalStreamSub: false), + listImportableStores, + cancelInternalStreamSub; + + const _CmdType({this.hasInternalStreamSub}); + + /// Whether this command streams multiple results back + /// + /// If `true`, then this command does stream results, and it has an internal + /// [StreamSubscription] that should be cancelled (using + /// [cancelInternalStreamSub]) when it no longer needs to stream results. + /// + /// If `false`, then this command does stream results, but has no stream sub + /// to be cancelled. + /// + /// If `null`, then this command does not stream results, and just returns a + /// single result. + final bool? hasInternalStreamSub; +} diff --git a/lib/src/backend/impls/objectbox/backend/internal_workers/standard/incoming_cmd.dart b/lib/src/backend/impls/objectbox/backend/internal_workers/standard/incoming_cmd.dart new file mode 100644 index 00000000..38dd2db8 --- /dev/null +++ b/lib/src/backend/impls/objectbox/backend/internal_workers/standard/incoming_cmd.dart @@ -0,0 +1,6 @@ +// Copyright © Luka S (JaffaKetchup) under GPL-v3 +// A full license can be found at .\LICENSE + +part of '../../backend.dart'; + +typedef _IncomingCmd = ({int id, _CmdType type, Map args}); diff --git a/lib/src/backend/impls/objectbox/backend/internal_worker.dart b/lib/src/backend/impls/objectbox/backend/internal_workers/standard/worker.dart similarity index 87% rename from lib/src/backend/impls/objectbox/backend/internal_worker.dart rename to lib/src/backend/impls/objectbox/backend/internal_workers/standard/worker.dart index 272427ad..4eccf47b 100644 --- a/lib/src/backend/impls/objectbox/backend/internal_worker.dart +++ b/lib/src/backend/impls/objectbox/backend/internal_workers/standard/worker.dart @@ -1,66 +1,7 @@ // Copyright © Luka S (JaffaKetchup) under GPL-v3 // A full license can be found at .\LICENSE -part of 'backend.dart'; - -typedef _IncomingCmd = ({ - int id, - _WorkerCmdType type, - Map args -}); - -enum _WorkerCmdType { - initialise_, // Only valid as a request - destroy, - realSize, - rootSize, - rootLength, - listStores, - storeExists, - createStore, - resetStore, - renameStore, - deleteStore, - getStoreStats, - tileExistsInStore, - readTile, - readLatestTile, - writeTile, - deleteTile, - registerHitOrMiss, - removeOldestTilesAboveLimit, - removeTilesOlderThan, - readMetadata, - setMetadata, - setBulkMetadata, - removeMetadata, - resetMetadata, - listRecoverableRegions, - getRecoverableRegion, - startRecovery, - cancelRecovery, - watchRecovery(hasInternalStreamSub: true), - watchStores(hasInternalStreamSub: true), - exportStores, - importStores(hasInternalStreamSub: false), - listImportableStores, - cancelInternalStreamSub; - - const _WorkerCmdType({this.hasInternalStreamSub}); - - /// Whether this command streams multiple results back - /// - /// If `true`, then this command does stream results, and it has an internal - /// [StreamSubscription] that should be cancelled (using - /// [cancelInternalStreamSub]) when it no longer needs to stream results. - /// - /// If `false`, then this command does stream results, but has no stream sub - /// to be cancelled. - /// - /// If `null`, then this command does not stream results, and just returns a - /// single result. - final bool? hasInternalStreamSub; -} +part of '../../backend.dart'; Future _worker( ({ @@ -232,9 +173,9 @@ Future _worker( void mainHandler(_IncomingCmd cmd) { switch (cmd.type) { - case _WorkerCmdType.initialise_: + case _CmdType.initialise_: throw UnsupportedError('Invalid operation'); - case _WorkerCmdType.destroy: + case _CmdType.destroy: root.close(); if (cmd.args['deleteRoot'] == true) { @@ -248,7 +189,7 @@ Future _worker( sendRes(id: cmd.id); Isolate.exit(); - case _WorkerCmdType.realSize: + case _CmdType.realSize: sendRes( id: cmd.id, data: { @@ -256,17 +197,17 @@ Future _worker( Store.dbFileSize(input.rootDirectory) / 1024, // Convert to KiB }, ); - case _WorkerCmdType.rootSize: + case _CmdType.rootSize: sendRes( id: cmd.id, data: {'size': root.box().get(1)!.size / 1024}, ); - case _WorkerCmdType.rootLength: + case _CmdType.rootLength: sendRes( id: cmd.id, data: {'length': root.box().get(1)!.length}, ); - case _WorkerCmdType.listStores: + case _CmdType.listStores: final query = root .box() .query() @@ -276,7 +217,7 @@ Future _worker( sendRes(id: cmd.id, data: {'stores': query.find()}); query.close(); - case _WorkerCmdType.storeExists: + case _CmdType.storeExists: final query = root .box() .query( @@ -287,7 +228,7 @@ Future _worker( sendRes(id: cmd.id, data: {'exists': query.count() == 1}); query.close(); - case _WorkerCmdType.getStoreStats: + case _CmdType.getStoreStats: final storeName = cmd.args['storeName']! as String; final query = root @@ -311,7 +252,7 @@ Future _worker( ); query.close(); - case _WorkerCmdType.createStore: + case _CmdType.createStore: final storeName = cmd.args['storeName']! as String; try { @@ -332,7 +273,7 @@ Future _worker( } sendRes(id: cmd.id); - case _WorkerCmdType.resetStore: + case _CmdType.resetStore: final storeName = cmd.args['storeName']! as String; final tiles = root.box(); @@ -369,7 +310,7 @@ Future _worker( ); sendRes(id: cmd.id); - case _WorkerCmdType.renameStore: + case _CmdType.renameStore: final currentStoreName = cmd.args['currentStoreName']! as String; final newStoreName = cmd.args['newStoreName']! as String; @@ -390,7 +331,7 @@ Future _worker( ); sendRes(id: cmd.id); - case _WorkerCmdType.deleteStore: + case _CmdType.deleteStore: final storeName = cmd.args['storeName']! as String; final storesQuery = root @@ -416,7 +357,7 @@ Future _worker( storesQuery.close(); tilesQuery.close(); - case _WorkerCmdType.tileExistsInStore: + case _CmdType.tileExistsInStore: final storeName = cmd.args['storeName']! as String; final url = cmd.args['url']! as String; @@ -431,7 +372,7 @@ Future _worker( sendRes(id: cmd.id, data: {'exists': query.count() == 1}); query.close(); - case _WorkerCmdType.readTile: + case _CmdType.readTile: final url = cmd.args['url']! as String; final storeName = cmd.args['storeName'] as String?; @@ -450,7 +391,7 @@ Future _worker( sendRes(id: cmd.id, data: {'tile': query.findUnique()}); query.close(); - case _WorkerCmdType.readLatestTile: + case _CmdType.readLatestTile: final storeName = cmd.args['storeName']! as String; final query = (root @@ -466,106 +407,20 @@ Future _worker( sendRes(id: cmd.id, data: {'tile': query.findFirst()}); query.close(); - case _WorkerCmdType.writeTile: + case _CmdType.writeTile: final storeName = cmd.args['storeName']! as String; final url = cmd.args['url']! as String; + final bytes = cmd.args['bytes']! as Uint8List; - // TODO: `null` `bytes` is never actually used. Do we need to keep it? - final bytes = cmd.args['bytes'] as Uint8List?; - - final tiles = root.box(); - final stores = root.box(); - - final tilesQuery = tiles.query(ObjectBoxTile_.url.equals(url)).build(); - final storeQuery = - stores.query(ObjectBoxStore_.name.equals(storeName)).build(); - - root.runInTransaction( - TxMode.write, - () { - final existingTile = tilesQuery.findUnique(); - - final store = storeQuery.findUnique() ?? - (throw StoreNotExists(storeName: storeName)); - - switch ((existingTile == null, bytes == null)) { - case (true, false): // No existing tile - tiles.put( - ObjectBoxTile( - url: url, - lastModified: DateTime.timestamp(), - bytes: bytes!, - )..stores.add(store), - ); - stores.put( - store - ..length += 1 - ..size += bytes.lengthInBytes, - ); - updateRootStatistics( - deltaLength: 1, - deltaSize: bytes.lengthInBytes, - ); - case (false, true): // Existing tile, no update - // Only take action if it's not already belonging to the store - if (!existingTile!.stores.contains(store)) { - tiles.put(existingTile..stores.add(store)); - stores.put( - store - ..length += 1 - ..size += existingTile.bytes.lengthInBytes, - ); - } - case (false, false): // Existing tile, update required - final storesToUpdate = {}; - - // If tile exists in this store, just update size, otherwise - // length and size - // Also update size of all related stores - bool didContainAlready = false; - - for (final relatedStore in existingTile!.stores) { - if (relatedStore.name == storeName) didContainAlready = true; - - storesToUpdate[relatedStore.name] = - (storesToUpdate[relatedStore.name] ?? relatedStore) - ..size += -existingTile.bytes.lengthInBytes + - bytes!.lengthInBytes; - } - - updateRootStatistics( - deltaSize: - -existingTile.bytes.lengthInBytes + bytes!.lengthInBytes, - ); - - if (!didContainAlready) { - storesToUpdate[storeName] = store - ..length += 1 - ..size += bytes.lengthInBytes; - } - - tiles.put( - ObjectBoxTile( - url: url, - lastModified: DateTime.timestamp(), - bytes: bytes, - )..stores.addAll({store, ...existingTile.stores}), - ); - stores.putMany( - storesToUpdate.values.toList(), - mode: PutMode.update, - ); - case (true, true): // FMTC internal error - throw UnsupportedError('Unpossible.'); - } - }, + _sharedWriteSingleTile( + root: root, + storeName: storeName, + url: url, + bytes: bytes, ); sendRes(id: cmd.id); - - storeQuery.close(); - tilesQuery.close(); - case _WorkerCmdType.deleteTile: + case _CmdType.deleteTile: final storeName = cmd.args['storeName']! as String; final url = cmd.args['url']! as String; @@ -589,7 +444,7 @@ Future _worker( storesQuery.close(); tilesQuery.close(); - case _WorkerCmdType.registerHitOrMiss: + case _CmdType.registerHitOrMiss: final storeName = cmd.args['storeName']! as String; final hit = cmd.args['hit']! as bool; @@ -614,7 +469,7 @@ Future _worker( ); sendRes(id: cmd.id); - case _WorkerCmdType.removeOldestTilesAboveLimit: + case _CmdType.removeOldestTilesAboveLimit: final storeName = cmd.args['storeName']! as String; final tilesLimit = cmd.args['tilesLimit']! as int; @@ -654,7 +509,7 @@ Future _worker( storeQuery.close(); tilesQuery.close(); - case _WorkerCmdType.removeTilesOlderThan: + case _CmdType.removeTilesOlderThan: final storeName = cmd.args['storeName']! as String; final expiry = cmd.args['expiry']! as DateTime; @@ -680,7 +535,7 @@ Future _worker( ); tilesQuery.close(); - case _WorkerCmdType.readMetadata: + case _CmdType.readMetadata: final storeName = cmd.args['storeName']! as String; final query = root @@ -700,7 +555,7 @@ Future _worker( ); query.close(); - case _WorkerCmdType.setMetadata: + case _CmdType.setMetadata: final storeName = cmd.args['storeName']! as String; final key = cmd.args['key']! as String; final value = cmd.args['value']! as String; @@ -729,7 +584,7 @@ Future _worker( ); sendRes(id: cmd.id); - case _WorkerCmdType.setBulkMetadata: + case _CmdType.setBulkMetadata: final storeName = cmd.args['storeName']! as String; final kvs = cmd.args['kvs']! as Map; @@ -757,7 +612,7 @@ Future _worker( ); sendRes(id: cmd.id); - case _WorkerCmdType.removeMetadata: + case _CmdType.removeMetadata: final storeName = cmd.args['storeName']! as String; final key = cmd.args['key']! as String; @@ -790,7 +645,7 @@ Future _worker( ), }, ); - case _WorkerCmdType.resetMetadata: + case _CmdType.resetMetadata: final storeName = cmd.args['storeName']! as String; final stores = root.box(); @@ -813,7 +668,7 @@ Future _worker( ); sendRes(id: cmd.id); - case _WorkerCmdType.listRecoverableRegions: + case _CmdType.listRecoverableRegions: sendRes( id: cmd.id, data: { @@ -824,7 +679,7 @@ Future _worker( .toList(growable: false), }, ); - case _WorkerCmdType.getRecoverableRegion: + case _CmdType.getRecoverableRegion: final id = cmd.args['id']! as int; sendRes( @@ -839,7 +694,7 @@ Future _worker( ?.toRegion(), }, ); - case _WorkerCmdType.startRecovery: + case _CmdType.startRecovery: final id = cmd.args['id']! as int; final storeName = cmd.args['storeName']! as String; final region = cmd.args['region']! as DownloadableRegion; @@ -853,7 +708,7 @@ Future _worker( ); sendRes(id: cmd.id); - case _WorkerCmdType.cancelRecovery: + case _CmdType.cancelRecovery: final id = cmd.args['id']! as int; root @@ -864,7 +719,7 @@ Future _worker( ..close(); sendRes(id: cmd.id); - case _WorkerCmdType.watchRecovery: + case _CmdType.watchRecovery: final triggerImmediately = cmd.args['triggerImmediately']! as bool; streamedOutputSubscriptions[cmd.id] = root @@ -872,7 +727,7 @@ Future _worker( .query() .watch(triggerImmediately: triggerImmediately) .listen((_) => sendRes(id: cmd.id, data: {'expectStream': true})); - case _WorkerCmdType.watchStores: + case _CmdType.watchStores: final storeNames = cmd.args['storeNames']! as List; final triggerImmediately = cmd.args['triggerImmediately']! as bool; @@ -885,7 +740,7 @@ Future _worker( ) .watch(triggerImmediately: triggerImmediately) .listen((_) => sendRes(id: cmd.id, data: {'expectStream': true})); - case _WorkerCmdType.cancelInternalStreamSub: + case _CmdType.cancelInternalStreamSub: final id = cmd.args['id']! as int; if (streamedOutputSubscriptions[id] == null) { @@ -898,7 +753,7 @@ Future _worker( streamedOutputSubscriptions.remove(id); sendRes(id: cmd.id); - case _WorkerCmdType.exportStores: + case _CmdType.exportStores: final storeNames = cmd.args['storeNames']! as List; final outputPath = cmd.args['outputPath']! as String; @@ -1024,7 +879,7 @@ Future _worker( ); }, ); - case _WorkerCmdType.importStores: + case _CmdType.importStores: final importPath = cmd.args['path']! as String; final strategy = cmd.args['strategy'] as ImportConflictStrategy; final storesToImport = cmd.args['stores'] as List?; @@ -1434,7 +1289,7 @@ Future _worker( }); }, ); - case _WorkerCmdType.listImportableStores: + case _CmdType.listImportableStores: final importPath = cmd.args['path']! as String; final importDir = path.join(input.rootDirectory, 'import_tmp'); diff --git a/lib/src/backend/impls/objectbox/backend/internal_thread_safe.dart b/lib/src/backend/impls/objectbox/backend/internal_workers/thread_safe.dart similarity index 54% rename from lib/src/backend/impls/objectbox/backend/internal_thread_safe.dart rename to lib/src/backend/impls/objectbox/backend/internal_workers/thread_safe.dart index 6fdf9a9b..99e09f3e 100644 --- a/lib/src/backend/impls/objectbox/backend/internal_thread_safe.dart +++ b/lib/src/backend/impls/objectbox/backend/internal_workers/thread_safe.dart @@ -1,7 +1,7 @@ // Copyright © Luka S (JaffaKetchup) under GPL-v3 // A full license can be found at .\LICENSE -part of 'backend.dart'; +part of '../backend.dart'; class _ObjectBoxBackendThreadSafeImpl implements FMTCBackendInternalThreadSafe { _ObjectBoxBackendThreadSafeImpl._({ @@ -11,22 +11,21 @@ class _ObjectBoxBackendThreadSafeImpl implements FMTCBackendInternalThreadSafe { @override String get friendlyIdentifier => 'ObjectBox'; - void get expectInitialised => root ?? (throw RootUnavailable()); - final ByteData storeReference; - Store? root; + Store get expectInitialisedRoot => _root ?? (throw RootUnavailable()); + Store? _root; @override void initialise() { - if (root != null) throw RootAlreadyInitialised(); - root = Store.fromReference(getObjectBoxModel(), storeReference); + if (_root != null) throw RootAlreadyInitialised(); + _root = Store.fromReference(getObjectBoxModel(), storeReference); } @override void uninitialise() { - if (root == null) throw RootUnavailable(); - root!.close(); - root = null; + expectInitialisedRoot; + _root!.close(); + _root = null; } @override @@ -34,9 +33,7 @@ class _ObjectBoxBackendThreadSafeImpl implements FMTCBackendInternalThreadSafe { required String url, String? storeName, }) async { - expectInitialised; - - final stores = root!.box(); + final stores = expectInitialisedRoot.box(); final query = storeName == null ? stores.query(ObjectBoxTile_.url.equals(url)).build() @@ -55,91 +52,29 @@ class _ObjectBoxBackendThreadSafeImpl implements FMTCBackendInternalThreadSafe { } @override - void htWriteTile({ + void writeTile({ required String storeName, required String url, required Uint8List bytes, - }) { - expectInitialised; - - final tiles = root!.box(); - final stores = root!.box(); - final rootBox = root!.box(); - - final tilesQuery = tiles.query(ObjectBoxTile_.url.equals(url)).build(); - final storeQuery = - stores.query(ObjectBoxStore_.name.equals(storeName)).build(); - - final storesToUpdate = {}; - - root!.runInTransaction( - TxMode.write, - () { - final existingTile = tilesQuery.findUnique(); - final store = storeQuery.findUnique() ?? - (throw StoreNotExists(storeName: storeName)); - - // If tile exists in this store, just update size, otherwise - // length and size - // Also update size of all related stores - bool didContainAlready = false; - - if (existingTile != null) { - for (final relatedStore in existingTile.stores) { - if (relatedStore.name == storeName) didContainAlready = true; - - storesToUpdate[relatedStore.name] = - (storesToUpdate[relatedStore.name] ?? relatedStore) - ..size += - -existingTile.bytes.lengthInBytes + bytes.lengthInBytes; - } - - rootBox.put( - rootBox.get(1)! - ..size += -existingTile.bytes.lengthInBytes + bytes.lengthInBytes, - mode: PutMode.update, - ); - } else { - rootBox.put( - rootBox.get(1)! - ..length += 1 - ..size += bytes.lengthInBytes, - mode: PutMode.update, - ); - } - - if (!didContainAlready || existingTile == null) { - storesToUpdate[storeName] = store - ..length += 1 - ..size += bytes.lengthInBytes; - } - - tiles.put( - ObjectBoxTile( - url: url, - lastModified: DateTime.timestamp(), - bytes: bytes, - )..stores.addAll({store, ...?existingTile?.stores}), - ); - stores.putMany(storesToUpdate.values.toList(), mode: PutMode.update); - }, - ); - - tilesQuery.close(); - storeQuery.close(); - } + }) => + _sharedWriteSingleTile( + root: expectInitialisedRoot, + storeName: storeName, + url: url, + bytes: bytes, + ); @override - void htWriteTiles({ + void writeTiles({ required String storeName, required List urls, required List bytess, }) { - expectInitialised; + expectInitialisedRoot; - final tiles = root!.box(); - final stores = root!.box(); - final rootBox = root!.box(); + final tiles = _root!.box(); + final stores = _root!.box(); + final rootBox = _root!.box(); final tilesQuery = tiles.query(ObjectBoxTile_.url.equals('')).build(); final storeQuery = @@ -147,7 +82,7 @@ class _ObjectBoxBackendThreadSafeImpl implements FMTCBackendInternalThreadSafe { final storesToUpdate = {}; - root!.runInTransaction( + _root!.runInTransaction( TxMode.write, () { final rootData = rootBox.get(1)!; diff --git a/lib/src/backend/interfaces/backend/internal.dart b/lib/src/backend/interfaces/backend/internal.dart index 55400632..381bee64 100644 --- a/lib/src/backend/interfaces/backend/internal.dart +++ b/lib/src/backend/interfaces/backend/internal.dart @@ -148,17 +148,10 @@ abstract interface class FMTCBackendInternal /// Create or update a tile (given a [url] and its [bytes]) in the specified /// store - /// - /// If the tile already existed, it will be added to the specified store. - /// Otherwise, [bytes] must be specified, and the tile will be created and - /// added. - /// - /// If [bytes] is provided and the tile already existed, it will be updated for - /// all stores. Future writeTile({ required String storeName, required String url, - required Uint8List? bytes, + required Uint8List bytes, }); /// Remove the tile from the specified store, deleting it if was orphaned diff --git a/lib/src/backend/interfaces/backend/internal_thread_safe.dart b/lib/src/backend/interfaces/backend/internal_thread_safe.dart index 121759dc..61486b30 100644 --- a/lib/src/backend/interfaces/backend/internal_thread_safe.dart +++ b/lib/src/backend/interfaces/backend/internal_thread_safe.dart @@ -46,10 +46,8 @@ abstract interface class FMTCBackendInternalThreadSafe { /// Create or update a tile (given a [url] and its [bytes]) in the specified /// store /// - /// Logic is simpler than the respective [FMTCBackendInternal.writeTile] - /// method, and designed for high throughput: existing tiles will always be - /// overwritten (if they exist). - FutureOr htWriteTile({ + /// May share logic with [FMTCBackendInternal.writeTile]. + FutureOr writeTile({ required String storeName, required String url, required Uint8List bytes, @@ -58,9 +56,9 @@ abstract interface class FMTCBackendInternalThreadSafe { /// Create or update multiple tiles (given given their respective [urls] and /// [bytess]) in the specified store /// - /// Designed for high throughput: existing tiles will always be overwritten - /// (if they exist). - FutureOr htWriteTiles({ + /// Implementation should avoid iterating [writeTile], as this should be + /// targeted for high throughput and efficiency. + FutureOr writeTiles({ required String storeName, required List urls, required List bytess, diff --git a/lib/src/bulk_download/thread.dart b/lib/src/bulk_download/thread.dart index 6f512a7e..0454eba9 100644 --- a/lib/src/bulk_download/thread.dart +++ b/lib/src/bulk_download/thread.dart @@ -46,7 +46,7 @@ Future _singleDownloadThread( httpClient.close(); if (tileUrlsBuffer.isNotEmpty) { - await input.backend.htWriteTiles( + await input.backend.writeTiles( storeName: input.storeName, urls: tileUrlsBuffer, bytess: tileBytesBuffer, @@ -135,7 +135,7 @@ Future _singleDownloadThread( // Write tile directly to database or place in buffer queue if (input.maxBufferLength == 0) { - await input.backend.htWriteTile( + await input.backend.writeTile( storeName: input.storeName, url: matcherUrl, bytes: response.bodyBytes, @@ -148,7 +148,7 @@ Future _singleDownloadThread( // Write buffer to database if necessary final wasBufferReset = tileUrlsBuffer.length >= input.maxBufferLength; if (wasBufferReset) { - await input.backend.htWriteTiles( + await input.backend.writeTiles( storeName: input.storeName, urls: tileUrlsBuffer, bytess: tileBytesBuffer, diff --git a/test/general_test.dart b/test/general_test.dart index 1788770c..fd0bc64f 100644 --- a/test/general_test.dart +++ b/test/general_test.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'dart:typed_data'; +import 'package:flutter/widgets.dart'; import 'package:flutter_map_tile_caching/custom_backend_api.dart'; import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; import 'package:path/path.dart' as p; @@ -38,7 +39,7 @@ void main() { ); test( - '"store1" creation', + 'Create "store1"', () async { await const FMTCStore('store1').manage.create(); @@ -66,7 +67,7 @@ void main() { ); test( - '"store2" creation', + 'Create "store2"', () async { await const FMTCStore('store2').manage.create(); @@ -80,7 +81,7 @@ void main() { ); test( - '"store2" deletion', + 'Delete "store2"', () async { await const FMTCStore('store2').manage.delete(); @@ -129,7 +130,7 @@ void main() { ); test( - '"store1" reset', + 'Reset "store1"', () async { await const FMTCStore('store1').manage.reset(); @@ -143,7 +144,7 @@ void main() { ); test( - '"store1" rename to "store3"', + 'Rename "store1" to "store3"', () async { await const FMTCStore('store1').manage.rename('store3'); @@ -275,10 +276,14 @@ void main() { (url: 'https://example.com/0/0/0.png', bytes: Uint8List(64)); final tileA128 = (url: 'https://example.com/0/0/0.png', bytes: Uint8List(128)); - final tileB64 = - (url: 'https://example.com/1/1/1.png', bytes: Uint8List(64)); - final tileB128 = - (url: 'https://example.com/1/1/1.png', bytes: Uint8List(128)); + final tileB64 = ( + url: 'https://example.com/1/1/1.png', + bytes: Uint8List.fromList(List.filled(64, 1)), + ); + final tileB128 = ( + url: 'https://example.com/1/1/1.png', + bytes: Uint8List.fromList(List.filled(128, 1)), + ); test( 'Initially semi-zero/empty', @@ -291,6 +296,8 @@ void main() { await const FMTCStore('store2').stats.all, (length: 0, size: 0, hits: 0, misses: 0), ); + expect(await const FMTCStore('store1').stats.tileImage(), null); + expect(await const FMTCStore('store2').stats.tileImage(), null); expect(await FMTCRoot.stats.length, 0); expect(await FMTCRoot.stats.size, 0); expect( @@ -314,6 +321,12 @@ void main() { ); expect(await FMTCRoot.stats.length, 1); expect(await FMTCRoot.stats.size, 0.0625); + expect( + ((await const FMTCStore('store1').stats.tileImage())?.image + as MemoryImage?) + ?.bytes, + tileA64.bytes, + ); }, ); @@ -331,6 +344,12 @@ void main() { ); expect(await FMTCRoot.stats.length, 1); expect(await FMTCRoot.stats.size, 0.0625); + expect( + ((await const FMTCStore('store1').stats.tileImage())?.image + as MemoryImage?) + ?.bytes, + tileA64.bytes, + ); }, ); @@ -348,6 +367,12 @@ void main() { ); expect(await FMTCRoot.stats.length, 1); expect(await FMTCRoot.stats.size, 0.125); + expect( + ((await const FMTCStore('store1').stats.tileImage())?.image + as MemoryImage?) + ?.bytes, + tileA128.bytes, + ); }, ); @@ -365,6 +390,12 @@ void main() { ); expect(await FMTCRoot.stats.length, 2); expect(await FMTCRoot.stats.size, 0.1875); + expect( + ((await const FMTCStore('store1').stats.tileImage())?.image + as MemoryImage?) + ?.bytes, + tileB64.bytes, + ); }, ); @@ -382,6 +413,12 @@ void main() { ); expect(await FMTCRoot.stats.length, 2); expect(await FMTCRoot.stats.size, 0.25); + expect( + ((await const FMTCStore('store1').stats.tileImage())?.image + as MemoryImage?) + ?.bytes, + tileB128.bytes, + ); }, ); @@ -399,6 +436,12 @@ void main() { ); expect(await FMTCRoot.stats.length, 2); expect(await FMTCRoot.stats.size, 0.1875); + expect( + ((await const FMTCStore('store1').stats.tileImage())?.image + as MemoryImage?) + ?.bytes, + tileB64.bytes, + ); }, ); @@ -415,16 +458,55 @@ void main() { ); expect(await FMTCRoot.stats.length, 1); expect(await FMTCRoot.stats.size, 0.125); + expect( + ((await const FMTCStore('store1').stats.tileImage())?.image + as MemoryImage?) + ?.bytes, + tileA128.bytes, + ); + }, + ); + + test( + 'Write tile (A64) to "store2"', + () async { + await FMTCBackendAccess.internal.writeTile( + storeName: 'store2', + url: tileA64.url, + bytes: tileA64.bytes, + ); + expect( + await const FMTCStore('store1').stats.all, + (length: 1, size: 0.0625, hits: 0, misses: 0), + ); + expect( + await const FMTCStore('store2').stats.all, + (length: 1, size: 0.0625, hits: 0, misses: 0), + ); + expect(await FMTCRoot.stats.length, 1); + expect(await FMTCRoot.stats.size, 0.0625); + expect( + ((await const FMTCStore('store1').stats.tileImage())?.image + as MemoryImage?) + ?.bytes, + tileA64.bytes, + ); + expect( + ((await const FMTCStore('store2').stats.tileImage())?.image + as MemoryImage?) + ?.bytes, + tileA64.bytes, + ); }, ); test( - 'Semi-write tile (A(128)) to "store2"', + 'Write tile (A128) to "store2"', () async { await FMTCBackendAccess.internal.writeTile( storeName: 'store2', url: tileA128.url, - bytes: null, + bytes: tileA128.bytes, ); expect( await const FMTCStore('store1').stats.all, @@ -436,11 +518,23 @@ void main() { ); expect(await FMTCRoot.stats.length, 1); expect(await FMTCRoot.stats.size, 0.125); + expect( + ((await const FMTCStore('store1').stats.tileImage())?.image + as MemoryImage?) + ?.bytes, + tileA128.bytes, + ); + expect( + ((await const FMTCStore('store2').stats.tileImage())?.image + as MemoryImage?) + ?.bytes, + tileA128.bytes, + ); }, ); test( - 'Semi-delete tile (A(128)) from "store2"', + 'Delete tile (A(128)) from "store2"', () async { await FMTCBackendAccess.internal.deleteTile( storeName: 'store2', @@ -456,11 +550,105 @@ void main() { ); expect(await FMTCRoot.stats.length, 1); expect(await FMTCRoot.stats.size, 0.125); + expect( + ((await const FMTCStore('store1').stats.tileImage())?.image + as MemoryImage?) + ?.bytes, + tileA128.bytes, + ); + expect(await const FMTCStore('store2').stats.tileImage(), null); + }, + ); + + test( + 'Write tile (B64) to "store2"', + () async { + await FMTCBackendAccess.internal.writeTile( + storeName: 'store2', + url: tileB64.url, + bytes: tileB64.bytes, + ); + expect( + await const FMTCStore('store1').stats.all, + (length: 1, size: 0.125, hits: 0, misses: 0), + ); + expect( + await const FMTCStore('store2').stats.all, + (length: 1, size: 0.0625, hits: 0, misses: 0), + ); + expect(await FMTCRoot.stats.length, 2); + expect(await FMTCRoot.stats.size, 0.1875); + expect( + ((await const FMTCStore('store1').stats.tileImage())?.image + as MemoryImage?) + ?.bytes, + tileA128.bytes, + ); + expect( + ((await const FMTCStore('store2').stats.tileImage())?.image + as MemoryImage?) + ?.bytes, + tileB64.bytes, + ); + }, + ); + + test( + 'Write tile (A64) to "store2"', + () async { + await FMTCBackendAccess.internal.writeTile( + storeName: 'store2', + url: tileA64.url, + bytes: tileA64.bytes, + ); + expect( + await const FMTCStore('store1').stats.all, + (length: 1, size: 0.0625, hits: 0, misses: 0), + ); + expect( + await const FMTCStore('store2').stats.all, + (length: 2, size: 0.125, hits: 0, misses: 0), + ); + expect(await FMTCRoot.stats.length, 2); + expect(await FMTCRoot.stats.size, 0.125); + expect( + ((await const FMTCStore('store1').stats.tileImage())?.image + as MemoryImage?) + ?.bytes, + tileA64.bytes, + ); + expect( + ((await const FMTCStore('store2').stats.tileImage())?.image + as MemoryImage?) + ?.bytes, + tileA64.bytes, + ); + }, + ); + + test( + 'Reset "store2"', + () async { + await const FMTCStore('store2').manage.reset(); + expect( + await const FMTCStore('store1').stats.all, + (length: 1, size: 0.0625, hits: 0, misses: 0), + ); + expect( + await const FMTCStore('store2').stats.all, + (length: 0, size: 0, hits: 0, misses: 0), + ); + expect(await FMTCRoot.stats.length, 1); + expect(await FMTCRoot.stats.size, 0.0625); + expect( + ((await const FMTCStore('store1').stats.tileImage())?.image + as MemoryImage?) + ?.bytes, + tileA64.bytes, + ); + expect(await const FMTCStore('store2').stats.tileImage(), null); }, ); - // then real semi-write to store2 - // then delete as above - // then other sizes tearDownAll( () => FMTCObjectBoxBackend() From 94b8de7ca3c95eb496ebfd79e4ba3430abe6008c Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sun, 7 Apr 2024 23:35:29 +0100 Subject: [PATCH 161/168] Fixed "User-Agent" "FMTC" identifier injection Added loading monitoring between `ImageProvider` & `TileProvider` to reduce chance of early `HttpClient` closure Minor miscellaneous improvements --- lib/flutter_map_tile_caching.dart | 1 + lib/src/bulk_download/manager.dart | 18 +++-- lib/src/providers/image_provider.dart | 40 ++++++---- lib/src/providers/tile_provider.dart | 108 +++++++++++++++++++------- lib/src/store/store.dart | 10 +-- 5 files changed, 120 insertions(+), 57 deletions(-) diff --git a/lib/flutter_map_tile_caching.dart b/lib/flutter_map_tile_caching.dart index 0aec5424..d1b714bc 100644 --- a/lib/flutter_map_tile_caching.dart +++ b/lib/flutter_map_tile_caching.dart @@ -11,6 +11,7 @@ library flutter_map_tile_caching; import 'dart:async'; +import 'dart:collection'; import 'dart:io'; import 'dart:isolate'; import 'dart:math' as math; diff --git a/lib/src/bulk_download/manager.dart b/lib/src/bulk_download/manager.dart index c27e9a46..f3f92593 100644 --- a/lib/src/bulk_download/manager.dart +++ b/lib/src/bulk_download/manager.dart @@ -18,15 +18,21 @@ Future _downloadManager( FMTCBackendInternalThreadSafe backend, }) input, ) async { - // Precalculate shared inputs for all threads + // Precalculate how large the tile buffers should be for each thread final threadBufferLength = (input.maxBufferLength / input.parallelThreads).floor(); + + // Generate appropriate headers for network requests + final inputHeaders = input.region.options.tileProvider.headers; final headers = { - ...input.region.options.tileProvider.headers, - 'User-Agent': input.region.options.tileProvider.headers['User-Agent'] == - null - ? 'flutter_map_tile_caching for flutter_map (unknown)' - : 'flutter_map_tile_caching for ${input.region.options.tileProvider.headers['User-Agent']}', + ...inputHeaders, + 'User-Agent': inputHeaders['User-Agent'] == null + ? 'flutter_map (unknown)' + : 'flutter_map + FMTC ${inputHeaders['User-Agent']!.replaceRange( + 0, + inputHeaders['User-Agent']!.length.clamp(0, 12), + '', + )}', }; // Count number of tiles diff --git a/lib/src/providers/image_provider.dart b/lib/src/providers/image_provider.dart index 17065e71..fac44747 100644 --- a/lib/src/providers/image_provider.dart +++ b/lib/src/providers/image_provider.dart @@ -15,19 +15,19 @@ import '../../flutter_map_tile_caching.dart'; import '../backend/export_internal.dart'; import '../misc/obscure_query_params.dart'; -/// A specialised [ImageProvider] dedicated to 'flutter_map_tile_caching' +/// A specialised [ImageProvider] that uses FMTC internals to enable browse +/// caching class FMTCImageProvider extends ImageProvider { - /// Create a specialised [ImageProvider] dedicated to 'flutter_map_tile_caching' + /// Create a specialised [ImageProvider] that uses FMTC internals to enable + /// browse caching FMTCImageProvider({ - required this.storeName, required this.provider, required this.options, required this.coords, + required this.startedLoading, + required this.finishedLoadingBytes, }); - /// The name of the store associated with this provider - final String storeName; - /// An instance of the [FMTCTileProvider] in use final FMTCTileProvider provider; @@ -37,6 +37,18 @@ class FMTCImageProvider extends ImageProvider { /// The coordinates of the tile to be fetched final TileCoordinates coords; + /// Function invoked when the image starts loading (not from cache) + /// + /// Used with [finishedLoadingBytes] to safely dispose of the `httpClient` only + /// after all tiles have loaded. + final void Function() startedLoading; + + /// Function invoked when the image completes loading bytes from the network + /// + /// Used with [finishedLoadingBytes] to safely dispose of the `httpClient` only + /// after all tiles have loaded. + final void Function() finishedLoadingBytes; + @override ImageStreamCompleter loadImage( FMTCImageProvider key, @@ -49,7 +61,7 @@ class FMTCImageProvider extends ImageProvider { scale: 1, debugLabel: coords.toString(), informationCollector: () => [ - DiagnosticsProperty('Store name', storeName), + DiagnosticsProperty('Store name', provider.storeName), DiagnosticsProperty('Tile coordinates', coords), DiagnosticsProperty('Current provider', key), ], @@ -64,7 +76,7 @@ class FMTCImageProvider extends ImageProvider { Future finishWithError(FMTCBrowsingError err) async { scheduleMicrotask(() => PaintingBinding.instance.imageCache.evict(key)); unawaited(chunkEvents.close()); - await evict(); + finishedLoadingBytes(); provider.settings.errorHandler?.call(err); throw err; @@ -76,11 +88,11 @@ class FMTCImageProvider extends ImageProvider { }) async { scheduleMicrotask(() => PaintingBinding.instance.imageCache.evict(key)); unawaited(chunkEvents.close()); - await evict(); + finishedLoadingBytes(); unawaited( FMTCBackendAccess.internal - .registerHitOrMiss(storeName: storeName, hit: cacheHit), + .registerHitOrMiss(storeName: provider.storeName, hit: cacheHit), ); return decode(await ImmutableBuffer.fromUint8List(bytes)); } @@ -98,6 +110,8 @@ class FMTCImageProvider extends ImageProvider { return null; } + startedLoading(); + final networkUrl = provider.getTileUrl(coords, options); final matcherUrl = obscureQueryParams( url: networkUrl, @@ -106,7 +120,7 @@ class FMTCImageProvider extends ImageProvider { final existingTile = await FMTCBackendAccess.internal.readTile( url: matcherUrl, - storeName: storeName, + storeName: provider.storeName, ); final needsCreating = existingTile == null; @@ -241,7 +255,7 @@ class FMTCImageProvider extends ImageProvider { // Cache the tile retrieved from the network response unawaited( FMTCBackendAccess.internal.writeTile( - storeName: storeName, + storeName: provider.storeName, url: matcherUrl, bytes: responseBytes, ), @@ -251,7 +265,7 @@ class FMTCImageProvider extends ImageProvider { if (needsCreating && provider.settings.maxStoreLength != 0) { unawaited( FMTCBackendAccess.internal.removeOldestTilesAboveLimit( - storeName: storeName, + storeName: provider.storeName, tilesLimit: provider.settings.maxStoreLength, ), ); diff --git a/lib/src/providers/tile_provider.dart b/lib/src/providers/tile_provider.dart index c3fc0120..c25557fa 100644 --- a/lib/src/providers/tile_provider.dart +++ b/lib/src/providers/tile_provider.dart @@ -3,56 +3,74 @@ part of '../../flutter_map_tile_caching.dart'; -/// FMTC's custom [TileProvider] for use in a [TileLayer] +/// Specialised [TileProvider] that uses a specialised [ImageProvider] to connect +/// to FMTC internals +/// +/// An "FMTC" identifying mark is injected into the "User-Agent" header generated +/// by flutter_map, except if specified in the constructor. For technical +/// details, see [_CustomUserAgentCompatMap]. /// /// Create from the store directory chain, eg. [FMTCStore.getTileProvider]. class FMTCTileProvider extends TileProvider { FMTCTileProvider._( - this._store, { - required FMTCTileProviderSettings? settings, - required Map headers, - required http.Client? httpClient, - }) : settings = settings ?? FMTCTileProviderSettings.instance, + this.storeName, + FMTCTileProviderSettings? settings, + Map? headers, + http.Client? httpClient, + ) : settings = settings ?? FMTCTileProviderSettings.instance, httpClient = httpClient ?? IOClient(HttpClient()..userAgent = null), super( - headers: { - ...headers, - 'User-Agent': headers['User-Agent'] == null - ? 'flutter_map_tile_caching for flutter_map (unknown)' - : 'flutter_map_tile_caching for ${headers['User-Agent']}', - }, + headers: (headers?.containsKey('User-Agent') ?? false) + ? headers + : _CustomUserAgentCompatMap(headers ?? {}), ); - /// The store directory attached to this provider - final FMTCStore _store; + /// The store name of the [FMTCStore] used when generating this provider + final String storeName; /// The tile provider settings to use + /// + /// Defaults to the ambient [FMTCTileProviderSettings.instance]. final FMTCTileProviderSettings settings; /// [http.Client] (such as a [IOClient]) used to make all network requests /// - /// Defaults to a standard [IOClient]/[HttpClient] for HTTP/1.1 servers. + /// Do not close manually. + /// + /// Defaults to a standard [IOClient]/[HttpClient]. final http.Client httpClient; - /// Closes the open [httpClient] - this will make the provider unable to - /// perform network requests - @override - void dispose() { - httpClient.close(); - super.dispose(); - } + /// Each [Completer] is completed once the corresponding tile has finished + /// loading + /// + /// Used to avoid disposing of [httpClient] whilst HTTP requests are still + /// underway. + /// + /// Does not include tiles loaded from session cache. + final _tilesInProgress = HashMap>(); - /// Get a browsed tile as an image, paint it on the map and save it's bytes to - /// cache for later (dependent on the [CacheBehavior]) @override - ImageProvider getImage(TileCoordinates coords, TileLayer options) => + ImageProvider getImage(TileCoordinates coordinates, TileLayer options) => FMTCImageProvider( - storeName: _store.storeName, provider: this, options: options, - coords: coords, + coords: coordinates, + startedLoading: () => _tilesInProgress[coordinates] = Completer(), + finishedLoadingBytes: () { + _tilesInProgress[coordinates]?.complete(); + _tilesInProgress.remove(coordinates); + }, ); + @override + Future dispose() async { + if (_tilesInProgress.isNotEmpty) { + await Future.wait(_tilesInProgress.values.map((c) => c.future)); + } + httpClient.close(); + super.dispose(); + } + /// Check whether a specified tile is cached in the current store @Deprecated(''' Migrate to `checkTileCached`. @@ -72,7 +90,7 @@ member will be removed in a future version.''') required TileLayer options, }) => FMTCBackendAccess.internal.tileExistsInStore( - storeName: _store.storeName, + storeName: storeName, url: obscureQueryParams( url: getTileUrl(coords, options), obscuredQueryParams: settings.obscuredQueryParams, @@ -83,11 +101,41 @@ member will be removed in a future version.''') bool operator ==(Object other) => identical(this, other) || (other is FMTCTileProvider && - other._store == _store && + other.storeName == storeName && other.headers == headers && other.settings == settings && other.httpClient == httpClient); @override - int get hashCode => Object.hash(_store, settings, headers, httpClient); + int get hashCode => Object.hash(storeName, settings, headers, httpClient); +} + +/// Custom override of [Map] that only overrides the [MapView.putIfAbsent] +/// method, to enable injection of an identifying mark ("FMTC") +class _CustomUserAgentCompatMap extends MapView { + const _CustomUserAgentCompatMap(super.map); + + /// Modified implementation of [MapView.putIfAbsent], that overrides behaviour + /// only when [key] is "User-Agent" + /// + /// flutter_map's [TileLayer] constructor calls this method after the + /// [TileLayer.tileProvider] has been constructed to customize the + /// "User-Agent" header with `TileLayer.userAgentPackageName`. + /// This method intercepts any call with [key] equal to "User-Agent" and + /// replacement value that matches the expected format, and adds an "FMTC" + /// identifying mark. + /// + /// The identifying mark is injected to seperate traffic sent via FMTC from + /// standard flutter_map traffic, as it significantly changes the behaviour of + /// tile retrieval, and could generate more traffic. + @override + String putIfAbsent(String key, String Function() ifAbsent) { + if (key != 'User-Agent') return super.putIfAbsent(key, ifAbsent); + + final replacementValue = ifAbsent(); + if (!RegExp(r'flutter_map \(.+\)').hasMatch(replacementValue)) { + return super.putIfAbsent(key, ifAbsent); + } + return this[key] = replacementValue.replaceRange(11, 12, ' + FMTC '); + } } diff --git a/lib/src/store/store.dart b/lib/src/store/store.dart index 0031c624..ef0f8233 100644 --- a/lib/src/store/store.dart +++ b/lib/src/store/store.dart @@ -57,8 +57,7 @@ class FMTCStore { /// background downloading functionality. DownloadManagement get download => DownloadManagement._(this); - /// Get the [TileProvider] suitable to connect the [TileLayer] to FMTC's - /// internals + /// Generate a [TileProvider] that connects to FMTC internals /// /// [settings] defaults to the current ambient /// [FMTCTileProviderSettings.instance], which defaults to the initial @@ -68,12 +67,7 @@ class FMTCStore { Map? headers, http.Client? httpClient, }) => - FMTCTileProvider._( - this, - settings: settings, - headers: headers ?? {}, - httpClient: httpClient, - ); + FMTCTileProvider._(storeName, settings, headers, httpClient); @override bool operator ==(Object other) => From 0c270eb8b8be33cb1b8079339d277bad2b66cbfe Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Mon, 8 Apr 2024 16:22:45 +0100 Subject: [PATCH 162/168] Improved `removeOldestTilesAboveLimit` debouncing mechanism and return values Made `BaseRegion` and descendants immutable and constant Removed redundant `BaseRegion.name` Minor miscellaneous improvements --- .../impls/objectbox/backend/internal.dart | 69 ++++++++++--------- .../backend/interfaces/backend/internal.dart | 11 +-- lib/src/bulk_download/manager.dart | 8 +-- lib/src/regions/base_region.dart | 28 ++------ lib/src/regions/circle.dart | 7 +- lib/src/regions/custom_polygon.dart | 8 +-- lib/src/regions/line.dart | 8 +-- lib/src/regions/rectangle.dart | 6 +- test/region_tile_test.dart | 30 ++++---- 9 files changed, 80 insertions(+), 95 deletions(-) diff --git a/lib/src/backend/impls/objectbox/backend/internal.dart b/lib/src/backend/impls/objectbox/backend/internal.dart index b2e8e8dc..67da63a0 100644 --- a/lib/src/backend/impls/objectbox/backend/internal.dart +++ b/lib/src/backend/impls/objectbox/backend/internal.dart @@ -35,6 +35,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { Timer? _rotalDebouncer; String? _rotalStore; + Completer? _rotalResultCompleter; // Define communicators @@ -174,19 +175,20 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { return; } + final isStreamedResult = evt.data?['expectStream'] == true; + // Handle errors - final err = evt.data?['error']; - if (err != null) { - if (evt.data?['expectStream'] == true) { - _workerResStreamed[evt.id]?.addError(err, evt.data!['stackTrace']); + if (evt.data?['error'] case final err?) { + final stackTrace = evt.data!['stackTrace']; + if (isStreamedResult) { + _workerResStreamed[evt.id]?.addError(err, stackTrace); } else { - _workerResOneShot[evt.id]! - .completeError(err, evt.data!['stackTrace']); + _workerResOneShot[evt.id]!.completeError(err, stackTrace); } return; } - if (evt.data?['expectStream'] == true) { + if (isStreamedResult) { // May be `null` if cmd was streamed result, but has no way to prevent // future results even after the listener has stopped // @@ -217,11 +219,10 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { final initResult = await workerInitialRes; // Check whether initialisation was successful - if (initResult.storeRef != null) { + if (initResult.storeRef case final storeRef?) { FMTCBackendAccess.internal = this; - FMTCBackendAccessThreadSafe.internal = _ObjectBoxBackendThreadSafeImpl._( - storeReference: initResult.storeRef!, - ); + FMTCBackendAccessThreadSafe.internal = + _ObjectBoxBackendThreadSafeImpl._(storeReference: storeRef); } else { Error.throwWithStackTrace(initResult.err!, initResult.stackTrace!); } @@ -266,6 +267,8 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { _rotalDebouncer?.cancel(); _rotalDebouncer = null; _rotalStore = null; + _rotalResultCompleter?.completeError(RootUnavailable()); + _rotalResultCompleter = null; FMTCBackendAccess.internal = null; FMTCBackendAccessThreadSafe.internal = null; @@ -412,39 +415,39 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { required String storeName, required int tilesLimit, }) async { - const type = _CmdType.removeOldestTilesAboveLimit; - final args = {'storeName': storeName, 'tilesLimit': tilesLimit}; - - // Attempts to avoid flooding worker with requests to delete oldest tile, - // and 'batches' them instead + // By sharing a single completer, all invocations of this method during the + // debounce period will return the same result at the same time + if (_rotalResultCompleter?.isCompleted ?? true) { + _rotalResultCompleter = Completer(); + } + void sendCmdAndComplete() => _rotalResultCompleter!.complete( + _sendCmdOneShot( + type: _CmdType.removeOldestTilesAboveLimit, + args: {'storeName': storeName, 'tilesLimit': tilesLimit}, + ).then((v) => v!['numOrphans']), + ); + // If the store has changed, failing to reset the batch/queue will mean + // tiles are removed from the wrong store if (_rotalStore != storeName) { - // If the store has changed, failing to reset the batch/queue will mean - // tiles are removed from the wrong store _rotalStore = storeName; if (_rotalDebouncer?.isActive ?? false) { _rotalDebouncer!.cancel(); - return (await _sendCmdOneShot(type: type, args: args))!['numOrphans']; + sendCmdAndComplete(); + return _rotalResultCompleter!.future; } } - if (_rotalDebouncer?.isActive ?? false) { - _rotalDebouncer!.cancel(); - _rotalDebouncer = Timer( - const Duration(milliseconds: 500), - () async => - (await _sendCmdOneShot(type: type, args: args))!['numOrphans'], - ); - return -1; - } - + // If the timer is already running, debouncing is required: cancel the + // current timer, and start a new one with a shorter timeout + final isAlreadyActive = _rotalDebouncer?.isActive ?? false; + if (isAlreadyActive) _rotalDebouncer!.cancel(); _rotalDebouncer = Timer( - const Duration(seconds: 1), - () async => - (await _sendCmdOneShot(type: type, args: args))!['numOrphans'], + Duration(milliseconds: isAlreadyActive ? 500 : 1000), + sendCmdAndComplete, ); - return -1; + return _rotalResultCompleter!.future; } @override diff --git a/lib/src/backend/interfaces/backend/internal.dart b/lib/src/backend/interfaces/backend/internal.dart index 381bee64..57d6ba50 100644 --- a/lib/src/backend/interfaces/backend/internal.dart +++ b/lib/src/backend/interfaces/backend/internal.dart @@ -180,12 +180,15 @@ abstract interface class FMTCBackendInternal /// Remove tiles in excess of the specified limit from the specified store, /// oldest first /// - /// May internally debounce, as this is a potentially expensive operation that - /// is likely to have no effect. When an invocation has been debounced, this - /// method will return `-1`. + /// Should internally debounce, as this is a repeatedly invoked & potentially + /// expensive operation, that will have no effect when the number of tiles in + /// the store is below the limit. /// /// Returns the number of tiles that were actually deleted (they were - /// orphaned). See [deleteTile] for more information about orphan tiles. + /// orphaned (see [deleteTile] for more info)). + /// + /// Throws [RootUnavailable] if the root is uninitialised whilst the + /// debouncing mechanism is running. Future removeOldestTilesAboveLimit({ required String storeName, required int tilesLimit, diff --git a/lib/src/bulk_download/manager.dart b/lib/src/bulk_download/manager.dart index f3f92593..d20aea51 100644 --- a/lib/src/bulk_download/manager.dart +++ b/lib/src/bulk_download/manager.dart @@ -72,7 +72,7 @@ Future _downloadManager( } // Setup tile generator isolate - final tilereceivePort = ReceivePort(); + final tileReceivePort = ReceivePort(); final tileIsolate = await Isolate.spawn( input.region.when( rectangle: (_) => TileGenerators.rectangleTiles, @@ -80,11 +80,11 @@ Future _downloadManager( line: (_) => TileGenerators.lineTiles, customPolygon: (_) => TileGenerators.customPolygonTiles, ), - (sendPort: tilereceivePort.sendPort, region: input.region), - onExit: tilereceivePort.sendPort, + (sendPort: tileReceivePort.sendPort, region: input.region), + onExit: tileReceivePort.sendPort, debugName: '[FMTC] Tile Coords Generator Thread', ); - final rawTileStream = tilereceivePort.skip(input.region.start).take( + final rawTileStream = tileReceivePort.skip(input.region.start).take( input.region.end == null ? largestInt : (input.region.end! - input.region.start), diff --git a/lib/src/regions/base_region.dart b/lib/src/regions/base_region.dart index 32645262..e13d49f5 100644 --- a/lib/src/regions/base_region.dart +++ b/lib/src/regions/base_region.dart @@ -14,6 +14,8 @@ part of '../../flutter_map_tile_caching.dart'; /// - [RectangleRegion] /// - [CircleRegion] /// - [LineRegion] +/// - [CustomPolygonRegion] +@immutable sealed class BaseRegion { /// Create a geographical region that forms a particular shape /// @@ -26,23 +28,8 @@ sealed class BaseRegion { /// - [RectangleRegion] /// - [CircleRegion] /// - [LineRegion] - BaseRegion({required String? name}) - : name = (name?.isEmpty ?? false) - ? throw ArgumentError.value(name, 'name', 'Must not be empty') - : name; - - /// The user friendly name for the region - /// - /// This is used within the recovery system, as well as to delete a particular - /// downloaded region within a store. - /// - /// If `null`, this region will have no name. If specified, this must not be - /// empty. - /// - /// _This property is currently redundant, but usage is planned in future - /// versions._ - @experimental - final String? name; + /// - [CustomPolygonRegion] + const BaseRegion(); /// Output a value of type [T] dependent on `this` and its type T when({ @@ -84,13 +71,10 @@ sealed class BaseRegion { Iterable toOutline(); @override - @mustCallSuper @mustBeOverridden - bool operator ==(Object other) => - identical(this, other) || (other is BaseRegion && other.name == name); + bool operator ==(Object other); @override - @mustCallSuper @mustBeOverridden - int get hashCode => name.hashCode; + int get hashCode; } diff --git a/lib/src/regions/circle.dart b/lib/src/regions/circle.dart index b87588dd..73f53d88 100644 --- a/lib/src/regions/circle.dart +++ b/lib/src/regions/circle.dart @@ -16,7 +16,7 @@ class CircleRegion extends BaseRegion { /// - [DownloadableRegion] for downloading: [toDownloadable] /// - [Widget] layer to be placed in a map: [toDrawable] /// - list of [LatLng]s forming the outline: [toOutline] - CircleRegion(this.center, this.radius, {super.name}) : super(); + const CircleRegion(this.center, this.radius); /// Center coordinate final LatLng center; @@ -85,9 +85,8 @@ class CircleRegion extends BaseRegion { identical(this, other) || (other is CircleRegion && other.center == center && - other.radius == radius && - super == other); + other.radius == radius); @override - int get hashCode => Object.hash(center, radius, super.hashCode); + int get hashCode => Object.hash(center, radius); } diff --git a/lib/src/regions/custom_polygon.dart b/lib/src/regions/custom_polygon.dart index 496830f1..2d043959 100644 --- a/lib/src/regions/custom_polygon.dart +++ b/lib/src/regions/custom_polygon.dart @@ -16,7 +16,7 @@ class CustomPolygonRegion extends BaseRegion { /// - [DownloadableRegion] for downloading: [toDownloadable] /// - [Widget] layer to be placed in a map: [toDrawable] /// - list of [LatLng]s forming the outline: [toOutline] - CustomPolygonRegion(this.outline, {super.name}) : super(); + const CustomPolygonRegion(this.outline); /// The outline coordinates final List outline; @@ -72,10 +72,8 @@ class CustomPolygonRegion extends BaseRegion { @override bool operator ==(Object other) => identical(this, other) || - (other is CustomPolygonRegion && - other.outline == outline && - super == other); + (other is CustomPolygonRegion && listEquals(outline, other.outline)); @override - int get hashCode => Object.hash(outline, super.hashCode); + int get hashCode => outline.hashCode; } diff --git a/lib/src/regions/line.dart b/lib/src/regions/line.dart index a23aeff3..9af8183b 100644 --- a/lib/src/regions/line.dart +++ b/lib/src/regions/line.dart @@ -16,7 +16,7 @@ class LineRegion extends BaseRegion { /// - [DownloadableRegion] for downloading: [toDownloadable] /// - [Widget] layer to be placed in a map: [toDrawable] /// - list of [LatLng]s forming the outline: [LineRegion.toOutlines] - LineRegion(this.line, this.radius, {super.name}) : super(); + const LineRegion(this.line, this.radius); /// The center line defined by a list of coordinates final List line; @@ -157,11 +157,9 @@ class LineRegion extends BaseRegion { bool operator ==(Object other) => identical(this, other) || (other is LineRegion && - other.line == line && - listEquals(other.line, line) && other.radius == radius && - super == other); + listEquals(line, other.line)); @override - int get hashCode => Object.hash(line, radius, super.hashCode); + int get hashCode => Object.hash(line, radius); } diff --git a/lib/src/regions/rectangle.dart b/lib/src/regions/rectangle.dart index e9788039..8c72dc44 100644 --- a/lib/src/regions/rectangle.dart +++ b/lib/src/regions/rectangle.dart @@ -18,7 +18,7 @@ class RectangleRegion extends BaseRegion { /// - [DownloadableRegion] for downloading: [toDownloadable] /// - [Widget] layer to be placed in a map: [toDrawable] /// - list of [LatLng]s forming the outline: [toOutline] - RectangleRegion(this.bounds, {super.name}) : super(); + const RectangleRegion(this.bounds); /// The coordinate bounds final LatLngBounds bounds; @@ -75,8 +75,8 @@ class RectangleRegion extends BaseRegion { @override bool operator ==(Object other) => identical(this, other) || - (other is RectangleRegion && other.bounds == bounds && super == other); + (other is RectangleRegion && other.bounds == bounds); @override - int get hashCode => Object.hash(bounds, super.hashCode); + int get hashCode => bounds.hashCode; } diff --git a/test/region_tile_test.dart b/test/region_tile_test.dart index 652bfde6..232aaab2 100644 --- a/test/region_tile_test.dart +++ b/test/region_tile_test.dart @@ -92,7 +92,7 @@ void main() { group( 'Circle Region', () { - final circleRegion = CircleRegion(const LatLng(0, 0), 200) + final circleRegion = const CircleRegion(LatLng(0, 0), 200) .toDownloadable(minZoom: 1, maxZoom: 15, options: TileLayer()); test( @@ -137,8 +137,8 @@ void main() { group( 'Line Region', () { - final lineRegion = LineRegion( - [const LatLng(-1, -1), const LatLng(1, 1), const LatLng(1, -1)], + final lineRegion = const LineRegion( + [LatLng(-1, -1), LatLng(1, 1), LatLng(1, -1)], 5000, ).toDownloadable(minZoom: 1, maxZoom: 15, options: TileLayer()); @@ -184,20 +184,20 @@ void main() { group( 'Custom Polygon Region', () { - final customPolygonRegion1 = CustomPolygonRegion([ - const LatLng(51.45818683312154, -0.9674646220840917), - const LatLng(51.55859639937614, -0.9185366064186982), - const LatLng(51.476641197796724, -0.7494743298246318), - const LatLng(51.56029831737391, -0.5322770067805148), - const LatLng(51.235701626195365, -0.5746290119276093), - const LatLng(51.38781341753136, -0.6779891095601829), + final customPolygonRegion1 = const CustomPolygonRegion([ + LatLng(51.45818683312154, -0.9674646220840917), + LatLng(51.55859639937614, -0.9185366064186982), + LatLng(51.476641197796724, -0.7494743298246318), + LatLng(51.56029831737391, -0.5322770067805148), + LatLng(51.235701626195365, -0.5746290119276093), + LatLng(51.38781341753136, -0.6779891095601829), ]).toDownloadable(minZoom: 1, maxZoom: 17, options: TileLayer()); - final customPolygonRegion2 = CustomPolygonRegion([ - const LatLng(-1, -1), - const LatLng(1, -1), - const LatLng(1, 1), - const LatLng(-1, 1), + final customPolygonRegion2 = const CustomPolygonRegion([ + LatLng(-1, -1), + LatLng(1, -1), + LatLng(1, 1), + LatLng(-1, 1), ]).toDownloadable(minZoom: 1, maxZoom: 17, options: TileLayer()); test( From 8177408762d5b57ebb97ca227051807dad87d8ca Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Tue, 9 Apr 2024 16:36:16 +0100 Subject: [PATCH 163/168] Improved documentation Renamed `DownloadManagement` to `StoreDownload` to improve consistency and better reflect scope --- lib/custom_backend_api.dart | 8 +- .../backend/interfaces/backend/internal.dart | 14 +-- lib/src/bulk_download/download_progress.dart | 9 +- lib/src/bulk_download/tile_event.dart | 10 ++ lib/src/root/recovery.dart | 23 ++++- lib/src/store/download.dart | 91 ++++++++++--------- lib/src/store/manage.dart | 3 +- lib/src/store/metadata.dart | 3 +- lib/src/store/statistics.dart | 3 +- lib/src/store/store.dart | 27 ++---- 10 files changed, 106 insertions(+), 85 deletions(-) diff --git a/lib/custom_backend_api.dart b/lib/custom_backend_api.dart index 8844af85..ff029497 100644 --- a/lib/custom_backend_api.dart +++ b/lib/custom_backend_api.dart @@ -5,8 +5,12 @@ /// internals necessary to create custom backends or work more directly with /// them /// -/// Use this import/library with caution! Assistance with non-typical usecases -/// may be limited. Always use the standard import unless necessary. +/// Many of the methods available through this import are exported and visible +/// via the more friendly interface of the main import and function set. +/// +/// > [!CAUTION] +/// > Use this import/library with caution! Assistance with non-typical usecases +/// > may be limited. Always use the standard import unless necessary. /// /// Importing the standard library will also likely be necessary. library flutter_map_tile_caching.custom_backend_api; diff --git a/lib/src/backend/interfaces/backend/internal.dart b/lib/src/backend/interfaces/backend/internal.dart index 57d6ba50..26e61043 100644 --- a/lib/src/backend/interfaces/backend/internal.dart +++ b/lib/src/backend/interfaces/backend/internal.dart @@ -79,8 +79,9 @@ abstract interface class FMTCBackendInternal /// {@template fmtc.backend.deleteStore} /// Delete the specified store /// - /// This operation cannot be undone! Ensure you confirm with the user that - /// this action is expected. + /// > [!WARNING] + /// > This operation cannot be undone! Ensure you confirm with the user that + /// > this action is expected. /// /// Does nothing if the store does not already exist. /// {@endtemplate} @@ -94,8 +95,9 @@ abstract interface class FMTCBackendInternal /// Also resets the hits & misses stats. Does not reset any associated /// metadata. /// - /// This operation cannot be undone! Ensure you confirm with the user that - /// this action is expected. + /// > [!WARNING] + /// > This operation cannot be undone! Ensure you confirm with the user that + /// > this action is expected. /// /// Does nothing if the store does not already exist. /// {@endtemplate} @@ -215,8 +217,8 @@ abstract interface class FMTCBackendInternal /// {@template fmtc.backend.setMetadata} /// Set a key-value pair in the metadata for the specified store /// - /// Note that this operation will overwrite any existing value for the - /// specified key. + /// > [!WARNING] + /// > Any existing value for the specified key will be overwritten. /// /// Prefer using [setBulkMetadata] when setting multiple keys. Only one backend /// operation is required to set them all at once, and so is more efficient. diff --git a/lib/src/bulk_download/download_progress.dart b/lib/src/bulk_download/download_progress.dart index f54ecfa3..36190855 100644 --- a/lib/src/bulk_download/download_progress.dart +++ b/lib/src/bulk_download/download_progress.dart @@ -5,10 +5,6 @@ part of '../../flutter_map_tile_caching.dart'; /// Statistics and information about the current progress of the download /// -/// Note that there a number of things to keep in mind when tracking the progress -/// of a download. See https://fmtc.jaffaketchup.dev/bulk-downloading/foreground -/// for more information. -/// /// See the documentation on each individual property for more information. @immutable class DownloadProgress { @@ -47,10 +43,7 @@ class DownloadProgress { /// The result of the latest attempted tile /// - /// Note that there a number of things to keep in mind when tracking the - /// progress of a download. See - /// https://fmtc.jaffaketchup.dev/bulk-downloading/foreground for more - /// information. + /// {@macro fmtc.tileevent.extraConsiderations} TileEvent get latestTileEvent => _latestTileEvent!; final TileEvent? _latestTileEvent; diff --git a/lib/src/bulk_download/tile_event.dart b/lib/src/bulk_download/tile_event.dart index 89ab3644..07660bbd 100644 --- a/lib/src/bulk_download/tile_event.dart +++ b/lib/src/bulk_download/tile_event.dart @@ -63,6 +63,14 @@ enum TileEventResult { /// /// Does not contain information about the download as a whole, that is /// [DownloadProgress]' responsibility. +/// +/// {@template fmtc.tileevent.extraConsiderations} +/// > [!TIP] +/// > When tracking [TileEvent]s across multiple [DownloadProgress] events, +/// > extra considerations are necessary. See +/// > [the documentation](https://fmtc.jaffaketchup.dev/bulk-downloading/start#keeping-track-across-events) +/// > for more information. +/// {@endtemplate} @immutable class TileEvent { const TileEvent._( @@ -127,6 +135,8 @@ class TileEvent { /// Events will occasionally be repeated due to the `maxReportInterval` /// functionality. If using other members, such as [result], to keep count of /// important events, do not count an event where this is `true`. + /// + /// {@macro fmtc.tileevent.extraConsiderations} final bool isRepeat; final bool _wasBufferReset; diff --git a/lib/src/root/recovery.dart b/lib/src/root/recovery.dart index 231580f4..1fb3ae5f 100644 --- a/lib/src/root/recovery.dart +++ b/lib/src/root/recovery.dart @@ -7,9 +7,8 @@ part of '../../flutter_map_tile_caching.dart'; /// /// --- /// -/// When a download is started, a recovery region is stored in a database, and -/// the download ID is stored in memory (in a singleton to ensure it is never -/// disposed except when the application is closed). +/// When a download is started, a recovery region is stored in a non-volatile +/// database, and the download ID is stored in volatile memory. /// /// If the download finishes normally, both entries are removed, otherwise, the /// memory is cleared when the app is closed, but the database record is not @@ -18,8 +17,24 @@ part of '../../flutter_map_tile_caching.dart'; /// {@template fmtc.rootRecovery.failedDefinition} /// A failed download is one that was found in the recovery database, but not /// in the application memory. It can therefore be assumed that the download -/// is also no longer in memory, and therefore stopped unexpectedly. +/// is also no longer in memory, and was therefore stopped unexpectedly, for +/// example after a fatal crash. /// {@endtemplate} +/// +/// The recovery system then allows the original [BaseRegion] and +/// [DownloadableRegion] to be recovered (via [RecoveredRegion]) from the failed +/// download, and the download can be restarted as normal. +/// +/// > [!NOTE] +/// > Options set at download time, in [StoreDownload.startForeground], are not +/// > included. +/// +/// > [!IMPORTANT] +/// > The recovery system does not keep track of the download, therefore it is +/// > unknown where the download failed, and the download must be restarted from +/// > the beginning. +/// > +/// > To workaround this limitation, set `skipExistingTiles` `true`. class RootRecovery { RootRecovery._() { _instance = this; diff --git a/lib/src/store/download.dart b/lib/src/store/download.dart index 5c3674c0..8123153f 100644 --- a/lib/src/store/download.dart +++ b/lib/src/store/download.dart @@ -3,38 +3,39 @@ part of '../../flutter_map_tile_caching.dart'; -/// Provides tools to manage bulk downloading to a specific [FMTCStore] -/// -/// The 'fmtc_plus_background_downloading' module must be installed to add the -/// background downloading functionality. +/// Provides bulk downloading functionality for a specific [FMTCStore] /// /// --- /// /// {@template num_instances} -/// By default, only one download per store at the same time is tolerated. -/// Attempting to start more than one at the same time may cause poor -/// performance or other poor behaviour. +/// 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. /// {@endtemplate} /// /// --- /// /// Does not keep state. State and download instances are held internally by /// [DownloadInstance]. -class DownloadManagement { - const DownloadManagement._(this._storeDirectory); - final FMTCStore _storeDirectory; +@immutable +class StoreDownload { + const StoreDownload._(this._storeName); + final String _storeName; /// Download a specified [DownloadableRegion] in the foreground, with a /// recovery session by default /// - /// To check the number of tiles that need to be downloaded before using this - /// function, use [check]. + /// > [!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 @@ -50,21 +51,24 @@ class DownloadManagement { /// - [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 `true`): whether to skip downloading + /// - [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) /// - /// 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 too many parallel threads may place significant strain on the tile + /// > server, so check your tile server's ToS for more information. /// - /// 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. - /// The output stream's statistics do not account for buffering. + /// > [!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. /// - /// Note that skipping sea tiles will not reduce the number of downloads - - /// tiles must be downloaded to be compared against the sample sea tile. + /// > [!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. /// /// --- /// @@ -81,6 +85,24 @@ class DownloadManagement { /// /// --- /// + /// 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. + /// + /// {@macro fmtc.tileevent.extraConsiderations} + /// + /// --- + /// + /// 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](https://fmtc.jaffaketchup.dev/usage/integration#obscuring-query-parameters). /// Will default to the value in the default [FMTCTileProviderSettings]. @@ -96,7 +118,7 @@ class DownloadManagement { required DownloadableRegion region, int parallelThreads = 5, int maxBufferLength = 200, - bool skipExistingTiles = true, + bool skipExistingTiles = false, bool skipSeaTiles = true, int? rateLimit, Duration? maxReportInterval = const Duration(seconds: 1), @@ -115,14 +137,6 @@ class DownloadManagement { ); } - if (region.options.tileProvider.headers['User-Agent'] == - 'flutter_map (unknown)') { - throw ArgumentError( - "`.toDownloadable`'s `TileLayer` argument must specify an appropriate `userAgentPackageName` or other `headers['User-Agent']`", - 'region.options.userAgentPackageName', - ); - } - if (parallelThreads < 1) { throw ArgumentError.value( parallelThreads, @@ -151,8 +165,9 @@ class DownloadManagement { final instance = DownloadInstance.registerIfAvailable(instanceId); if (instance == null) { throw StateError( - "Download instance with ID $instanceId already exists\nIf you're sure " - 'you want to start multiple downloads, use a unique `instanceId`.', + '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.', ); } @@ -162,7 +177,7 @@ class DownloadManagement { if (!disableRecovery) { await FMTCRoot.recovery._start( id: recoveryId, - storeName: _storeDirectory.storeName, + storeName: _storeName, region: region, ); } @@ -174,7 +189,7 @@ class DownloadManagement { ( sendPort: receivePort.sendPort, region: region, - storeName: _storeDirectory.storeName, + storeName: _storeName, parallelThreads: parallelThreads, maxBufferLength: maxBufferLength, skipExistingTiles: skipExistingTiles, @@ -264,8 +279,6 @@ class DownloadManagement { /// {@macro num_instances} /// /// Does nothing (returns immediately) if there is no ongoing download. - /// - /// Do not use to interact with background downloads. Future cancel({Object instanceId = 0}) async => await DownloadInstance.get(instanceId)?.requestCancel?.call(); @@ -282,8 +295,6 @@ class DownloadManagement { /// /// Does nothing (returns immediately) if there is no ongoing download or the /// download is already paused. - /// - /// Do not use to interact with background downloads. Future pause({Object instanceId = 0}) async => await DownloadInstance.get(instanceId)?.requestPause?.call(); @@ -293,8 +304,6 @@ class DownloadManagement { /// /// Does nothing if there is no ongoing download or the download is already /// running. - /// - /// Do not use to interact with background downloads. void resume({Object instanceId = 0}) => DownloadInstance.get(instanceId)?.requestResume?.call(); @@ -304,8 +313,6 @@ class DownloadManagement { /// {@macro num_instances} /// /// Also returns `false` if there is no ongoing download. - /// - /// Do not use to interact with background downloads. bool isPaused({Object instanceId = 0}) => DownloadInstance.get(instanceId)?.isPaused ?? false; } diff --git a/lib/src/store/manage.dart b/lib/src/store/manage.dart index a489093e..e8edbacc 100644 --- a/lib/src/store/manage.dart +++ b/lib/src/store/manage.dart @@ -10,8 +10,7 @@ part of '../../flutter_map_tile_caching.dart'; /// operation, then an error will be thrown ([StoreNotExists]). It is /// recommended to check [ready] when necessary. class StoreManagement { - StoreManagement._(FMTCStore store) : _storeName = store.storeName; - + StoreManagement._(this._storeName); final String _storeName; /// {@macro fmtc.backend.storeExists} diff --git a/lib/src/store/metadata.dart b/lib/src/store/metadata.dart index 7a7a1a4f..c4e6c145 100644 --- a/lib/src/store/metadata.dart +++ b/lib/src/store/metadata.dart @@ -9,8 +9,7 @@ part of '../../flutter_map_tile_caching.dart'; /// advanced requirements should use another package, as this is a basic /// implementation. class StoreMetadata { - StoreMetadata._(FMTCStore store) : _storeName = store.storeName; - + StoreMetadata._(this._storeName); final String _storeName; /// {@macro fmtc.backend.readMetadata} diff --git a/lib/src/store/statistics.dart b/lib/src/store/statistics.dart index a8d4117d..f59119af 100644 --- a/lib/src/store/statistics.dart +++ b/lib/src/store/statistics.dart @@ -11,8 +11,7 @@ part of '../../flutter_map_tile_caching.dart'; /// operation, then an error will be thrown ([StoreNotExists]). It is /// recommended to check [StoreManagement.ready] when necessary. class StoreStats { - StoreStats._(FMTCStore store) : _storeName = store.storeName; - + StoreStats._(this._storeName); final String _storeName; /// {@macro fmtc.backend.getStoreStats} diff --git a/lib/src/store/store.dart b/lib/src/store/store.dart index ef0f8233..ea88e25f 100644 --- a/lib/src/store/store.dart +++ b/lib/src/store/store.dart @@ -18,44 +18,37 @@ only. It will be removed in a future version. ) typedef StoreDirectory = FMTCStore; +/// {@template fmtc.fmtcStore} /// Provides access to management, statistics, metadata, bulk download, /// the tile provider (and the export functionality) on the store named /// [storeName] /// -/// {@template fmtc.fmtcstore.sub.noautocreate} -/// Note that constructing an instance of this class will not automatically -/// create it, as this is an asynchronous operation. To create this store, use -/// [manage] > [StoreManagement.create]. +/// > [!IMPORTANT] +/// > Constructing an instance of this class will not automatically create it. +/// > To create this store, use [manage] > [StoreManagement.create]. /// {@endtemplate} class FMTCStore { - /// Provides access to management, statistics, metadata, bulk download, - /// the tile provider (and the export functionality) on the store named - /// [storeName] - /// - /// {@macro fmtc.fmtcstore.sub.noautocreate} + /// {@macro fmtc.fmtcStore} const FMTCStore(this.storeName); /// The user-friendly name of the store directory final String storeName; /// Manage this store's representation on the filesystem - StoreManagement get manage => StoreManagement._(this); + StoreManagement get manage => StoreManagement._(storeName); /// Get statistics about this store - StoreStats get stats => StoreStats._(this); + StoreStats get stats => StoreStats._(storeName); /// Manage custom miscellaneous information tied to this store /// /// Uses a key-value format where both key and value must be [String]. More /// advanced requirements should use another package, as this is a basic /// implementation. - StoreMetadata get metadata => StoreMetadata._(this); + StoreMetadata get metadata => StoreMetadata._(storeName); - /// Get tools to manage bulk downloading to this store - /// - /// The 'fmtc_plus_background_downloading' module must be installed to add the - /// background downloading functionality. - DownloadManagement get download => DownloadManagement._(this); + /// Provides bulk downloading functionality + StoreDownload get download => StoreDownload._(storeName); /// Generate a [TileProvider] that connects to FMTC internals /// From 9de462fa1d2821cd379c35ba744d627c5165eb94 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Tue, 9 Apr 2024 18:21:51 +0100 Subject: [PATCH 164/168] Fixed deprecations Minor improvements --- example/lib/main.dart | 4 +-- .../components/options_pane.dart | 2 +- .../components/start_download_button.dart | 2 +- .../multi_linear_progress_indicator.dart | 4 +-- .../download_progress_indicator.dart | 4 +-- .../quit_tiles_preview_indicator.dart | 2 +- .../components/region_shape.dart | 3 +-- .../additional_panes/slider_panel_base.dart | 2 +- .../components/side_panel/primary_pane.dart | 6 ++--- lib/src/regions/downloadable_region.dart | 7 +++++- lib/src/regions/recovered_region.dart | 25 +++++++------------ 11 files changed, 29 insertions(+), 32 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index dd714e5e..2589a9f7 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -46,9 +46,9 @@ class _AppContainer extends StatelessWidget { textTheme: GoogleFonts.ubuntuTextTheme(ThemeData.dark().textTheme), colorSchemeSeed: Colors.red, switchTheme: SwitchThemeData( - thumbIcon: MaterialStateProperty.resolveWith( + thumbIcon: WidgetStateProperty.resolveWith( (states) => Icon( - states.contains(MaterialState.selected) ? Icons.check : Icons.close, + states.contains(WidgetState.selected) ? Icons.check : Icons.close, ), ), ), diff --git a/example/lib/screens/configure_download/components/options_pane.dart b/example/lib/screens/configure_download/components/options_pane.dart index 3318bd9e..1993455b 100644 --- a/example/lib/screens/configure_download/components/options_pane.dart +++ b/example/lib/screens/configure_download/components/options_pane.dart @@ -25,7 +25,7 @@ class OptionsPane extends StatelessWidget { const SizedBox.square(dimension: 4), DecoratedBox( decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceVariant, + color: Theme.of(context).colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(12), ), child: Padding( diff --git a/example/lib/screens/configure_download/components/start_download_button.dart b/example/lib/screens/configure_download/components/start_download_button.dart index e2b0ff82..e78a7bc9 100644 --- a/example/lib/screens/configure_download/components/start_download_button.dart +++ b/example/lib/screens/configure_download/components/start_download_button.dart @@ -46,7 +46,7 @@ class StartDownloadButton extends StatelessWidget { alignment: Alignment.bottomRight, child: Container( decoration: BoxDecoration( - color: Theme.of(context).colorScheme.onBackground, + color: Theme.of(context).colorScheme.onSurface, borderRadius: const BorderRadius.only( topLeft: Radius.circular(12), topRight: Radius.circular(12), diff --git a/example/lib/screens/main/pages/downloading/components/multi_linear_progress_indicator.dart b/example/lib/screens/main/pages/downloading/components/multi_linear_progress_indicator.dart index 8fe16e63..1881fc65 100644 --- a/example/lib/screens/main/pages/downloading/components/multi_linear_progress_indicator.dart +++ b/example/lib/screens/main/pages/downloading/components/multi_linear_progress_indicator.dart @@ -43,11 +43,11 @@ class _MulitLinearProgressIndicatorState Positioned.fill( child: Container( decoration: BoxDecoration( - color: Theme.of(context).colorScheme.background, + color: Theme.of(context).colorScheme.surface, borderRadius: widget.radius ?? BorderRadius.circular(widget.height / 2), border: Border.all( - color: Theme.of(context).colorScheme.onBackground, + color: Theme.of(context).colorScheme.onSurface, ), ), padding: EdgeInsets.symmetric( diff --git a/example/lib/screens/main/pages/map/components/download_progress_indicator.dart b/example/lib/screens/main/pages/map/components/download_progress_indicator.dart index b5fba889..33d0a314 100644 --- a/example/lib/screens/main/pages/map/components/download_progress_indicator.dart +++ b/example/lib/screens/main/pages/map/components/download_progress_indicator.dart @@ -33,7 +33,7 @@ class DownloadProgressIndicator extends StatelessWidget { child: CustomPaint( painter: BubbleArrowIndicator( borderRadius: BorderRadius.circular(12), - color: Theme.of(context).colorScheme.background, + color: Theme.of(context).colorScheme.surface, ), child: child, ), @@ -49,7 +49,7 @@ class DownloadProgressIndicator extends StatelessWidget { painter: SideIndicatorPainter( startRadius: const Radius.circular(8), endRadius: const Radius.circular(25), - color: Theme.of(context).colorScheme.background, + color: Theme.of(context).colorScheme.surface, ), child: Padding( padding: const EdgeInsets.only(left: 20), diff --git a/example/lib/screens/main/pages/map/components/quit_tiles_preview_indicator.dart b/example/lib/screens/main/pages/map/components/quit_tiles_preview_indicator.dart index bf39beec..6ac0c028 100644 --- a/example/lib/screens/main/pages/map/components/quit_tiles_preview_indicator.dart +++ b/example/lib/screens/main/pages/map/components/quit_tiles_preview_indicator.dart @@ -46,7 +46,7 @@ class QuitTilesPreviewIndicator extends StatelessWidget { painter: SideIndicatorPainter( startRadius: const Radius.circular(8), endRadius: const Radius.circular(25), - color: Theme.of(context).colorScheme.background, + color: Theme.of(context).colorScheme.surface, ), child: Padding( padding: const EdgeInsets.only(left: 20), diff --git a/example/lib/screens/main/pages/region_selection/components/region_shape.dart b/example/lib/screens/main/pages/region_selection/components/region_shape.dart index 9cc00621..0caef237 100644 --- a/example/lib/screens/main/pages/region_selection/components/region_shape.dart +++ b/example/lib/screens/main/pages/region_selection/components/region_shape.dart @@ -90,8 +90,7 @@ class RegionShape extends StatelessWidget { isFilled: true, borderColor: Colors.black, borderStrokeWidth: 2, - color: - Theme.of(context).colorScheme.background.withOpacity(0.5), + color: Theme.of(context).colorScheme.surface.withOpacity(0.5), ), ], ); diff --git a/example/lib/screens/main/pages/region_selection/components/side_panel/additional_panes/slider_panel_base.dart b/example/lib/screens/main/pages/region_selection/components/side_panel/additional_panes/slider_panel_base.dart index 17da68f8..3e8c1af8 100644 --- a/example/lib/screens/main/pages/region_selection/components/side_panel/additional_panes/slider_panel_base.dart +++ b/example/lib/screens/main/pages/region_selection/components/side_panel/additional_panes/slider_panel_base.dart @@ -41,7 +41,7 @@ class _SliderPanelBase extends StatelessWidget { ? constraints.maxHeight : null, decoration: BoxDecoration( - color: Theme.of(context).colorScheme.background, + color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(1028), ), padding: layoutDirection == Axis.vertical diff --git a/example/lib/screens/main/pages/region_selection/components/side_panel/primary_pane.dart b/example/lib/screens/main/pages/region_selection/components/side_panel/primary_pane.dart index 034cadc4..1c0fd3e5 100644 --- a/example/lib/screens/main/pages/region_selection/components/side_panel/primary_pane.dart +++ b/example/lib/screens/main/pages/region_selection/components/side_panel/primary_pane.dart @@ -51,7 +51,7 @@ class _PrimaryPane extends StatelessWidget { children: [ Container( decoration: BoxDecoration( - color: Theme.of(context).colorScheme.background, + color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(1028), ), padding: const EdgeInsets.all(12), @@ -95,7 +95,7 @@ class _PrimaryPane extends StatelessWidget { const SizedBox.square(dimension: 12), Container( decoration: BoxDecoration( - color: Theme.of(context).colorScheme.background, + color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(1028), ), padding: const EdgeInsets.all(12), @@ -135,7 +135,7 @@ class _PrimaryPane extends StatelessWidget { const SizedBox.square(dimension: 12), Container( decoration: BoxDecoration( - color: Theme.of(context).colorScheme.background, + color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(1028), ), padding: const EdgeInsets.all(12), diff --git a/lib/src/regions/downloadable_region.dart b/lib/src/regions/downloadable_region.dart index 7b316176..d284d6e7 100644 --- a/lib/src/regions/downloadable_region.dart +++ b/lib/src/regions/downloadable_region.dart @@ -18,7 +18,12 @@ class DownloadableRegion { }) { if (minZoom > maxZoom) { throw ArgumentError( - '`minZoom` should be less than or equal to `maxZoom`', + '`minZoom` must be less than or equal to `maxZoom`', + ); + } + if (start < 0 || start > (end ?? 0)) { + throw ArgumentError( + '`start` must be greater or equal to 0 and less than or equal to `end`', ); } } diff --git a/lib/src/regions/recovered_region.dart b/lib/src/regions/recovered_region.dart index c11c6fdf..88a1e15e 100644 --- a/lib/src/regions/recovered_region.dart +++ b/lib/src/regions/recovered_region.dart @@ -36,43 +36,36 @@ class RecoveredRegion { }); /// A unique ID created for every bulk download operation - /// - /// Not actually used when converting to [DownloadableRegion]. final int id; - /// The store name originally associated with this download. - /// - /// Not actually used when converting to [DownloadableRegion]. + /// The store name originally associated with this download final String storeName; /// The time at which this recovery was started - /// - /// Not actually used when converting to [DownloadableRegion]. final DateTime time; - /// The minimum zoom level to fetch tiles for + /// Corresponds to [DownloadableRegion.minZoom] final int minZoom; - /// The maximum zoom level to fetch tiles for + /// Corresponds to [DownloadableRegion.maxZoom] final int maxZoom; - /// Optionally skip past a number of tiles 'at the start' of a region + ///Corresponds to [DownloadableRegion.start] final int start; - /// Optionally skip a number of tiles 'at the end' of a region + /// Corresponds to [DownloadableRegion.end] final int? end; - /// The bounds for a rectangular region + /// Corresponds to [RectangleRegion.bounds] final LatLngBounds? bounds; - /// The line making a line-based region or the outline making a custom polygon - /// region + /// Corrresponds to [LineRegion.line] & [CustomPolygonRegion.outline] final List? line; - /// The center of a circular region + /// Corrresponds to [CircleRegion.center] final LatLng? center; - /// The radius of a circular region + /// Corrresponds to [LineRegion.radius] & [CircleRegion.radius] final double? radius; /// Convert this region into a [BaseRegion] From f3f5bc7a775c07293998d33f232e2bcaed9fd90a Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 11 Apr 2024 11:57:25 +0100 Subject: [PATCH 165/168] Added tracking support to recovery system to skip downloading already downloaded tiles Fixed tile counters and generators (and downloader) to respect `DownloadableRegion.start` and `.end` Reduced memory consumption of `deleteTiles` in backend worker Added tests for bounded tile counters and generators Improved example application --- .../components/region_information.dart | 24 +- .../components/start_download_button.dart | 6 + .../configure_download.dart | 8 + .../export_import/components/path_picker.dart | 4 +- .../recovery/components/recovery_list.dart | 7 +- .../components/recovery_start_button.dart | 2 + .../region_selection/region_selection.dart | 2 + .../state/region_selection_provider.dart | 14 + lib/flutter_map_tile_caching.dart | 1 + .../impls/objectbox/backend/internal.dart | 11 - .../internal_workers/standard/cmd_type.dart | 1 - .../internal_workers/standard/worker.dart | 514 +++++++++--------- .../backend/internal_workers/thread_safe.dart | 49 +- .../models/generated/objectbox.g.dart | 2 +- .../impls/objectbox/models/src/recovery.dart | 4 +- .../backend/interfaces/backend/internal.dart | 8 - .../backend/internal_thread_safe.dart | 33 +- lib/src/bulk_download/download_progress.dart | 5 + lib/src/bulk_download/manager.dart | 40 +- lib/src/bulk_download/tile_loops/count.dart | 14 +- .../bulk_download/tile_loops/generate.dart | 24 + lib/src/regions/base_region.dart | 2 +- lib/src/regions/circle.dart | 2 +- lib/src/regions/custom_polygon.dart | 2 +- lib/src/regions/downloadable_region.dart | 10 +- lib/src/regions/line.dart | 2 +- lib/src/regions/recovered_region.dart | 10 +- lib/src/regions/rectangle.dart | 2 +- lib/src/root/recovery.dart | 25 +- lib/src/store/download.dart | 35 +- test/region_tile_test.dart | 107 +++- tile_server/.gitignore | 3 +- tile_server/static/generated/favicon.dart | 2 - tile_server/static/generated/land.dart | 2 - tile_server/static/generated/sea.dart | 2 - 35 files changed, 604 insertions(+), 375 deletions(-) delete mode 100644 tile_server/static/generated/favicon.dart delete mode 100644 tile_server/static/generated/land.dart delete mode 100644 tile_server/static/generated/sea.dart diff --git a/example/lib/screens/configure_download/components/region_information.dart b/example/lib/screens/configure_download/components/region_information.dart index 6c1c1527..b661e49e 100644 --- a/example/lib/screens/configure_download/components/region_information.dart +++ b/example/lib/screens/configure_download/components/region_information.dart @@ -14,11 +14,15 @@ class RegionInformation extends StatefulWidget { required this.region, required this.minZoom, required this.maxZoom, + required this.startTile, + required this.endTile, }); final BaseRegion region; final int minZoom; final int maxZoom; + final int startTile; + final int? endTile; @override State createState() => _RegionInformationState(); @@ -180,7 +184,7 @@ class _RegionInformationState extends State { Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ - const Text('MIN/MAX ZOOM LEVELS'), + const Text('ZOOM LEVELS'), Text( '${widget.minZoom} - ${widget.maxZoom}', style: const TextStyle( @@ -218,6 +222,24 @@ class _RegionInformationState extends State { ), ), ), + const SizedBox(height: 10), + const Text('TILES RANGE'), + if (widget.startTile == 1 && widget.endTile == null) + const Text( + '*', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 24, + ), + ) + else + Text( + '${NumberFormat('###,###').format(widget.startTile)} - ${widget.endTile != null ? NumberFormat('###,###').format(widget.endTile) : '*'}', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 24, + ), + ), ], ), ], diff --git a/example/lib/screens/configure_download/components/start_download_button.dart b/example/lib/screens/configure_download/components/start_download_button.dart index e78a7bc9..10c7da60 100644 --- a/example/lib/screens/configure_download/components/start_download_button.dart +++ b/example/lib/screens/configure_download/components/start_download_button.dart @@ -13,11 +13,15 @@ class StartDownloadButton extends StatelessWidget { required this.region, required this.minZoom, required this.maxZoom, + required this.startTile, + required this.endTile, }); final BaseRegion region; final int minZoom; final int maxZoom; + final int startTile; + final int? endTile; @override Widget build(BuildContext context) => @@ -98,6 +102,8 @@ class StartDownloadButton extends StatelessWidget { region: region.toDownloadable( minZoom: minZoom, maxZoom: maxZoom, + start: startTile, + end: endTile, options: TileLayer( urlTemplate: metadata['sourceURL'], userAgentPackageName: diff --git a/example/lib/screens/configure_download/configure_download.dart b/example/lib/screens/configure_download/configure_download.dart index d085dbf4..35953448 100644 --- a/example/lib/screens/configure_download/configure_download.dart +++ b/example/lib/screens/configure_download/configure_download.dart @@ -16,11 +16,15 @@ class ConfigureDownloadPopup extends StatelessWidget { required this.region, required this.minZoom, required this.maxZoom, + required this.startTile, + required this.endTile, }); final BaseRegion region; final int minZoom; final int maxZoom; + final int startTile; + final int? endTile; @override Widget build(BuildContext context) => Scaffold( @@ -29,6 +33,8 @@ class ConfigureDownloadPopup extends StatelessWidget { region: region, minZoom: minZoom, maxZoom: maxZoom, + startTile: startTile, + endTile: endTile, ), body: Stack( fit: StackFit.expand, @@ -44,6 +50,8 @@ class ConfigureDownloadPopup extends StatelessWidget { region: region, minZoom: minZoom, maxZoom: maxZoom, + startTile: startTile, + endTile: endTile, ), const Divider(thickness: 2, height: 8), const OptionsPane( diff --git a/example/lib/screens/export_import/components/path_picker.dart b/example/lib/screens/export_import/components/path_picker.dart index 2ab84817..ab42d2ae 100644 --- a/example/lib/screens/export_import/components/path_picker.dart +++ b/example/lib/screens/export_import/components/path_picker.dart @@ -33,7 +33,7 @@ class PathPicker extends StatelessWidget { ); if (picked != null) { pathController.value = TextEditingValue( - text: picked, + text: '$picked.fmtc', selection: TextSelection.collapsed( offset: picked.length, ), @@ -88,7 +88,7 @@ class PathPicker extends StatelessWidget { ); if (picked != null) { pathController.value = TextEditingValue( - text: picked, + text: '$picked.fmtc', selection: TextSelection.collapsed( offset: picked.length, ), diff --git a/example/lib/screens/main/pages/recovery/components/recovery_list.dart b/example/lib/screens/main/pages/recovery/components/recovery_list.dart index fe423af4..4c310fbc 100644 --- a/example/lib/screens/main/pages/recovery/components/recovery_list.dart +++ b/example/lib/screens/main/pages/recovery/components/recovery_list.dart @@ -20,7 +20,7 @@ class RecoveryList extends StatefulWidget { class _RecoveryListState extends State { @override - Widget build(BuildContext context) => ListView.builder( + Widget build(BuildContext context) => ListView.separated( itemCount: widget.all.length, itemBuilder: (context, index) { final result = widget.all.elementAt(index); @@ -52,10 +52,10 @@ class _RecoveryListState extends State { addressDetails: true, ), builder: (context, response) => Text( - 'Started at ${region.time} (~${DateTime.timestamp().difference(region.time).inMinutes} minutes ago)\n${response.hasData ? 'Center near ${response.data!.address!['postcode']}, ${response.data!.address!['country']}' : response.hasError ? 'Unable To Reverse Geocode Location' : 'Please Wait...'}', + 'Started at ${region.time} (~${DateTime.timestamp().difference(region.time).inMinutes} minutes ago)\nCompleted ${region.start - 1} of ${region.end}\n${response.hasData ? 'Center near ${response.data!.address!['postcode']}, ${response.data!.address!['country']}' : response.hasError ? 'Unable To Reverse Geocode Location' : 'Please Wait...'}', ), ), - onTap: () {}, + isThreeLine: true, trailing: Row( mainAxisSize: MainAxisSize.min, children: [ @@ -80,5 +80,6 @@ class _RecoveryListState extends State { ), ); }, + separatorBuilder: (context, index) => const Divider(), ); } diff --git a/example/lib/screens/main/pages/recovery/components/recovery_start_button.dart b/example/lib/screens/main/pages/recovery/components/recovery_start_button.dart index 44fcfbf2..b7045901 100644 --- a/example/lib/screens/main/pages/recovery/components/recovery_start_button.dart +++ b/example/lib/screens/main/pages/recovery/components/recovery_start_button.dart @@ -39,6 +39,8 @@ class RecoveryStartButton extends StatelessWidget { region: regionSelectionProvider.region!, minZoom: result.region.minZoom, maxZoom: result.region.maxZoom, + startTile: result.region.start, + endTile: result.region.end, ), fullscreenDialog: true, ), diff --git a/example/lib/screens/main/pages/region_selection/region_selection.dart b/example/lib/screens/main/pages/region_selection/region_selection.dart index 89e72aad..9e4ad313 100644 --- a/example/lib/screens/main/pages/region_selection/region_selection.dart +++ b/example/lib/screens/main/pages/region_selection/region_selection.dart @@ -188,6 +188,8 @@ class _RegionSelectionPageState extends State { region: provider.region!, minZoom: provider.minZoom, maxZoom: provider.maxZoom, + startTile: provider.startTile, + endTile: provider.endTile, ), fullscreenDialog: true, ), diff --git a/example/lib/screens/main/pages/region_selection/state/region_selection_provider.dart b/example/lib/screens/main/pages/region_selection/state/region_selection_provider.dart index f78deeb6..fac3ec8d 100644 --- a/example/lib/screens/main/pages/region_selection/state/region_selection_provider.dart +++ b/example/lib/screens/main/pages/region_selection/state/region_selection_provider.dart @@ -107,6 +107,20 @@ class RegionSelectionProvider extends ChangeNotifier { notifyListeners(); } + int _startTile = 1; + int get startTile => _startTile; + set startTile(int newNum) { + _startTile = newNum; + notifyListeners(); + } + + int? _endTile; + int? get endTile => _endTile; + set endTile(int? newNum) { + _endTile = endTile; + notifyListeners(); + } + FMTCStore? _selectedStore; FMTCStore? get selectedStore => _selectedStore; void setSelectedStore(FMTCStore? newStore, {bool notify = true}) { diff --git a/lib/flutter_map_tile_caching.dart b/lib/flutter_map_tile_caching.dart index d1b714bc..a731dddb 100644 --- a/lib/flutter_map_tile_caching.dart +++ b/lib/flutter_map_tile_caching.dart @@ -15,6 +15,7 @@ import 'dart:collection'; import 'dart:io'; import 'dart:isolate'; import 'dart:math' as math; +import 'dart:math'; import 'package:async/async.dart'; import 'package:collection/collection.dart'; diff --git a/lib/src/backend/impls/objectbox/backend/internal.dart b/lib/src/backend/impls/objectbox/backend/internal.dart index 67da63a0..9cdcac45 100644 --- a/lib/src/backend/impls/objectbox/backend/internal.dart +++ b/lib/src/backend/impls/objectbox/backend/internal.dart @@ -523,17 +523,6 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { type: _CmdType.getRecoverableRegion, ))!['recoverableRegion']; - @override - Future startRecovery({ - required int id, - required String storeName, - required DownloadableRegion region, - }) => - _sendCmdOneShot( - type: _CmdType.startRecovery, - args: {'id': id, 'storeName': storeName, 'region': region}, - ); - @override Future cancelRecovery({ required int id, diff --git a/lib/src/backend/impls/objectbox/backend/internal_workers/standard/cmd_type.dart b/lib/src/backend/impls/objectbox/backend/internal_workers/standard/cmd_type.dart index f9bed693..a469cd2f 100644 --- a/lib/src/backend/impls/objectbox/backend/internal_workers/standard/cmd_type.dart +++ b/lib/src/backend/impls/objectbox/backend/internal_workers/standard/cmd_type.dart @@ -31,7 +31,6 @@ enum _CmdType { resetMetadata, listRecoverableRegions, getRecoverableRegion, - startRecovery, cancelRecovery, watchRecovery(hasInternalStreamSub: true), watchStores(hasInternalStreamSub: true), diff --git a/lib/src/backend/impls/objectbox/backend/internal_workers/standard/worker.dart b/lib/src/backend/impls/objectbox/backend/internal_workers/standard/worker.dart index 4eccf47b..a5134af7 100644 --- a/lib/src/backend/impls/objectbox/backend/internal_workers/standard/worker.dart +++ b/lib/src/backend/impls/objectbox/backend/internal_workers/standard/worker.dart @@ -76,60 +76,65 @@ Future _worker( /// Note that a transaction is used internally as necessary. /// /// Returns the number of orphaned (deleted) tiles. - int deleteTiles({ + Future deleteTiles({ required Query storesQuery, required Query tilesQuery, - }) { + }) async { final stores = root.box(); final tiles = root.box(); - int rootDeltaLength = 0; + bool hadTilesToUpdate = false; int rootDeltaSize = 0; + final tilesToRemove = []; + //final tileRelationsToUpdate = >[]; final storesToUpdate = {}; - root.runInTransaction( - TxMode.write, - () { - final queriedStores = storesQuery.property(ObjectBoxStore_.name).find(); - final queriedTiles = tilesQuery.find(); + final queriedStores = storesQuery.property(ObjectBoxStore_.name).find(); + if (queriedStores.isEmpty) return 0; - if (queriedStores.isEmpty || queriedTiles.isEmpty) return; + // For each store, remove it from the tile if requested + // For each store & if removed, update that store's stats + await for (final tile in tilesQuery.stream()) { + tile.stores.removeWhere((store) { + if (!queriedStores.contains(store.name)) return false; - for (final tile in queriedTiles) { - // For each store, remove it from the tile if requested - // For each store & if removed, update that store's stats - tile.stores.removeWhere((store) { - if (!queriedStores.contains(store.name)) return false; + storesToUpdate[store.name] = (storesToUpdate[store.name] ?? store) + ..length -= 1 + ..size -= tile.bytes.lengthInBytes; - storesToUpdate[store.name] = (storesToUpdate[store.name] ?? store) - ..length -= 1 - ..size -= tile.bytes.lengthInBytes; + return true; + }); - return true; - }); + if (tile.stores.isNotEmpty) { + tile.stores.applyToDb(mode: PutMode.update); + hadTilesToUpdate = true; + continue; + } - // Delete the tile if it is now orphaned - if (tile.stores.isEmpty) { - rootDeltaLength -= 1; - rootDeltaSize -= tile.bytes.lengthInBytes; - tiles.remove(tile.id); - continue; - } + rootDeltaSize -= tile.bytes.lengthInBytes; + tilesToRemove.add(tile.id); + } - // Otherwise apply the new relation - tile.stores.applyToDb(mode: PutMode.update); - } + if (!hadTilesToUpdate && tilesToRemove.isEmpty) return 0; + + root.runInTransaction( + TxMode.write, + () { + tilesToRemove.forEach(tiles.remove); updateRootStatistics( - deltaLength: rootDeltaLength, + deltaLength: -tilesToRemove.length, deltaSize: rootDeltaSize, ); - stores.putMany(storesToUpdate.values.toList(), mode: PutMode.update); + stores.putMany( + storesToUpdate.values.toList(), + mode: PutMode.update, + ); }, ); - return rootDeltaLength.abs(); + return tilesToRemove.length; } /// Verify that the specified file is a valid FMTC format archive, compatible @@ -279,6 +284,12 @@ Future _worker( final tiles = root.box(); final stores = root.box(); + final storeQuery = + stores.query(ObjectBoxStore_.name.equals(storeName)).build(); + + final store = storeQuery.findUnique() ?? + (throw StoreNotExists(storeName: storeName)); + final tilesQuery = (tiles.query() ..linkMany( ObjectBoxTile_.stores, @@ -286,30 +297,21 @@ Future _worker( )) .build(); - final storeQuery = - stores.query(ObjectBoxStore_.name.equals(storeName)).build(); - - root.runInTransaction( - TxMode.write, - () { - deleteTiles(storesQuery: storeQuery, tilesQuery: tilesQuery); - - final store = storeQuery.findUnique() ?? - (throw StoreNotExists(storeName: storeName)); - storeQuery.close(); + deleteTiles(storesQuery: storeQuery, tilesQuery: tilesQuery).then((_) { + stores.put( + store + ..length = 0 + ..size = 0 + ..hits = 0 + ..misses = 0, + mode: PutMode.update, + ); - stores.put( - store - ..length = 0 - ..size = 0 - ..hits = 0 - ..misses = 0, - mode: PutMode.update, - ); - }, - ); + sendRes(id: cmd.id); - sendRes(id: cmd.id); + storeQuery.close(); + tilesQuery.close(); + }); case _CmdType.renameStore: final currentStoreName = cmd.args['currentStoreName']! as String; final newStoreName = cmd.args['newStoreName']! as String; @@ -345,18 +347,14 @@ Future _worker( )) .build(); - root.runInTransaction( - TxMode.write, - () { - deleteTiles(storesQuery: storesQuery, tilesQuery: tilesQuery); - storesQuery.remove(); - }, - ); + deleteTiles(storesQuery: storesQuery, tilesQuery: tilesQuery).then((_) { + storesQuery.remove(); - sendRes(id: cmd.id); + sendRes(id: cmd.id); - storesQuery.close(); - tilesQuery.close(); + storesQuery.close(); + tilesQuery.close(); + }); case _CmdType.tileExistsInStore: final storeName = cmd.args['storeName']! as String; final url = cmd.args['url']! as String; @@ -433,17 +431,16 @@ Future _worker( .query(ObjectBoxTile_.url.equals(url)) .build(); - sendRes( - id: cmd.id, - data: { - 'wasOrphan': - deleteTiles(storesQuery: storesQuery, tilesQuery: tilesQuery) == - 1, - }, - ); + deleteTiles(storesQuery: storesQuery, tilesQuery: tilesQuery) + .then((orphans) { + sendRes( + id: cmd.id, + data: {'wasOrphan': orphans == 1}, + ); - storesQuery.close(); - tilesQuery.close(); + storesQuery.close(); + tilesQuery.close(); + }); case _CmdType.registerHitOrMiss: final storeName = cmd.args['storeName']! as String; final hit = cmd.args['hit']! as bool; @@ -495,20 +492,23 @@ Future _worker( if (numToRemove <= 0) { sendRes(id: cmd.id, data: {'numOrphans': 0}); + + storeQuery.close(); + tilesQuery.close(); } else { tilesQuery.limit = numToRemove; - sendRes( - id: cmd.id, - data: { - 'numOrphans': - deleteTiles(storesQuery: storeQuery, tilesQuery: tilesQuery), - }, - ); - } + deleteTiles(storesQuery: storeQuery, tilesQuery: tilesQuery) + .then((orphans) { + sendRes( + id: cmd.id, + data: {'numOrphans': orphans}, + ); - storeQuery.close(); - tilesQuery.close(); + storeQuery.close(); + tilesQuery.close(); + }); + } case _CmdType.removeTilesOlderThan: final storeName = cmd.args['storeName']! as String; final expiry = cmd.args['expiry']! as DateTime; @@ -526,15 +526,17 @@ Future _worker( )) .build(); - sendRes( - id: cmd.id, - data: { - 'numOrphans': - deleteTiles(storesQuery: storesQuery, tilesQuery: tilesQuery), - }, - ); + deleteTiles(storesQuery: storesQuery, tilesQuery: tilesQuery) + .then((orphans) { + sendRes( + id: cmd.id, + data: {'numOrphans': orphans}, + ); + + storesQuery.close(); + tilesQuery.close(); + }); - tilesQuery.close(); case _CmdType.readMetadata: final storeName = cmd.args['storeName']! as String; @@ -694,20 +696,6 @@ Future _worker( ?.toRegion(), }, ); - case _CmdType.startRecovery: - final id = cmd.args['id']! as int; - final storeName = cmd.args['storeName']! as String; - final region = cmd.args['region']! as DownloadableRegion; - - root.box().put( - ObjectBoxRecovery.fromRegion( - refId: id, - storeName: storeName, - region: region, - ), - ); - - sendRes(id: cmd.id); case _CmdType.cancelRecovery: final id = cmd.args['id']! as int; @@ -1050,7 +1038,7 @@ Future _worker( ).toList(), }) .then( - (storesToImport) { + (storesToImport) async { sendRes( id: cmd.id, data: { @@ -1105,188 +1093,184 @@ Future _worker( .findUnique()!, ); - root - .runInTransaction( - TxMode.write, - () { - if (strategy == ImportConflictStrategy.replace) { - final storesQuery = root - .box() - .query(ObjectBoxStore_.name.oneOf(storesToImport)) - .build(); - final tilesQuery = (root.box().query() - ..linkMany( - ObjectBoxTile_.stores, - ObjectBoxStore_.name.oneOf(storesToImport), - )) - .build(); - - deleteTiles( - storesQuery: storesQuery, - tilesQuery: tilesQuery, - ); - - final importingStoresQuery = importingRoot - .box() - .query(ObjectBoxStore_.name.oneOf(storesToImport)) - .build(); - - final importingStores = importingStoresQuery.find(); - - storesQuery.remove(); - - root.box().putMany( - List.generate( - importingStores.length, - (i) => ObjectBoxStore( - name: importingStores[i].name, - length: importingStores[i].length, - size: importingStores[i].size, - hits: importingStores[i].hits, - misses: importingStores[i].misses, - metadataJson: importingStores[i].metadataJson, - ), - growable: false, - ), - mode: PutMode.insert, - ); - - storesQuery.close(); - tilesQuery.close(); - importingStoresQuery.close(); - } - - return importingTiles.map((importingTile) { - final convertedRelatedStores = - convertToExistingStores(importingTile.stores); - - final existingTile = (existingTilesQuery - ..param(ObjectBoxTile_.url).value = - importingTile.url) - .findUnique(); - - if (existingTile == null) { - root.box().put( - ObjectBoxTile( - url: importingTile.url, - bytes: importingTile.bytes, - lastModified: importingTile.lastModified, - )..stores.addAll(convertedRelatedStores), - mode: PutMode.insert, - ); - - // No need to modify store stats, because if tile didn't - // already exist, then was not present in an existing - // store that needs changing, and all importing stores - // are brand new and already contain accurate stats. - // EXCEPT in merge mode - importing stores may not be - // new. - if (strategy == ImportConflictStrategy.merge) { - // No need to worry if it was brand new, we use the - // same logic, treating it as an existing related - // store, because when we created it, we made it - // empty. - for (final convertedRelatedStore - in convertedRelatedStores) { - storesToUpdate[convertedRelatedStore.name] = - (storesToUpdate[convertedRelatedStore.name] ?? - convertedRelatedStore) - ..length += 1 - ..size += importingTile.bytes.lengthInBytes; - } - } + if (strategy == ImportConflictStrategy.replace) { + final storesQuery = root + .box() + .query(ObjectBoxStore_.name.oneOf(storesToImport)) + .build(); + final tilesQuery = (root.box().query() + ..linkMany( + ObjectBoxTile_.stores, + ObjectBoxStore_.name.oneOf(storesToImport), + )) + .build(); + + await deleteTiles( + storesQuery: storesQuery, + tilesQuery: tilesQuery, + ); - rootDeltaLength++; - rootDeltaSize += importingTile.bytes.lengthInBytes; + final importingStoresQuery = importingRoot + .box() + .query(ObjectBoxStore_.name.oneOf(storesToImport)) + .build(); + + final importingStores = importingStoresQuery.find(); + + storesQuery.remove(); + + root.box().putMany( + List.generate( + importingStores.length, + (i) => ObjectBoxStore( + name: importingStores[i].name, + length: importingStores[i].length, + size: importingStores[i].size, + hits: importingStores[i].hits, + misses: importingStores[i].misses, + metadataJson: importingStores[i].metadataJson, + ), + growable: false, + ), + mode: PutMode.insert, + ); - return 1; - } + storesQuery.close(); + tilesQuery.close(); + importingStoresQuery.close(); + } - final existingTileIsNewer = existingTile.lastModified - .isAfter(importingTile.lastModified) || - existingTile.lastModified == - importingTile.lastModified; + final numImportedTiles = await root + .runInTransaction( + TxMode.write, + () => importingTiles.map((importingTile) { + final convertedRelatedStores = + convertToExistingStores(importingTile.stores); - final relations = { - ...existingTile.stores, - ...convertedRelatedStores, - }; + final existingTile = (existingTilesQuery + ..param(ObjectBoxTile_.url).value = importingTile.url) + .findUnique(); + if (existingTile == null) { root.box().put( ObjectBoxTile( url: importingTile.url, - bytes: existingTileIsNewer - ? existingTile.bytes - : importingTile.bytes, - lastModified: existingTileIsNewer - ? existingTile.lastModified - : importingTile.lastModified, - )..stores.addAll(relations), + bytes: importingTile.bytes, + lastModified: importingTile.lastModified, + )..stores.addAll(convertedRelatedStores), + mode: PutMode.insert, ); + // No need to modify store stats, because if tile didn't + // already exist, then was not present in an existing + // store that needs changing, and all importing stores + // are brand new and already contain accurate stats. + // EXCEPT in merge mode - importing stores may not be + // new. if (strategy == ImportConflictStrategy.merge) { - for (final newConvertedRelatedStore + // No need to worry if it was brand new, we use the + // same logic, treating it as an existing related + // store, because when we created it, we made it + // empty. + for (final convertedRelatedStore in convertedRelatedStores) { - if (existingTile.stores - .map((e) => e.name) - .contains(newConvertedRelatedStore.name)) { - continue; - } - - storesToUpdate[newConvertedRelatedStore.name] = - (storesToUpdate[newConvertedRelatedStore.name] ?? - newConvertedRelatedStore) + storesToUpdate[convertedRelatedStore.name] = + (storesToUpdate[convertedRelatedStore.name] ?? + convertedRelatedStore) ..length += 1 - ..size += (existingTileIsNewer - ? existingTile - : importingTile) - .bytes - .lengthInBytes; + ..size += importingTile.bytes.lengthInBytes; } } - if (existingTileIsNewer) return null; + rootDeltaLength++; + rootDeltaSize += importingTile.bytes.lengthInBytes; + + return 1; + } + + final existingTileIsNewer = existingTile.lastModified + .isAfter(importingTile.lastModified) || + existingTile.lastModified == importingTile.lastModified; + + final relations = { + ...existingTile.stores, + ...convertedRelatedStores, + }; + + root.box().put( + ObjectBoxTile( + url: importingTile.url, + bytes: existingTileIsNewer + ? existingTile.bytes + : importingTile.bytes, + lastModified: existingTileIsNewer + ? existingTile.lastModified + : importingTile.lastModified, + )..stores.addAll(relations), + ); + + if (strategy == ImportConflictStrategy.merge) { + for (final newConvertedRelatedStore + in convertedRelatedStores) { + if (existingTile.stores + .map((e) => e.name) + .contains(newConvertedRelatedStore.name)) { + continue; + } - for (final existingTileStore in existingTile.stores) { - storesToUpdate[existingTileStore.name] = - (storesToUpdate[existingTileStore.name] ?? - existingTileStore) - ..size += -existingTile.bytes.lengthInBytes + - importingTile.bytes.lengthInBytes; + storesToUpdate[newConvertedRelatedStore.name] = + (storesToUpdate[newConvertedRelatedStore.name] ?? + newConvertedRelatedStore) + ..length += 1 + ..size += (existingTileIsNewer + ? existingTile + : importingTile) + .bytes + .lengthInBytes; } + } - rootDeltaSize += -existingTile.bytes.lengthInBytes + - importingTile.bytes.lengthInBytes; + if (existingTileIsNewer) return null; - return 1; - }); - }, + for (final existingTileStore in existingTile.stores) { + storesToUpdate[existingTileStore.name] = + (storesToUpdate[existingTileStore.name] ?? + existingTileStore) + ..size += -existingTile.bytes.lengthInBytes + + importingTile.bytes.lengthInBytes; + } + + rootDeltaSize += -existingTile.bytes.lengthInBytes + + importingTile.bytes.lengthInBytes; + + return 1; + }), ) .where((e) => e != null) - .length - .then((numImportedTiles) { - root.box().putMany( - storesToUpdate.values.toList(), - mode: PutMode.update, - ); - updateRootStatistics( - deltaLength: rootDeltaLength, - deltaSize: rootDeltaSize, - ); + .length; - importingTilesQuery.close(); - existingStoresQuery.close(); - existingTilesQuery.close(); - cleanup(); + root.box().putMany( + storesToUpdate.values.toList(), + mode: PutMode.update, + ); - sendRes( - id: cmd.id, - data: { - 'expectStream': true, - 'complete': numImportedTiles, - }, - ); - }); + updateRootStatistics( + deltaLength: rootDeltaLength, + deltaSize: rootDeltaSize, + ); + + importingTilesQuery.close(); + existingStoresQuery.close(); + existingTilesQuery.close(); + cleanup(); + + sendRes( + id: cmd.id, + data: { + 'expectStream': true, + 'complete': numImportedTiles, + }, + ); }, ); case _CmdType.listImportableStores: diff --git a/lib/src/backend/impls/objectbox/backend/internal_workers/thread_safe.dart b/lib/src/backend/impls/objectbox/backend/internal_workers/thread_safe.dart index 99e09f3e..297b2580 100644 --- a/lib/src/backend/impls/objectbox/backend/internal_workers/thread_safe.dart +++ b/lib/src/backend/impls/objectbox/backend/internal_workers/thread_safe.dart @@ -23,11 +23,14 @@ class _ObjectBoxBackendThreadSafeImpl implements FMTCBackendInternalThreadSafe { @override void uninitialise() { - expectInitialisedRoot; - _root!.close(); + expectInitialisedRoot.close(); _root = null; } + @override + _ObjectBoxBackendThreadSafeImpl duplicate() => + _ObjectBoxBackendThreadSafeImpl._(storeReference: storeReference); + @override Future readTile({ required String url, @@ -142,4 +145,46 @@ class _ObjectBoxBackendThreadSafeImpl implements FMTCBackendInternalThreadSafe { tilesQuery.close(); storeQuery.close(); } + + @override + void startRecovery({ + required int id, + required String storeName, + required DownloadableRegion region, + required int endTile, + }) => + expectInitialisedRoot.box().put( + ObjectBoxRecovery.fromRegion( + refId: id, + storeName: storeName, + region: region, + endTile: endTile, + ), + mode: PutMode.insert, + ); + + @override + void updateRecovery({ + required int id, + required int newStartTile, + }) { + expectInitialisedRoot; + + final existingRecoveryQuery = _root! + .box() + .query(ObjectBoxRecovery_.refId.equals(id)) + .build(); + + _root!.runInTransaction( + TxMode.write, + () { + _root!.box().put( + existingRecoveryQuery.findUnique()!..startTile = newStartTile, + mode: PutMode.update, + ); + }, + ); + + existingRecoveryQuery.close(); + } } diff --git a/lib/src/backend/impls/objectbox/models/generated/objectbox.g.dart b/lib/src/backend/impls/objectbox/models/generated/objectbox.g.dart index 558df525..34e04171 100644 --- a/lib/src/backend/impls/objectbox/models/generated/objectbox.g.dart +++ b/lib/src/backend/impls/objectbox/models/generated/objectbox.g.dart @@ -361,7 +361,7 @@ obx_int.ModelDefinition getObjectBoxModel() { final startTileParam = const fb.Int64Reader().vTableGet(buffer, rootOffset, 16, 0); final endTileParam = - const fb.Int64Reader().vTableGetNullable(buffer, rootOffset, 18); + const fb.Int64Reader().vTableGet(buffer, rootOffset, 18, 0); final rectNwLatParam = const fb.Float64Reader() .vTableGetNullable(buffer, rootOffset, 22); final rectNwLngParam = const fb.Float64Reader() diff --git a/lib/src/backend/impls/objectbox/models/src/recovery.dart b/lib/src/backend/impls/objectbox/models/src/recovery.dart index 097a69e7..603bc5a1 100644 --- a/lib/src/backend/impls/objectbox/models/src/recovery.dart +++ b/lib/src/backend/impls/objectbox/models/src/recovery.dart @@ -43,6 +43,7 @@ base class ObjectBoxRecovery { required this.refId, required this.storeName, required DownloadableRegion region, + required this.endTile, }) : creationTime = DateTime.timestamp(), typeId = region.when( rectangle: (_) => 0, @@ -53,7 +54,6 @@ base class ObjectBoxRecovery { minZoom = region.minZoom, maxZoom = region.maxZoom, startTile = region.start, - endTile = region.end, rectNwLat = region.originalRegion is RectangleRegion ? (region.originalRegion as RectangleRegion) .bounds @@ -144,7 +144,7 @@ base class ObjectBoxRecovery { int startTile; /// Corresponds to [RecoveredRegion.end] & [DownloadableRegion.end] - int? endTile; + int endTile; /// Corresponds to the generic type of [DownloadableRegion] /// diff --git a/lib/src/backend/interfaces/backend/internal.dart b/lib/src/backend/interfaces/backend/internal.dart index 26e61043..2935697e 100644 --- a/lib/src/backend/interfaces/backend/internal.dart +++ b/lib/src/backend/interfaces/backend/internal.dart @@ -275,14 +275,6 @@ abstract interface class FMTCBackendInternal required int id, }); - /// Create a recovery store with a recoverable region from the specified - /// components - Future startRecovery({ - required int id, - required String storeName, - required DownloadableRegion region, - }); - /// {@template fmtc.backend.cancelRecovery} /// Safely cancel the specified recoverable region /// {@endtemplate} diff --git a/lib/src/backend/interfaces/backend/internal_thread_safe.dart b/lib/src/backend/interfaces/backend/internal_thread_safe.dart index 61486b30..e38d173a 100644 --- a/lib/src/backend/interfaces/backend/internal_thread_safe.dart +++ b/lib/src/backend/interfaces/backend/internal_thread_safe.dart @@ -4,15 +4,16 @@ import 'dart:async'; import 'dart:typed_data'; -import '../../export_external.dart'; +import '../../../../flutter_map_tile_caching.dart'; import '../../export_internal.dart'; /// An abstract interface that FMTC will use to communicate with a storage /// 'backend' (usually one root), from an existing bulk downloading thread /// /// Should implement methods that operate in the same thread. Must be sendable -/// between isolates, because it will be operated in another thread. Must be -/// suitable for simultaneous [initialise]ation across multiple threads. +/// between isolates when uninitialised, because it will be operated in another +/// thread. Must be suitable for simultaneous [initialise]ation across multiple +/// threads. /// /// Should be set-up ready for intialisation, and set in the /// [FMTCBackendAccessThreadSafe], from the initialisation of @@ -34,6 +35,13 @@ abstract interface class FMTCBackendInternalThreadSafe { /// Stop this thread safe database operator FutureOr uninitialise(); + /// Create another instance of this internal thread that relies on the same + /// root + /// + /// This method makes another uninitialised instance which must be safe to + /// send through isolates, unlike an initialised instance. + FMTCBackendInternalThreadSafe duplicate(); + /// Retrieve a raw tile by the specified URL /// /// If [storeName] is specified, the tile will be limited to the specified @@ -55,12 +63,25 @@ abstract interface class FMTCBackendInternalThreadSafe { /// Create or update multiple tiles (given given their respective [urls] and /// [bytess]) in the specified store - /// - /// Implementation should avoid iterating [writeTile], as this should be - /// targeted for high throughput and efficiency. FutureOr writeTiles({ required String storeName, required List urls, required List bytess, }); + + /// Create a recovery entity with a recoverable region from the specified + /// components + FutureOr startRecovery({ + required int id, + required String storeName, + required DownloadableRegion region, + required int endTile, + }); + + /// Update the specified recovery entity with the new [RecoveredRegion.start] + /// (equivalent) + FutureOr updateRecovery({ + required int id, + required int newStartTile, + }); } diff --git a/lib/src/bulk_download/download_progress.dart b/lib/src/bulk_download/download_progress.dart index 36190855..0321af29 100644 --- a/lib/src/bulk_download/download_progress.dart +++ b/lib/src/bulk_download/download_progress.dart @@ -105,6 +105,11 @@ class DownloadProgress { /// The total number of tiles available to be potentially downloaded and /// cached + /// + /// The difference between [DownloadableRegion.end] - + /// [DownloadableRegion.start] (assuming the maximum number of tiles actually + /// available in the region, as determined by [StoreDownload.check], if + /// [DownloadableRegion.end] is `null`). final int maxTiles; /// The current elapsed duration of the download diff --git a/lib/src/bulk_download/manager.dart b/lib/src/bulk_download/manager.dart index d20aea51..867aa177 100644 --- a/lib/src/bulk_download/manager.dart +++ b/lib/src/bulk_download/manager.dart @@ -14,7 +14,8 @@ Future _downloadManager( bool skipSeaTiles, Duration? maxReportInterval, int? rateLimit, - Iterable obscuredQueryParams, + List obscuredQueryParams, + int? recoveryId, FMTCBackendInternalThreadSafe backend, }) input, ) async { @@ -84,15 +85,10 @@ Future _downloadManager( onExit: tileReceivePort.sendPort, debugName: '[FMTC] Tile Coords Generator Thread', ); - final rawTileStream = tileReceivePort.skip(input.region.start).take( - input.region.end == null - ? largestInt - : (input.region.end! - input.region.start), - ); final tileQueue = StreamQueue( input.rateLimit == null - ? rawTileStream - : rawTileStream.rateLimit( + ? tileReceivePort + : tileReceivePort.rateLimit( minimumSpacing: Duration( microseconds: ((1 / input.rateLimit!) * 1000000).ceil(), ), @@ -176,6 +172,21 @@ Future _downloadManager( }, ); + // Start recovery system (unless disabled) + if (input.recoveryId case final recoveryId?) { + await input.backend.initialise(); + await input.backend.startRecovery( + id: recoveryId, + storeName: input.storeName, + region: input.region, + endTile: min(input.region.end ?? largestInt, maxTiles), + ); + send(2); + } + + // Duplicate the backend to make it safe to send through isolates + final threadBackend = input.backend.duplicate(); + // Now it's safe, start accepting communications from the root send(rootReceivePort.sendPort); @@ -200,7 +211,7 @@ Future _downloadManager( seaTileBytes: seaTileBytes, obscuredQueryParams: input.obscuredQueryParams, headers: headers, - backend: input.backend, + backend: threadBackend, ), onExit: downloadThreadReceivePort.sendPort, debugName: '[FMTC] Bulk Download Thread #$threadNo', @@ -265,6 +276,16 @@ Future _downloadManager( ), ); } + + if (input.recoveryId case final recoveryId?) { + input.backend.updateRecovery( + id: recoveryId, + newStartTile: 1 + + (lastDownloadProgress.cachedTiles - + lastDownloadProgress.bufferedTiles), + ); + } + return; } @@ -329,6 +350,7 @@ Future _downloadManager( // Cleanup resources and shutdown rootReceivePort.close(); + if (input.recoveryId != null) await input.backend.uninitialise(); tileIsolate.kill(priority: Isolate.immediate); await tileQueue.cancel(immediate: true); Isolate.exit(); diff --git a/lib/src/bulk_download/tile_loops/count.dart b/lib/src/bulk_download/tile_loops/count.dart index a0ab80b8..f4c5ae39 100644 --- a/lib/src/bulk_download/tile_loops/count.dart +++ b/lib/src/bulk_download/tile_loops/count.dart @@ -24,6 +24,12 @@ part of 'shared.dart'; /// automated tests. @internal class TileCounters { + /// Trim [numOfTiles] to between the [region]'s [DownloadableRegion.start] and + /// [DownloadableRegion.end] + static int _trimToRange(DownloadableRegion region, int numOfTiles) => + min(region.end ?? largestInt, numOfTiles) - + min(region.start - 1, numOfTiles); + /// Returns the number of tiles within a [DownloadableRegion] with generic type /// [RectangleRegion] @internal @@ -50,7 +56,7 @@ class TileCounters { (sePoint.x - nwPoint.x + 1) * (sePoint.y - nwPoint.y + 1); } - return numberOfTiles; + return _trimToRange(region, numberOfTiles); } /// Returns the number of tiles within a [DownloadableRegion] with generic type @@ -94,7 +100,7 @@ class TileCounters { } } - return numberOfTiles; + return _trimToRange(region, numberOfTiles); } /// Returns the number of tiles within a [DownloadableRegion] with generic type @@ -236,7 +242,7 @@ class TileCounters { } } - return numberOfTiles; + return _trimToRange(region, numberOfTiles); } /// Returns the number of tiles within a [DownloadableRegion] with generic type @@ -302,6 +308,6 @@ class TileCounters { numberOfTiles += allOutlineTiles.length; } - return numberOfTiles; + return _trimToRange(region, numberOfTiles); } } diff --git a/lib/src/bulk_download/tile_loops/generate.dart b/lib/src/bulk_download/tile_loops/generate.dart index 4982f6a1..430491ca 100644 --- a/lib/src/bulk_download/tile_loops/generate.dart +++ b/lib/src/bulk_download/tile_loops/generate.dart @@ -37,6 +37,10 @@ class TileGenerators { input.sendPort.send(receivePort.sendPort); final requestQueue = StreamQueue(receivePort); + int tileCounter = -1; + final start = region.start - 1; + final end = (region.end ?? double.infinity) - 1; + for (double zoomLvl = region.minZoom.toDouble(); zoomLvl <= region.maxZoom; zoomLvl++) { @@ -50,6 +54,8 @@ class TileGenerators { for (int x = nwPoint.x; x <= sePoint.x; x++) { for (int y = nwPoint.y; y <= sePoint.y; y++) { + tileCounter++; + if (tileCounter < start || tileCounter > end) continue; await requestQueue.next; input.sendPort.send((x, y, zoomLvl.toInt())); } @@ -87,6 +93,10 @@ class TileGenerators { // Format: Map>> final Map>> outlineTileNums = {}; + int tileCounter = -1; + final start = region.start - 1; + final end = (region.end ?? double.infinity) - 1; + for (int zoomLvl = region.minZoom; zoomLvl <= region.maxZoom; zoomLvl++) { outlineTileNums[zoomLvl] = {}; @@ -112,6 +122,8 @@ class TileGenerators { for (int y = outlineTileNums[zoomLvl]![x]![0]; y <= outlineTileNums[zoomLvl]![x]![1]; y++) { + tileCounter++; + if (tileCounter < start || tileCounter > end) continue; await requestQueue.next; input.sendPort.send((x, y, zoomLvl)); } @@ -176,6 +188,10 @@ class TileGenerators { input.sendPort.send(receivePort.sendPort); final requestQueue = StreamQueue(receivePort); + int tileCounter = -1; + final start = region.start - 1; + final end = (region.end ?? double.infinity) - 1; + for (double zoomLvl = region.minZoom.toDouble(); zoomLvl <= region.maxZoom; zoomLvl++) { @@ -242,6 +258,8 @@ class TileGenerators { for (int x = straightRectangleNW.x; x <= straightRectangleSE.x; x++) { bool foundOverlappingTile = false; for (int y = straightRectangleNW.y; y <= straightRectangleSE.y; y++) { + tileCounter++; + if (tileCounter < start || tileCounter > end) continue; final tile = _Polygon( Point(x, y), Point(x + 1, y), @@ -286,6 +304,10 @@ class TileGenerators { input.sendPort.send(receivePort.sendPort); final requestQueue = StreamQueue(receivePort); + int tileCounter = -1; + final start = region.start - 1; + final end = (region.end ?? double.infinity) - 1; + for (double zoomLvl = region.minZoom.toDouble(); zoomLvl <= region.maxZoom; zoomLvl++) { @@ -340,6 +362,8 @@ class TileGenerators { } for (final Point(:x, :y) in allOutlineTiles) { + tileCounter++; + if (tileCounter < start || tileCounter > end) continue; await requestQueue.next; input.sendPort.send((x, y, zoomLvl.toInt())); } diff --git a/lib/src/regions/base_region.dart b/lib/src/regions/base_region.dart index e13d49f5..9b293fad 100644 --- a/lib/src/regions/base_region.dart +++ b/lib/src/regions/base_region.dart @@ -52,7 +52,7 @@ sealed class BaseRegion { required int minZoom, required int maxZoom, required TileLayer options, - int start = 0, + int start = 1, int? end, Crs crs = const Epsg3857(), }); diff --git a/lib/src/regions/circle.dart b/lib/src/regions/circle.dart index 73f53d88..ec94b8d1 100644 --- a/lib/src/regions/circle.dart +++ b/lib/src/regions/circle.dart @@ -29,7 +29,7 @@ class CircleRegion extends BaseRegion { required int minZoom, required int maxZoom, required TileLayer options, - int start = 0, + int start = 1, int? end, Crs crs = const Epsg3857(), }) => diff --git a/lib/src/regions/custom_polygon.dart b/lib/src/regions/custom_polygon.dart index 2d043959..3f88324a 100644 --- a/lib/src/regions/custom_polygon.dart +++ b/lib/src/regions/custom_polygon.dart @@ -26,7 +26,7 @@ class CustomPolygonRegion extends BaseRegion { required int minZoom, required int maxZoom, required TileLayer options, - int start = 0, + int start = 1, int? end, Crs crs = const Epsg3857(), }) => diff --git a/lib/src/regions/downloadable_region.dart b/lib/src/regions/downloadable_region.dart index d284d6e7..8ef5d3f0 100644 --- a/lib/src/regions/downloadable_region.dart +++ b/lib/src/regions/downloadable_region.dart @@ -21,9 +21,9 @@ class DownloadableRegion { '`minZoom` must be less than or equal to `maxZoom`', ); } - if (start < 0 || start > (end ?? 0)) { + if (start < 1 || start > (end ?? double.infinity)) { throw ArgumentError( - '`start` must be greater or equal to 0 and less than or equal to `end`', + '`start` must be greater or equal to 1 and less than or equal to `end`', ); } } @@ -43,15 +43,15 @@ class DownloadableRegion { /// The options used to fetch tiles final TileLayer options; - /// Optionally skip past a number of tiles 'at the start' of a region + /// Optionally skip any tiles before this tile /// /// The order of the tiles in a region is directly chosen by the underlying /// tile generators, and so may not be stable between updates. /// - /// Set to 0 to skip none, which is the default. + /// Set to 1 to skip none, which is the default. final int start; - /// Optionally skip a number of tiles 'at the end' of a region + /// Optionally skip any tiles after this tile /// /// The order of the tiles in a region is directly chosen by the underlying /// tile generators, and so may not be stable between updates. diff --git a/lib/src/regions/line.dart b/lib/src/regions/line.dart index 9af8183b..467549eb 100644 --- a/lib/src/regions/line.dart +++ b/lib/src/regions/line.dart @@ -77,7 +77,7 @@ class LineRegion extends BaseRegion { required int minZoom, required int maxZoom, required TileLayer options, - int start = 0, + int start = 1, int? end, Crs crs = const Epsg3857(), }) => diff --git a/lib/src/regions/recovered_region.dart b/lib/src/regions/recovered_region.dart index 88a1e15e..357e6d58 100644 --- a/lib/src/regions/recovered_region.dart +++ b/lib/src/regions/recovered_region.dart @@ -50,11 +50,17 @@ class RecoveredRegion { /// Corresponds to [DownloadableRegion.maxZoom] final int maxZoom; - ///Corresponds to [DownloadableRegion.start] + /// Corresponds to [DownloadableRegion.start] + /// + /// May not match as originally created, may be the last successful tile. The + /// interval between [start] and [end] is the failed interval. final int start; /// Corresponds to [DownloadableRegion.end] - final int? end; + /// + /// If originally created as `null`, this will be the number of tiles in the + /// region, as determined by [StoreDownload.check]. + final int end; /// Corresponds to [RectangleRegion.bounds] final LatLngBounds? bounds; diff --git a/lib/src/regions/rectangle.dart b/lib/src/regions/rectangle.dart index 8c72dc44..62a0bf0c 100644 --- a/lib/src/regions/rectangle.dart +++ b/lib/src/regions/rectangle.dart @@ -28,7 +28,7 @@ class RectangleRegion extends BaseRegion { required int minZoom, required int maxZoom, required TileLayer options, - int start = 0, + int start = 1, int? end, Crs crs = const Epsg3857(), }) => diff --git a/lib/src/root/recovery.dart b/lib/src/root/recovery.dart index 1fb3ae5f..592e3665 100644 --- a/lib/src/root/recovery.dart +++ b/lib/src/root/recovery.dart @@ -23,18 +23,17 @@ part of '../../flutter_map_tile_caching.dart'; /// /// The recovery system then allows the original [BaseRegion] and /// [DownloadableRegion] to be recovered (via [RecoveredRegion]) from the failed -/// download, and the download can be restarted as normal. +/// download, and the download can be restarted. +/// +/// During a download, the database recovery entity is updated every tile (or +/// every batch) with the number of completed tiles: this allows the +/// [DownloadableRegion.start] to have it's value set to skip tiles that have +/// been successfully downloaded. Therefore, no unnecessary tiles are downloaded +/// again. /// /// > [!NOTE] /// > Options set at download time, in [StoreDownload.startForeground], are not /// > included. -/// -/// > [!IMPORTANT] -/// > The recovery system does not keep track of the download, therefore it is -/// > unknown where the download failed, and the download must be restarted from -/// > the beginning. -/// > -/// > To workaround this limitation, set `skipExistingTiles` `true`. class RootRecovery { RootRecovery._() { _instance = this; @@ -72,16 +71,6 @@ class RootRecovery { return (isFailed: !_downloadsOngoing.contains(region.id), region: region); } - Future _start({ - required int id, - required String storeName, - required DownloadableRegion region, - }) async { - _downloadsOngoing.add(id); - await FMTCBackendAccess.internal - .startRecovery(id: id, storeName: storeName, region: region); - } - /// {@macro fmtc.backend.cancelRecovery} Future cancel(int id) async => FMTCBackendAccess.internal.cancelRecovery(id: id); diff --git a/lib/src/store/download.dart b/lib/src/store/download.dart index 8123153f..52c2257b 100644 --- a/lib/src/store/download.dart +++ b/lib/src/store/download.dart @@ -171,16 +171,10 @@ class StoreDownload { ); } - // Start recovery system (unless disabled) - final recoveryId = - Object.hash(instanceId, DateTime.timestamp().millisecondsSinceEpoch); - if (!disableRecovery) { - await FMTCRoot.recovery._start( - id: recoveryId, - storeName: _storeName, - region: region, - ); - } + // Generate recovery ID (unless disabled) + final recoveryId = disableRecovery + ? null + : Object.hash(instanceId, DateTime.timestamp().millisecondsSinceEpoch); // Start download thread final receivePort = ReceivePort(); @@ -197,8 +191,9 @@ class StoreDownload { maxReportInterval: maxReportInterval, rateLimit: rateLimit, obscuredQueryParams: - obscuredQueryParams?.map((e) => RegExp('$e=[^&]*')) ?? - FMTCTileProviderSettings.instance.obscuredQueryParams, + obscuredQueryParams?.map((e) => RegExp('$e=[^&]*')).toList() ?? + FMTCTileProviderSettings.instance.obscuredQueryParams.toList(), + recoveryId: recoveryId, backend: FMTCBackendAccessThreadSafe.internal, ), onExit: receivePort.sendPort, @@ -216,15 +211,21 @@ class StoreDownload { continue; } - // Handle shutdown (both normal and cancellation) - if (evt == null) break; - // 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 @@ -243,11 +244,13 @@ class StoreDownload { }; continue; } + + throw UnimplementedError('Unrecognised message'); } // Handle shutdown (both normal and cancellation) receivePort.close(); - await FMTCRoot.recovery.cancel(recoveryId); + if (recoveryId != null) await FMTCRoot.recovery.cancel(recoveryId); DownloadInstance.unregister(instanceId); cancelCompleter.complete(); } diff --git a/test/region_tile_test.dart b/test/region_tile_test.dart index 232aaab2..3ec60989 100644 --- a/test/region_tile_test.dart +++ b/test/region_tile_test.dart @@ -55,11 +55,6 @@ void main() { () => expect(TileCounters.rectangleTiles(rectRegion), 179196), ); - test( - 'Count By Generator', - () async => expect(await countByGenerator(rectRegion), 179196), - ); - test( 'Counter Duration', () => print( @@ -77,18 +72,116 @@ void main() { ); test( - 'Generator Duration', + 'Generator Duration & Count', () async { final clock = Stopwatch()..start(); - await countByGenerator(rectRegion); + final tiles = await countByGenerator(rectRegion); clock.stop(); print('${clock.elapsedMilliseconds / 1000} s'); + expect(tiles, 179196); }, ); }, timeout: const Timeout(Duration(minutes: 1)), ); + group( + 'Ranged Region', + () { + test( + 'Start Offset Count', + () { + final region = RectangleRegion( + LatLngBounds(const LatLng(-1, -1), const LatLng(1, 1)), + ).toDownloadable( + minZoom: 1, + maxZoom: 16, + options: TileLayer(), + start: 10, + ); + expect(TileCounters.rectangleTiles(region), 179187); + }, + ); + + test( + 'End Offset Count', + () { + final region = RectangleRegion( + LatLngBounds(const LatLng(-1, -1), const LatLng(1, 1)), + ).toDownloadable( + minZoom: 1, + maxZoom: 16, + options: TileLayer(), + end: 100, + ); + expect(TileCounters.rectangleTiles(region), 100); + }, + ); + + test( + 'Start & End Offset Count', + () { + final region = RectangleRegion( + LatLngBounds(const LatLng(-1, -1), const LatLng(1, 1)), + ).toDownloadable( + minZoom: 1, + maxZoom: 16, + options: TileLayer(), + start: 10, + end: 100, + ); + expect(TileCounters.rectangleTiles(region), 91); + }, + ); + + test( + 'Start Offset Generate', + () async { + final region = RectangleRegion( + LatLngBounds(const LatLng(-1, -1), const LatLng(1, 1)), + ).toDownloadable( + minZoom: 1, + maxZoom: 16, + options: TileLayer(), + start: 10, + ); + expect(await countByGenerator(region), 179187); + }, + ); + + test( + 'End Offset Generate', + () async { + final region = RectangleRegion( + LatLngBounds(const LatLng(-1, -1), const LatLng(1, 1)), + ).toDownloadable( + minZoom: 1, + maxZoom: 16, + options: TileLayer(), + end: 100, + ); + expect(await countByGenerator(region), 100); + }, + ); + + test( + 'Start & End Offset Generate', + () async { + final region = RectangleRegion( + LatLngBounds(const LatLng(-1, -1), const LatLng(1, 1)), + ).toDownloadable( + minZoom: 1, + maxZoom: 16, + options: TileLayer(), + start: 10, + end: 100, + ); + expect(await countByGenerator(region), 91); + }, + ); + }, + ); + group( 'Circle Region', () { diff --git a/tile_server/.gitignore b/tile_server/.gitignore index fc374886..d83edd1b 100644 --- a/tile_server/.gitignore +++ b/tile_server/.gitignore @@ -1,2 +1,3 @@ bin/tile_server.exe -source/placeholders \ No newline at end of file +source/placeholders +generated/ \ No newline at end of file diff --git a/tile_server/static/generated/favicon.dart b/tile_server/static/generated/favicon.dart deleted file mode 100644 index 7dd43c1b..00000000 --- a/tile_server/static/generated/favicon.dart +++ /dev/null @@ -1,2 +0,0 @@ -// Will be replaced automatically by GitHub Actions -final faviconTileBytes = []; diff --git a/tile_server/static/generated/land.dart b/tile_server/static/generated/land.dart deleted file mode 100644 index 1029ac5f..00000000 --- a/tile_server/static/generated/land.dart +++ /dev/null @@ -1,2 +0,0 @@ -// Will be replaced automatically by GitHub Actions -final landTileBytes = []; diff --git a/tile_server/static/generated/sea.dart b/tile_server/static/generated/sea.dart deleted file mode 100644 index 196bd5af..00000000 --- a/tile_server/static/generated/sea.dart +++ /dev/null @@ -1,2 +0,0 @@ -// Will be replaced automatically by GitHub Actions -final seaTileBytes = []; From 67c0c16dfc34fe7afec56378575f2b58fbc33bd3 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 11 Apr 2024 11:59:34 +0100 Subject: [PATCH 166/168] Fixed failing tests --- tile_server/.gitignore | 3 +-- tile_server/static/generated/favicon.dart | 1 + tile_server/static/generated/land.dart | 1 + tile_server/static/generated/sea.dart | 1 + 4 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 tile_server/static/generated/favicon.dart create mode 100644 tile_server/static/generated/land.dart create mode 100644 tile_server/static/generated/sea.dart diff --git a/tile_server/.gitignore b/tile_server/.gitignore index d83edd1b..fc374886 100644 --- a/tile_server/.gitignore +++ b/tile_server/.gitignore @@ -1,3 +1,2 @@ bin/tile_server.exe -source/placeholders -generated/ \ No newline at end of file +source/placeholders \ No newline at end of file diff --git a/tile_server/static/generated/favicon.dart b/tile_server/static/generated/favicon.dart new file mode 100644 index 00000000..259e34b4 --- /dev/null +++ b/tile_server/static/generated/favicon.dart @@ -0,0 +1 @@ +final faviconTileBytes = [0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 32, 0, 201, 45, 0, 0, 22, 0, 0, 0, 137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 1, 0, 0, 0, 1, 0, 8, 6, 0, 0, 0, 92, 114, 168, 102, 0, 0, 45, 144, 73, 68, 65, 84, 120, 218, 237, 157, 123, 124, 27, 229, 153, 239, 127, 26, 141, 70, 23, 91, 146, 109, 249, 126, 139, 147, 216, 9, 185, 185, 9, 16, 72, 40, 183, 64, 129, 246, 208, 238, 129, 237, 210, 82, 122, 202, 161, 91, 2, 11, 45, 44, 108, 161, 13, 112, 246, 0, 165, 52, 180, 205, 46, 109, 225, 244, 190, 103, 217, 148, 101, 89, 216, 93, 232, 133, 179, 101, 11, 9, 105, 32, 9, 73, 73, 76, 156, 224, 196, 151, 92, 124, 209, 197, 182, 44, 75, 178, 46, 51, 26, 73, 231, 15, 89, 178, 36, 75, 214, 109, 164, 153, 145, 222, 239, 231, 147, 79, 28, 105, 52, 126, 53, 153, 231, 55, 207, 251, 188, 207, 243, 188, 10, 254, 163, 23, 195, 40, 0, 191, 215, 9, 243, 88, 63, 204, 163, 199, 97, 29, 59, 1, 149, 190, 25, 205, 61, 215, 160, 121, 213, 39, 80, 219, 177, 9, 52, 83, 93, 200, 233, 9, 4, 66, 17, 81, 20, 42, 0, 201, 204, 76, 157, 195, 196, 249, 62, 88, 198, 250, 225, 152, 49, 163, 174, 227, 34, 52, 245, 92, 131, 166, 238, 109, 208, 55, 174, 18, 251, 251, 18, 8, 132, 56, 4, 23, 128, 120, 120, 158, 133, 101, 52, 226, 29, 152, 199, 250, 17, 166, 52, 104, 90, 181, 13, 77, 221, 219, 208, 216, 125, 37, 241, 14, 8, 4, 145, 201, 73, 0, 38, 236, 62, 184, 61, 172, 216, 99, 134, 182, 190, 153, 76, 47, 8, 4, 1, 160, 179, 61, 112, 194, 238, 131, 66, 93, 139, 21, 203, 87, 138, 61, 102, 76, 155, 71, 224, 24, 59, 134, 134, 149, 87, 136, 61, 20, 2, 65, 214, 80, 217, 30, 232, 246, 176, 168, 111, 21, 223, 248, 1, 160, 190, 117, 37, 124, 211, 86, 177, 135, 65, 32, 200, 158, 172, 5, 128, 64, 32, 148, 31, 178, 22, 0, 235, 224, 31, 192, 115, 115, 98, 15, 131, 64, 144, 45, 89, 199, 0, 164, 200, 200, 219, 79, 227, 200, 244, 121, 212, 117, 94, 130, 166, 158, 107, 208, 176, 242, 10, 24, 155, 214, 138, 61, 44, 2, 65, 54, 228, 45, 0, 111, 255, 231, 191, 225, 143, 111, 255, 22, 0, 160, 209, 106, 209, 220, 186, 12, 87, 95, 127, 19, 150, 175, 92, 19, 59, 230, 223, 94, 252, 49, 250, 251, 222, 143, 253, 123, 109, 239, 197, 248, 252, 237, 247, 1, 0, 206, 142, 12, 224, 159, 255, 225, 239, 97, 53, 143, 97, 237, 134, 139, 241, 63, 255, 234, 155, 168, 170, 210, 3, 0, 142, 31, 61, 128, 87, 95, 252, 9, 156, 179, 51, 184, 120, 203, 85, 184, 253, 174, 111, 164, 28, 195, 117, 55, 61, 6, 158, 103, 97, 29, 63, 9, 203, 232, 126, 28, 57, 240, 99, 4, 194, 10, 52, 245, 92, 131, 198, 149, 87, 162, 177, 231, 106, 48, 154, 26, 177, 175, 49, 129, 32, 89, 242, 22, 0, 135, 125, 18, 77, 45, 29, 248, 243, 47, 109, 199, 156, 195, 137, 211, 39, 251, 240, 204, 223, 222, 139, 47, 221, 245, 16, 46, 191, 250, 70, 0, 192, 208, 233, 227, 184, 230, 147, 159, 197, 138, 158, 200, 83, 89, 171, 173, 2, 0, 112, 156, 31, 127, 247, 212, 131, 248, 226, 95, 62, 136, 222, 139, 46, 195, 203, 255, 247, 7, 120, 101, 247, 243, 248, 242, 61, 143, 192, 238, 180, 227, 255, 236, 122, 12, 247, 239, 248, 30, 58, 186, 186, 241, 179, 103, 255, 55, 222, 248, 143, 221, 184, 241, 207, 111, 79, 253, 5, 104, 53, 218, 187, 46, 68, 123, 215, 133, 0, 0, 183, 203, 6, 243, 249, 15, 49, 241, 254, 79, 113, 236, 245, 7, 161, 111, 90, 131, 166, 149, 87, 161, 185, 231, 19, 168, 237, 188, 72, 236, 235, 77, 32, 72, 138, 130, 166, 0, 90, 93, 21, 154, 27, 151, 1, 141, 64, 247, 234, 94, 116, 116, 117, 227, 185, 239, 237, 192, 37, 151, 93, 11, 134, 209, 192, 53, 235, 192, 138, 158, 181, 232, 88, 214, 157, 240, 185, 161, 83, 253, 48, 24, 106, 176, 245, 202, 27, 0, 0, 55, 221, 126, 47, 118, 220, 125, 51, 190, 248, 149, 7, 209, 119, 96, 47, 214, 125, 108, 51, 214, 245, 110, 6, 0, 252, 217, 95, 124, 25, 191, 124, 254, 219, 105, 5, 32, 25, 189, 161, 9, 171, 55, 92, 143, 213, 27, 174, 7, 207, 179, 176, 219, 206, 96, 226, 124, 31, 250, 254, 253, 85, 120, 125, 30, 52, 173, 186, 38, 226, 33, 116, 95, 5, 117, 85, 189, 216, 215, 159, 64, 16, 21, 65, 99, 0, 189, 23, 94, 6, 138, 82, 98, 232, 84, 63, 214, 245, 110, 134, 219, 237, 68, 223, 159, 246, 227, 248, 7, 7, 208, 209, 213, 141, 222, 11, 47, 3, 0, 88, 39, 206, 163, 61, 78, 20, 76, 70, 19, 0, 192, 53, 235, 128, 213, 60, 138, 214, 182, 174, 216, 123, 237, 93, 61, 152, 180, 78, 228, 247, 229, 104, 53, 154, 218, 214, 160, 169, 109, 13, 112, 217, 23, 224, 247, 58, 49, 113, 190, 15, 230, 99, 187, 113, 252, 119, 223, 132, 198, 216, 129, 166, 85, 215, 162, 105, 229, 85, 36, 177, 136, 80, 145, 8, 30, 4, 172, 51, 53, 194, 237, 114, 0, 0, 62, 245, 103, 183, 197, 94, 255, 247, 151, 126, 134, 55, 127, 251, 47, 120, 248, 241, 231, 224, 247, 121, 161, 102, 212, 9, 159, 211, 234, 244, 112, 187, 103, 193, 113, 44, 106, 106, 23, 158, 204, 85, 85, 122, 4, 2, 28, 60, 30, 119, 44, 70, 144, 47, 26, 157, 17, 43, 215, 92, 133, 149, 107, 174, 2, 0, 76, 217, 134, 97, 29, 59, 129, 83, 255, 185, 3, 179, 179, 54, 152, 150, 109, 65, 211, 170, 107, 208, 188, 234, 90, 84, 213, 46, 43, 222, 85, 39, 16, 36, 130, 224, 2, 48, 61, 101, 129, 222, 80, 11, 0, 9, 110, 251, 117, 159, 254, 28, 190, 122, 251, 245, 24, 59, 63, 140, 106, 67, 13, 216, 179, 131, 9, 159, 243, 121, 221, 208, 84, 49, 208, 235, 141, 240, 121, 23, 150, 246, 60, 30, 55, 0, 20, 108, 252, 169, 104, 104, 234, 70, 67, 83, 55, 54, 92, 124, 19, 252, 94, 39, 108, 230, 83, 176, 12, 255, 30, 251, 247, 124, 15, 148, 218, 128, 166, 85, 159, 64, 211, 170, 109, 168, 239, 218, 74, 188, 3, 66, 89, 66, 191, 251, 135, 31, 163, 181, 179, 23, 173, 29, 27, 160, 209, 25, 11, 58, 217, 161, 119, 255, 11, 20, 165, 68, 207, 5, 27, 22, 189, 199, 48, 26, 104, 117, 122, 240, 124, 0, 109, 29, 93, 120, 243, 55, 47, 197, 222, 179, 59, 237, 224, 88, 22, 166, 186, 54, 52, 183, 47, 195, 209, 247, 247, 197, 222, 27, 63, 55, 132, 150, 214, 206, 162, 95, 8, 141, 206, 136, 101, 221, 151, 98, 89, 247, 165, 0, 0, 215, 172, 5, 227, 231, 142, 225, 204, 158, 157, 56, 50, 117, 22, 198, 214, 141, 104, 238, 185, 6, 77, 171, 175, 37, 75, 141, 132, 178, 129, 174, 93, 115, 51, 206, 13, 239, 197, 7, 7, 94, 65, 85, 149, 30, 45, 29, 189, 104, 237, 236, 141, 204, 155, 51, 224, 243, 122, 96, 157, 60, 15, 231, 148, 29, 253, 199, 14, 225, 205, 223, 189, 140, 237, 247, 63, 30, 9, 0, 186, 28, 24, 57, 221, 143, 85, 107, 55, 1, 0, 222, 121, 243, 53, 168, 213, 106, 180, 117, 44, 7, 195, 104, 16, 8, 112, 120, 247, 157, 55, 176, 105, 243, 149, 120, 125, 247, 143, 113, 233, 229, 215, 129, 97, 52, 216, 180, 249, 74, 188, 244, 15, 207, 226, 228, 241, 35, 232, 232, 234, 198, 111, 254, 237, 31, 99, 193, 194, 82, 98, 168, 105, 193, 218, 141, 45, 88, 187, 241, 191, 197, 150, 26, 173, 227, 135, 113, 228, 240, 47, 17, 8, 43, 208, 184, 242, 42, 52, 245, 108, 67, 195, 138, 203, 73, 48, 145, 32, 91, 20, 46, 135, 45, 86, 13, 232, 24, 253, 0, 214, 161, 183, 96, 27, 217, 7, 183, 109, 0, 77, 173, 23, 160, 181, 243, 99, 104, 237, 236, 197, 248, 172, 10, 43, 214, 127, 60, 246, 193, 228, 60, 128, 182, 142, 21, 216, 118, 195, 159, 199, 34, 254, 30, 143, 27, 255, 240, 252, 83, 56, 63, 114, 26, 148, 82, 137, 229, 221, 107, 241, 185, 47, 125, 21, 245, 141, 45, 0, 128, 177, 243, 195, 248, 167, 159, 125, 23, 147, 86, 51, 46, 88, 183, 41, 33, 15, 224, 228, 241, 35, 120, 249, 133, 31, 97, 110, 206, 137, 141, 23, 95, 142, 47, 220, 113, 63, 24, 70, 147, 48, 240, 51, 39, 222, 195, 5, 157, 53, 162, 92, 180, 57, 215, 84, 164, 196, 121, 244, 67, 216, 204, 167, 200, 82, 35, 65, 182, 36, 8, 64, 60, 172, 103, 26, 83, 103, 222, 133, 109, 104, 47, 172, 131, 111, 99, 195, 182, 199, 19, 4, 64, 108, 196, 20, 128, 100, 108, 19, 3, 176, 140, 245, 195, 60, 250, 33, 60, 30, 55, 76, 203, 183, 160, 169, 123, 27, 154, 87, 93, 11, 173, 177, 77, 236, 225, 17, 8, 105, 73, 43, 0, 201, 140, 190, 255, 42, 17, 128, 44, 32, 45, 210, 8, 114, 66, 214, 181, 0, 82, 68, 163, 51, 98, 197, 234, 203, 177, 98, 245, 229, 0, 22, 90, 164, 157, 126, 243, 49, 56, 102, 204, 48, 45, 219, 130, 198, 149, 87, 144, 22, 105, 4, 73, 64, 4, 160, 200, 212, 53, 116, 161, 174, 161, 11, 27, 46, 190, 105, 161, 69, 218, 249, 119, 112, 224, 221, 231, 72, 139, 52, 130, 232, 100, 61, 5, 152, 26, 217, 15, 85, 8, 146, 104, 10, 50, 109, 30, 65, 152, 117, 160, 205, 164, 21, 123, 40, 5, 17, 93, 106, 180, 140, 245, 99, 218, 54, 2, 99, 75, 47, 154, 87, 125, 2, 77, 221, 87, 195, 216, 186, 161, 240, 95, 64, 32, 100, 32, 107, 1, 224, 185, 57, 56, 198, 142, 73, 162, 19, 143, 190, 74, 141, 38, 35, 5, 154, 86, 23, 126, 50, 137, 192, 243, 44, 166, 204, 167, 99, 241, 3, 63, 199, 161, 169, 251, 42, 82, 183, 64, 40, 42, 89, 11, 128, 80, 36, 47, 53, 54, 182, 172, 70, 107, 231, 6, 180, 116, 108, 128, 161, 166, 69, 236, 235, 33, 25, 230, 92, 83, 176, 77, 124, 20, 105, 177, 62, 126, 18, 213, 166, 110, 52, 175, 254, 4, 26, 86, 92, 137, 250, 229, 91, 197, 30, 30, 161, 76, 40, 185, 0, 196, 195, 249, 103, 49, 125, 230, 0, 172, 131, 111, 193, 54, 180, 7, 84, 152, 71, 107, 199, 6, 180, 117, 109, 68, 115, 251, 186, 178, 122, 194, 23, 202, 148, 109, 24, 230, 115, 125, 48, 143, 126, 136, 57, 247, 52, 76, 43, 34, 129, 196, 166, 158, 109, 208, 213, 116, 136, 61, 60, 130, 76, 17, 85, 0, 146, 113, 218, 62, 194, 212, 200, 126, 216, 134, 246, 96, 102, 244, 48, 234, 234, 151, 161, 165, 115, 3, 90, 59, 122, 81, 215, 208, 37, 246, 240, 36, 67, 116, 169, 209, 58, 118, 2, 214, 241, 147, 80, 86, 53, 160, 113, 197, 21, 164, 110, 129, 144, 51, 146, 18, 128, 120, 120, 110, 14, 211, 231, 14, 98, 106, 228, 93, 88, 135, 246, 32, 224, 182, 162, 181, 179, 23, 205, 29, 235, 5, 169, 91, 40, 39, 102, 166, 206, 193, 60, 118, 28, 150, 209, 126, 204, 144, 22, 105, 132, 28, 144, 172, 0, 36, 227, 157, 29, 131, 109, 104, 47, 108, 195, 123, 97, 63, 179, 31, 213, 250, 250, 72, 154, 114, 215, 70, 52, 52, 117, 23, 254, 11, 202, 132, 133, 22, 105, 253, 176, 140, 245, 147, 22, 105, 132, 37, 145, 141, 0, 36, 51, 125, 246, 32, 166, 206, 252, 17, 214, 211, 111, 97, 206, 62, 140, 150, 246, 117, 104, 110, 95, 143, 214, 206, 94, 84, 27, 26, 196, 30, 158, 100, 136, 182, 72, 51, 143, 246, 99, 210, 114, 154, 212, 45, 16, 18, 144, 173, 0, 196, 195, 122, 166, 49, 57, 188, 15, 182, 161, 61, 176, 13, 239, 131, 134, 97, 98, 37, 206, 13, 173, 171, 73, 48, 113, 158, 248, 22, 105, 150, 209, 227, 164, 69, 26, 161, 60, 4, 32, 25, 167, 185, 31, 182, 225, 119, 96, 29, 124, 11, 78, 203, 113, 212, 55, 173, 68, 75, 199, 6, 180, 119, 109, 34, 75, 141, 113, 196, 90, 164, 141, 246, 195, 58, 113, 146, 180, 72, 171, 64, 202, 82, 0, 226, 225, 185, 57, 76, 14, 255, 17, 182, 225, 189, 176, 13, 238, 133, 34, 228, 71, 107, 199, 6, 180, 118, 246, 162, 165, 115, 3, 241, 14, 226, 136, 182, 72, 51, 159, 239, 35, 45, 210, 42, 132, 178, 23, 128, 100, 220, 147, 131, 17, 49, 152, 95, 106, 172, 53, 117, 160, 165, 99, 3, 218, 150, 109, 36, 75, 141, 113, 68, 91, 164, 153, 199, 142, 195, 114, 254, 120, 66, 139, 52, 125, 195, 42, 208, 76, 21, 153, 50, 148, 1, 21, 39, 0, 241, 68, 211, 155, 173, 131, 111, 193, 58, 180, 7, 156, 219, 130, 150, 121, 239, 128, 44, 53, 38, 50, 51, 117, 14, 214, 137, 143, 96, 62, 255, 33, 188, 115, 118, 176, 172, 7, 1, 206, 15, 5, 165, 4, 173, 214, 67, 165, 174, 134, 74, 173, 135, 82, 93, 13, 70, 87, 3, 90, 165, 131, 74, 99, 4, 163, 53, 66, 169, 210, 65, 165, 53, 130, 102, 170, 160, 210, 26, 64, 171, 170, 161, 82, 87, 131, 214, 26, 136, 144, 136, 76, 69, 11, 64, 50, 62, 231, 4, 172, 131, 111, 71, 150, 26, 207, 30, 202, 185, 69, 90, 37, 194, 243, 44, 120, 206, 143, 0, 239, 71, 128, 245, 33, 192, 249, 16, 8, 248, 192, 177, 94, 4, 56, 47, 2, 129, 200, 235, 28, 235, 137, 252, 204, 249, 16, 224, 188, 224, 3, 126, 176, 108, 228, 239, 69, 66, 162, 49, 66, 165, 49, 128, 214, 84, 167, 20, 18, 149, 198, 0, 90, 93, 5, 149, 218, 0, 90, 163, 135, 74, 173, 7, 173, 209, 147, 37, 206, 60, 32, 2, 176, 4, 75, 181, 72, 35, 75, 141, 194, 146, 74, 72, 22, 68, 195, 11, 142, 245, 130, 15, 176, 139, 132, 36, 192, 122, 193, 5, 252, 224, 3, 126, 112, 172, 7, 180, 74, 3, 74, 85, 181, 72, 72, 84, 234, 136, 183, 65, 132, 36, 17, 34, 0, 89, 18, 223, 34, 109, 114, 100, 31, 84, 138, 48, 90, 58, 54, 160, 165, 115, 3, 169, 91, 144, 16, 81, 33, 225, 184, 136, 96, 68, 133, 132, 99, 61, 224, 121, 118, 222, 51, 137, 122, 42, 139, 133, 132, 99, 61, 224, 3, 126, 208, 42, 13, 104, 141, 17, 180, 90, 63, 239, 133, 44, 8, 9, 163, 173, 1, 205, 232, 202, 66, 72, 136, 0, 228, 137, 211, 246, 17, 108, 167, 223, 134, 117, 104, 15, 156, 230, 62, 152, 26, 150, 163, 117, 217, 199, 208, 220, 182, 150, 4, 19, 203, 128, 120, 33, 97, 89, 15, 120, 214, 11, 158, 231, 82, 10, 9, 199, 121, 192, 7, 184, 140, 66, 162, 154, 23, 7, 37, 163, 75, 16, 18, 149, 182, 6, 74, 149, 54, 38, 36, 106, 77, 29, 40, 181, 22, 140, 198, 56, 31, 59, 41, 222, 114, 44, 17, 0, 1, 136, 214, 45, 216, 6, 247, 194, 54, 248, 22, 66, 172, 11, 45, 203, 122, 209, 218, 209, 139, 166, 214, 11, 72, 48, 177, 130, 89, 74, 72, 88, 214, 131, 32, 207, 45, 8, 9, 235, 141, 136, 9, 231, 3, 199, 249, 22, 4, 39, 224, 7, 205, 84, 129, 86, 87, 47, 18, 18, 70, 91, 27, 241, 56, 210, 8, 9, 173, 209, 71, 188, 147, 52, 66, 66, 4, 160, 8, 120, 28, 231, 35, 193, 196, 193, 61, 176, 159, 63, 132, 154, 154, 38, 180, 46, 219, 136, 230, 142, 245, 164, 110, 129, 144, 23, 169, 132, 132, 227, 124, 243, 193, 212, 136, 144, 112, 172, 7, 1, 214, 139, 0, 239, 143, 196, 76, 230, 133, 36, 242, 26, 155, 82, 72, 136, 0, 20, 153, 232, 82, 163, 109, 100, 31, 108, 131, 111, 195, 239, 28, 67, 115, 219, 58, 180, 118, 70, 114, 15, 136, 119, 64, 40, 37, 60, 207, 194, 239, 117, 33, 20, 226, 193, 178, 30, 34, 0, 165, 198, 231, 156, 192, 244, 185, 67, 145, 186, 133, 193, 61, 208, 105, 171, 208, 210, 217, 139, 150, 246, 117, 164, 110, 129, 80, 114, 136, 0, 136, 12, 105, 145, 70, 16, 19, 34, 0, 18, 130, 243, 207, 98, 114, 232, 29, 76, 142, 252, 17, 182, 161, 61, 100, 169, 145, 80, 116, 136, 0, 72, 24, 210, 34, 141, 80, 108, 136, 0, 200, 132, 132, 165, 198, 161, 61, 8, 249, 103, 209, 220, 190, 142, 180, 72, 35, 20, 4, 17, 0, 153, 226, 113, 156, 143, 52, 65, 33, 45, 210, 8, 5, 64, 4, 160, 76, 152, 62, 123, 16, 214, 161, 183, 49, 53, 188, 47, 214, 34, 173, 109, 217, 70, 52, 181, 173, 37, 117, 11, 132, 180, 16, 1, 40, 67, 72, 139, 52, 66, 182, 16, 1, 168, 0, 72, 139, 52, 66, 58, 136, 0, 84, 24, 164, 69, 26, 33, 30, 34, 0, 21, 78, 66, 139, 180, 177, 15, 80, 91, 215, 74, 90, 164, 85, 16, 68, 0, 8, 49, 146, 91, 164, 5, 220, 86, 52, 119, 172, 71, 93, 67, 23, 12, 53, 205, 208, 85, 213, 129, 86, 169, 193, 48, 58, 208, 140, 134, 120, 11, 101, 0, 17, 0, 66, 90, 162, 117, 11, 179, 230, 227, 112, 79, 143, 128, 157, 155, 66, 192, 239, 68, 128, 157, 67, 40, 176, 80, 239, 206, 168, 171, 192, 168, 52, 80, 169, 117, 80, 49, 58, 168, 24, 45, 84, 42, 77, 228, 111, 70, 11, 38, 250, 250, 252, 177, 42, 70, 75, 132, 68, 34, 16, 1, 32, 20, 4, 231, 159, 5, 239, 119, 35, 192, 186, 231, 255, 118, 129, 103, 61, 8, 248, 93, 8, 248, 156, 8, 6, 188, 224, 124, 78, 240, 156, 39, 242, 158, 127, 46, 242, 222, 188, 144, 240, 172, 27, 225, 80, 16, 42, 70, 19, 17, 147, 20, 66, 194, 168, 171, 34, 130, 145, 66, 72, 84, 106, 45, 84, 180, 134, 8, 73, 158, 16, 1, 32, 72, 2, 214, 51, 13, 158, 243, 68, 254, 204, 11, 73, 192, 231, 138, 8, 71, 156, 144, 4, 252, 78, 240, 1, 47, 56, 239, 44, 130, 156, 55, 173, 144, 168, 213, 58, 208, 42, 77, 76, 72, 212, 234, 170, 200, 191, 213, 81, 239, 68, 23, 17, 20, 149, 182, 162, 133, 132, 8, 0, 161, 172, 136, 9, 137, 207, 21, 17, 134, 192, 92, 130, 144, 112, 126, 103, 76, 56, 162, 158, 73, 144, 157, 139, 8, 9, 231, 77, 16, 18, 245, 188, 231, 17, 21, 17, 38, 234, 149, 48, 90, 208, 140, 166, 44, 132, 132, 8, 0, 129, 144, 4, 207, 205, 33, 24, 240, 39, 8, 73, 128, 117, 130, 103, 61, 17, 1, 137, 254, 157, 78, 72, 230, 167, 68, 0, 192, 196, 98, 32, 218, 121, 143, 100, 177, 144, 48, 76, 220, 20, 71, 21, 141, 155, 84, 129, 166, 153, 162, 215, 120, 16, 1, 32, 16, 138, 68, 182, 66, 18, 152, 23, 19, 206, 231, 4, 207, 186, 17, 240, 187, 16, 228, 60, 105, 133, 68, 197, 232, 98, 65, 212, 168, 88, 40, 85, 76, 94, 66, 66, 4, 128, 64, 144, 56, 201, 66, 194, 249, 102, 17, 96, 221, 8, 6, 124, 25, 133, 132, 103, 61, 145, 159, 211, 8, 9, 17, 0, 2, 161, 66, 136, 10, 9, 231, 153, 65, 136, 103, 193, 249, 102, 137, 0, 16, 8, 149, 12, 37, 246, 0, 8, 4, 130, 120, 16, 1, 32, 16, 42, 24, 34, 0, 4, 66, 5, 67, 4, 128, 64, 168, 96, 136, 0, 16, 8, 21, 12, 17, 0, 2, 161, 130, 161, 197, 30, 0, 129, 16, 101, 192, 113, 2, 78, 191, 3, 0, 64, 81, 74, 208, 97, 26, 148, 82, 1, 53, 173, 5, 173, 160, 161, 85, 234, 208, 174, 239, 20, 123, 152, 101, 5, 201, 3, 32, 136, 206, 128, 227, 4, 166, 125, 147, 240, 243, 190, 172, 142, 167, 20, 20, 40, 5, 5, 5, 148, 80, 82, 20, 40, 80, 80, 42, 148, 160, 40, 37, 40, 80, 160, 41, 37, 104, 74, 5, 37, 69, 67, 163, 212, 194, 200, 212, 160, 78, 99, 18, 251, 107, 74, 18, 226, 1, 16, 68, 35, 87, 195, 143, 18, 10, 135, 16, 10, 135, 0, 240, 8, 132, 178, 255, 28, 77, 69, 110, 119, 74, 161, 140, 136, 72, 156, 112, 68, 189, 13, 154, 82, 65, 173, 212, 84, 140, 183, 65, 60, 0, 66, 201, 201, 215, 240, 197, 32, 87, 111, 163, 90, 85, 141, 6, 109, 147, 216, 195, 206, 26, 226, 1, 16, 74, 134, 156, 12, 63, 74, 185, 123, 27, 196, 3, 32, 20, 29, 57, 26, 190, 24, 228, 228, 109, 64, 141, 106, 141, 161, 96, 111, 131, 120, 0, 132, 162, 81, 206, 134, 207, 7, 120, 76, 78, 78, 161, 177, 177, 1, 180, 138, 70, 40, 20, 134, 195, 49, 3, 214, 207, 65, 167, 211, 193, 104, 212, 67, 65, 229, 182, 202, 158, 179, 183, 17, 169, 240, 205, 217, 219, 48, 168, 141, 48, 48, 145, 254, 0, 196, 3, 32, 8, 78, 57, 27, 126, 148, 57, 183, 27, 44, 199, 65, 205, 48, 168, 214, 235, 49, 235, 112, 2, 0, 12, 70, 3, 92, 78, 23, 0, 160, 166, 86, 186, 59, 54, 71, 189, 13, 146, 8, 68, 16, 140, 1, 199, 9, 236, 55, 239, 193, 184, 251, 124, 89, 27, 63, 0, 120, 61, 126, 24, 13, 70, 120, 61, 254, 200, 191, 189, 94, 24, 140, 6, 80, 148, 2, 6, 163, 1, 94, 175, 87, 236, 33, 46, 73, 40, 28, 2, 31, 226, 201, 20, 128, 80, 56, 149, 240, 196, 143, 135, 15, 240, 80, 170, 40, 208, 42, 26, 74, 21, 5, 62, 192, 47, 58, 134, 202, 209, 253, 23, 11, 34, 0, 132, 188, 17, 218, 240, 139, 49, 175, 46, 6, 126, 191, 15, 106, 134, 1, 0, 168, 25, 6, 126, 191, 15, 58, 157, 14, 46, 167, 43, 54, 5, 208, 104, 52, 98, 15, 51, 43, 196, 191, 154, 4, 217, 81, 44, 87, 223, 239, 247, 65, 173, 137, 24, 20, 0, 184, 156, 46, 40, 41, 26, 205, 45, 205, 0, 0, 167, 211, 45, 246, 87, 7, 16, 113, 255, 53, 26, 45, 0, 64, 163, 209, 194, 235, 241, 195, 96, 52, 32, 24, 226, 97, 181, 88, 17, 12, 241, 48, 24, 13, 98, 15, 51, 43, 136, 7, 64, 200, 154, 98, 187, 250, 94, 143, 31, 117, 166, 90, 204, 216, 29, 168, 214, 235, 225, 245, 122, 209, 220, 210, 28, 155, 87, 91, 45, 86, 73, 4, 214, 248, 96, 196, 83, 137, 135, 162, 20, 48, 153, 76, 48, 79, 88, 96, 50, 201, 39, 237, 152, 8, 0, 33, 35, 165, 152, 227, 203, 105, 94, 221, 218, 214, 2, 0, 48, 79, 88, 98, 63, 203, 21, 34, 0, 132, 180, 148, 50, 184, 87, 78, 243, 106, 57, 65, 4, 128, 176, 8, 49, 162, 250, 81, 247, 31, 136, 204, 171, 103, 236, 14, 212, 55, 214, 195, 225, 152, 129, 213, 98, 133, 90, 195, 160, 182, 182, 78, 236, 75, 83, 118, 144, 68, 32, 66, 12, 49, 151, 243, 204, 19, 150, 69, 175, 149, 147, 171, 45, 4, 233, 86, 69, 146, 87, 79, 114, 129, 120, 0, 4, 73, 172, 227, 19, 99, 207, 204, 194, 170, 72, 29, 92, 78, 23, 156, 78, 55, 106, 106, 141, 9, 171, 39, 213, 42, 125, 78, 231, 148, 70, 84, 133, 32, 10, 149, 148, 185, 87, 14, 164, 203, 54, 76, 206, 74, 204, 5, 226, 1, 84, 32, 82, 120, 226, 19, 10, 135, 154, 119, 255, 147, 87, 79, 114, 153, 6, 144, 24, 64, 5, 65, 12, 95, 222, 164, 42, 56, 162, 233, 136, 19, 95, 173, 215, 99, 206, 237, 142, 253, 156, 45, 68, 0, 42, 0, 98, 248, 229, 65, 124, 16, 48, 186, 42, 50, 61, 57, 141, 58, 83, 45, 104, 21, 13, 62, 192, 99, 198, 238, 64, 99, 115, 67, 194, 231, 194, 161, 16, 154, 221, 141, 9, 175, 241, 170, 32, 0, 50, 5, 40, 107, 136, 225, 151, 23, 169, 178, 13, 83, 101, 37, 46, 194, 27, 68, 107, 221, 10, 212, 183, 45, 8, 3, 207, 205, 97, 206, 21, 34, 2, 80, 142, 16, 195, 175, 28, 178, 90, 61, 209, 41, 17, 240, 37, 222, 11, 52, 83, 141, 154, 122, 226, 1, 148, 21, 196, 240, 9, 169, 80, 80, 20, 248, 32, 159, 242, 61, 34, 0, 101, 0, 49, 124, 66, 190, 16, 1, 144, 57, 135, 44, 251, 225, 14, 184, 196, 30, 6, 161, 132, 164, 114, 245, 51, 37, 79, 133, 66, 169, 61, 0, 146, 8, 36, 115, 124, 65, 105, 183, 158, 34, 72, 27, 34, 0, 50, 102, 220, 61, 10, 62, 141, 178, 19, 8, 217, 64, 4, 64, 198, 204, 114, 51, 98, 15, 129, 32, 115, 136, 0, 200, 24, 63, 159, 123, 238, 55, 161, 50, 225, 130, 108, 202, 215, 137, 0, 200, 24, 54, 72, 4, 32, 19, 161, 80, 24, 118, 187, 29, 230, 9, 11, 102, 29, 78, 132, 67, 145, 29, 55, 248, 0, 15, 243, 132, 37, 101, 231, 161, 74, 130, 8, 128, 140, 153, 117, 146, 27, 58, 19, 233, 26, 139, 38, 55, 32, 173, 84, 136, 0, 200, 24, 46, 228, 35, 55, 116, 6, 138, 81, 66, 43, 71, 248, 112, 32, 229, 235, 68, 0, 100, 202, 89, 215, 8, 116, 85, 213, 21, 123, 67, 231, 75, 186, 18, 218, 114, 39, 0, 34, 0, 101, 133, 39, 16, 121, 242, 87, 234, 13, 157, 45, 209, 198, 162, 161, 80, 56, 214, 88, 52, 85, 3, 210, 74, 133, 8, 128, 76, 241, 242, 94, 114, 67, 103, 65, 170, 13, 59, 82, 109, 236, 81, 238, 240, 170, 32, 102, 167, 23, 103, 140, 18, 1, 144, 41, 211, 147, 230, 138, 190, 161, 179, 37, 90, 66, 11, 0, 38, 147, 9, 20, 165, 136, 149, 208, 154, 39, 44, 152, 156, 156, 74, 91, 40, 83, 78, 76, 135, 38, 83, 190, 78, 106, 1, 100, 138, 39, 196, 229, 86, 19, 94, 36, 138, 209, 169, 182, 216, 84, 100, 3, 82, 154, 2, 207, 46, 206, 5, 32, 30, 128, 76, 241, 251, 23, 158, 242, 173, 109, 45, 177, 27, 57, 254, 231, 82, 64, 150, 217, 100, 2, 163, 128, 223, 51, 183, 232, 101, 34, 0, 50, 100, 200, 121, 26, 161, 249, 245, 127, 177, 33, 203, 108, 242, 64, 65, 81, 8, 135, 23, 119, 255, 35, 2, 32, 67, 60, 156, 116, 203, 127, 165, 186, 42, 145, 79, 9, 109, 33, 72, 49, 3, 49, 85, 73, 48, 17, 0, 25, 194, 133, 2, 37, 191, 161, 211, 65, 150, 217, 82, 35, 151, 169, 17, 17, 0, 25, 34, 165, 26, 0, 178, 204, 150, 26, 185, 76, 141, 164, 21, 158, 37, 100, 5, 31, 10, 20, 126, 18, 129, 200, 187, 83, 109, 133, 33, 196, 38, 30, 197, 128, 8, 128, 204, 152, 241, 219, 37, 223, 4, 164, 34, 151, 217, 146, 72, 181, 181, 121, 170, 169, 81, 174, 123, 249, 21, 66, 170, 146, 96, 50, 5, 144, 25, 54, 159, 165, 240, 147, 16, 138, 142, 92, 166, 70, 196, 3, 144, 25, 44, 233, 252, 43, 11, 228, 50, 53, 34, 2, 32, 51, 164, 218, 5, 72, 42, 171, 18, 82, 70, 236, 169, 81, 170, 146, 96, 50, 5, 144, 25, 129, 176, 116, 2, 128, 4, 121, 145, 170, 36, 152, 8, 128, 204, 144, 210, 10, 0, 65, 254, 16, 1, 144, 17, 22, 239, 132, 228, 87, 0, 8, 137, 72, 105, 106, 196, 171, 130, 224, 185, 196, 122, 0, 34, 0, 50, 194, 230, 49, 139, 61, 4, 130, 140, 9, 168, 188, 152, 115, 37, 214, 144, 144, 32, 160, 76, 56, 106, 61, 12, 59, 39, 173, 8, 50, 65, 94, 56, 217, 185, 69, 37, 193, 68, 0, 100, 192, 97, 219, 1, 56, 57, 135, 216, 195, 32, 72, 156, 76, 189, 25, 26, 76, 245, 139, 183, 9, 23, 123, 208, 132, 165, 169, 228, 205, 63, 127, 254, 244, 175, 48, 126, 102, 233, 105, 79, 199, 202, 54, 108, 127, 244, 127, 136, 61, 84, 73, 176, 80, 128, 84, 7, 151, 211, 5, 167, 211, 141, 154, 90, 99, 172, 0, 137, 13, 248, 193, 211, 137, 49, 36, 34, 0, 18, 197, 197, 57, 241, 145, 253, 120, 197, 26, 127, 40, 20, 198, 96, 255, 8, 206, 14, 140, 46, 121, 92, 128, 35, 65, 209, 40, 94, 175, 23, 205, 45, 205, 177, 2, 36, 171, 197, 138, 154, 218, 72, 225, 81, 157, 169, 22, 51, 118, 7, 66, 122, 34, 0, 146, 103, 198, 111, 199, 192, 76, 63, 188, 188, 71, 236, 161, 136, 134, 203, 233, 130, 66, 161, 0, 0, 236, 220, 185, 19, 91, 182, 108, 73, 120, 255, 224, 193, 131, 120, 244, 209, 71, 197, 30, 166, 164, 73, 85, 128, 148, 12, 17, 0, 137, 97, 241, 78, 96, 120, 246, 52, 252, 21, 158, 242, 235, 245, 122, 161, 84, 42, 1, 0, 87, 95, 125, 245, 34, 1, 80, 171, 213, 98, 15, 81, 114, 100, 83, 128, 148, 92, 16, 68, 4, 64, 66, 140, 187, 71, 49, 226, 26, 76, 187, 145, 35, 129, 176, 20, 6, 163, 1, 14, 199, 12, 172, 22, 43, 212, 26, 6, 181, 181, 117, 152, 158, 156, 70, 157, 169, 22, 64, 164, 0, 105, 118, 102, 22, 227, 131, 103, 65, 205, 183, 7, 19, 77, 0, 44, 222, 9, 76, 184, 199, 176, 194, 216, 131, 58, 141, 73, 236, 107, 39, 58, 103, 93, 35, 56, 231, 26, 38, 137, 62, 243, 232, 116, 58, 4, 131, 65, 177, 135, 33, 43, 178, 41, 64, 82, 53, 48, 216, 220, 189, 60, 246, 111, 81, 4, 96, 198, 111, 143, 185, 185, 31, 78, 59, 209, 92, 213, 134, 53, 181, 235, 69, 190, 124, 226, 49, 228, 60, 141, 113, 247, 57, 98, 252, 113, 24, 140, 134, 148, 77, 44, 9, 185, 145, 92, 128, 100, 80, 27, 19, 222, 23, 69, 0, 6, 102, 250, 99, 115, 92, 62, 196, 99, 220, 125, 30, 78, 191, 3, 203, 140, 43, 208, 162, 107, 19, 249, 146, 149, 248, 90, 56, 78, 192, 234, 33, 41, 190, 201, 80, 148, 2, 52, 77, 102, 168, 197, 166, 228, 169, 192, 135, 44, 251, 83, 70, 183, 221, 1, 23, 78, 205, 156, 192, 9, 123, 159, 216, 215, 164, 100, 12, 56, 78, 192, 60, 55, 86, 144, 241, 75, 177, 251, 44, 65, 62, 148, 84, 98, 15, 219, 14, 44, 185, 174, 205, 135, 120, 88, 60, 19, 112, 176, 51, 104, 215, 118, 96, 121, 109, 143, 216, 215, 167, 104, 156, 176, 247, 193, 230, 181, 32, 20, 46, 172, 191, 127, 166, 228, 143, 82, 183, 157, 34, 136, 207, 82, 5, 72, 20, 40, 88, 92, 67, 177, 215, 75, 230, 1, 252, 201, 118, 8, 78, 54, 187, 116, 86, 235, 148, 21, 125, 230, 15, 208, 55, 117, 164, 84, 195, 43, 41, 125, 83, 71, 96, 241, 76, 20, 108, 252, 128, 124, 186, 207, 18, 164, 129, 46, 204, 193, 225, 181, 196, 254, 148, 68, 0, 250, 166, 142, 192, 193, 218, 179, 62, 222, 235, 245, 66, 87, 85, 141, 41, 223, 36, 222, 25, 127, 11, 3, 142, 19, 162, 93, 48, 161, 57, 106, 61, 140, 41, 223, 100, 225, 39, 74, 131, 84, 55, 230, 32, 72, 147, 162, 11, 192, 9, 123, 95, 65, 55, 60, 203, 251, 113, 116, 232, 79, 56, 100, 217, 143, 25, 127, 246, 34, 34, 69, 14, 219, 14, 8, 94, 209, 71, 54, 230, 32, 20, 66, 81, 5, 96, 192, 113, 2, 22, 207, 68, 206, 159, 75, 190, 169, 25, 53, 3, 203, 204, 4, 14, 141, 238, 151, 173, 55, 112, 200, 178, 63, 235, 41, 80, 46, 200, 165, 251, 44, 65, 154, 20, 45, 8, 24, 141, 112, 231, 67, 186, 140, 38, 141, 73, 139, 113, 247, 121, 204, 248, 166, 177, 162, 166, 71, 22, 75, 134, 197, 46, 234, 145, 75, 247, 89, 130, 52, 41, 138, 0, 12, 57, 79, 195, 60, 55, 150, 119, 144, 43, 155, 155, 122, 204, 54, 138, 245, 157, 189, 88, 111, 218, 88, 250, 171, 150, 37, 98, 21, 245, 136, 221, 125, 182, 156, 200, 84, 99, 223, 216, 216, 32, 250, 238, 62, 133, 32, 248, 20, 224, 172, 107, 4, 227, 238, 115, 130, 68, 184, 227, 137, 223, 247, 190, 181, 173, 5, 38, 147, 9, 22, 207, 4, 222, 51, 191, 131, 113, 247, 104, 129, 103, 23, 30, 139, 119, 2, 39, 103, 62, 172, 232, 138, 190, 114, 64, 46, 155, 124, 230, 139, 160, 2, 48, 238, 30, 45, 121, 62, 187, 151, 247, 224, 244, 236, 73, 73, 45, 25, 142, 187, 71, 49, 232, 24, 168, 248, 138, 190, 114, 160, 220, 151, 89, 5, 19, 128, 41, 159, 13, 35, 174, 65, 81, 82, 90, 67, 225, 16, 166, 124, 147, 216, 111, 222, 131, 179, 174, 145, 146, 255, 254, 120, 206, 186, 70, 48, 228, 28, 40, 121, 69, 159, 148, 186, 207, 150, 51, 229, 182, 204, 42, 136, 0, 204, 248, 237, 56, 229, 56, 41, 248, 77, 159, 235, 77, 237, 231, 125, 56, 227, 28, 196, 159, 108, 135, 4, 190, 76, 217, 49, 228, 60, 77, 42, 250, 202, 140, 114, 95, 102, 21, 68, 0, 226, 139, 123, 196, 38, 20, 14, 193, 193, 218, 177, 111, 226, 45, 12, 205, 158, 42, 217, 239, 29, 112, 156, 192, 168, 235, 12, 49, 254, 50, 163, 220, 151, 89, 11, 14, 95, 166, 43, 238, 17, 27, 46, 200, 226, 156, 107, 4, 124, 152, 47, 122, 169, 113, 223, 212, 17, 156, 60, 123, 18, 140, 154, 41, 187, 40, 113, 165, 83, 238, 203, 172, 5, 121, 0, 153, 138, 123, 42, 129, 163, 214, 195, 56, 59, 57, 130, 250, 198, 122, 0, 229, 23, 37, 38, 44, 38, 121, 69, 74, 206, 177, 150, 188, 5, 32, 151, 226, 30, 49, 169, 215, 52, 20, 237, 220, 125, 83, 71, 112, 242, 252, 9, 232, 170, 170, 203, 54, 74, 76, 40, 111, 242, 18, 128, 92, 139, 123, 196, 130, 166, 104, 52, 104, 155, 138, 114, 238, 104, 81, 79, 40, 148, 152, 239, 80, 110, 81, 98, 66, 121, 147, 151, 0, 84, 49, 6, 168, 40, 233, 119, 101, 165, 20, 202, 162, 156, 55, 190, 168, 167, 220, 163, 196, 132, 8, 197, 94, 102, 21, 171, 177, 75, 94, 2, 208, 99, 92, 141, 222, 250, 77, 208, 209, 85, 146, 238, 58, 67, 43, 132, 15, 190, 37, 23, 245, 148, 123, 148, 56, 27, 146, 111, 210, 116, 55, 51, 33, 61, 98, 101, 28, 230, 29, 3, 168, 211, 152, 240, 241, 214, 171, 177, 97, 121, 47, 66, 156, 8, 87, 44, 11, 84, 74, 70, 176, 115, 185, 56, 103, 202, 109, 186, 162, 81, 98, 0, 48, 153, 76, 160, 40, 69, 44, 74, 108, 158, 176, 96, 114, 114, 10, 124, 80, 154, 2, 41, 20, 201, 55, 105, 186, 155, 153, 144, 30, 177, 50, 14, 11, 206, 3, 184, 176, 249, 18, 172, 109, 91, 15, 29, 93, 85, 218, 43, 150, 5, 12, 165, 18, 228, 60, 51, 126, 59, 250, 167, 143, 101, 189, 226, 81, 78, 81, 226, 108, 72, 190, 73, 211, 221, 204, 132, 236, 41, 85, 44, 73, 144, 68, 160, 229, 134, 149, 248, 120, 235, 213, 104, 208, 54, 130, 82, 148, 188, 207, 104, 90, 170, 4, 232, 133, 71, 138, 122, 150, 38, 155, 155, 148, 162, 164, 115, 79, 72, 21, 177, 98, 73, 130, 254, 207, 108, 108, 216, 140, 78, 195, 10, 201, 4, 8, 77, 2, 44, 1, 218, 125, 83, 146, 201, 114, 148, 34, 169, 110, 210, 84, 55, 51, 97, 105, 196, 138, 37, 9, 46, 205, 209, 0, 161, 94, 101, 40, 254, 85, 91, 2, 154, 162, 5, 217, 113, 200, 207, 103, 119, 209, 43, 181, 24, 39, 213, 77, 154, 234, 102, 38, 44, 141, 88, 177, 164, 162, 228, 168, 214, 105, 76, 216, 210, 114, 197, 124, 63, 64, 91, 73, 243, 227, 163, 13, 28, 148, 20, 13, 180, 23, 126, 62, 62, 20, 40, 217, 216, 229, 72, 170, 180, 216, 84, 233, 179, 132, 220, 41, 69, 99, 151, 162, 38, 169, 175, 55, 109, 196, 184, 123, 20, 231, 221, 103, 74, 54, 135, 142, 70, 160, 91, 27, 90, 5, 57, 95, 32, 76, 4, 96, 41, 72, 247, 33, 121, 83, 244, 232, 76, 187, 190, 179, 164, 1, 194, 104, 4, 90, 168, 37, 192, 32, 89, 195, 38, 148, 49, 37, 11, 207, 150, 58, 64, 168, 163, 117, 130, 156, 39, 72, 60, 0, 66, 9, 41, 117, 44, 169, 164, 235, 51, 165, 8, 16, 70, 35, 208, 106, 101, 225, 145, 103, 139, 87, 152, 221, 123, 42, 1, 177, 130, 160, 28, 199, 145, 61, 17, 11, 160, 228, 11, 180, 209, 0, 97, 75, 85, 27, 104, 74, 248, 16, 68, 52, 2, 45, 196, 18, 224, 172, 12, 170, 29, 43, 29, 134, 137, 36, 123, 145, 50, 236, 252, 16, 45, 67, 99, 189, 105, 35, 122, 140, 107, 4, 207, 32, 164, 40, 5, 154, 26, 154, 4, 89, 2, 100, 201, 250, 191, 12, 32, 101, 216, 133, 32, 106, 138, 86, 177, 2, 132, 180, 64, 41, 192, 28, 89, 2, 148, 13, 164, 12, 59, 63, 36, 145, 163, 41, 116, 128, 80, 165, 16, 70, 0, 66, 161, 160, 152, 151, 37, 143, 241, 138, 83, 82, 42, 46, 164, 12, 187, 16, 36, 33, 0, 128, 176, 1, 66, 161, 60, 0, 185, 229, 0, 148, 251, 38, 22, 169, 224, 184, 64, 197, 150, 97, 11, 129, 100, 4, 0, 16, 46, 64, 168, 161, 133, 201, 61, 151, 91, 22, 160, 216, 155, 88, 240, 1, 30, 167, 250, 134, 75, 254, 189, 133, 74, 157, 61, 117, 108, 168, 76, 189, 164, 244, 72, 74, 0, 162, 20, 26, 32, 84, 211, 90, 65, 198, 33, 247, 22, 223, 165, 156, 23, 7, 184, 0, 190, 115, 223, 15, 241, 248, 157, 223, 195, 177, 247, 250, 69, 249, 190, 133, 148, 97, 31, 123, 175, 31, 143, 111, 255, 62, 190, 115, 223, 15, 43, 74, 4, 36, 41, 0, 64, 97, 1, 194, 38, 109, 115, 193, 191, 95, 236, 29, 134, 242, 65, 172, 146, 210, 0, 23, 192, 83, 247, 62, 139, 15, 15, 158, 4, 31, 224, 241, 204, 95, 63, 39, 154, 8, 228, 195, 177, 247, 250, 241, 204, 95, 63, 7, 62, 192, 227, 195, 131, 39, 241, 157, 251, 126, 136, 0, 39, 47, 239, 47, 95, 36, 43, 0, 81, 114, 13, 16, 210, 20, 13, 3, 99, 44, 248, 247, 122, 2, 242, 235, 98, 35, 70, 73, 41, 235, 231, 240, 212, 189, 207, 226, 228, 145, 83, 88, 182, 108, 25, 190, 254, 245, 175, 203, 74, 4, 226, 141, 223, 100, 50, 129, 97, 24, 124, 120, 240, 36, 158, 186, 247, 217, 138, 16, 1, 201, 11, 0, 144, 91, 128, 80, 168, 0, 160, 220, 230, 255, 64, 233, 75, 74, 89, 63, 135, 39, 239, 222, 133, 147, 71, 78, 161, 189, 189, 29, 239, 190, 251, 46, 118, 237, 218, 133, 103, 159, 125, 54, 38, 2, 253, 135, 7, 138, 250, 157, 25, 102, 113, 205, 71, 182, 174, 127, 188, 241, 223, 124, 243, 205, 176, 217, 108, 216, 183, 111, 31, 170, 170, 170, 112, 242, 200, 41, 60, 117, 239, 179, 96, 253, 18, 237, 119, 39, 16, 178, 16, 0, 32, 251, 0, 161, 80, 75, 128, 229, 146, 3, 80, 172, 246, 100, 62, 175, 31, 79, 222, 189, 11, 167, 251, 134, 209, 222, 222, 142, 253, 251, 247, 163, 189, 61, 82, 127, 253, 192, 3, 15, 196, 68, 224, 233, 175, 254, 160, 232, 34, 144, 15, 241, 198, 127, 235, 173, 183, 226, 213, 87, 95, 133, 82, 169, 196, 150, 45, 91, 176, 111, 223, 62, 24, 12, 6, 156, 60, 114, 10, 79, 222, 189, 171, 172, 69, 64, 54, 2, 16, 37, 83, 128, 80, 176, 37, 192, 96, 249, 254, 167, 23, 138, 207, 235, 199, 19, 219, 191, 159, 96, 252, 93, 93, 93, 9, 199, 68, 69, 32, 192, 5, 36, 39, 2, 253, 135, 7, 18, 140, 255, 165, 151, 94, 130, 82, 185, 208, 66, 254, 162, 139, 46, 194, 254, 253, 251, 81, 83, 83, 131, 211, 125, 195, 120, 242, 238, 93, 240, 121, 203, 115, 73, 81, 118, 2, 0, 44, 29, 32, 20, 108, 9, 48, 92, 57, 145, 224, 92, 57, 221, 55, 140, 225, 19, 103, 1, 0, 59, 118, 236, 88, 100, 252, 81, 164, 40, 2, 253, 135, 7, 240, 244, 87, 127, 144, 96, 252, 10, 133, 98, 209, 113, 189, 189, 189, 120, 244, 209, 71, 99, 223, 119, 232, 248, 25, 177, 135, 94, 20, 100, 41, 0, 81, 162, 1, 66, 70, 185, 16, 32, 20, 162, 17, 40, 0, 132, 194, 242, 202, 2, 140, 167, 216, 149, 121, 27, 47, 91, 143, 135, 118, 221, 11, 74, 73, 225, 129, 7, 30, 192, 27, 111, 188, 145, 246, 88, 41, 137, 64, 212, 248, 3, 92, 96, 73, 227, 7, 128, 215, 94, 123, 13, 143, 62, 250, 40, 40, 37, 133, 135, 118, 221, 139, 222, 45, 107, 83, 30, 39, 247, 61, 17, 100, 45, 0, 64, 36, 64, 184, 193, 20, 9, 16, 82, 10, 10, 203, 13, 43, 11, 62, 167, 139, 115, 22, 156, 3, 32, 247, 27, 35, 19, 91, 175, 187, 24, 127, 243, 221, 191, 66, 40, 28, 194, 77, 55, 221, 36, 121, 17, 200, 213, 248, 111, 185, 229, 22, 132, 194, 33, 252, 205, 119, 255, 10, 91, 175, 187, 56, 237, 121, 229, 190, 39, 130, 236, 5, 0, 88, 8, 16, 118, 234, 151, 11, 114, 62, 155, 207, 154, 242, 117, 69, 128, 135, 225, 131, 97, 180, 253, 234, 109, 172, 248, 238, 43, 184, 224, 161, 95, 96, 245, 55, 127, 137, 21, 207, 188, 130, 182, 95, 189, 13, 195, 7, 195, 80, 204, 27, 188, 220, 111, 140, 108, 136, 138, 64, 48, 24, 196, 77, 55, 221, 132, 215, 94, 123, 45, 237, 177, 15, 60, 240, 0, 118, 238, 220, 41, 138, 8, 196, 27, 255, 237, 183, 223, 158, 149, 241, 135, 17, 206, 104, 252, 128, 252, 247, 68, 40, 11, 1, 136, 210, 83, 115, 129, 32, 231, 73, 85, 6, 92, 253, 209, 40, 86, 62, 253, 47, 104, 255, 167, 63, 192, 120, 100, 16, 154, 9, 59, 40, 142, 135, 210, 23, 128, 198, 108, 135, 241, 200, 32, 218, 255, 233, 15, 88, 249, 244, 191, 160, 250, 163, 81, 217, 223, 24, 217, 178, 245, 186, 139, 113, 255, 211, 119, 34, 24, 12, 226, 150, 91, 110, 89, 82, 4, 118, 236, 216, 145, 32, 2, 3, 199, 6, 139, 62, 190, 100, 227, 127, 225, 133, 23, 4, 51, 254, 114, 216, 19, 65, 218, 163, 19, 137, 228, 86, 224, 245, 111, 126, 128, 206, 159, 190, 1, 102, 102, 46, 227, 103, 153, 153, 57, 116, 254, 244, 13, 44, 63, 120, 74, 214, 55, 70, 46, 92, 121, 227, 86, 220, 255, 244, 157, 8, 133, 66, 57, 137, 192, 83, 247, 60, 91, 84, 17, 200, 197, 248, 95, 126, 249, 229, 156, 140, 31, 40, 143, 61, 17, 202, 231, 46, 20, 144, 248, 36, 160, 250, 55, 63, 64, 227, 27, 135, 115, 62, 199, 178, 189, 39, 80, 255, 230, 7, 178, 189, 49, 114, 37, 31, 17, 96, 125, 108, 209, 68, 96, 224, 216, 96, 78, 198, 127, 219, 109, 183, 229, 100, 252, 64, 121, 236, 137, 64, 191, 49, 240, 107, 180, 212, 182, 160, 86, 103, 18, 36, 128, 38, 22, 238, 57, 15, 94, 248, 231, 215, 240, 238, 193, 163, 176, 216, 166, 10, 63, 33, 128, 13, 138, 0, 30, 81, 185, 129, 52, 55, 78, 38, 26, 223, 56, 140, 185, 118, 19, 70, 77, 85, 168, 111, 172, 135, 195, 49, 3, 171, 197, 10, 181, 134, 65, 109, 109, 157, 216, 151, 76, 112, 174, 188, 113, 43, 0, 224, 71, 143, 253, 18, 183, 220, 114, 11, 94, 124, 241, 69, 220, 122, 235, 173, 41, 143, 221, 177, 99, 7, 0, 224, 145, 71, 30, 193, 83, 247, 60, 139, 191, 253, 201, 131, 88, 179, 105, 149, 32, 227, 24, 56, 54, 136, 167, 238, 137, 4, 29, 183, 111, 223, 142, 159, 253, 236, 103, 25, 141, 95, 65, 41, 114, 50, 126, 64, 62, 123, 34, 68, 247, 202, 96, 253, 28, 140, 29, 106, 84, 211, 11, 217, 147, 148, 219, 239, 196, 121, 251, 25, 12, 207, 158, 194, 222, 241, 55, 241, 158, 249, 29, 28, 181, 30, 198, 144, 243, 180, 216, 227, 206, 137, 127, 124, 241, 63, 240, 234, 235, 111, 46, 105, 252, 52, 157, 125, 137, 49, 131, 48, 238, 86, 121, 210, 222, 56, 217, 210, 250, 210, 59, 8, 249, 217, 148, 105, 186, 229, 72, 188, 39, 112, 219, 109, 183, 225, 229, 151, 95, 78, 123, 108, 49, 60, 129, 168, 241, 179, 62, 22, 219, 183, 111, 199, 207, 127, 254, 243, 140, 198, 15, 32, 103, 227, 7, 228, 179, 9, 108, 114, 0, 58, 30, 218, 104, 48, 98, 198, 238, 64, 181, 94, 15, 62, 196, 131, 15, 241, 240, 194, 3, 59, 55, 133, 113, 247, 57, 208, 148, 10, 90, 165, 14, 85, 76, 53, 214, 212, 174, 23, 251, 187, 164, 229, 205, 183, 222, 77, 255, 166, 174, 26, 45, 219, 191, 129, 150, 143, 93, 136, 41, 127, 16, 142, 131, 123, 225, 126, 233, 121, 40, 150, 200, 139, 191, 68, 193, 193, 164, 8, 167, 125, 127, 142, 15, 194, 50, 95, 44, 210, 194, 168, 80, 77, 43, 83, 30, 167, 113, 251, 176, 122, 210, 11, 103, 151, 216, 87, 168, 116, 196, 123, 2, 81, 3, 91, 202, 19, 96, 89, 22, 79, 60, 241, 68, 193, 158, 64, 42, 227, 79, 199, 238, 221, 187, 113, 199, 29, 119, 0, 0, 238, 127, 250, 206, 156, 141, 95, 78, 120, 189, 94, 52, 183, 52, 131, 162, 20, 9, 79, 127, 0, 160, 227, 3, 85, 180, 42, 241, 9, 25, 21, 4, 63, 239, 131, 131, 181, 195, 60, 55, 6, 70, 169, 134, 90, 169, 129, 158, 49, 160, 73, 219, 34, 72, 243, 77, 33, 112, 123, 210, 68, 213, 181, 85, 104, 186, 243, 27, 168, 89, 191, 9, 238, 64, 16, 246, 183, 126, 13, 239, 235, 47, 64, 145, 97, 29, 126, 147, 50, 125, 45, 192, 144, 215, 143, 253, 206, 196, 157, 142, 174, 48, 86, 161, 71, 151, 122, 94, 95, 125, 242, 28, 156, 151, 8, 227, 222, 202, 133, 92, 68, 224, 241, 199, 31, 135, 223, 239, 199, 51, 207, 60, 147, 183, 8, 20, 98, 252, 209, 177, 86, 34, 52, 176, 16, 193, 172, 206, 144, 69, 23, 10, 135, 224, 231, 125, 240, 243, 62, 56, 89, 7, 198, 221, 231, 35, 130, 64, 169, 81, 205, 232, 97, 210, 54, 160, 69, 215, 38, 246, 119, 138, 161, 168, 50, 96, 217, 87, 31, 5, 213, 189, 1, 254, 96, 16, 83, 7, 246, 194, 243, 250, 238, 140, 198, 15, 0, 203, 21, 169, 51, 1, 231, 248, 224, 34, 227, 7, 128, 253, 78, 79, 90, 79, 64, 59, 110, 23, 251, 82, 136, 66, 178, 8, 112, 28, 135, 219, 111, 191, 61, 229, 177, 59, 119, 238, 4, 128, 152, 8, 60, 241, 139, 135, 179, 254, 61, 241, 198, 255, 181, 175, 125, 13, 207, 61, 247, 92, 218, 99, 139, 97, 252, 82, 223, 24, 54, 26, 128, 54, 24, 13, 152, 227, 185, 196, 24, 0, 80, 88, 157, 56, 23, 100, 225, 14, 184, 96, 241, 76, 224, 196, 116, 31, 222, 25, 127, 11, 135, 44, 251, 209, 55, 117, 4, 227, 238, 81, 209, 190, 180, 66, 163, 67, 199, 87, 30, 132, 113, 213, 90, 240, 60, 15, 219, 127, 189, 14, 247, 139, 207, 67, 145, 101, 134, 95, 157, 34, 181, 72, 156, 91, 162, 50, 204, 146, 166, 126, 156, 158, 93, 72, 250, 145, 210, 141, 33, 20, 75, 101, 61, 246, 94, 182, 22, 247, 125, 251, 43, 0, 128, 59, 238, 184, 3, 187, 119, 239, 78, 123, 158, 157, 59, 119, 70, 166, 4, 62, 22, 223, 186, 107, 23, 134, 250, 51, 231, 223, 15, 30, 31, 17, 213, 248, 229, 64, 252, 202, 68, 50, 148, 208, 117, 226, 129, 80, 68, 16, 166, 124, 147, 24, 112, 244, 199, 2, 139, 125, 83, 71, 74, 215, 101, 167, 74, 143, 186, 175, 125, 11, 117, 31, 219, 12, 132, 21, 152, 57, 184, 23, 222, 223, 254, 10, 10, 74, 1, 208, 42, 132, 149, 153, 43, 6, 133, 44, 5, 10, 169, 138, 186, 7, 171, 232, 100, 202, 122, 252, 216, 199, 215, 225, 158, 199, 239, 0, 144, 189, 8, 228, 82, 125, 151, 141, 241, 255, 226, 23, 191, 168, 72, 227, 7, 18, 251, 68, 44, 138, 1, 0, 197, 125, 42, 197, 2, 139, 188, 7, 83, 190, 73, 156, 115, 13, 131, 161, 34, 113, 4, 163, 186, 70, 176, 236, 189, 120, 212, 23, 94, 14, 69, 215, 42, 216, 217, 0, 102, 222, 250, 29, 60, 191, 255, 87, 104, 175, 255, 28, 148, 109, 93, 64, 56, 4, 238, 232, 1, 176, 71, 247, 67, 177, 196, 182, 95, 147, 97, 10, 203, 83, 120, 1, 93, 26, 6, 135, 221, 139, 227, 13, 12, 20, 104, 97, 82, 11, 11, 215, 80, 120, 135, 34, 41, 227, 245, 248, 81, 103, 170, 141, 5, 147, 227, 131, 78, 6, 163, 1, 86, 139, 21, 215, 222, 124, 5, 0, 224, 39, 79, 190, 16, 51, 196, 108, 166, 3, 217, 144, 141, 241, 223, 117, 215, 93, 80, 40, 20, 21, 103, 252, 153, 40, 249, 163, 41, 94, 16, 28, 172, 29, 163, 238, 179, 9, 129, 197, 182, 170, 142, 130, 91, 122, 113, 71, 246, 33, 112, 225, 229, 240, 217, 38, 48, 247, 155, 221, 80, 223, 248, 37, 168, 55, 109, 5, 20, 0, 123, 244, 61, 176, 199, 150, 54, 126, 0, 24, 11, 41, 177, 156, 90, 124, 76, 53, 173, 196, 21, 198, 170, 69, 113, 128, 75, 141, 186, 180, 43, 1, 190, 174, 194, 123, 20, 74, 149, 92, 210, 97, 139, 33, 2, 196, 248, 11, 67, 116, 223, 52, 57, 176, 104, 158, 27, 139, 44, 61, 210, 58, 232, 104, 29, 154, 116, 45, 104, 208, 54, 229, 116, 206, 176, 223, 11, 231, 79, 191, 5, 168, 212, 96, 62, 243, 151, 160, 47, 222, 6, 62, 200, 130, 63, 250, 14, 216, 255, 122, 37, 171, 32, 224, 31, 130, 106, 92, 73, 167, 158, 211, 247, 232, 52, 104, 97, 84, 89, 45, 3, 2, 128, 227, 202, 117, 98, 95, 230, 162, 177, 84, 58, 172, 193, 104, 88, 148, 245, 152, 44, 2, 44, 27, 137, 218, 167, 34, 42, 2, 233, 120, 232, 161, 135, 240, 253, 239, 127, 63, 237, 251, 196, 248, 51, 67, 75, 45, 40, 21, 10, 135, 192, 5, 89, 112, 65, 22, 78, 214, 1, 139, 103, 34, 97, 165, 161, 134, 169, 67, 187, 190, 51, 243, 137, 88, 22, 225, 48, 64, 85, 233, 17, 6, 48, 113, 98, 20, 179, 239, 157, 70, 135, 207, 151, 85, 254, 243, 48, 84, 24, 8, 41, 177, 134, 74, 189, 26, 80, 77, 43, 209, 179, 132, 209, 71, 241, 244, 180, 130, 107, 168, 17, 251, 178, 22, 141, 168, 251, 15, 68, 130, 201, 51, 118, 71, 198, 172, 199, 120, 17, 184, 235, 174, 187, 0, 96, 73, 17, 240, 249, 22, 23, 103, 109, 218, 180, 9, 91, 183, 166, 55, 232, 231, 159, 127, 30, 247, 221, 119, 31, 49, 254, 56, 82, 217, 186, 232, 30, 64, 54, 68, 5, 33, 186, 218, 48, 236, 28, 132, 70, 169, 134, 134, 214, 192, 168, 174, 75, 155, 194, 172, 224, 88, 176, 175, 60, 15, 254, 234, 219, 48, 49, 228, 65, 64, 213, 12, 218, 176, 1, 45, 174, 126, 80, 8, 103, 252, 189, 154, 155, 84, 192, 27, 65, 32, 207, 222, 32, 97, 37, 5, 243, 231, 175, 18, 251, 242, 21, 149, 124, 211, 97, 115, 17, 1, 173, 118, 241, 62, 15, 75, 213, 82, 16, 227, 207, 30, 89, 8, 64, 50, 129, 16, 155, 176, 218, 112, 206, 181, 196, 110, 52, 156, 31, 138, 63, 236, 134, 174, 230, 34, 184, 116, 29, 176, 87, 119, 131, 87, 170, 209, 225, 248, 0, 20, 210, 79, 5, 158, 252, 10, 135, 79, 93, 14, 216, 41, 26, 83, 175, 231, 183, 38, 48, 245, 169, 139, 17, 104, 172, 17, 251, 114, 21, 149, 232, 83, 197, 60, 97, 201, 57, 152, 156, 139, 8, 100, 75, 188, 241, 255, 229, 35, 183, 226, 178, 235, 55, 139, 125, 137, 36, 141, 44, 5, 32, 153, 76, 221, 123, 148, 97, 30, 93, 142, 247, 1, 199, 251, 89, 157, 239, 201, 175, 112, 248, 179, 203, 35, 143, 125, 211, 13, 74, 120, 6, 130, 240, 158, 206, 236, 49, 196, 227, 233, 105, 197, 244, 117, 23, 138, 125, 105, 36, 79, 178, 8, 176, 108, 100, 73, 47, 31, 226, 141, 255, 158, 199, 239, 192, 165, 159, 216, 152, 85, 130, 91, 37, 67, 202, 129, 147, 136, 55, 126, 0, 128, 66, 129, 182, 237, 12, 168, 26, 38, 235, 115, 132, 141, 70, 140, 127, 249, 134, 188, 171, 8, 43, 141, 107, 111, 190, 34, 150, 39, 112, 223, 125, 247, 225, 249, 231, 159, 207, 249, 28, 187, 118, 237, 138, 25, 255, 221, 255, 235, 118, 92, 123, 243, 21, 100, 131, 208, 44, 32, 2, 16, 199, 34, 227, 159, 199, 161, 217, 12, 207, 23, 238, 201, 250, 60, 190, 47, 126, 25, 193, 234, 242, 170, 247, 207, 68, 161, 233, 176, 215, 222, 124, 5, 238, 122, 236, 75, 0, 114, 23, 129, 93, 187, 118, 225, 225, 135, 31, 134, 66, 161, 192, 23, 254, 250, 102, 172, 219, 186, 170, 40, 27, 161, 148, 35, 68, 0, 230, 73, 103, 252, 211, 252, 197, 56, 199, 222, 138, 96, 119, 15, 184, 109, 215, 102, 60, 15, 119, 245, 53, 8, 173, 236, 46, 201, 152, 211, 53, 26, 77, 78, 205, 149, 11, 55, 124, 110, 91, 206, 34, 16, 111, 252, 247, 60, 126, 7, 62, 251, 229, 79, 203, 162, 68, 87, 42, 16, 1, 64, 102, 227, 143, 194, 221, 240, 105, 132, 140, 53, 105, 207, 19, 170, 51, 129, 251, 228, 103, 74, 54, 238, 116, 141, 70, 147, 83, 115, 229, 68, 178, 8, 236, 218, 181, 43, 237, 177, 201, 198, 31, 141, 39, 16, 178, 167, 226, 5, 32, 157, 241, 115, 218, 107, 224, 53, 62, 144, 248, 162, 74, 5, 246, 150, 91, 211, 158, 139, 253, 252, 109, 128, 74, 152, 157, 137, 178, 33, 93, 163, 209, 228, 134, 164, 114, 35, 94, 4, 30, 126, 248, 225, 148, 34, 240, 228, 147, 79, 38, 184, 253, 196, 248, 243, 163, 162, 5, 32, 147, 241, 27, 244, 6, 52, 54, 38, 166, 241, 6, 215, 172, 71, 112, 213, 234, 69, 159, 9, 174, 90, 141, 96, 247, 106, 136, 9, 69, 81, 89, 165, 230, 202, 129, 100, 17, 248, 193, 15, 126, 16, 123, 239, 153, 103, 158, 193, 19, 79, 60, 145, 224, 246, 39, 67, 92, 255, 236, 40, 139, 101, 192, 124, 200, 246, 201, 111, 208, 71, 154, 58, 78, 78, 46, 148, 82, 114, 87, 108, 131, 118, 48, 177, 101, 26, 119, 229, 182, 146, 127, 135, 84, 41, 183, 169, 82, 115, 229, 186, 12, 118, 195, 231, 34, 215, 244, 23, 223, 121, 17, 15, 62, 248, 32, 212, 106, 53, 88, 150, 197, 35, 143, 60, 146, 16, 237, 39, 100, 134, 166, 104, 52, 209, 139, 19, 170, 42, 82, 0, 210, 25, 255, 107, 254, 110, 120, 212, 159, 69, 242, 243, 36, 89, 4, 130, 107, 214, 33, 100, 170, 7, 101, 159, 6, 16, 153, 251, 7, 47, 40, 125, 190, 191, 193, 104, 88, 148, 114, 59, 61, 57, 189, 40, 53, 183, 90, 47, 79, 1, 0, 34, 34, 160, 173, 210, 224, 71, 143, 253, 18, 247, 222, 123, 47, 128, 72, 166, 225, 125, 223, 38, 25, 126, 217, 98, 164, 212, 139, 202, 128, 163, 84, 156, 0, 44, 101, 252, 223, 116, 95, 5, 184, 143, 2, 0, 62, 93, 219, 145, 240, 126, 130, 8, 40, 20, 8, 108, 190, 20, 234, 223, 71, 182, 195, 10, 92, 178, 69, 148, 53, 255, 84, 41, 183, 169, 82, 115, 229, 206, 149, 55, 110, 5, 173, 162, 241, 119, 15, 255, 4, 0, 240, 240, 223, 127, 13, 151, 108, 219, 36, 246, 176, 100, 65, 27, 179, 180, 248, 87, 148, 0, 100, 52, 254, 121, 190, 53, 158, 89, 4, 66, 221, 61, 177, 215, 67, 43, 165, 211, 239, 175, 144, 212, 92, 41, 115, 217, 245, 155, 161, 98, 104, 168, 181, 106, 244, 94, 186, 182, 240, 19, 150, 57, 26, 138, 134, 41, 133, 203, 159, 76, 197, 8, 64, 182, 198, 31, 37, 163, 8, 4, 131, 192, 124, 155, 241, 224, 178, 46, 177, 191, 94, 69, 176, 249, 106, 242, 212, 207, 134, 165, 92, 254, 100, 42, 66, 0, 114, 53, 254, 40, 153, 68, 192, 181, 124, 190, 10, 81, 153, 185, 44, 152, 64, 200, 23, 62, 16, 153, 214, 53, 54, 54, 128, 86, 209, 9, 27, 125, 232, 116, 58, 24, 141, 122, 40, 230, 155, 174, 100, 114, 249, 147, 41, 251, 101, 192, 124, 141, 63, 202, 183, 198, 143, 226, 119, 142, 177, 69, 175, 27, 244, 6, 232, 46, 189, 12, 225, 246, 44, 122, 19, 20, 25, 169, 119, 165, 37, 20, 70, 54, 59, 77, 27, 41, 117, 206, 198, 15, 228, 233, 1, 164, 83, 160, 100, 165, 18, 155, 66, 141, 63, 74, 58, 79, 160, 238, 47, 190, 0, 218, 237, 74, 88, 34, 44, 71, 254, 245, 39, 191, 22, 123, 8, 146, 228, 243, 247, 252, 247, 146, 252, 158, 76, 61, 23, 89, 199, 92, 214, 46, 127, 50, 121, 89, 233, 130, 2, 213, 193, 229, 116, 193, 233, 116, 163, 166, 214, 152, 160, 84, 98, 175, 61, 11, 101, 252, 81, 178, 9, 12, 150, 43, 175, 252, 148, 8, 64, 42, 62, 123, 231, 141, 69, 127, 224, 101, 74, 236, 234, 208, 24, 128, 150, 252, 55, 32, 205, 107, 212, 169, 186, 190, 214, 212, 26, 23, 41, 149, 88, 8, 109, 252, 81, 42, 89, 4, 128, 200, 14, 62, 132, 72, 26, 50, 128, 146, 60, 240, 210, 245, 92, 12, 186, 253, 232, 104, 104, 44, 248, 252, 130, 200, 86, 186, 20, 84, 49, 166, 1, 197, 50, 254, 40, 149, 44, 2, 79, 60, 241, 132, 216, 67, 144, 4, 81, 1, 40, 197, 3, 47, 85, 207, 197, 158, 214, 246, 188, 93, 254, 100, 242, 178, 80, 169, 166, 160, 22, 219, 248, 163, 84, 178, 8, 16, 22, 40, 197, 3, 47, 62, 177, 139, 86, 40, 209, 94, 215, 32, 152, 241, 3, 121, 174, 2, 196, 111, 53, 20, 12, 241, 145, 74, 52, 143, 31, 26, 77, 36, 241, 64, 140, 78, 44, 165, 50, 254, 40, 75, 173, 14, 24, 141, 181, 37, 253, 238, 4, 113, 72, 126, 224, 21, 131, 104, 79, 3, 131, 90, 139, 77, 93, 43, 209, 96, 200, 127, 190, 159, 138, 188, 36, 75, 138, 41, 168, 215, 94, 180, 184, 193, 103, 177, 140, 63, 74, 58, 79, 32, 28, 38, 173, 192, 42, 129, 248, 7, 94, 49, 167, 1, 109, 140, 30, 104, 45, 206, 185, 5, 243, 89, 196, 78, 65, 221, 127, 174, 25, 159, 92, 99, 137, 253, 187, 216, 198, 31, 37, 149, 8, 124, 56, 212, 135, 112, 147, 56, 49, 16, 66, 233, 40, 246, 3, 47, 219, 116, 222, 66, 40, 155, 59, 212, 187, 242, 147, 216, 51, 240, 91, 172, 107, 158, 193, 1, 170, 19, 223, 228, 74, 215, 143, 255, 91, 227, 71, 209, 74, 169, 209, 30, 160, 240, 209, 217, 147, 240, 25, 205, 128, 31, 162, 47, 133, 10, 141, 130, 52, 57, 77, 160, 181, 173, 165, 104, 15, 188, 92, 210, 121, 11, 161, 108, 4, 0, 0, 102, 87, 126, 6, 239, 205, 255, 252, 140, 54, 243, 246, 95, 249, 18, 159, 8, 21, 45, 195, 29, 60, 253, 58, 166, 77, 181, 160, 155, 104, 104, 2, 242, 47, 195, 37, 136, 71, 62, 25, 125, 249, 82, 144, 0, 84, 106, 10, 170, 20, 99, 32, 197, 228, 185, 255, 247, 116, 44, 239, 35, 20, 10, 99, 210, 54, 137, 250, 122, 19, 156, 46, 39, 76, 38, 19, 236, 118, 59, 140, 6, 99, 217, 79, 121, 138, 61, 189, 213, 64, 13, 19, 83, 252, 167, 126, 60, 229, 253, 63, 86, 66, 196, 142, 129, 20, 19, 169, 46, 251, 150, 154, 98, 62, 240, 244, 42, 3, 12, 138, 220, 54, 159, 17, 130, 178, 47, 6, 34, 20, 142, 20, 151, 125, 203, 137, 54, 70, 47, 138, 241, 3, 50, 244, 0, 114, 41, 141, 36, 8, 67, 165, 77, 121, 74, 69, 169, 2, 125, 75, 33, 59, 75, 201, 166, 52, 178, 84, 84, 106, 12, 36, 250, 61, 201, 6, 28, 249, 19, 53, 254, 169, 201, 105, 81, 199, 33, 59, 1, 72, 238, 121, 159, 174, 55, 62, 129, 32, 69, 52, 20, 13, 35, 165, 198, 220, 212, 12, 92, 46, 23, 212, 26, 226, 1, 100, 77, 54, 61, 239, 41, 226, 254, 151, 13, 229, 182, 245, 153, 145, 82, 195, 68, 107, 225, 155, 113, 1, 0, 88, 63, 7, 131, 192, 169, 189, 185, 34, 43, 107, 73, 87, 26, 233, 114, 186, 16, 10, 133, 99, 17, 106, 66, 113, 40, 245, 148, 167, 156, 182, 62, 107, 99, 244, 240, 205, 184, 34, 129, 212, 249, 13, 75, 131, 193, 72, 96, 213, 106, 17, 174, 120, 44, 215, 115, 201, 42, 8, 152, 170, 52, 178, 190, 177, 126, 81, 111, 124, 66, 121, 32, 245, 190, 19, 217, 16, 159, 206, 219, 208, 88, 47, 246, 112, 22, 33, 43, 15, 32, 26, 121, 142, 223, 250, 57, 26, 161, 6, 0, 147, 201, 4, 138, 34, 233, 170, 229, 138, 220, 182, 62, 139, 186, 252, 165, 32, 57, 152, 152, 109, 112, 81, 86, 30, 64, 57, 39, 219, 16, 22, 35, 231, 4, 164, 82, 166, 243, 2, 128, 90, 195, 192, 229, 138, 196, 22, 114, 9, 46, 202, 202, 3, 32, 84, 22, 114, 76, 64, 202, 183, 59, 111, 193, 215, 202, 96, 0, 235, 231, 0, 228, 22, 92, 148, 149, 7, 64, 168, 44, 228, 150, 128, 36, 86, 98, 79, 114, 224, 47, 26, 92, 4, 16, 11, 160, 166, 67, 150, 2, 80, 201, 9, 56, 149, 142, 20, 167, 129, 233, 118, 222, 45, 21, 153, 140, 124, 201, 177, 139, 54, 106, 2, 161, 12, 144, 66, 58, 111, 33, 16, 1, 32, 16, 242, 68, 140, 185, 190, 208, 16, 1, 32, 72, 30, 169, 77, 249, 74, 209, 170, 171, 84, 144, 85, 0, 153, 144, 156, 254, 154, 46, 77, 150, 80, 92, 74, 185, 182, 95, 10, 136, 0, 200, 4, 41, 85, 65, 86, 42, 26, 138, 150, 245, 124, 63, 21, 68, 0, 100, 2, 169, 130, 20, 31, 127, 136, 199, 4, 231, 134, 157, 151, 79, 13, 66, 38, 72, 12, 64, 6, 144, 42, 72, 105, 17, 21, 2, 64, 254, 241, 0, 34, 0, 50, 96, 169, 42, 200, 248, 52, 89, 66, 233, 137, 23, 3, 154, 162, 81, 5, 165, 172, 166, 9, 228, 177, 33, 3, 82, 165, 191, 166, 74, 147, 37, 136, 11, 31, 226, 225, 12, 177, 152, 224, 220, 152, 224, 220, 152, 227, 57, 184, 61, 156, 216, 195, 90, 18, 193, 61, 128, 116, 61, 250, 146, 123, 249, 17, 178, 39, 85, 250, 107, 170, 52, 89, 130, 180, 112, 134, 88, 152, 39, 45, 104, 110, 105, 70, 45, 173, 65, 53, 205, 96, 142, 231, 36, 229, 33, 8, 238, 1, 148, 83, 19, 7, 169, 64, 250, 239, 201, 31, 103, 136, 197, 152, 223, 133, 211, 19, 163, 176, 115, 28, 166, 230, 43, 247, 196, 70, 112, 1, 72, 23, 157, 78, 142, 98, 151, 3, 229, 214, 178, 138, 32, 60, 169, 58, 86, 77, 187, 167, 49, 19, 152, 195, 4, 231, 198, 148, 203, 37, 234, 52, 161, 232, 49, 0, 185, 53, 113, 200, 5, 226, 237, 16, 50, 145, 169, 164, 217, 171, 12, 98, 216, 62, 30, 139, 25, 148, 26, 193, 5, 32, 149, 226, 165, 138, 98, 151, 3, 165, 246, 118, 164, 150, 18, 75, 200, 76, 170, 142, 85, 169, 58, 91, 1, 88, 20, 64, 44, 201, 248, 132, 62, 161, 28, 155, 56, 8, 69, 57, 123, 59, 4, 225, 200, 38, 166, 147, 73, 12, 132, 106, 36, 42, 120, 56, 94, 110, 77, 28, 10, 65, 206, 45, 171, 42, 25, 185, 237, 46, 229, 12, 177, 112, 114, 44, 128, 133, 196, 35, 93, 149, 78, 144, 115, 151, 228, 91, 150, 107, 20, 187, 146, 189, 29, 57, 35, 231, 186, 138, 104, 226, 145, 91, 163, 128, 157, 247, 21, 60, 85, 144, 142, 204, 201, 144, 92, 230, 119, 149, 140, 212, 42, 25, 197, 168, 171, 40, 70, 252, 198, 31, 151, 120, 100, 203, 83, 12, 72, 70, 142, 192, 72, 177, 101, 149, 216, 196, 63, 113, 171, 85, 250, 184, 39, 110, 29, 92, 78, 23, 156, 78, 55, 106, 106, 141, 37, 25, 75, 185, 214, 85, 240, 33, 30, 78, 240, 177, 169, 66, 192, 233, 67, 87, 67, 99, 198, 207, 21, 237, 155, 146, 136, 53, 33, 138, 148, 42, 25, 43, 101, 119, 169, 169, 185, 89, 28, 57, 59, 136, 35, 103, 7, 49, 108, 49, 167, 77, 60, 146, 159, 212, 17, 100, 133, 212, 158, 184, 149, 82, 87, 17, 31, 107, 211, 154, 244, 224, 52, 10, 216, 82, 148, 49, 147, 41, 128, 0, 16, 111, 39, 61, 82, 171, 100, 172, 228, 186, 10, 62, 196, 199, 226, 4, 209, 122, 4, 226, 1, 16, 138, 138, 212, 158, 184, 229, 186, 34, 149, 45, 206, 16, 11, 235, 212, 194, 182, 97, 196, 3, 32, 20, 149, 74, 126, 226, 74, 129, 84, 2, 167, 53, 45, 228, 165, 16, 1, 32, 20, 21, 178, 42, 34, 77, 38, 56, 55, 218, 24, 61, 254, 63, 91, 213, 67, 145, 233, 13, 169, 197, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130]; diff --git a/tile_server/static/generated/land.dart b/tile_server/static/generated/land.dart new file mode 100644 index 00000000..48881eab --- /dev/null +++ b/tile_server/static/generated/land.dart @@ -0,0 +1 @@ +final landTileBytes = [137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 1, 0, 0, 0, 1, 0, 8, 3, 0, 0, 0, 107, 172, 88, 84, 0, 0, 2, 253, 80, 76, 84, 69, 51, 52, 49, 72, 52, 55, 60, 65, 58, 76, 85, 24, 157, 17, 17, 83, 81, 48, 162, 28, 28, 73, 74, 72, 84, 75, 67, 78, 83, 73, 92, 100, 43, 84, 85, 71, 85, 87, 84, 169, 46, 46, 110, 80, 76, 125, 90, 55, 104, 112, 57, 89, 110, 87, 129, 96, 61, 102, 109, 86, 113, 94, 97, 130, 88, 87, 134, 105, 70, 104, 105, 102, 94, 131, 93, 104, 115, 103, 121, 128, 77, 139, 112, 78, 140, 101, 88, 141, 89, 99, 113, 117, 107, 182, 78, 78, 142, 116, 84, 117, 117, 116, 149, 107, 95, 146, 121, 90, 151, 150, 59, 145, 110, 113, 117, 141, 112, 128, 134, 111, 138, 144, 96, 148, 150, 83, 172, 121, 91, 130, 132, 125, 154, 135, 104, 176, 113, 111, 129, 154, 119, 177, 153, 75, 136, 137, 133, 147, 153, 107, 150, 171, 86, 141, 169, 102, 157, 140, 115, 212, 117, 89, 153, 166, 102, 157, 149, 115, 165, 165, 91, 134, 164, 124, 200, 110, 114, 125, 185, 115, 151, 138, 138, 141, 150, 137, 134, 167, 129, 218, 89, 126, 224, 83, 126, 151, 136, 147, 206, 145, 85, 149, 150, 138, 142, 189, 107, 166, 171, 101, 172, 150, 116, 153, 171, 115, 156, 181, 103, 132, 189, 120, 221, 92, 129, 182, 122, 144, 224, 93, 131, 210, 110, 130, 162, 166, 123, 168, 179, 105, 180, 172, 102, 140, 184, 131, 148, 170, 137, 207, 140, 108, 206, 164, 86, 152, 187, 120, 155, 155, 151, 142, 195, 125, 147, 182, 133, 142, 194, 129, 149, 178, 138, 149, 184, 135, 167, 155, 147, 182, 183, 104, 157, 182, 131, 172, 165, 133, 183, 166, 121, 168, 184, 120, 151, 185, 137, 154, 179, 141, 157, 164, 153, 147, 197, 133, 157, 187, 134, 228, 110, 142, 148, 205, 128, 154, 189, 139, 150, 197, 137, 155, 196, 133, 172, 146, 168, 155, 185, 147, 180, 168, 139, 170, 188, 130, 184, 186, 118, 204, 173, 112, 165, 169, 156, 207, 142, 141, 228, 143, 119, 157, 192, 142, 153, 206, 133, 186, 195, 115, 155, 201, 141, 156, 209, 135, 158, 198, 145, 168, 168, 165, 161, 196, 145, 182, 172, 148, 169, 196, 138, 183, 186, 135, 168, 184, 157, 171, 198, 146, 163, 204, 149, 167, 193, 156, 182, 183, 151, 163, 213, 141, 171, 182, 166, 195, 202, 122, 188, 197, 135, 180, 171, 171, 231, 141, 150, 172, 203, 148, 203, 154, 166, 167, 205, 152, 171, 196, 157, 204, 179, 141, 184, 167, 178, 171, 205, 156, 195, 201, 136, 180, 199, 154, 228, 186, 120, 185, 183, 167, 172, 202, 163, 200, 174, 164, 173, 209, 158, 232, 146, 162, 175, 210, 161, 201, 210, 136, 181, 214, 155, 197, 186, 167, 212, 199, 139, 182, 202, 168, 219, 167, 166, 178, 212, 163, 198, 201, 155, 205, 169, 180, 187, 186, 183, 213, 185, 162, 182, 216, 164, 197, 197, 168, 182, 213, 169, 187, 212, 165, 196, 216, 153, 210, 215, 140, 186, 199, 181, 184, 225, 158, 214, 201, 153, 194, 189, 187, 186, 219, 166, 187, 214, 171, 209, 172, 191, 188, 219, 171, 248, 177, 155, 190, 225, 166, 189, 216, 178, 197, 201, 185, 215, 220, 148, 215, 201, 168, 199, 215, 171, 217, 186, 184, 191, 206, 193, 241, 203, 146, 198, 197, 196, 234, 180, 178, 216, 199, 183, 216, 212, 170, 226, 202, 170, 198, 228, 173, 199, 218, 183, 221, 226, 155, 224, 229, 159, 210, 204, 200, 247, 194, 173, 205, 234, 176, 213, 217, 185, 204, 216, 197, 233, 201, 184, 237, 188, 198, 204, 226, 196, 210, 230, 187, 229, 233, 166, 246, 198, 184, 232, 200, 198, 252, 214, 164, 214, 217, 200, 228, 218, 187, 215, 228, 201, 244, 199, 203, 232, 231, 184, 217, 217, 215, 232, 219, 199, 245, 220, 185, 217, 240, 195, 237, 241, 178, 243, 203, 210, 249, 230, 178, 226, 221, 219, 229, 233, 204, 228, 227, 212, 220, 234, 214, 252, 215, 204, 228, 226, 220, 229, 234, 211, 232, 227, 215, 245, 230, 200, 244, 216, 218, 227, 236, 219, 235, 238, 211, 237, 228, 219, 243, 231, 214, 247, 250, 191, 234, 234, 222, 238, 240, 213, 232, 231, 230, 247, 220, 226, 247, 250, 196, 241, 242, 214, 234, 243, 229, 243, 238, 233, 243, 243, 242, 246, 247, 235, 252, 238, 241, 244, 249, 241, 251, 243, 244, 251, 251, 245, 253, 246, 248, 254, 254, 254, 34, 181, 220, 167, 0, 0, 69, 129, 73, 68, 65, 84, 120, 156, 205, 125, 15, 124, 19, 215, 157, 167, 46, 221, 75, 66, 22, 54, 9, 189, 66, 73, 110, 113, 56, 183, 185, 101, 33, 9, 203, 166, 65, 33, 205, 101, 91, 214, 148, 166, 80, 177, 161, 136, 73, 90, 47, 56, 81, 2, 52, 9, 38, 50, 156, 147, 51, 24, 185, 118, 194, 31, 23, 145, 137, 11, 25, 11, 136, 173, 172, 107, 11, 66, 99, 251, 140, 229, 198, 161, 38, 169, 38, 85, 101, 23, 145, 176, 98, 178, 202, 110, 56, 217, 194, 161, 216, 92, 192, 28, 150, 13, 24, 125, 238, 247, 123, 111, 102, 52, 163, 153, 145, 70, 134, 238, 221, 239, 3, 99, 253, 153, 25, 205, 251, 190, 223, 255, 247, 123, 239, 89, 184, 255, 95, 201, 243, 238, 91, 23, 252, 60, 147, 246, 105, 107, 103, 69, 167, 68, 129, 80, 35, 199, 5, 66, 89, 168, 179, 147, 207, 244, 181, 229, 255, 69, 219, 76, 145, 119, 240, 165, 193, 11, 199, 153, 180, 79, 15, 0, 0, 53, 18, 2, 33, 175, 167, 77, 209, 22, 93, 44, 248, 206, 204, 16, 253, 255, 11, 0, 215, 251, 225, 135, 23, 248, 227, 124, 218, 167, 53, 21, 53, 53, 41, 30, 224, 36, 0, 218, 218, 2, 141, 92, 99, 22, 102, 8, 232, 48, 195, 159, 22, 128, 45, 91, 174, 227, 226, 227, 131, 47, 93, 184, 112, 161, 55, 237, 83, 0, 128, 54, 126, 253, 219, 192, 2, 82, 239, 6, 232, 151, 89, 154, 143, 76, 243, 239, 11, 192, 143, 126, 164, 247, 233, 78, 102, 39, 199, 174, 101, 54, 178, 89, 174, 62, 254, 214, 103, 128, 128, 154, 5, 188, 85, 21, 64, 50, 0, 18, 181, 227, 119, 45, 134, 204, 14, 122, 32, 16, 234, 252, 247, 7, 96, 203, 119, 191, 171, 199, 2, 155, 214, 110, 226, 54, 110, 100, 55, 110, 204, 118, 125, 231, 46, 0, 64, 80, 127, 68, 9, 94, 109, 219, 66, 164, 187, 157, 182, 58, 224, 81, 169, 131, 180, 230, 75, 164, 163, 14, 115, 7, 192, 108, 255, 113, 28, 251, 93, 32, 157, 211, 158, 222, 249, 52, 199, 176, 28, 203, 100, 189, 3, 202, 128, 79, 245, 9, 105, 199, 1, 242, 114, 41, 109, 129, 200, 247, 1, 3, 21, 40, 178, 190, 104, 54, 110, 4, 0, 166, 251, 143, 123, 9, 1, 88, 165, 249, 120, 231, 90, 110, 237, 78, 4, 224, 199, 89, 239, 240, 198, 201, 11, 131, 234, 79, 14, 64, 227, 189, 244, 101, 33, 109, 65, 139, 162, 53, 109, 222, 52, 54, 64, 166, 231, 101, 0, 16, 39, 95, 0, 193, 106, 191, 14, 0, 76, 247, 95, 205, 119, 9, 213, 164, 127, 190, 105, 19, 252, 51, 7, 97, 247, 73, 141, 18, 76, 209, 54, 218, 161, 156, 87, 108, 10, 188, 109, 75, 87, 3, 164, 209, 138, 246, 183, 113, 158, 0, 81, 153, 129, 113, 3, 96, 190, 255, 94, 162, 0, 188, 148, 254, 249, 211, 59, 57, 192, 112, 45, 179, 54, 187, 16, 85, 108, 17, 90, 13, 191, 20, 219, 32, 189, 8, 116, 106, 165, 128, 182, 154, 39, 127, 240, 75, 210, 114, 138, 192, 184, 1, 200, 161, 255, 12, 136, 65, 130, 63, 38, 78, 173, 176, 101, 254, 190, 69, 234, 205, 0, 85, 242, 105, 8, 212, 117, 18, 37, 16, 74, 233, 126, 196, 168, 5, 46, 51, 0, 192, 140, 126, 203, 161, 255, 12, 137, 145, 15, 217, 200, 154, 241, 219, 98, 250, 12, 141, 33, 165, 148, 43, 25, 32, 245, 82, 1, 13, 241, 25, 244, 69, 192, 140, 126, 203, 161, 255, 140, 239, 33, 31, 178, 145, 53, 227, 183, 197, 34, 131, 164, 236, 156, 214, 206, 203, 141, 86, 182, 95, 225, 50, 170, 0, 48, 167, 223, 204, 63, 254, 245, 223, 193, 154, 153, 207, 10, 172, 214, 130, 114, 21, 0, 250, 8, 96, 163, 201, 159, 198, 22, 122, 161, 62, 0, 38, 245, 155, 249, 199, 191, 126, 42, 200, 42, 104, 213, 240, 255, 237, 165, 153, 76, 125, 128, 52, 58, 16, 106, 135, 191, 237, 1, 160, 70, 165, 207, 104, 81, 252, 130, 73, 253, 198, 200, 135, 63, 61, 89, 205, 156, 4, 110, 241, 27, 70, 12, 208, 142, 102, 2, 41, 128, 92, 160, 3, 143, 165, 200, 90, 45, 221, 232, 70, 232, 183, 27, 76, 214, 236, 167, 180, 66, 179, 183, 109, 241, 28, 192, 238, 15, 144, 22, 43, 136, 229, 218, 219, 83, 167, 102, 244, 4, 109, 197, 55, 68, 191, 113, 173, 60, 15, 166, 219, 227, 247, 131, 199, 198, 243, 126, 207, 245, 220, 171, 216, 90, 148, 253, 36, 108, 249, 250, 26, 206, 67, 187, 95, 221, 202, 198, 167, 149, 103, 182, 235, 180, 63, 5, 0, 91, 204, 221, 8, 238, 14, 247, 172, 92, 217, 195, 251, 226, 85, 29, 126, 95, 124, 243, 188, 170, 248, 184, 17, 96, 109, 214, 34, 29, 86, 164, 102, 26, 13, 182, 68, 128, 192, 82, 15, 231, 37, 28, 64, 186, 59, 213, 208, 181, 140, 242, 82, 221, 104, 65, 237, 8, 49, 28, 203, 142, 31, 0, 120, 42, 175, 48, 121, 247, 238, 201, 3, 241, 217, 147, 55, 251, 195, 15, 46, 236, 200, 175, 226, 57, 79, 56, 46, 8, 225, 176, 16, 246, 101, 191, 133, 130, 138, 202, 117, 63, 166, 58, 10, 13, 182, 76, 32, 2, 30, 194, 8, 36, 74, 36, 238, 17, 168, 190, 54, 226, 31, 41, 47, 213, 107, 191, 6, 128, 10, 107, 49, 147, 211, 115, 42, 8, 158, 202, 215, 51, 37, 46, 76, 142, 11, 85, 11, 1, 128, 252, 166, 248, 202, 117, 60, 231, 111, 218, 60, 59, 191, 105, 221, 61, 243, 198, 193, 13, 197, 154, 79, 168, 153, 70, 131, 45, 81, 43, 9, 16, 83, 97, 34, 198, 6, 129, 181, 32, 134, 161, 144, 164, 1, 41, 233, 3, 160, 200, 55, 8, 131, 131, 189, 194, 23, 233, 57, 40, 243, 132, 79, 37, 172, 188, 231, 158, 205, 130, 63, 14, 0, 248, 249, 41, 179, 243, 227, 62, 206, 95, 117, 187, 208, 115, 123, 85, 60, 127, 191, 223, 236, 157, 100, 235, 103, 213, 124, 69, 204, 52, 49, 216, 244, 253, 129, 148, 11, 128, 97, 131, 183, 157, 178, 250, 38, 15, 253, 171, 184, 82, 63, 97, 102, 105, 232, 168, 19, 127, 77, 128, 224, 251, 194, 96, 239, 113, 146, 129, 168, 230, 114, 38, 124, 42, 46, 146, 191, 114, 246, 236, 184, 135, 71, 14, 168, 202, 95, 57, 133, 247, 3, 0, 179, 195, 241, 219, 227, 248, 145, 241, 197, 42, 209, 182, 201, 29, 95, 80, 145, 126, 34, 17, 1, 98, 176, 85, 237, 167, 41, 2, 42, 254, 68, 214, 3, 212, 214, 43, 174, 212, 109, 127, 200, 146, 28, 233, 15, 118, 53, 188, 230, 116, 213, 117, 247, 18, 4, 224, 159, 208, 219, 187, 30, 37, 80, 248, 162, 247, 184, 121, 0, 240, 169, 124, 155, 31, 20, 132, 252, 166, 86, 108, 109, 124, 114, 60, 220, 148, 31, 6, 0, 30, 164, 0, 172, 211, 3, 160, 133, 250, 41, 58, 162, 141, 84, 93, 160, 185, 0, 205, 52, 53, 216, 72, 168, 251, 58, 183, 73, 33, 119, 64, 12, 128, 67, 41, 231, 223, 203, 165, 190, 211, 82, 128, 179, 36, 69, 26, 25, 138, 118, 53, 55, 52, 247, 124, 65, 81, 248, 227, 111, 187, 59, 143, 227, 43, 243, 0, 224, 83, 185, 155, 238, 137, 68, 166, 244, 80, 0, 166, 116, 196, 215, 61, 152, 17, 128, 70, 154, 209, 106, 211, 136, 118, 133, 196, 190, 58, 190, 48, 35, 7, 36, 132, 14, 116, 30, 0, 125, 217, 70, 148, 94, 40, 149, 248, 224, 196, 68, 73, 91, 38, 0, 64, 94, 100, 0, 68, 26, 10, 54, 55, 116, 8, 255, 74, 232, 143, 36, 35, 199, 11, 38, 181, 55, 62, 212, 143, 133, 117, 247, 228, 239, 134, 139, 170, 154, 252, 124, 207, 236, 252, 121, 16, 207, 251, 59, 54, 135, 227, 15, 198, 241, 35, 205, 53, 50, 167, 170, 69, 59, 180, 109, 105, 59, 245, 224, 203, 181, 44, 192, 200, 7, 137, 138, 139, 41, 127, 75, 201, 47, 244, 135, 72, 144, 12, 250, 176, 81, 58, 201, 171, 69, 0, 141, 166, 165, 161, 161, 185, 43, 218, 63, 162, 4, 97, 36, 218, 213, 176, 255, 3, 64, 96, 16, 84, 2, 128, 96, 90, 12, 224, 169, 60, 60, 31, 230, 193, 42, 135, 195, 30, 206, 79, 77, 159, 39, 28, 246, 249, 195, 126, 242, 81, 58, 97, 122, 130, 36, 40, 100, 209, 102, 193, 105, 7, 117, 21, 40, 124, 131, 178, 111, 145, 198, 23, 210, 2, 80, 94, 196, 41, 219, 143, 44, 128, 40, 194, 109, 3, 33, 133, 33, 12, 133, 246, 150, 22, 2, 137, 9, 148, 125, 69, 168, 106, 44, 200, 241, 189, 145, 142, 134, 134, 134, 174, 96, 76, 129, 67, 127, 87, 67, 7, 81, 10, 231, 211, 242, 178, 153, 1, 200, 209, 143, 242, 134, 2, 91, 169, 160, 138, 162, 141, 253, 4, 207, 8, 159, 189, 177, 180, 240, 32, 158, 82, 144, 238, 15, 104, 127, 165, 186, 128, 40, 63, 146, 252, 59, 126, 60, 5, 0, 116, 113, 72, 233, 9, 7, 14, 238, 219, 199, 181, 96, 64, 116, 176, 192, 90, 96, 43, 174, 150, 0, 16, 233, 11, 192, 161, 57, 56, 148, 226, 132, 224, 110, 80, 137, 253, 253, 125, 127, 58, 0, 160, 135, 218, 69, 141, 213, 24, 104, 223, 4, 130, 141, 13, 225, 15, 22, 46, 45, 44, 124, 32, 244, 6, 120, 50, 1, 86, 235, 11, 104, 200, 138, 130, 196, 119, 66, 219, 143, 35, 12, 60, 0, 208, 142, 247, 109, 76, 157, 82, 110, 37, 127, 52, 142, 164, 18, 0, 17, 133, 230, 134, 174, 168, 200, 10, 35, 13, 145, 254, 88, 44, 150, 75, 147, 114, 34, 73, 24, 121, 49, 97, 31, 96, 20, 198, 106, 239, 86, 250, 215, 196, 125, 88, 188, 21, 54, 254, 184, 148, 7, 12, 144, 192, 40, 117, 70, 133, 168, 77, 53, 214, 70, 3, 0, 33, 1, 88, 33, 74, 32, 136, 54, 68, 99, 177, 158, 158, 158, 27, 221, 116, 74, 26, 171, 180, 79, 124, 65, 26, 241, 6, 125, 109, 42, 54, 109, 36, 151, 28, 15, 73, 0, 64, 231, 123, 149, 223, 23, 136, 142, 141, 198, 145, 212, 7, 128, 128, 176, 59, 74, 229, 160, 171, 33, 24, 139, 141, 223, 63, 204, 68, 129, 144, 222, 152, 46, 178, 51, 52, 68, 228, 138, 118, 171, 153, 59, 177, 235, 81, 252, 241, 226, 3, 7, 90, 85, 170, 143, 82, 145, 40, 72, 233, 142, 100, 6, 0, 64, 55, 54, 196, 168, 36, 4, 1, 129, 136, 113, 126, 122, 220, 68, 218, 126, 176, 240, 160, 170, 249, 141, 34, 99, 16, 177, 88, 31, 8, 88, 205, 57, 165, 197, 214, 183, 65, 6, 148, 92, 79, 168, 220, 74, 175, 151, 0, 72, 115, 36, 1, 128, 15, 79, 246, 26, 67, 208, 211, 64, 149, 65, 3, 40, 130, 232, 13, 106, 181, 130, 8, 0, 45, 7, 81, 93, 173, 47, 220, 10, 56, 128, 138, 110, 84, 121, 44, 7, 151, 154, 108, 63, 208, 122, 48, 27, 129, 182, 180, 15, 139, 217, 106, 107, 5, 8, 17, 43, 9, 146, 218, 145, 68, 0, 222, 221, 245, 150, 49, 2, 131, 29, 13, 93, 96, 22, 134, 250, 135, 64, 10, 188, 40, 85, 94, 31, 24, 116, 62, 12, 1, 174, 233, 208, 198, 144, 218, 69, 111, 132, 114, 194, 214, 165, 235, 169, 214, 150, 51, 118, 45, 109, 251, 178, 100, 69, 213, 132, 151, 87, 107, 162, 232, 10, 117, 64, 193, 168, 29, 73, 73, 4, 222, 59, 137, 199, 75, 163, 151, 210, 33, 232, 143, 53, 36, 147, 99, 93, 35, 253, 177, 248, 232, 112, 152, 227, 227, 64, 71, 226, 66, 207, 240, 145, 120, 110, 241, 189, 222, 3, 99, 43, 177, 207, 90, 218, 209, 37, 14, 28, 92, 42, 250, 125, 162, 159, 214, 216, 146, 83, 251, 9, 177, 54, 171, 108, 55, 53, 145, 20, 18, 35, 31, 8, 89, 46, 93, 186, 112, 9, 254, 247, 190, 123, 18, 254, 140, 38, 8, 2, 151, 144, 232, 95, 0, 0, 88, 32, 122, 203, 200, 80, 255, 104, 67, 89, 220, 19, 127, 109, 193, 240, 192, 140, 89, 19, 34, 61, 183, 196, 253, 62, 31, 231, 105, 109, 5, 190, 240, 121, 91, 91, 61, 222, 214, 92, 17, 9, 132, 210, 180, 149, 151, 180, 183, 200, 38, 13, 120, 6, 172, 57, 222, 17, 137, 45, 2, 8, 138, 172, 64, 186, 30, 4, 35, 31, 8, 89, 70, 175, 92, 25, 5, 26, 134, 255, 87, 198, 34, 95, 29, 29, 77, 92, 24, 165, 116, 137, 28, 207, 15, 141, 0, 0, 19, 16, 0, 251, 172, 97, 79, 220, 62, 45, 62, 16, 159, 115, 104, 24, 0, 144, 40, 44, 200, 47, 115, 211, 148, 45, 90, 109, 77, 168, 220, 186, 158, 74, 193, 62, 109, 36, 96, 138, 138, 170, 139, 13, 89, 135, 145, 15, 132, 44, 19, 163, 67, 51, 38, 206, 74, 140, 53, 79, 156, 56, 35, 49, 209, 50, 113, 243, 103, 189, 163, 101, 219, 167, 78, 180, 143, 94, 25, 158, 51, 113, 106, 100, 100, 164, 97, 234, 84, 199, 196, 4, 1, 32, 142, 0, 12, 15, 60, 58, 97, 98, 67, 240, 150, 184, 48, 173, 231, 202, 107, 19, 39, 44, 24, 24, 126, 244, 181, 59, 39, 190, 230, 152, 120, 231, 145, 120, 110, 15, 218, 238, 53, 250, 166, 96, 41, 232, 196, 64, 69, 238, 249, 233, 114, 155, 153, 76, 106, 138, 44, 177, 177, 137, 101, 163, 115, 102, 141, 89, 250, 71, 131, 99, 13, 83, 135, 71, 19, 131, 163, 115, 110, 9, 118, 223, 178, 123, 108, 250, 156, 225, 134, 155, 18, 253, 150, 232, 208, 212, 137, 35, 67, 177, 17, 0, 96, 19, 2, 96, 159, 208, 211, 19, 7, 14, 152, 232, 24, 237, 184, 233, 72, 207, 68, 251, 149, 169, 19, 123, 14, 89, 102, 9, 143, 78, 189, 126, 189, 32, 17, 107, 43, 88, 159, 115, 86, 6, 20, 128, 77, 63, 149, 104, 72, 150, 43, 67, 150, 237, 219, 23, 220, 50, 54, 117, 70, 100, 20, 0, 24, 29, 189, 112, 97, 116, 206, 156, 209, 81, 231, 140, 107, 150, 196, 232, 149, 137, 13, 101, 211, 199, 18, 13, 19, 65, 9, 2, 0, 163, 155, 4, 251, 180, 145, 137, 206, 120, 120, 160, 99, 194, 157, 143, 198, 135, 103, 205, 137, 199, 95, 155, 120, 101, 170, 61, 30, 183, 244, 196, 143, 128, 94, 200, 245, 153, 51, 80, 121, 78, 93, 137, 196, 230, 216, 122, 14, 117, 64, 212, 98, 183, 219, 203, 198, 174, 148, 77, 152, 142, 0, 92, 66, 0, 22, 140, 142, 150, 77, 29, 177, 36, 18, 35, 83, 183, 151, 205, 74, 140, 118, 77, 28, 139, 17, 0, 188, 225, 5, 211, 175, 76, 120, 77, 240, 9, 135, 44, 19, 167, 14, 12, 79, 95, 16, 15, 31, 154, 112, 101, 170, 19, 0, 16, 180, 0, 104, 171, 196, 180, 89, 109, 99, 170, 182, 226, 81, 27, 15, 235, 157, 90, 92, 84, 96, 34, 104, 210, 33, 203, 232, 168, 37, 54, 58, 58, 54, 6, 170, 240, 150, 104, 51, 112, 192, 40, 0, 0, 127, 102, 205, 25, 187, 169, 249, 179, 161, 155, 162, 93, 19, 0, 141, 137, 35, 111, 245, 143, 118, 221, 50, 58, 32, 76, 125, 244, 202, 52, 232, 118, 84, 130, 192, 251, 11, 166, 197, 227, 143, 78, 55, 4, 64, 91, 37, 102, 144, 250, 210, 39, 104, 123, 53, 74, 116, 150, 42, 1, 56, 169, 160, 168, 120, 28, 89, 76, 36, 203, 240, 104, 217, 77, 179, 102, 60, 58, 54, 113, 206, 172, 9, 163, 67, 55, 205, 234, 64, 0, 38, 76, 159, 113, 83, 255, 88, 195, 45, 179, 38, 206, 73, 142, 76, 157, 58, 107, 234, 196, 196, 201, 183, 134, 71, 103, 220, 50, 109, 194, 68, 97, 224, 200, 77, 211, 166, 149, 129, 18, 236, 185, 233, 200, 240, 196, 169, 211, 111, 233, 25, 53, 0, 64, 167, 74, 76, 155, 213, 206, 68, 5, 212, 145, 213, 166, 70, 111, 28, 89, 46, 13, 143, 158, 111, 14, 142, 142, 245, 55, 119, 129, 69, 60, 223, 124, 30, 1, 176, 199, 58, 18, 35, 67, 137, 33, 112, 1, 134, 18, 87, 130, 93, 137, 216, 200, 165, 119, 223, 29, 16, 58, 94, 59, 36, 8, 173, 241, 200, 107, 187, 7, 192, 17, 138, 247, 244, 140, 14, 28, 58, 20, 31, 24, 232, 1, 67, 216, 65, 60, 36, 165, 18, 212, 171, 18, 211, 4, 35, 25, 72, 174, 215, 40, 207, 198, 2, 215, 65, 22, 116, 255, 192, 248, 147, 3, 121, 121, 9, 1, 24, 1, 7, 32, 22, 75, 38, 250, 199, 198, 162, 49, 64, 34, 57, 244, 197, 133, 119, 7, 253, 126, 33, 188, 231, 199, 204, 218, 58, 28, 233, 9, 251, 91, 225, 63, 15, 47, 189, 254, 112, 216, 199, 135, 121, 47, 175, 118, 143, 245, 170, 196, 52, 193, 72, 6, 74, 213, 107, 88, 179, 156, 89, 161, 81, 126, 237, 161, 128, 151, 75, 15, 12, 244, 72, 47, 26, 28, 181, 151, 141, 68, 35, 209, 88, 114, 36, 26, 11, 98, 52, 4, 0, 196, 6, 49, 66, 102, 240, 18, 198, 196, 109, 145, 244, 171, 196, 210, 131, 145, 12, 148, 170, 215, 80, 122, 196, 45, 152, 234, 72, 107, 26, 107, 77, 19, 18, 18, 103, 55, 30, 64, 125, 187, 49, 32, 70, 24, 250, 169, 149, 52, 0, 6, 251, 34, 145, 232, 208, 8, 16, 201, 16, 67, 16, 24, 196, 236, 208, 72, 127, 132, 124, 203, 224, 37, 76, 246, 71, 39, 100, 80, 37, 198, 168, 131, 17, 99, 82, 212, 107, 20, 43, 100, 128, 134, 144, 105, 231, 166, 141, 31, 180, 137, 121, 133, 77, 27, 1, 175, 125, 212, 219, 50, 200, 45, 41, 0, 192, 182, 203, 233, 225, 145, 88, 180, 43, 24, 133, 246, 67, 44, 24, 13, 10, 95, 208, 83, 102, 178, 156, 33, 0, 102, 43, 72, 25, 249, 160, 71, 224, 202, 88, 173, 180, 185, 202, 122, 141, 20, 11, 144, 17, 63, 173, 7, 109, 85, 190, 17, 163, 201, 128, 215, 203, 120, 3, 161, 125, 36, 73, 160, 101, 27, 74, 41, 0, 190, 32, 217, 143, 145, 254, 104, 48, 8, 124, 223, 21, 140, 141, 37, 163, 93, 93, 35, 201, 161, 200, 160, 204, 31, 229, 25, 162, 51, 179, 21, 164, 140, 124, 208, 35, 28, 19, 246, 82, 169, 81, 214, 107, 40, 134, 138, 3, 217, 1, 144, 83, 75, 236, 143, 189, 52, 231, 26, 8, 24, 48, 0, 103, 25, 28, 28, 236, 237, 21, 142, 251, 143, 15, 6, 163, 208, 246, 40, 166, 62, 250, 41, 19, 16, 110, 136, 246, 41, 4, 132, 203, 128, 128, 217, 10, 82, 70, 62, 24, 145, 167, 49, 16, 240, 84, 168, 234, 53, 108, 210, 207, 146, 129, 164, 128, 118, 128, 65, 37, 2, 18, 7, 132, 90, 48, 172, 228, 54, 137, 3, 230, 186, 63, 38, 15, 143, 247, 146, 216, 159, 80, 116, 100, 44, 58, 20, 13, 70, 131, 152, 13, 27, 188, 160, 4, 128, 171, 176, 26, 120, 155, 146, 196, 250, 48, 52, 12, 103, 106, 159, 49, 129, 38, 19, 37, 160, 177, 209, 102, 211, 197, 170, 49, 100, 192, 202, 74, 106, 33, 249, 240, 198, 181, 107, 215, 178, 108, 11, 178, 81, 27, 230, 25, 245, 207, 149, 1, 56, 222, 43, 3, 16, 139, 38, 163, 132, 19, 128, 255, 21, 253, 127, 129, 140, 17, 25, 165, 233, 37, 137, 221, 211, 145, 191, 110, 158, 56, 148, 226, 51, 12, 246, 116, 169, 88, 236, 198, 98, 162, 210, 177, 102, 7, 255, 50, 234, 147, 196, 174, 12, 120, 165, 131, 30, 209, 1, 87, 83, 250, 86, 6, 96, 211, 166, 227, 66, 127, 159, 32, 28, 63, 30, 18, 130, 99, 9, 4, 32, 56, 150, 12, 166, 25, 9, 122, 46, 171, 227, 153, 201, 18, 27, 111, 90, 24, 167, 57, 100, 62, 34, 228, 130, 128, 56, 116, 33, 147, 88, 170, 193, 128, 167, 171, 248, 61, 194, 212, 59, 55, 98, 186, 156, 13, 168, 228, 81, 27, 7, 50, 242, 193, 152, 100, 0, 176, 1, 21, 84, 118, 132, 232, 208, 88, 144, 48, 66, 255, 23, 105, 46, 2, 29, 38, 100, 11, 180, 174, 153, 36, 177, 124, 71, 254, 148, 217, 116, 60, 53, 60, 187, 41, 135, 4, 73, 133, 53, 253, 19, 41, 188, 145, 218, 79, 236, 121, 35, 152, 56, 239, 166, 181, 192, 210, 7, 2, 94, 149, 194, 101, 53, 55, 96, 228, 131, 49, 201, 0, 144, 177, 93, 17, 0, 104, 250, 80, 127, 44, 58, 54, 18, 73, 247, 145, 6, 197, 241, 129, 34, 157, 100, 45, 253, 185, 248, 237, 29, 224, 21, 163, 38, 8, 3, 0, 112, 108, 5, 157, 48, 48, 0, 135, 204, 163, 204, 213, 198, 234, 85, 108, 88, 128, 72, 113, 0, 147, 254, 79, 147, 190, 10, 164, 143, 16, 234, 63, 81, 166, 95, 85, 0, 32, 158, 25, 32, 0, 4, 135, 198, 200, 120, 128, 214, 75, 148, 16, 168, 176, 106, 152, 128, 254, 92, 252, 246, 72, 216, 239, 139, 207, 190, 123, 74, 83, 124, 118, 126, 254, 228, 221, 241, 149, 43, 167, 172, 220, 63, 27, 249, 34, 211, 131, 84, 27, 182, 95, 236, 217, 0, 117, 127, 2, 33, 15, 81, 184, 88, 247, 47, 149, 180, 210, 75, 205, 39, 208, 149, 148, 14, 64, 69, 11, 2, 16, 67, 103, 32, 184, 107, 80, 11, 64, 106, 2, 131, 230, 231, 200, 245, 76, 120, 247, 237, 243, 34, 66, 213, 236, 184, 48, 121, 120, 246, 194, 184, 112, 251, 240, 194, 187, 227, 241, 42, 248, 63, 37, 146, 155, 78, 36, 100, 147, 37, 32, 68, 45, 57, 56, 249, 84, 225, 178, 7, 151, 206, 156, 105, 45, 175, 46, 22, 155, 46, 87, 18, 148, 231, 4, 132, 197, 37, 35, 143, 13, 104, 220, 4, 50, 38, 136, 198, 96, 151, 78, 251, 213, 37, 35, 58, 249, 39, 79, 56, 190, 238, 118, 97, 225, 148, 252, 252, 219, 135, 103, 239, 246, 11, 147, 251, 86, 174, 228, 253, 155, 31, 12, 135, 239, 233, 200, 40, 3, 250, 121, 143, 130, 10, 177, 96, 136, 20, 189, 5, 72, 213, 119, 128, 40, 220, 34, 235, 143, 193, 202, 21, 96, 227, 139, 241, 218, 114, 150, 45, 199, 199, 177, 229, 150, 20, 179, 216, 237, 46, 183, 252, 142, 186, 15, 8, 64, 52, 18, 17, 116, 1, 80, 149, 75, 84, 131, 217, 182, 169, 77, 2, 207, 251, 133, 121, 85, 43, 87, 162, 228, 207, 174, 226, 227, 183, 15, 172, 92, 7, 0, 204, 227, 179, 1, 160, 175, 3, 138, 173, 98, 199, 66, 219, 219, 209, 163, 67, 43, 80, 141, 10, 203, 170, 144, 110, 188, 182, 218, 106, 53, 40, 45, 204, 72, 22, 206, 229, 176, 75, 63, 237, 161, 74, 240, 120, 172, 63, 12, 13, 57, 174, 15, 64, 122, 213, 144, 58, 115, 231, 17, 22, 86, 85, 77, 238, 137, 76, 222, 188, 127, 243, 192, 236, 187, 155, 86, 230, 15, 44, 204, 14, 64, 113, 65, 145, 126, 18, 159, 227, 30, 16, 27, 5, 166, 29, 213, 30, 169, 4, 10, 181, 51, 56, 216, 199, 164, 158, 193, 90, 93, 174, 204, 154, 228, 144, 76, 70, 29, 0, 28, 224, 34, 111, 164, 41, 87, 125, 208, 205, 188, 112, 65, 31, 128, 11, 122, 3, 197, 69, 214, 2, 49, 19, 31, 223, 188, 112, 101, 79, 152, 239, 89, 185, 178, 41, 222, 20, 89, 183, 46, 30, 238, 232, 240, 251, 123, 154, 248, 240, 110, 93, 175, 128, 58, 215, 198, 121, 124, 182, 160, 72, 52, 0, 196, 171, 15, 144, 33, 240, 16, 131, 166, 33, 5, 0, 176, 162, 234, 34, 43, 253, 227, 53, 161, 116, 168, 18, 100, 237, 174, 20, 0, 255, 6, 186, 207, 15, 188, 124, 193, 96, 212, 84, 127, 164, 156, 45, 178, 146, 166, 120, 253, 60, 15, 61, 237, 3, 81, 240, 250, 225, 181, 151, 212, 77, 251, 253, 30, 47, 175, 117, 10, 224, 34, 155, 153, 222, 146, 74, 94, 100, 98, 80, 236, 25, 195, 243, 69, 53, 96, 224, 254, 171, 72, 180, 2, 110, 242, 15, 188, 140, 192, 241, 65, 177, 145, 126, 253, 230, 95, 248, 194, 196, 3, 155, 164, 98, 35, 190, 79, 39, 16, 254, 55, 10, 183, 97, 203, 247, 110, 45, 44, 44, 180, 22, 88, 51, 207, 164, 96, 169, 73, 108, 52, 138, 128, 20, 148, 170, 22, 167, 76, 128, 5, 179, 4, 0, 159, 158, 13, 76, 87, 130, 90, 42, 150, 187, 148, 205, 125, 84, 199, 144, 14, 22, 22, 110, 107, 59, 136, 134, 224, 13, 60, 22, 102, 59, 191, 45, 176, 148, 252, 5, 196, 178, 213, 39, 167, 252, 0, 151, 172, 11, 1, 130, 94, 174, 245, 130, 129, 8, 100, 169, 25, 243, 135, 79, 190, 7, 188, 206, 119, 111, 43, 216, 242, 225, 135, 130, 112, 67, 70, 74, 80, 245, 43, 74, 125, 151, 102, 195, 54, 20, 58, 72, 186, 190, 45, 7, 14, 160, 50, 32, 210, 241, 11, 199, 249, 11, 6, 74, 48, 51, 0, 173, 145, 252, 252, 252, 15, 252, 194, 254, 123, 226, 92, 252, 158, 41, 224, 254, 129, 33, 43, 174, 0, 109, 228, 81, 245, 69, 121, 65, 78, 142, 155, 56, 5, 64, 44, 166, 202, 90, 53, 32, 135, 255, 109, 57, 112, 0, 144, 211, 73, 255, 82, 61, 96, 0, 64, 230, 106, 161, 240, 131, 85, 241, 205, 15, 198, 215, 205, 190, 29, 0, 152, 12, 206, 0, 156, 206, 11, 66, 55, 196, 6, 66, 167, 205, 106, 35, 161, 46, 14, 225, 229, 232, 183, 42, 203, 70, 2, 129, 242, 204, 3, 160, 98, 121, 5, 121, 153, 141, 5, 84, 0, 176, 78, 84, 223, 199, 37, 214, 223, 165, 39, 3, 10, 55, 160, 145, 78, 136, 241, 146, 40, 141, 22, 153, 115, 252, 202, 121, 241, 121, 235, 132, 72, 252, 246, 184, 71, 152, 2, 177, 16, 244, 189, 112, 247, 131, 249, 147, 87, 230, 79, 89, 24, 102, 43, 138, 208, 94, 149, 143, 195, 97, 73, 33, 64, 154, 84, 148, 137, 9, 196, 124, 16, 125, 200, 44, 247, 213, 76, 157, 85, 100, 64, 122, 95, 210, 65, 32, 5, 64, 235, 224, 133, 65, 15, 138, 11, 9, 144, 224, 13, 66, 224, 139, 223, 125, 123, 126, 220, 231, 3, 0, 188, 194, 148, 123, 38, 175, 132, 15, 133, 41, 85, 194, 186, 124, 33, 114, 123, 142, 131, 231, 42, 162, 89, 173, 84, 253, 151, 54, 122, 78, 17, 130, 117, 176, 212, 76, 129, 161, 22, 0, 231, 235, 89, 16, 72, 1, 64, 202, 136, 125, 100, 154, 1, 207, 209, 19, 7, 123, 135, 87, 206, 139, 204, 94, 25, 230, 0, 0, 100, 253, 248, 228, 30, 47, 0, 208, 225, 175, 154, 231, 143, 95, 23, 0, 56, 30, 0, 189, 153, 170, 2, 51, 230, 34, 226, 208, 238, 125, 35, 27, 0, 52, 179, 166, 225, 128, 134, 215, 187, 84, 105, 192, 193, 116, 115, 40, 159, 201, 247, 246, 10, 131, 130, 7, 186, 254, 184, 71, 212, 26, 23, 122, 113, 146, 128, 48, 69, 64, 0, 60, 97, 8, 11, 238, 238, 1, 136, 166, 244, 212, 85, 205, 131, 176, 192, 72, 127, 230, 50, 100, 108, 130, 2, 98, 70, 52, 227, 73, 222, 0, 213, 143, 218, 217, 227, 205, 175, 55, 71, 101, 141, 15, 62, 221, 247, 150, 190, 117, 82, 9, 64, 42, 37, 201, 99, 171, 27, 249, 227, 66, 10, 35, 33, 252, 224, 131, 251, 103, 47, 36, 28, 224, 239, 88, 88, 53, 15, 140, 1, 2, 224, 203, 8, 64, 78, 67, 198, 217, 137, 170, 139, 204, 194, 223, 40, 105, 73, 157, 233, 243, 108, 243, 235, 255, 20, 19, 219, 195, 97, 174, 172, 188, 96, 102, 97, 39, 47, 32, 47, 12, 14, 42, 22, 52, 160, 115, 108, 252, 190, 148, 200, 0, 176, 241, 166, 117, 77, 113, 31, 23, 7, 207, 63, 190, 123, 221, 126, 172, 26, 226, 247, 11, 117, 61, 29, 62, 161, 202, 40, 89, 156, 219, 144, 177, 68, 134, 245, 67, 1, 227, 36, 184, 76, 109, 18, 68, 186, 235, 7, 116, 116, 93, 184, 16, 5, 165, 70, 4, 94, 78, 207, 87, 20, 96, 202, 90, 161, 124, 143, 83, 33, 241, 202, 137, 67, 68, 199, 227, 231, 209, 247, 241, 250, 189, 228, 37, 178, 217, 218, 86, 223, 206, 202, 214, 181, 107, 247, 24, 217, 228, 92, 134, 140, 83, 100, 53, 250, 194, 43, 27, 193, 12, 212, 38, 78, 169, 51, 88, 64, 65, 232, 123, 189, 11, 186, 142, 186, 189, 140, 124, 0, 213, 163, 240, 222, 121, 177, 223, 143, 167, 68, 64, 215, 79, 206, 58, 35, 53, 151, 33, 227, 20, 21, 233, 4, 18, 226, 248, 28, 27, 48, 17, 6, 81, 50, 92, 65, 162, 193, 233, 150, 50, 37, 140, 124, 160, 4, 17, 42, 141, 189, 5, 218, 106, 159, 191, 55, 29, 0, 159, 32, 8, 189, 189, 95, 244, 246, 242, 186, 119, 72, 167, 28, 134, 140, 101, 10, 4, 116, 66, 41, 243, 43, 188, 72, 100, 188, 132, 134, 203, 110, 23, 227, 35, 70, 62, 136, 196, 22, 219, 200, 248, 141, 31, 92, 128, 65, 210, 106, 17, 10, 201, 77, 228, 133, 54, 95, 43, 88, 193, 222, 11, 90, 30, 210, 37, 198, 236, 144, 113, 138, 192, 37, 88, 47, 15, 164, 74, 148, 113, 124, 14, 7, 94, 53, 238, 83, 166, 53, 68, 92, 246, 74, 206, 229, 202, 228, 118, 159, 20, 88, 239, 113, 210, 203, 148, 9, 36, 39, 65, 230, 8, 81, 101, 50, 242, 193, 128, 178, 158, 160, 67, 242, 112, 103, 234, 9, 51, 175, 128, 96, 43, 214, 153, 131, 149, 113, 17, 149, 74, 187, 211, 9, 49, 162, 34, 105, 40, 147, 203, 225, 112, 186, 185, 109, 75, 11, 196, 62, 224, 121, 190, 55, 21, 40, 137, 0, 12, 230, 50, 221, 202, 24, 0, 253, 129, 119, 175, 12, 64, 129, 28, 24, 100, 95, 1, 193, 150, 30, 68, 100, 4, 128, 117, 216, 237, 228, 159, 83, 253, 219, 24, 234, 187, 92, 240, 49, 97, 15, 116, 201, 192, 45, 190, 144, 90, 241, 166, 77, 52, 162, 230, 211, 224, 140, 124, 208, 33, 125, 193, 110, 73, 13, 120, 203, 243, 170, 50, 172, 128, 32, 105, 111, 101, 53, 29, 152, 170, 214, 204, 28, 224, 178, 59, 42, 57, 183, 203, 254, 29, 187, 83, 228, 2, 55, 70, 140, 118, 59, 125, 13, 106, 66, 252, 165, 162, 15, 85, 61, 222, 171, 208, 7, 148, 52, 133, 252, 57, 144, 66, 176, 149, 61, 145, 2, 128, 149, 68, 59, 131, 189, 145, 165, 223, 138, 135, 234, 226, 114, 120, 47, 4, 227, 81, 75, 134, 220, 18, 91, 233, 182, 83, 21, 0, 189, 141, 92, 224, 118, 83, 0, 228, 75, 88, 208, 146, 78, 23, 126, 96, 91, 223, 185, 145, 239, 245, 3, 248, 24, 240, 130, 98, 80, 79, 186, 109, 9, 181, 141, 99, 84, 132, 146, 82, 176, 93, 82, 222, 138, 35, 0, 72, 247, 76, 137, 54, 35, 31, 212, 148, 42, 183, 196, 135, 47, 40, 40, 2, 133, 88, 16, 127, 225, 133, 102, 75, 121, 134, 188, 28, 203, 166, 84, 160, 203, 238, 116, 124, 199, 46, 126, 14, 128, 200, 39, 33, 0, 128, 78, 249, 67, 51, 31, 2, 70, 181, 205, 252, 94, 129, 182, 132, 192, 67, 166, 242, 55, 142, 143, 11, 84, 130, 237, 118, 218, 157, 34, 6, 33, 69, 209, 135, 60, 221, 154, 145, 15, 42, 162, 69, 167, 34, 137, 77, 246, 133, 71, 95, 120, 33, 110, 161, 115, 244, 179, 231, 239, 92, 174, 74, 55, 138, 129, 195, 85, 233, 4, 3, 233, 80, 126, 199, 186, 128, 45, 22, 216, 37, 70, 101, 53, 217, 10, 82, 214, 210, 34, 251, 167, 222, 156, 22, 18, 72, 19, 108, 214, 101, 151, 111, 154, 242, 247, 165, 252, 18, 35, 31, 84, 164, 170, 34, 171, 102, 113, 62, 176, 63, 250, 122, 215, 240, 235, 175, 139, 58, 160, 200, 116, 141, 185, 219, 233, 112, 161, 13, 168, 84, 125, 234, 176, 131, 85, 120, 166, 242, 219, 208, 61, 162, 5, 178, 89, 139, 228, 164, 79, 139, 152, 161, 144, 122, 204, 212, 92, 64, 153, 100, 193, 198, 62, 128, 247, 149, 34, 248, 158, 128, 34, 222, 81, 117, 177, 134, 104, 165, 165, 28, 116, 122, 133, 96, 115, 56, 254, 66, 48, 46, 244, 189, 32, 43, 193, 34, 235, 67, 38, 98, 82, 41, 227, 155, 202, 160, 74, 244, 226, 139, 168, 40, 236, 246, 239, 124, 199, 69, 205, 38, 91, 76, 147, 215, 96, 50, 82, 217, 28, 236, 122, 47, 166, 53, 114, 226, 1, 134, 30, 92, 78, 7, 128, 207, 85, 234, 158, 147, 177, 154, 150, 106, 64, 57, 232, 12, 119, 125, 167, 89, 8, 190, 16, 108, 126, 61, 18, 73, 89, 129, 141, 203, 11, 204, 199, 164, 218, 156, 55, 48, 234, 139, 118, 199, 51, 79, 61, 227, 64, 7, 178, 18, 254, 195, 57, 196, 74, 90, 173, 197, 138, 165, 63, 73, 185, 78, 91, 246, 116, 181, 146, 24, 249, 224, 6, 223, 44, 247, 116, 187, 168, 33, 228, 160, 179, 53, 30, 253, 78, 100, 160, 225, 245, 230, 174, 23, 4, 2, 0, 239, 85, 126, 157, 243, 253, 233, 221, 25, 230, 9, 208, 133, 12, 209, 83, 196, 123, 112, 74, 58, 19, 7, 206, 170, 27, 113, 100, 83, 177, 222, 225, 56, 1, 112, 216, 53, 188, 39, 145, 241, 44, 25, 197, 172, 81, 49, 232, 228, 187, 94, 136, 135, 227, 241, 224, 235, 130, 101, 73, 29, 186, 113, 32, 81, 244, 235, 153, 21, 185, 197, 164, 202, 199, 100, 29, 110, 250, 152, 132, 63, 48, 150, 176, 139, 131, 142, 92, 69, 53, 142, 223, 21, 203, 11, 220, 100, 11, 86, 13, 8, 180, 45, 0, 107, 240, 101, 69, 182, 217, 34, 202, 160, 83, 120, 253, 245, 248, 192, 11, 175, 199, 125, 150, 218, 197, 111, 250, 133, 4, 31, 15, 251, 194, 62, 248, 122, 99, 129, 245, 31, 199, 247, 112, 233, 10, 184, 18, 4, 150, 117, 43, 236, 54, 87, 94, 4, 182, 119, 91, 168, 165, 197, 212, 28, 8, 61, 114, 179, 174, 74, 206, 161, 175, 4, 56, 156, 33, 168, 87, 86, 94, 46, 115, 134, 34, 232, 244, 198, 95, 104, 142, 14, 8, 62, 206, 82, 91, 187, 122, 117, 19, 159, 136, 183, 250, 54, 210, 175, 43, 30, 210, 137, 153, 76, 80, 107, 15, 186, 126, 12, 190, 186, 50, 6, 47, 235, 98, 61, 110, 3, 251, 202, 22, 93, 79, 253, 187, 203, 225, 52, 132, 64, 179, 216, 128, 186, 118, 138, 73, 5, 157, 225, 230, 23, 200, 228, 6, 0, 160, 182, 118, 241, 146, 58, 175, 242, 235, 114, 163, 106, 200, 76, 52, 92, 114, 85, 116, 204, 132, 146, 18, 136, 138, 134, 239, 59, 75, 134, 197, 116, 33, 208, 121, 208, 28, 8, 32, 72, 221, 85, 29, 174, 106, 199, 155, 148, 63, 197, 200, 7, 46, 28, 161, 115, 252, 8, 0, 0, 65, 9, 171, 252, 154, 213, 71, 32, 211, 120, 187, 39, 121, 115, 18, 180, 201, 240, 240, 216, 192, 112, 222, 137, 100, 220, 155, 60, 113, 109, 56, 121, 101, 152, 119, 234, 201, 44, 107, 118, 92, 216, 128, 92, 14, 41, 66, 5, 173, 160, 144, 50, 173, 63, 80, 161, 12, 128, 25, 249, 0, 1, 60, 81, 195, 46, 17, 128, 218, 29, 139, 151, 108, 86, 124, 205, 166, 164, 128, 31, 144, 114, 153, 252, 213, 171, 56, 208, 117, 69, 55, 189, 239, 63, 149, 55, 204, 241, 151, 243, 242, 142, 214, 39, 39, 213, 231, 149, 12, 95, 158, 159, 44, 169, 207, 203, 75, 180, 218, 117, 181, 214, 245, 1, 64, 114, 21, 34, 185, 29, 74, 4, 210, 74, 164, 88, 149, 56, 51, 242, 65, 188, 212, 105, 151, 0, 0, 90, 253, 88, 73, 141, 142, 27, 57, 54, 63, 41, 190, 138, 207, 175, 7, 44, 174, 228, 37, 244, 74, 93, 194, 245, 37, 113, 248, 174, 254, 218, 164, 250, 203, 127, 86, 127, 237, 230, 107, 239, 207, 31, 203, 203, 187, 156, 87, 31, 134, 144, 210, 32, 171, 98, 203, 232, 189, 100, 166, 202, 20, 2, 172, 177, 90, 228, 50, 10, 179, 27, 92, 22, 203, 142, 20, 2, 192, 6, 143, 151, 84, 201, 95, 211, 146, 35, 95, 98, 210, 21, 16, 235, 100, 50, 121, 229, 202, 164, 139, 201, 97, 95, 242, 98, 18, 139, 8, 211, 83, 220, 3, 243, 223, 15, 3, 23, 192, 73, 103, 223, 207, 187, 50, 118, 243, 181, 146, 250, 228, 159, 93, 76, 230, 189, 207, 115, 123, 48, 117, 224, 209, 161, 234, 2, 107, 133, 222, 231, 166, 168, 210, 33, 189, 218, 227, 176, 87, 42, 239, 202, 166, 94, 23, 20, 43, 190, 224, 210, 124, 15, 183, 29, 4, 201, 178, 100, 113, 173, 138, 86, 63, 190, 100, 29, 173, 215, 167, 234, 51, 124, 116, 254, 0, 39, 156, 154, 148, 7, 13, 154, 52, 127, 82, 201, 240, 169, 249, 99, 243, 75, 38, 77, 74, 164, 10, 94, 240, 222, 220, 149, 73, 151, 7, 134, 235, 231, 15, 95, 188, 57, 89, 82, 50, 144, 152, 148, 204, 59, 117, 118, 210, 149, 228, 205, 151, 137, 222, 128, 56, 138, 117, 234, 176, 1, 171, 19, 58, 154, 37, 201, 33, 192, 216, 84, 169, 7, 20, 53, 167, 122, 137, 99, 197, 143, 99, 146, 195, 194, 85, 45, 89, 173, 134, 160, 118, 7, 128, 80, 178, 153, 165, 213, 219, 200, 247, 222, 107, 147, 206, 38, 111, 62, 113, 234, 207, 78, 93, 195, 230, 37, 111, 94, 2, 61, 27, 230, 195, 97, 65, 136, 39, 40, 9, 87, 111, 62, 123, 54, 81, 159, 119, 53, 47, 239, 90, 222, 124, 224, 124, 208, 137, 8, 7, 178, 15, 37, 183, 93, 39, 179, 54, 174, 201, 158, 98, 179, 193, 217, 36, 81, 7, 188, 114, 56, 149, 122, 160, 194, 92, 225, 17, 71, 242, 43, 232, 10, 107, 33, 64, 16, 22, 63, 190, 228, 190, 191, 222, 92, 53, 156, 119, 214, 31, 134, 166, 36, 111, 190, 246, 243, 249, 3, 208, 172, 249, 239, 39, 254, 236, 90, 114, 210, 217, 163, 171, 255, 110, 245, 142, 29, 171, 69, 170, 189, 152, 151, 151, 247, 254, 181, 249, 121, 39, 234, 129, 249, 243, 74, 46, 95, 94, 114, 185, 254, 216, 192, 169, 146, 129, 212, 35, 143, 175, 165, 6, 132, 78, 177, 3, 250, 222, 69, 34, 117, 104, 139, 50, 64, 47, 199, 49, 156, 236, 51, 207, 49, 189, 66, 131, 161, 170, 116, 65, 144, 96, 88, 178, 26, 154, 92, 123, 172, 164, 228, 242, 177, 73, 215, 230, 215, 159, 61, 145, 151, 156, 116, 249, 253, 188, 139, 87, 225, 83, 53, 157, 186, 120, 246, 236, 169, 163, 103, 47, 94, 62, 123, 236, 226, 197, 139, 167, 106, 63, 61, 123, 236, 236, 167, 171, 143, 157, 104, 82, 254, 160, 75, 255, 65, 198, 227, 25, 130, 163, 9, 189, 190, 243, 41, 135, 152, 161, 98, 237, 89, 47, 73, 39, 22, 217, 70, 138, 6, 107, 74, 30, 215, 178, 1, 208, 209, 179, 147, 234, 235, 47, 214, 231, 157, 200, 155, 15, 42, 224, 196, 164, 19, 87, 39, 93, 91, 82, 114, 241, 68, 222, 69, 93, 196, 180, 180, 250, 241, 18, 121, 226, 156, 107, 193, 19, 250, 17, 183, 45, 119, 207, 112, 227, 90, 231, 51, 107, 49, 120, 149, 186, 222, 48, 72, 50, 110, 191, 19, 254, 43, 146, 162, 235, 150, 44, 222, 161, 121, 254, 159, 159, 45, 41, 41, 57, 123, 13, 254, 159, 72, 214, 31, 155, 127, 226, 98, 162, 254, 114, 61, 244, 245, 251, 159, 150, 148, 212, 214, 126, 211, 12, 4, 224, 98, 136, 166, 101, 227, 51, 236, 198, 181, 172, 94, 196, 157, 177, 224, 67, 151, 52, 193, 171, 211, 145, 91, 93, 26, 75, 178, 155, 78, 85, 86, 184, 166, 100, 201, 226, 52, 62, 184, 47, 250, 217, 216, 246, 99, 181, 167, 78, 157, 58, 118, 244, 83, 56, 212, 30, 251, 244, 232, 167, 120, 172, 253, 230, 15, 106, 107, 255, 66, 159, 105, 128, 78, 169, 62, 90, 188, 164, 70, 122, 232, 5, 118, 221, 136, 59, 211, 124, 52, 67, 0, 212, 3, 170, 46, 162, 21, 76, 223, 192, 73, 148, 50, 155, 158, 22, 103, 55, 151, 60, 174, 2, 97, 226, 185, 117, 37, 186, 29, 123, 223, 125, 6, 0, 92, 155, 15, 116, 236, 152, 250, 67, 234, 107, 103, 24, 5, 54, 158, 47, 160, 79, 250, 3, 170, 149, 134, 193, 114, 58, 185, 68, 163, 164, 55, 46, 80, 179, 185, 100, 9, 160, 176, 154, 8, 196, 15, 244, 155, 95, 91, 91, 2, 28, 112, 159, 226, 61, 90, 132, 197, 143, 63, 190, 100, 201, 229, 19, 39, 78, 76, 58, 11, 65, 166, 234, 244, 29, 143, 173, 147, 30, 218, 105, 191, 17, 149, 16, 250, 3, 170, 14, 209, 54, 102, 37, 105, 160, 195, 120, 96, 4, 96, 40, 89, 178, 228, 241, 199, 23, 3, 20, 96, 239, 106, 107, 63, 189, 136, 164, 70, 1, 49, 90, 189, 24, 44, 230, 146, 146, 146, 205, 85, 84, 219, 121, 255, 13, 28, 162, 209, 64, 205, 58, 64, 81, 169, 84, 86, 47, 113, 139, 15, 237, 92, 160, 219, 221, 24, 184, 7, 52, 107, 65, 25, 18, 163, 51, 160, 74, 108, 131, 60, 138, 147, 133, 200, 242, 14, 38, 54, 88, 168, 169, 218, 188, 110, 29, 168, 66, 240, 113, 242, 242, 78, 212, 62, 254, 248, 99, 139, 101, 33, 129, 191, 59, 74, 84, 21, 41, 237, 161, 208, 241, 171, 147, 46, 31, 39, 137, 223, 52, 219, 242, 216, 102, 241, 161, 23, 232, 50, 106, 185, 149, 20, 110, 28, 216, 100, 174, 94, 136, 145, 15, 50, 129, 83, 136, 90, 192, 149, 77, 14, 88, 7, 246, 0, 46, 239, 160, 209, 1, 198, 228, 25, 187, 122, 53, 49, 233, 42, 169, 86, 15, 253, 178, 68, 238, 92, 17, 0, 113, 17, 119, 4, 96, 120, 126, 201, 112, 168, 189, 133, 118, 165, 202, 197, 88, 125, 31, 125, 104, 183, 190, 168, 150, 91, 201, 20, 215, 118, 115, 185, 89, 70, 62, 40, 155, 134, 113, 23, 171, 239, 116, 166, 200, 141, 63, 79, 150, 119, 112, 231, 176, 197, 70, 75, 104, 116, 126, 125, 183, 148, 214, 11, 200, 16, 236, 88, 34, 87, 112, 6, 112, 105, 211, 127, 59, 59, 41, 25, 10, 144, 210, 94, 178, 50, 88, 167, 2, 130, 29, 75, 104, 222, 129, 77, 31, 111, 165, 84, 108, 13, 208, 132, 169, 153, 220, 44, 35, 31, 52, 228, 178, 103, 183, 6, 116, 121, 135, 92, 0, 224, 120, 104, 217, 65, 107, 106, 237, 183, 93, 127, 183, 67, 82, 241, 202, 101, 237, 199, 242, 142, 125, 17, 106, 75, 141, 5, 52, 42, 125, 237, 29, 143, 213, 144, 135, 118, 59, 244, 213, 128, 181, 58, 16, 106, 99, 217, 92, 235, 133, 210, 169, 210, 110, 224, 114, 33, 81, 135, 148, 46, 239, 224, 204, 5, 128, 43, 121, 39, 132, 80, 123, 185, 60, 53, 25, 36, 65, 20, 113, 240, 117, 222, 147, 218, 251, 111, 137, 146, 177, 80, 232, 233, 64, 91, 75, 75, 27, 206, 114, 36, 227, 97, 235, 30, 79, 41, 2, 234, 21, 177, 6, 193, 193, 198, 141, 129, 208, 206, 156, 235, 133, 52, 119, 121, 209, 254, 162, 145, 32, 209, 68, 173, 184, 188, 195, 79, 114, 0, 64, 128, 56, 31, 84, 129, 183, 113, 223, 65, 9, 0, 128, 224, 49, 145, 11, 86, 175, 254, 165, 136, 192, 23, 23, 142, 135, 26, 89, 17, 165, 182, 70, 58, 12, 86, 147, 98, 130, 199, 69, 191, 208, 165, 47, 169, 107, 25, 235, 250, 239, 85, 231, 86, 47, 164, 33, 6, 212, 161, 225, 32, 71, 138, 1, 190, 251, 221, 151, 44, 47, 167, 125, 107, 88, 181, 233, 187, 54, 233, 34, 102, 57, 219, 66, 1, 91, 1, 66, 208, 40, 66, 32, 121, 208, 171, 23, 75, 16, 132, 66, 21, 41, 137, 144, 234, 181, 214, 45, 214, 32, 96, 244, 240, 229, 15, 205, 156, 57, 147, 25, 71, 187, 21, 247, 224, 156, 79, 232, 186, 92, 106, 127, 217, 205, 89, 190, 49, 87, 29, 144, 27, 86, 109, 134, 47, 190, 79, 67, 91, 156, 184, 81, 100, 45, 88, 106, 45, 22, 87, 254, 147, 245, 225, 234, 197, 187, 196, 86, 115, 84, 39, 182, 181, 41, 38, 187, 87, 61, 150, 142, 0, 169, 47, 208, 62, 188, 124, 40, 182, 142, 119, 129, 32, 104, 197, 51, 246, 23, 245, 4, 201, 169, 84, 62, 160, 139, 45, 111, 189, 245, 163, 7, 10, 20, 191, 97, 60, 66, 198, 147, 44, 152, 199, 227, 1, 217, 198, 228, 18, 139, 249, 150, 162, 130, 245, 104, 186, 118, 137, 158, 193, 142, 199, 137, 46, 8, 52, 74, 166, 17, 57, 160, 93, 28, 7, 170, 145, 17, 120, 76, 12, 16, 117, 181, 117, 10, 0, 142, 44, 17, 53, 174, 25, 177, 107, 159, 128, 192, 64, 175, 240, 206, 173, 148, 59, 183, 203, 101, 217, 250, 214, 91, 191, 249, 205, 143, 30, 144, 115, 40, 90, 127, 189, 149, 238, 140, 224, 229, 121, 222, 227, 209, 14, 233, 85, 88, 109, 68, 31, 138, 66, 142, 114, 176, 148, 85, 24, 133, 246, 22, 121, 36, 172, 70, 82, 24, 181, 143, 137, 63, 151, 153, 3, 20, 191, 81, 80, 94, 93, 156, 75, 238, 136, 197, 225, 73, 109, 225, 157, 86, 235, 88, 214, 35, 2, 255, 252, 207, 191, 248, 209, 3, 52, 85, 175, 13, 50, 124, 137, 184, 23, 32, 104, 77, 24, 45, 162, 185, 141, 174, 127, 251, 30, 133, 96, 113, 103, 104, 169, 212, 120, 52, 134, 100, 133, 99, 113, 40, 159, 93, 34, 251, 14, 242, 213, 230, 226, 183, 234, 162, 2, 100, 5, 189, 149, 11, 244, 233, 169, 74, 183, 211, 197, 164, 125, 232, 86, 115, 28, 190, 181, 172, 23, 17, 248, 231, 127, 254, 13, 128, 240, 192, 3, 86, 208, 63, 214, 153, 214, 2, 235, 204, 130, 2, 91, 81, 81, 113, 121, 5, 23, 78, 36, 18, 126, 31, 175, 72, 131, 170, 200, 182, 84, 178, 249, 20, 130, 37, 235, 247, 42, 24, 64, 222, 1, 141, 122, 138, 50, 2, 37, 210, 67, 228, 16, 193, 234, 175, 92, 160, 79, 12, 58, 188, 105, 206, 6, 155, 38, 113, 36, 37, 246, 95, 231, 110, 123, 67, 68, 128, 208, 111, 148, 244, 139, 95, 252, 226, 165, 31, 33, 189, 244, 214, 111, 63, 51, 0, 0, 52, 65, 202, 233, 41, 129, 246, 45, 94, 26, 82, 2, 160, 120, 221, 162, 64, 96, 117, 149, 226, 65, 100, 206, 196, 250, 187, 204, 161, 76, 177, 201, 201, 86, 12, 30, 158, 80, 141, 24, 96, 10, 196, 67, 146, 212, 244, 200, 97, 17, 164, 101, 251, 244, 123, 191, 215, 249, 174, 2, 1, 125, 250, 13, 34, 241, 0, 114, 136, 173, 168, 92, 235, 193, 200, 221, 12, 14, 207, 142, 101, 33, 35, 194, 167, 144, 52, 97, 74, 8, 56, 57, 126, 115, 99, 89, 166, 211, 5, 214, 129, 36, 43, 88, 183, 78, 146, 167, 58, 151, 68, 122, 165, 50, 85, 10, 6, 128, 143, 71, 226, 97, 111, 24, 142, 126, 206, 23, 143, 52, 227, 138, 146, 101, 211, 190, 254, 176, 91, 232, 254, 215, 44, 8, 164, 56, 68, 134, 162, 192, 6, 50, 82, 84, 12, 82, 82, 205, 226, 212, 110, 148, 246, 191, 67, 13, 103, 8, 0, 198, 71, 146, 45, 216, 161, 104, 27, 169, 253, 130, 70, 75, 85, 105, 110, 0, 196, 109, 39, 164, 144, 16, 86, 100, 141, 92, 70, 213, 20, 169, 82, 112, 188, 252, 29, 83, 102, 79, 105, 138, 111, 190, 123, 246, 20, 129, 23, 242, 243, 123, 0, 128, 89, 51, 166, 79, 187, 243, 235, 47, 30, 234, 25, 29, 251, 223, 127, 52, 139, 130, 36, 44, 191, 32, 244, 18, 21, 19, 4, 229, 129, 249, 208, 180, 249, 7, 117, 183, 52, 18, 89, 192, 87, 133, 30, 209, 225, 195, 181, 37, 202, 7, 117, 187, 156, 105, 202, 128, 117, 3, 177, 232, 41, 0, 35, 112, 88, 186, 110, 48, 178, 144, 145, 228, 65, 84, 114, 53, 78, 234, 107, 154, 61, 60, 89, 136, 207, 171, 26, 200, 95, 71, 0, 24, 218, 78, 16, 120, 120, 237, 139, 135, 34, 209, 254, 243, 163, 99, 72, 9, 252, 255, 191, 79, 254, 246, 221, 93, 47, 1, 253, 34, 155, 124, 168, 233, 127, 125, 252, 209, 59, 123, 183, 110, 45, 45, 44, 213, 89, 54, 156, 243, 38, 18, 181, 59, 160, 253, 135, 15, 255, 165, 217, 36, 152, 211, 142, 255, 41, 60, 162, 186, 48, 157, 63, 115, 42, 24, 8, 231, 174, 220, 195, 231, 239, 142, 63, 248, 96, 211, 221, 17, 94, 232, 32, 0, 212, 5, 251, 23, 0, 2, 119, 222, 251, 15, 63, 101, 185, 61, 117, 117, 117, 29, 193, 161, 100, 236, 208, 33, 120, 37, 206, 240, 168, 46, 182, 89, 161, 111, 65, 19, 254, 226, 55, 166, 160, 248, 95, 95, 138, 180, 117, 155, 14, 11, 8, 137, 196, 98, 2, 192, 142, 7, 205, 182, 66, 17, 56, 137, 234, 194, 180, 30, 144, 148, 0, 245, 57, 125, 241, 123, 38, 223, 35, 240, 252, 237, 183, 47, 140, 111, 170, 108, 34, 0, 112, 92, 93, 52, 56, 235, 206, 59, 239, 159, 118, 255, 79, 201, 175, 120, 19, 177, 161, 228, 80, 157, 206, 99, 148, 23, 17, 36, 8, 20, 153, 184, 66, 6, 224, 203, 207, 239, 210, 2, 224, 227, 195, 77, 171, 17, 128, 195, 181, 198, 131, 186, 154, 223, 150, 35, 20, 55, 229, 5, 211, 19, 111, 69, 25, 160, 85, 10, 225, 217, 85, 241, 170, 252, 225, 41, 61, 241, 217, 155, 235, 188, 18, 0, 156, 231, 80, 172, 236, 206, 167, 124, 143, 78, 251, 111, 4, 1, 33, 113, 168, 95, 23, 129, 212, 227, 212, 108, 43, 42, 32, 88, 0, 125, 247, 71, 34, 201, 12, 242, 101, 138, 222, 181, 165, 139, 0, 161, 175, 149, 16, 4, 76, 179, 128, 42, 66, 193, 241, 48, 243, 139, 76, 186, 148, 75, 132, 8, 83, 34, 225, 248, 228, 129, 201, 241, 112, 213, 131, 126, 31, 238, 1, 36, 230, 4, 247, 28, 9, 222, 123, 239, 158, 67, 247, 222, 139, 125, 18, 78, 240, 61, 209, 100, 226, 144, 225, 61, 189, 32, 54, 66, 171, 15, 11, 94, 113, 204, 217, 159, 72, 128, 147, 196, 130, 215, 110, 67, 84, 126, 84, 88, 90, 186, 117, 235, 222, 119, 62, 166, 60, 96, 211, 105, 63, 199, 222, 74, 0, 56, 108, 218, 7, 82, 71, 40, 88, 44, 107, 53, 117, 29, 43, 150, 252, 187, 40, 179, 241, 85, 119, 207, 187, 123, 243, 192, 236, 252, 7, 167, 116, 248, 124, 205, 41, 0, 16, 130, 167, 190, 254, 112, 248, 181, 233, 143, 178, 60, 14, 246, 70, 1, 1, 35, 30, 240, 29, 234, 234, 106, 110, 238, 138, 14, 37, 112, 108, 216, 239, 5, 196, 6, 52, 181, 51, 108, 197, 242, 130, 15, 17, 129, 223, 45, 106, 9, 4, 210, 218, 207, 113, 15, 255, 53, 182, 255, 247, 111, 230, 2, 128, 34, 66, 1, 78, 157, 105, 210, 34, 84, 58, 112, 190, 131, 20, 117, 133, 193, 250, 133, 125, 225, 158, 142, 120, 221, 78, 79, 36, 200, 43, 179, 194, 123, 94, 188, 255, 235, 175, 133, 103, 204, 120, 109, 24, 218, 227, 241, 68, 13, 164, 192, 227, 111, 238, 34, 212, 140, 32, 32, 88, 62, 136, 18, 116, 127, 251, 142, 207, 17, 129, 143, 138, 7, 18, 3, 97, 127, 155, 106, 219, 7, 207, 159, 19, 14, 56, 173, 217, 149, 217, 128, 180, 17, 138, 211, 101, 110, 137, 134, 74, 187, 3, 43, 140, 193, 45, 38, 162, 224, 241, 145, 137, 124, 62, 15, 46, 156, 133, 252, 164, 200, 8, 177, 255, 240, 223, 190, 126, 239, 180, 105, 211, 23, 12, 97, 123, 234, 98, 201, 168, 78, 53, 167, 151, 23, 219, 47, 129, 16, 236, 231, 189, 58, 28, 64, 104, 46, 69, 224, 109, 82, 65, 192, 171, 238, 230, 36, 44, 240, 201, 251, 38, 154, 64, 72, 59, 12, 194, 162, 69, 200, 126, 161, 251, 59, 162, 115, 85, 73, 10, 171, 232, 82, 207, 12, 57, 144, 220, 160, 50, 37, 198, 174, 216, 120, 255, 180, 59, 167, 77, 159, 177, 189, 206, 195, 133, 99, 137, 164, 86, 13, 248, 142, 40, 219, 79, 168, 33, 193, 199, 19, 250, 245, 99, 62, 155, 10, 1, 250, 161, 136, 3, 213, 2, 151, 205, 178, 128, 222, 48, 72, 5, 182, 32, 115, 228, 224, 86, 70, 192, 136, 0, 93, 234, 153, 220, 133, 113, 160, 137, 84, 111, 188, 252, 253, 71, 94, 188, 119, 218, 244, 233, 51, 202, 162, 29, 231, 207, 31, 209, 10, 1, 31, 211, 180, 191, 171, 57, 8, 141, 211, 221, 74, 176, 39, 22, 179, 17, 77, 248, 241, 250, 243, 68, 86, 144, 3, 253, 3, 62, 63, 26, 180, 175, 19, 67, 112, 250, 132, 116, 178, 39, 203, 226, 131, 140, 124, 144, 201, 202, 97, 128, 227, 146, 93, 100, 13, 177, 78, 117, 209, 128, 203, 177, 138, 46, 245, 76, 111, 70, 68, 72, 157, 20, 253, 217, 35, 223, 127, 246, 225, 25, 211, 167, 79, 127, 180, 63, 26, 246, 196, 146, 65, 233, 78, 212, 12, 111, 74, 104, 154, 143, 8, 36, 18, 3, 58, 129, 162, 135, 172, 201, 104, 21, 93, 162, 245, 239, 157, 79, 224, 54, 164, 132, 23, 188, 96, 208, 158, 254, 26, 81, 131, 71, 59, 252, 180, 225, 158, 176, 224, 207, 180, 210, 148, 22, 0, 185, 172, 194, 165, 90, 13, 75, 209, 94, 205, 24, 153, 75, 187, 212, 115, 90, 86, 248, 251, 143, 60, 249, 236, 166, 89, 211, 203, 182, 207, 232, 226, 185, 67, 99, 146, 37, 16, 205, 48, 159, 72, 99, 128, 104, 20, 143, 231, 195, 94, 157, 76, 17, 135, 0, 196, 86, 21, 74, 62, 209, 222, 210, 45, 229, 8, 192, 64, 36, 220, 195, 111, 220, 249, 52, 85, 131, 181, 71, 19, 116, 153, 61, 47, 120, 136, 153, 22, 92, 209, 0, 160, 152, 2, 200, 146, 25, 108, 105, 141, 117, 59, 181, 249, 182, 85, 170, 165, 158, 49, 26, 86, 3, 112, 224, 192, 147, 143, 172, 120, 246, 103, 220, 130, 89, 253, 209, 89, 219, 235, 184, 126, 73, 11, 136, 102, 56, 158, 38, 1, 177, 161, 161, 32, 162, 16, 231, 117, 246, 81, 227, 124, 100, 65, 182, 254, 245, 31, 203, 110, 209, 71, 91, 75, 75, 215, 151, 174, 47, 180, 109, 59, 224, 89, 123, 47, 117, 134, 106, 207, 38, 112, 122, 157, 15, 218, 159, 219, 246, 20, 234, 130, 2, 172, 153, 82, 125, 173, 55, 68, 90, 161, 94, 234, 153, 84, 8, 168, 1, 168, 121, 18, 56, 96, 39, 119, 192, 53, 163, 121, 104, 86, 89, 115, 84, 146, 1, 209, 12, 243, 93, 105, 237, 143, 225, 159, 96, 208, 160, 235, 120, 4, 96, 40, 241, 198, 59, 95, 106, 232, 227, 119, 230, 254, 237, 95, 80, 0, 142, 129, 76, 12, 8, 3, 67, 67, 130, 222, 42, 141, 134, 164, 221, 124, 192, 141, 147, 183, 228, 119, 122, 202, 49, 109, 169, 103, 151, 6, 0, 174, 243, 201, 71, 158, 125, 22, 144, 237, 172, 155, 181, 125, 100, 142, 35, 153, 20, 55, 23, 18, 205, 176, 247, 144, 138, 255, 135, 98, 253, 244, 149, 96, 48, 251, 33, 74, 0, 72, 68, 108, 123, 63, 215, 98, 240, 249, 127, 36, 0, 36, 146, 167, 136, 94, 232, 143, 13, 213, 84, 20, 120, 175, 111, 135, 6, 201, 44, 152, 43, 17, 32, 222, 145, 26, 128, 157, 43, 86, 60, 251, 83, 174, 21, 247, 113, 157, 227, 24, 177, 63, 58, 38, 217, 1, 209, 12, 239, 81, 49, 64, 127, 87, 16, 68, 32, 216, 223, 229, 195, 237, 135, 116, 126, 1, 132, 96, 23, 105, 221, 153, 183, 215, 151, 254, 46, 29, 129, 149, 203, 136, 29, 72, 94, 62, 9, 103, 12, 197, 98, 31, 36, 60, 75, 195, 137, 204, 171, 244, 100, 39, 55, 106, 126, 199, 120, 1, 120, 249, 251, 207, 62, 91, 45, 110, 90, 248, 232, 130, 232, 140, 57, 35, 65, 159, 207, 135, 218, 153, 33, 102, 120, 163, 74, 4, 64, 254, 1, 129, 161, 46, 172, 160, 214, 127, 240, 104, 100, 127, 66, 162, 238, 109, 133, 91, 127, 253, 241, 231, 41, 94, 248, 156, 200, 192, 239, 147, 201, 83, 165, 219, 222, 139, 197, 130, 171, 122, 184, 165, 137, 156, 214, 161, 85, 82, 42, 94, 52, 157, 43, 112, 75, 0, 164, 24, 248, 167, 143, 60, 255, 51, 143, 180, 109, 227, 2, 176, 135, 179, 162, 177, 4, 209, 78, 12, 126, 205, 120, 52, 54, 48, 58, 20, 237, 138, 67, 60, 196, 235, 74, 1, 120, 2, 212, 7, 226, 19, 113, 76, 170, 151, 47, 47, 42, 42, 90, 52, 215, 86, 72, 20, 227, 87, 169, 47, 148, 76, 254, 235, 151, 123, 31, 88, 186, 75, 240, 122, 121, 147, 237, 215, 201, 140, 42, 226, 69, 179, 41, 51, 146, 36, 182, 212, 213, 237, 241, 138, 59, 54, 115, 220, 147, 143, 60, 95, 157, 218, 200, 215, 1, 14, 193, 140, 134, 126, 98, 158, 68, 0, 210, 253, 160, 32, 232, 193, 243, 9, 31, 154, 118, 61, 242, 199, 250, 177, 253, 97, 31, 215, 202, 135, 201, 41, 7, 106, 58, 59, 55, 117, 118, 238, 91, 100, 219, 250, 229, 58, 81, 6, 146, 103, 81, 45, 22, 70, 192, 159, 50, 183, 73, 77, 250, 10, 196, 228, 217, 82, 241, 162, 217, 100, 9, 229, 128, 177, 177, 68, 16, 100, 158, 118, 224, 247, 31, 217, 164, 0, 32, 24, 4, 30, 152, 177, 189, 63, 5, 128, 79, 211, 254, 40, 56, 130, 130, 215, 80, 114, 99, 49, 1, 1, 144, 223, 123, 17, 0, 32, 92, 91, 172, 200, 70, 100, 224, 147, 171, 127, 164, 34, 241, 206, 27, 102, 55, 233, 41, 208, 177, 22, 227, 88, 135, 132, 234, 128, 161, 161, 177, 100, 76, 106, 242, 147, 43, 42, 170, 229, 246, 119, 118, 36, 187, 102, 1, 19, 148, 197, 68, 43, 239, 225, 59, 52, 237, 71, 63, 144, 247, 27, 114, 110, 52, 22, 197, 240, 90, 86, 237, 98, 251, 1, 1, 79, 103, 162, 155, 4, 68, 135, 255, 248, 127, 68, 165, 240, 59, 179, 21, 179, 86, 157, 207, 198, 177, 14, 9, 73, 178, 90, 60, 158, 67, 67, 201, 126, 113, 251, 230, 21, 10, 0, 186, 35, 184, 194, 246, 172, 233, 51, 230, 148, 69, 105, 7, 183, 38, 162, 205, 154, 246, 67, 68, 44, 24, 75, 46, 223, 195, 251, 19, 66, 152, 79, 157, 80, 83, 33, 221, 255, 124, 231, 50, 154, 22, 73, 142, 137, 16, 236, 53, 201, 188, 186, 217, 32, 69, 188, 104, 114, 0, 141, 250, 1, 208, 185, 117, 253, 201, 40, 121, 166, 125, 43, 158, 175, 150, 1, 136, 196, 130, 201, 145, 40, 63, 203, 1, 14, 65, 148, 56, 105, 224, 199, 170, 21, 96, 148, 4, 2, 188, 42, 140, 81, 215, 23, 192, 221, 89, 175, 106, 243, 203, 206, 206, 214, 3, 7, 184, 198, 206, 78, 65, 144, 0, 248, 36, 153, 28, 123, 151, 32, 176, 200, 28, 0, 250, 196, 200, 241, 162, 118, 145, 109, 93, 114, 99, 157, 146, 69, 192, 25, 110, 35, 99, 29, 216, 230, 106, 0, 96, 159, 4, 64, 52, 58, 148, 196, 189, 54, 231, 148, 141, 60, 250, 104, 23, 185, 34, 158, 72, 15, 6, 154, 99, 233, 94, 160, 186, 190, 192, 163, 225, 215, 214, 3, 28, 189, 255, 0, 223, 249, 173, 195, 34, 11, 36, 63, 173, 93, 143, 0, 148, 94, 39, 0, 82, 180, 144, 121, 193, 77, 66, 210, 144, 145, 133, 52, 0, 34, 95, 68, 128, 251, 233, 203, 41, 29, 40, 45, 178, 207, 207, 177, 143, 216, 231, 28, 225, 60, 254, 214, 1, 112, 88, 212, 8, 52, 159, 79, 215, 255, 234, 236, 157, 22, 0, 128, 128, 222, 255, 139, 78, 137, 3, 14, 127, 90, 91, 91, 123, 116, 47, 6, 11, 215, 51, 155, 142, 145, 15, 102, 200, 206, 186, 201, 152, 129, 133, 136, 183, 39, 154, 28, 27, 138, 117, 236, 169, 60, 116, 36, 34, 68, 212, 0, 196, 248, 71, 31, 29, 41, 155, 211, 213, 74, 44, 122, 90, 56, 212, 220, 159, 72, 115, 0, 212, 218, 216, 195, 85, 232, 9, 108, 39, 192, 220, 219, 217, 185, 75, 4, 224, 40, 14, 149, 109, 53, 47, 3, 218, 5, 10, 228, 182, 51, 38, 219, 207, 58, 196, 177, 114, 11, 72, 176, 223, 207, 181, 70, 113, 191, 229, 61, 47, 214, 29, 249, 32, 26, 149, 0, 24, 162, 8, 244, 243, 79, 204, 73, 52, 204, 138, 6, 251, 81, 233, 165, 137, 64, 34, 29, 0, 181, 54, 246, 112, 213, 186, 10, 9, 0, 16, 224, 55, 68, 25, 216, 81, 15, 0, 252, 193, 52, 0, 6, 179, 26, 13, 79, 215, 171, 122, 114, 209, 226, 250, 74, 167, 197, 159, 8, 131, 167, 7, 108, 217, 209, 21, 243, 251, 195, 117, 235, 196, 246, 119, 131, 115, 154, 160, 59, 237, 68, 59, 157, 179, 202, 166, 207, 24, 26, 218, 189, 121, 221, 126, 185, 237, 13, 20, 128, 129, 244, 223, 83, 101, 239, 12, 231, 136, 119, 30, 64, 0, 34, 20, 129, 13, 103, 107, 79, 37, 175, 33, 0, 54, 51, 187, 213, 152, 216, 121, 75, 201, 34, 233, 85, 79, 36, 78, 192, 177, 5, 214, 141, 0, 196, 7, 60, 97, 31, 109, 115, 205, 63, 84, 134, 7, 146, 67, 93, 162, 17, 0, 4, 250, 147, 100, 167, 5, 190, 211, 133, 46, 81, 176, 106, 247, 238, 85, 155, 165, 190, 95, 69, 254, 36, 180, 75, 228, 49, 138, 236, 157, 241, 36, 121, 15, 2, 208, 93, 79, 1, 56, 119, 10, 248, 15, 109, 225, 86, 214, 196, 106, 88, 214, 172, 237, 87, 157, 146, 86, 245, 4, 238, 79, 101, 37, 153, 222, 15, 193, 179, 203, 105, 1, 255, 179, 213, 67, 182, 45, 71, 35, 80, 9, 78, 65, 50, 17, 4, 141, 72, 196, 63, 218, 65, 16, 0, 173, 80, 133, 8, 52, 40, 152, 127, 243, 238, 46, 146, 15, 212, 250, 128, 140, 124, 200, 4, 0, 215, 141, 63, 121, 142, 234, 193, 111, 157, 131, 136, 224, 18, 6, 72, 115, 179, 239, 86, 99, 198, 200, 165, 3, 160, 244, 17, 89, 92, 136, 194, 97, 119, 218, 237, 110, 223, 145, 30, 75, 92, 72, 180, 122, 5, 14, 226, 129, 206, 242, 21, 24, 9, 116, 196, 198, 146, 35, 65, 10, 64, 108, 85, 48, 57, 4, 175, 144, 61, 102, 76, 119, 136, 10, 96, 55, 182, 125, 85, 51, 213, 129, 90, 239, 53, 13, 0, 35, 126, 37, 160, 11, 162, 12, 28, 5, 22, 24, 35, 222, 224, 115, 25, 87, 195, 34, 100, 66, 3, 40, 1, 208, 245, 17, 113, 37, 24, 167, 47, 62, 47, 223, 194, 129, 4, 8, 224, 172, 183, 182, 118, 62, 191, 162, 156, 88, 193, 142, 232, 24, 8, 63, 34, 16, 217, 21, 141, 37, 163, 40, 3, 157, 157, 117, 79, 240, 61, 34, 2, 11, 197, 255, 232, 6, 104, 157, 192, 52, 0, 244, 28, 119, 36, 31, 178, 64, 183, 104, 9, 191, 117, 254, 34, 149, 129, 47, 109, 89, 87, 195, 50, 67, 42, 53, 161, 55, 181, 194, 141, 203, 99, 245, 204, 94, 25, 177, 248, 195, 100, 222, 163, 183, 85, 25, 9, 116, 244, 211, 205, 182, 86, 225, 86, 51, 67, 251, 99, 221, 146, 251, 74, 17, 216, 45, 169, 128, 230, 104, 150, 4, 142, 71, 220, 253, 64, 143, 252, 66, 183, 240, 197, 31, 14, 203, 44, 112, 145, 248, 66, 95, 206, 53, 92, 13, 43, 7, 170, 38, 33, 177, 180, 235, 8, 163, 25, 83, 112, 145, 185, 166, 194, 228, 224, 107, 22, 49, 93, 113, 30, 58, 89, 25, 9, 116, 196, 112, 159, 169, 88, 16, 141, 225, 174, 88, 68, 14, 16, 210, 236, 96, 71, 150, 7, 241, 232, 237, 125, 34, 145, 215, 15, 63, 188, 129, 34, 176, 44, 113, 106, 44, 65, 92, 129, 223, 253, 173, 209, 106, 88, 50, 153, 198, 70, 100, 4, 70, 62, 136, 4, 237, 71, 91, 208, 117, 207, 238, 168, 4, 64, 130, 135, 72, 224, 217, 106, 101, 44, 136, 149, 18, 201, 88, 52, 56, 118, 53, 152, 66, 0, 152, 64, 1, 65, 179, 241, 16, 50, 37, 84, 130, 198, 70, 11, 1, 16, 93, 129, 13, 39, 206, 94, 75, 246, 254, 154, 140, 33, 148, 115, 89, 0, 176, 102, 249, 85, 36, 50, 106, 32, 206, 29, 102, 228, 3, 33, 151, 180, 58, 92, 93, 48, 63, 95, 6, 64, 80, 71, 2, 148, 186, 18, 160, 1, 48, 38, 138, 69, 37, 41, 232, 228, 21, 92, 208, 156, 45, 128, 207, 188, 86, 12, 74, 223, 9, 145, 5, 54, 36, 64, 15, 190, 77, 18, 102, 54, 150, 243, 50, 153, 174, 179, 102, 249, 85, 36, 162, 121, 172, 116, 13, 107, 70, 62, 16, 74, 21, 232, 242, 241, 184, 12, 64, 162, 123, 185, 22, 128, 206, 32, 0, 64, 54, 28, 236, 82, 50, 129, 16, 36, 99, 195, 93, 205, 217, 36, 32, 51, 0, 30, 50, 243, 90, 84, 131, 203, 206, 163, 41, 164, 195, 40, 115, 189, 137, 112, 171, 241, 165, 166, 130, 61, 60, 167, 220, 102, 211, 170, 96, 87, 74, 132, 124, 66, 79, 85, 10, 128, 196, 198, 21, 202, 124, 152, 232, 15, 39, 163, 253, 65, 220, 119, 110, 36, 24, 12, 166, 62, 230, 187, 19, 231, 163, 193, 174, 96, 214, 12, 78, 102, 14, 104, 21, 20, 44, 112, 52, 113, 234, 218, 177, 29, 165, 100, 212, 96, 17, 124, 49, 192, 235, 100, 25, 196, 45, 72, 77, 76, 169, 178, 234, 126, 10, 206, 159, 61, 85, 107, 230, 239, 152, 189, 82, 1, 128, 239, 249, 229, 229, 105, 237, 239, 24, 75, 4, 163, 137, 100, 48, 26, 77, 142, 140, 13, 241, 10, 4, 128, 123, 195, 188, 63, 107, 246, 5, 1, 200, 96, 182, 125, 137, 148, 22, 248, 86, 34, 1, 65, 81, 9, 25, 69, 249, 117, 49, 10, 165, 206, 5, 104, 31, 159, 179, 62, 103, 98, 74, 149, 190, 241, 133, 0, 80, 57, 102, 192, 135, 249, 20, 0, 231, 189, 222, 231, 170, 211, 0, 8, 146, 141, 7, 49, 36, 130, 87, 81, 41, 157, 213, 217, 205, 175, 245, 122, 127, 108, 102, 17, 84, 4, 192, 200, 17, 64, 82, 176, 192, 134, 207, 18, 103, 33, 38, 90, 72, 18, 198, 91, 107, 48, 149, 232, 215, 48, 16, 113, 17, 171, 205, 76, 169, 178, 89, 213, 203, 135, 16, 210, 169, 40, 72, 1, 16, 247, 250, 170, 211, 1, 136, 129, 31, 24, 13, 10, 208, 245, 53, 29, 29, 171, 36, 0, 60, 94, 31, 121, 0, 38, 235, 83, 80, 14, 200, 80, 207, 68, 98, 108, 17, 128, 101, 126, 239, 155, 181, 71, 47, 83, 111, 96, 107, 185, 162, 164, 32, 69, 0, 64, 181, 201, 212, 103, 185, 118, 51, 48, 183, 93, 59, 85, 43, 5, 64, 79, 101, 101, 221, 145, 104, 164, 67, 41, 1, 201, 161, 126, 63, 231, 241, 122, 224, 117, 77, 133, 216, 254, 3, 228, 65, 228, 67, 102, 34, 93, 104, 77, 251, 80, 25, 159, 122, 253, 16, 142, 19, 33, 248, 214, 39, 171, 56, 238, 40, 120, 196, 84, 17, 190, 67, 170, 42, 210, 252, 44, 246, 161, 153, 214, 239, 129, 91, 203, 102, 77, 125, 234, 149, 207, 176, 14, 157, 249, 25, 20, 0, 232, 231, 232, 17, 127, 56, 62, 76, 246, 154, 149, 49, 8, 38, 131, 173, 100, 240, 186, 243, 128, 167, 174, 2, 8, 184, 159, 114, 37, 35, 31, 244, 136, 143, 156, 233, 139, 68, 120, 9, 128, 244, 103, 209, 204, 202, 217, 2, 8, 108, 56, 124, 250, 36, 199, 53, 93, 77, 38, 71, 75, 105, 142, 120, 61, 65, 64, 148, 130, 138, 7, 170, 177, 83, 203, 215, 50, 255, 104, 157, 105, 181, 150, 103, 147, 1, 189, 213, 67, 116, 198, 203, 1, 0, 127, 103, 247, 121, 178, 201, 104, 120, 207, 158, 61, 135, 58, 130, 49, 176, 121, 67, 168, 225, 35, 193, 14, 0, 64, 220, 26, 196, 203, 121, 168, 244, 155, 24, 187, 20, 206, 156, 19, 41, 204, 241, 242, 249, 138, 209, 106, 237, 172, 156, 85, 27, 118, 156, 6, 250, 229, 22, 238, 3, 120, 146, 51, 20, 129, 207, 11, 187, 81, 50, 121, 98, 106, 138, 37, 119, 138, 65, 183, 246, 123, 15, 49, 89, 30, 66, 171, 120, 244, 234, 5, 16, 0, 14, 56, 125, 4, 68, 61, 248, 227, 127, 144, 66, 161, 68, 82, 218, 123, 55, 25, 83, 168, 33, 31, 15, 148, 117, 12, 63, 114, 238, 156, 4, 64, 31, 128, 113, 46, 218, 71, 181, 185, 98, 126, 148, 206, 24, 198, 178, 79, 17, 128, 211, 27, 182, 112, 152, 25, 56, 81, 64, 211, 228, 191, 222, 114, 254, 252, 182, 210, 82, 149, 44, 147, 150, 51, 89, 199, 209, 53, 28, 192, 218, 245, 75, 170, 0, 128, 142, 177, 36, 154, 120, 69, 36, 208, 209, 21, 3, 153, 128, 64, 48, 57, 100, 114, 172, 70, 34, 63, 182, 63, 122, 46, 122, 230, 92, 4, 219, 207, 133, 17, 140, 51, 61, 234, 147, 116, 226, 211, 234, 183, 9, 0, 167, 151, 85, 215, 36, 78, 30, 171, 173, 181, 210, 49, 212, 207, 75, 145, 25, 62, 87, 38, 202, 24, 249, 144, 145, 216, 244, 8, 68, 191, 255, 9, 0, 67, 164, 253, 7, 86, 60, 169, 52, 2, 88, 177, 146, 0, 11, 192, 251, 114, 89, 249, 48, 34, 246, 253, 25, 145, 1, 56, 207, 25, 124, 25, 225, 84, 14, 152, 110, 124, 250, 30, 69, 160, 138, 91, 133, 11, 22, 212, 47, 250, 181, 98, 32, 253, 215, 229, 169, 139, 25, 249, 144, 153, 210, 0, 48, 154, 77, 204, 90, 246, 116, 209, 113, 33, 140, 4, 106, 82, 0, 28, 240, 144, 48, 185, 231, 208, 17, 243, 60, 224, 239, 59, 23, 147, 229, 255, 220, 57, 84, 130, 194, 153, 230, 186, 14, 4, 0, 87, 108, 177, 187, 20, 173, 72, 175, 121, 91, 181, 131, 34, 176, 165, 224, 77, 204, 17, 191, 95, 92, 168, 24, 72, 95, 158, 94, 17, 146, 107, 250, 156, 53, 154, 155, 85, 105, 129, 136, 143, 104, 253, 138, 21, 207, 111, 98, 21, 38, 176, 21, 66, 53, 33, 220, 115, 228, 136, 233, 202, 157, 72, 20, 27, 221, 23, 197, 46, 223, 222, 16, 62, 132, 53, 59, 117, 118, 187, 195, 33, 62, 2, 86, 173, 74, 43, 64, 203, 135, 20, 173, 58, 76, 0, 216, 245, 215, 92, 61, 120, 3, 201, 15, 216, 20, 19, 160, 12, 168, 3, 128, 204, 75, 68, 105, 200, 229, 48, 250, 198, 105, 25, 195, 33, 17, 80, 126, 47, 63, 242, 228, 179, 63, 83, 134, 2, 94, 193, 239, 241, 241, 71, 122, 76, 214, 172, 180, 246, 33, 227, 115, 100, 153, 245, 86, 182, 210, 225, 194, 150, 179, 78, 123, 144, 85, 172, 111, 35, 173, 1, 199, 200, 7, 5, 45, 251, 4, 218, 255, 201, 239, 55, 172, 242, 28, 189, 150, 76, 94, 107, 226, 158, 183, 237, 21, 235, 41, 62, 90, 196, 170, 57, 192, 56, 201, 160, 71, 172, 211, 112, 174, 9, 107, 233, 32, 174, 77, 231, 129, 239, 211, 242, 40, 232, 122, 9, 128, 156, 244, 159, 40, 253, 125, 135, 28, 78, 178, 234, 52, 46, 255, 90, 233, 178, 151, 53, 171, 24, 200, 69, 22, 69, 118, 56, 159, 192, 55, 76, 250, 61, 14, 64, 251, 193, 35, 186, 239, 222, 154, 95, 190, 151, 76, 94, 109, 130, 118, 46, 95, 62, 151, 50, 65, 193, 76, 117, 81, 152, 53, 203, 243, 40, 57, 196, 173, 55, 97, 82, 34, 75, 39, 125, 66, 246, 145, 71, 158, 125, 86, 124, 14, 17, 129, 44, 63, 161, 164, 214, 62, 0, 160, 15, 0, 104, 112, 116, 69, 35, 132, 103, 80, 230, 88, 71, 115, 36, 130, 85, 70, 78, 186, 16, 115, 37, 93, 146, 27, 254, 232, 79, 149, 216, 117, 154, 56, 197, 255, 249, 222, 42, 136, 140, 147, 151, 201, 34, 84, 207, 125, 68, 17, 248, 184, 112, 145, 18, 2, 171, 209, 163, 208, 97, 133, 135, 190, 151, 58, 217, 237, 200, 52, 215, 72, 170, 17, 194, 26, 209, 159, 138, 175, 69, 14, 200, 220, 102, 37, 17, 219, 127, 38, 118, 46, 184, 221, 190, 253, 92, 95, 68, 112, 224, 66, 95, 206, 237, 107, 153, 200, 153, 136, 189, 172, 161, 199, 229, 112, 145, 249, 95, 164, 217, 248, 100, 78, 29, 153, 132, 223, 171, 22, 115, 3, 95, 187, 31, 60, 194, 177, 99, 181, 187, 241, 243, 69, 82, 149, 221, 231, 133, 138, 110, 45, 54, 146, 1, 58, 172, 96, 83, 228, 84, 141, 12, 32, 37, 9, 128, 151, 1, 128, 151, 197, 215, 7, 48, 25, 97, 126, 243, 163, 86, 236, 250, 115, 193, 134, 237, 142, 50, 135, 195, 1, 29, 30, 105, 112, 108, 47, 43, 179, 55, 56, 252, 168, 23, 206, 52, 160, 19, 232, 112, 176, 149, 84, 21, 145, 137, 112, 96, 15, 100, 197, 212, 218, 215, 231, 231, 248, 62, 172, 77, 119, 46, 164, 8, 252, 197, 195, 77, 215, 142, 45, 91, 182, 140, 204, 230, 45, 159, 43, 21, 152, 109, 53, 161, 252, 53, 195, 10, 238, 204, 53, 229, 18, 0, 207, 2, 0, 255, 67, 249, 133, 233, 205, 143, 122, 160, 141, 205, 208, 228, 221, 117, 145, 51, 29, 101, 14, 103, 228, 76, 20, 154, 220, 16, 117, 148, 117, 81, 104, 136, 67, 224, 74, 49, 189, 219, 233, 116, 57, 237, 169, 79, 124, 224, 43, 156, 57, 215, 135, 30, 163, 243, 254, 175, 137, 8, 60, 177, 159, 76, 46, 36, 197, 237, 220, 242, 82, 209, 36, 170, 227, 59, 189, 128, 71, 179, 201, 70, 6, 5, 168, 2, 96, 5, 26, 1, 229, 23, 102, 23, 154, 70, 237, 215, 188, 189, 57, 122, 238, 76, 95, 95, 15, 91, 233, 239, 139, 110, 183, 59, 42, 215, 70, 250, 182, 67, 203, 225, 203, 24, 89, 182, 153, 85, 61, 44, 46, 178, 73, 38, 133, 186, 113, 50, 187, 0, 119, 232, 106, 112, 190, 214, 5, 74, 211, 125, 255, 127, 166, 8, 220, 250, 148, 19, 218, 127, 153, 234, 1, 142, 93, 100, 35, 170, 96, 175, 202, 5, 174, 208, 25, 36, 5, 239, 242, 57, 171, 114, 88, 193, 145, 101, 82, 129, 4, 0, 53, 2, 105, 0, 152, 136, 188, 209, 248, 7, 237, 209, 115, 61, 2, 196, 61, 108, 165, 243, 80, 7, 206, 80, 217, 184, 137, 139, 136, 118, 33, 170, 119, 21, 107, 39, 171, 111, 131, 165, 116, 56, 92, 125, 219, 203, 236, 101, 219, 241, 29, 230, 106, 239, 253, 47, 34, 2, 143, 218, 107, 65, 21, 94, 251, 64, 188, 228, 185, 173, 162, 71, 164, 188, 141, 118, 109, 90, 112, 48, 173, 140, 114, 88, 193, 46, 110, 4, 98, 8, 0, 21, 114, 133, 17, 16, 201, 76, 209, 145, 31, 2, 191, 232, 185, 134, 178, 32, 225, 114, 14, 55, 31, 112, 56, 220, 149, 78, 215, 83, 224, 231, 118, 40, 28, 194, 116, 194, 137, 60, 116, 233, 117, 28, 163, 114, 108, 7, 253, 129, 11, 244, 147, 185, 163, 211, 36, 4, 54, 98, 108, 156, 76, 74, 8, 216, 244, 134, 207, 53, 245, 130, 16, 41, 226, 226, 27, 140, 252, 129, 203, 105, 16, 5, 137, 74, 206, 178, 118, 83, 15, 248, 58, 74, 35, 32, 82, 150, 205, 143, 90, 59, 34, 125, 49, 108, 96, 179, 35, 122, 46, 162, 106, 165, 219, 69, 252, 92, 136, 4, 163, 125, 18, 52, 106, 114, 59, 20, 51, 58, 201, 94, 37, 14, 167, 179, 18, 193, 112, 185, 28, 83, 239, 19, 17, 216, 212, 132, 214, 48, 121, 148, 86, 119, 47, 210, 42, 1, 221, 109, 5, 136, 195, 192, 40, 63, 209, 103, 1, 81, 201, 89, 58, 234, 48, 120, 105, 85, 24, 1, 137, 24, 157, 181, 186, 100, 226, 203, 28, 246, 237, 196, 242, 219, 29, 13, 231, 116, 199, 200, 57, 254, 181, 142, 142, 115, 61, 154, 175, 56, 140, 141, 85, 43, 89, 224, 104, 181, 211, 141, 79, 233, 118, 58, 42, 43, 239, 76, 33, 112, 49, 153, 60, 85, 91, 75, 16, 208, 5, 64, 39, 247, 57, 83, 250, 249, 44, 36, 42, 57, 203, 185, 142, 62, 8, 94, 127, 150, 110, 4, 184, 204, 113, 151, 191, 204, 222, 28, 41, 3, 159, 191, 172, 204, 105, 119, 234, 12, 15, 224, 117, 236, 19, 206, 237, 93, 0, 64, 214, 169, 77, 44, 56, 10, 226, 73, 61, 188, 175, 149, 187, 83, 244, 7, 110, 221, 89, 119, 150, 172, 222, 182, 31, 190, 121, 254, 99, 115, 85, 84, 218, 199, 118, 233, 46, 110, 45, 42, 57, 203, 185, 158, 158, 51, 2, 175, 49, 2, 186, 119, 146, 201, 191, 187, 161, 185, 140, 181, 59, 65, 153, 243, 29, 187, 245, 236, 12, 94, 199, 58, 236, 101, 193, 8, 167, 251, 243, 106, 114, 87, 98, 247, 123, 234, 234, 26, 26, 182, 219, 65, 156, 238, 164, 147, 41, 118, 220, 202, 122, 176, 122, 166, 246, 242, 9, 15, 87, 141, 14, 209, 214, 172, 183, 210, 187, 187, 174, 33, 20, 149, 156, 5, 133, 220, 163, 53, 2, 92, 70, 0, 58, 202, 206, 0, 111, 87, 210, 53, 169, 140, 167, 239, 1, 103, 55, 235, 195, 175, 71, 158, 237, 101, 13, 205, 13, 46, 59, 250, 178, 19, 40, 2, 37, 203, 88, 14, 162, 99, 16, 132, 179, 77, 28, 154, 129, 95, 107, 7, 26, 171, 179, 77, 162, 101, 13, 6, 83, 169, 146, 179, 128, 144, 243, 125, 94, 141, 17, 200, 72, 188, 35, 72, 179, 28, 198, 63, 202, 162, 203, 135, 118, 206, 120, 41, 252, 116, 242, 159, 59, 35, 244, 113, 236, 51, 118, 240, 101, 215, 254, 121, 137, 56, 92, 82, 205, 237, 63, 134, 170, 240, 234, 7, 152, 28, 250, 152, 218, 65, 207, 219, 89, 72, 113, 91, 227, 133, 213, 24, 84, 114, 22, 6, 247, 70, 255, 25, 0, 240, 83, 131, 211, 180, 196, 55, 52, 159, 203, 12, 128, 3, 181, 26, 110, 198, 101, 24, 135, 235, 81, 43, 137, 63, 159, 118, 61, 221, 211, 225, 103, 111, 221, 65, 71, 11, 190, 85, 193, 213, 157, 34, 25, 202, 165, 41, 59, 248, 118, 247, 133, 75, 25, 232, 66, 119, 10, 1, 214, 56, 20, 98, 240, 96, 97, 34, 62, 117, 36, 144, 157, 58, 202, 208, 252, 101, 203, 19, 184, 156, 196, 213, 113, 186, 42, 115, 42, 117, 0, 223, 171, 174, 239, 76, 31, 235, 16, 231, 87, 31, 254, 214, 74, 142, 251, 0, 60, 130, 83, 4, 128, 185, 139, 158, 135, 219, 189, 157, 177, 253, 128, 64, 10, 128, 12, 43, 235, 49, 120, 176, 144, 163, 38, 18, 200, 68, 126, 112, 124, 98, 193, 51, 244, 141, 164, 0, 244, 27, 201, 186, 221, 24, 2, 131, 28, 144, 181, 143, 43, 77, 204, 100, 1, 199, 203, 221, 115, 46, 118, 38, 82, 247, 147, 122, 113, 220, 120, 21, 199, 53, 157, 189, 92, 187, 80, 140, 139, 183, 46, 98, 223, 206, 220, 254, 75, 151, 222, 174, 144, 30, 200, 97, 188, 222, 48, 105, 58, 117, 133, 159, 212, 49, 2, 74, 234, 233, 233, 59, 87, 86, 86, 22, 36, 189, 222, 188, 29, 195, 95, 158, 104, 23, 220, 80, 198, 137, 17, 103, 70, 102, 119, 211, 93, 16, 92, 70, 203, 236, 43, 9, 125, 47, 247, 153, 190, 136, 155, 173, 92, 32, 149, 14, 128, 42, 4, 93, 88, 34, 207, 58, 90, 148, 29, 128, 98, 209, 73, 206, 186, 180, 34, 5, 64, 207, 8, 40, 72, 32, 78, 125, 164, 185, 172, 25, 172, 186, 223, 14, 126, 3, 58, 120, 96, 4, 64, 195, 147, 121, 171, 208, 176, 44, 186, 14, 60, 28, 167, 221, 196, 58, 143, 212, 247, 226, 207, 96, 233, 137, 235, 190, 13, 146, 24, 128, 242, 175, 169, 95, 42, 1, 240, 142, 8, 64, 119, 119, 247, 201, 75, 151, 122, 155, 122, 241, 205, 96, 231, 165, 94, 248, 160, 187, 151, 114, 64, 193, 34, 91, 233, 214, 194, 69, 11, 244, 23, 110, 76, 3, 64, 27, 9, 168, 136, 151, 242, 188, 125, 142, 230, 62, 161, 161, 225, 12, 25, 244, 65, 35, 151, 203, 178, 46, 232, 238, 153, 178, 8, 140, 124, 112, 45, 147, 120, 96, 3, 78, 247, 124, 179, 80, 202, 14, 81, 0, 6, 191, 114, 215, 93, 63, 188, 212, 125, 219, 95, 221, 214, 13, 239, 254, 230, 43, 151, 106, 238, 186, 235, 174, 175, 84, 17, 0, 108, 123, 201, 153, 239, 175, 203, 182, 155, 9, 1, 32, 179, 17, 192, 176, 14, 58, 61, 22, 59, 215, 215, 12, 109, 46, 235, 19, 67, 252, 172, 145, 102, 58, 85, 58, 76, 77, 103, 99, 228, 3, 142, 153, 81, 0, 14, 147, 128, 192, 179, 255, 237, 111, 144, 28, 41, 5, 224, 228, 29, 120, 252, 171, 170, 75, 111, 223, 117, 233, 82, 211, 93, 183, 225, 187, 222, 219, 6, 9, 0, 20, 169, 238, 55, 247, 127, 249, 78, 230, 250, 99, 2, 64, 102, 35, 128, 13, 38, 97, 15, 230, 61, 236, 219, 207, 192, 123, 129, 196, 82, 110, 123, 101, 150, 132, 81, 250, 160, 138, 203, 97, 98, 201, 95, 70, 62, 112, 116, 220, 20, 0, 120, 243, 244, 233, 38, 100, 130, 58, 58, 116, 76, 1, 232, 188, 163, 24, 204, 1, 116, 255, 224, 109, 151, 122, 239, 232, 253, 10, 126, 246, 195, 121, 151, 82, 0, 156, 124, 243, 205, 127, 193, 234, 211, 140, 225, 48, 30, 50, 27, 1, 225, 92, 80, 12, 237, 207, 52, 59, 28, 93, 208, 253, 81, 116, 36, 129, 165, 157, 217, 18, 70, 218, 81, 37, 205, 62, 117, 25, 136, 60, 119, 245, 183, 54, 108, 216, 80, 242, 254, 233, 211, 167, 137, 57, 176, 41, 0, 232, 253, 225, 202, 187, 238, 184, 116, 27, 8, 253, 87, 46, 221, 213, 116, 9, 1, 184, 112, 91, 111, 10, 128, 223, 109, 126, 243, 36, 57, 221, 154, 225, 71, 8, 0, 89, 140, 0, 116, 249, 153, 62, 204, 238, 156, 11, 118, 149, 69, 34, 231, 130, 61, 24, 74, 64, 247, 103, 77, 24, 105, 1, 96, 215, 226, 142, 100, 36, 117, 235, 206, 166, 56, 69, 90, 184, 108, 179, 125, 59, 25, 52, 57, 122, 128, 91, 244, 121, 10, 0, 164, 175, 244, 222, 209, 13, 124, 255, 246, 109, 63, 252, 225, 87, 86, 94, 186, 180, 242, 175, 68, 43, 240, 229, 231, 165, 139, 158, 47, 127, 168, 84, 30, 88, 201, 8, 64, 22, 35, 64, 9, 60, 85, 224, 132, 51, 103, 98, 68, 1, 60, 5, 65, 118, 246, 132, 145, 22, 0, 12, 65, 158, 177, 59, 158, 217, 84, 233, 116, 178, 166, 116, 72, 37, 186, 149, 39, 232, 184, 217, 233, 63, 44, 35, 203, 114, 136, 74, 16, 152, 224, 43, 23, 254, 230, 135, 151, 170, 238, 234, 94, 183, 110, 29, 170, 63, 162, 14, 137, 18, 92, 206, 226, 234, 201, 236, 34, 98, 59, 11, 53, 119, 197, 181, 153, 128, 123, 31, 126, 130, 0, 240, 8, 157, 50, 157, 133, 34, 56, 208, 201, 247, 69, 207, 181, 226, 146, 36, 14, 51, 179, 212, 180, 0, 144, 40, 156, 125, 2, 130, 4, 187, 9, 167, 128, 60, 42, 154, 207, 147, 167, 37, 58, 252, 128, 173, 180, 144, 2, 80, 117, 219, 29, 183, 85, 129, 244, 223, 65, 155, 13, 34, 80, 117, 151, 228, 7, 112, 210, 250, 249, 132, 101, 52, 81, 244, 253, 95, 173, 169, 217, 127, 203, 137, 19, 223, 254, 54, 2, 96, 58, 18, 8, 99, 0, 176, 187, 3, 58, 197, 101, 110, 183, 108, 125, 0, 128, 115, 88, 135, 41, 167, 128, 80, 37, 180, 100, 11, 25, 55, 163, 244, 89, 231, 50, 81, 4, 46, 244, 74, 156, 160, 113, 132, 240, 66, 162, 111, 73, 30, 165, 148, 251, 153, 136, 118, 77, 83, 211, 7, 39, 78, 156, 45, 249, 11, 136, 49, 111, 73, 38, 187, 102, 89, 184, 64, 224, 229, 71, 254, 251, 190, 125, 166, 118, 43, 7, 39, 61, 226, 119, 80, 61, 198, 100, 74, 24, 25, 2, 32, 167, 26, 95, 52, 161, 13, 165, 245, 95, 192, 203, 100, 87, 41, 32, 56, 157, 221, 19, 196, 139, 201, 118, 143, 239, 124, 249, 229, 191, 252, 185, 197, 114, 235, 254, 15, 78, 156, 58, 90, 82, 242, 181, 178, 228, 208, 212, 169, 142, 163, 255, 161, 182, 246, 44, 0, 16, 155, 106, 9, 133, 66, 251, 214, 188, 186, 239, 87, 242, 186, 159, 153, 8, 93, 194, 136, 35, 195, 0, 103, 86, 0, 20, 156, 3, 194, 237, 204, 28, 44, 139, 38, 83, 76, 59, 108, 233, 148, 33, 200, 30, 12, 157, 61, 246, 194, 15, 234, 143, 157, 61, 75, 204, 230, 222, 251, 190, 153, 220, 30, 76, 206, 250, 249, 215, 254, 195, 15, 110, 25, 154, 213, 21, 180, 124, 10, 0, 156, 154, 48, 150, 28, 154, 136, 0, 188, 10, 0, 224, 10, 128, 217, 119, 65, 140, 40, 194, 224, 113, 2, 160, 228, 28, 136, 148, 204, 56, 135, 114, 2, 117, 213, 6, 17, 130, 15, 179, 133, 195, 221, 201, 19, 63, 248, 121, 237, 169, 100, 55, 169, 58, 92, 248, 131, 255, 148, 116, 148, 37, 23, 252, 228, 47, 255, 178, 246, 155, 13, 211, 167, 151, 53, 3, 0, 245, 239, 207, 154, 184, 96, 194, 189, 8, 192, 43, 0, 128, 185, 141, 13, 90, 207, 164, 234, 93, 174, 3, 0, 197, 117, 149, 68, 21, 24, 36, 149, 196, 134, 167, 226, 57, 118, 225, 178, 63, 80, 4, 50, 231, 67, 186, 199, 146, 255, 19, 0, 248, 52, 73, 43, 111, 151, 254, 245, 173, 99, 13, 115, 146, 101, 223, 190, 239, 107, 181, 223, 156, 245, 147, 91, 127, 50, 189, 230, 38, 188, 231, 253, 95, 125, 152, 67, 0, 214, 172, 217, 183, 207, 228, 78, 184, 125, 173, 17, 221, 52, 175, 1, 101, 7, 128, 174, 42, 91, 169, 171, 15, 196, 65, 173, 180, 145, 212, 3, 59, 78, 43, 233, 147, 195, 245, 157, 187, 150, 45, 92, 181, 5, 44, 91, 77, 211, 254, 55, 235, 235, 143, 93, 188, 124, 249, 234, 213, 159, 255, 160, 182, 246, 211, 107, 165, 226, 184, 234, 127, 220, 59, 243, 166, 205, 183, 126, 245, 153, 9, 220, 19, 247, 114, 15, 127, 245, 254, 84, 248, 14, 0, 4, 214, 128, 14, 84, 46, 244, 197, 53, 182, 27, 73, 67, 142, 43, 124, 152, 0, 0, 139, 103, 92, 14, 93, 73, 192, 205, 50, 200, 110, 64, 106, 170, 88, 182, 225, 247, 167, 211, 233, 147, 63, 156, 252, 229, 134, 101, 203, 182, 120, 86, 173, 90, 181, 165, 198, 83, 83, 245, 147, 205, 53, 64, 54, 49, 126, 252, 234, 151, 95, 222, 241, 231, 204, 83, 236, 211, 154, 233, 120, 0, 192, 175, 214, 188, 178, 239, 87, 82, 139, 27, 165, 197, 243, 233, 170, 225, 1, 67, 40, 76, 145, 169, 2, 43, 178, 224, 157, 174, 42, 192, 96, 222, 169, 99, 45, 217, 45, 7, 126, 171, 129, 64, 134, 226, 232, 225, 223, 118, 239, 90, 118, 223, 178, 42, 0, 163, 32, 181, 130, 205, 59, 140, 238, 116, 60, 139, 100, 4, 136, 10, 104, 108, 9, 4, 90, 26, 197, 157, 177, 189, 141, 109, 109, 129, 212, 178, 160, 184, 127, 132, 118, 93, 188, 27, 0, 0, 137, 148, 221, 110, 157, 40, 9, 139, 108, 12, 18, 58, 21, 203, 150, 29, 54, 196, 224, 244, 233, 79, 127, 254, 79, 20, 140, 151, 10, 183, 138, 121, 148, 143, 172, 11, 171, 117, 166, 227, 89, 36, 35, 208, 40, 238, 144, 205, 109, 82, 237, 12, 238, 85, 47, 141, 44, 66, 97, 118, 39, 24, 211, 37, 118, 46, 169, 150, 74, 69, 118, 123, 101, 134, 253, 82, 182, 108, 233, 252, 195, 39, 6, 0, 188, 255, 243, 247, 229, 215, 191, 221, 69, 108, 97, 233, 39, 191, 255, 195, 7, 245, 16, 89, 117, 119, 195, 97, 195, 135, 226, 209, 34, 26, 129, 128, 184, 65, 56, 18, 89, 16, 223, 219, 6, 127, 60, 45, 235, 3, 102, 123, 91, 143, 114, 169, 49, 212, 225, 117, 220, 51, 38, 163, 153, 172, 94, 181, 108, 89, 73, 247, 201, 223, 107, 112, 248, 167, 159, 159, 82, 188, 35, 170, 112, 239, 250, 127, 213, 131, 202, 66, 141, 64, 64, 177, 60, 182, 204, 236, 129, 150, 64, 104, 95, 233, 191, 23, 0, 227, 216, 46, 78, 186, 114, 213, 194, 101, 203, 90, 171, 54, 236, 56, 44, 41, 199, 19, 10, 6, 0, 18, 215, 51, 219, 186, 94, 23, 0, 98, 4, 168, 238, 11, 52, 182, 164, 51, 123, 142, 143, 146, 86, 87, 147, 17, 128, 198, 64, 91, 118, 223, 211, 60, 49, 108, 245, 150, 31, 238, 252, 199, 101, 64, 157, 251, 127, 80, 127, 88, 97, 41, 118, 73, 203, 153, 149, 106, 33, 176, 16, 35, 176, 147, 112, 62, 110, 239, 162, 106, 191, 66, 212, 205, 173, 202, 145, 94, 87, 147, 17, 0, 175, 98, 247, 137, 27, 64, 170, 130, 134, 234, 45, 91, 86, 113, 53, 4, 140, 247, 54, 28, 62, 188, 116, 175, 84, 104, 85, 186, 75, 3, 192, 190, 53, 175, 112, 191, 2, 77, 23, 240, 166, 107, 187, 84, 251, 43, 76, 110, 19, 156, 94, 87, 147, 89, 4, 2, 57, 237, 192, 158, 149, 82, 81, 134, 122, 129, 122, 182, 98, 203, 150, 10, 171, 92, 107, 86, 186, 158, 168, 191, 110, 241, 104, 1, 35, 240, 202, 190, 246, 0, 23, 106, 107, 76, 147, 129, 113, 60, 67, 122, 93, 77, 54, 0, 188, 28, 215, 22, 16, 235, 241, 204, 151, 165, 25, 254, 186, 20, 101, 232, 213, 69, 149, 203, 149, 183, 159, 151, 46, 34, 37, 75, 44, 57, 90, 66, 175, 172, 0, 43, 40, 234, 192, 64, 203, 38, 89, 252, 205, 239, 122, 150, 162, 244, 186, 154, 236, 34, 80, 46, 47, 54, 107, 186, 44, 205, 144, 24, 249, 160, 191, 137, 147, 77, 134, 160, 48, 149, 35, 179, 16, 35, 192, 114, 162, 18, 132, 235, 169, 62, 108, 131, 174, 25, 199, 51, 164, 165, 73, 50, 2, 128, 91, 212, 216, 222, 144, 89, 205, 108, 89, 154, 49, 49, 226, 193, 229, 54, 200, 53, 61, 111, 251, 72, 134, 64, 42, 183, 178, 4, 0, 128, 87, 65, 4, 218, 229, 39, 9, 72, 182, 112, 156, 15, 161, 72, 147, 100, 145, 241, 64, 160, 218, 35, 27, 155, 113, 172, 133, 165, 253, 109, 114, 200, 48, 28, 152, 130, 224, 119, 115, 233, 239, 88, 246, 97, 36, 112, 156, 184, 123, 180, 197, 237, 215, 161, 3, 210, 67, 157, 44, 0, 224, 98, 188, 30, 178, 43, 89, 227, 184, 214, 194, 210, 167, 204, 165, 161, 207, 217, 164, 33, 198, 66, 50, 102, 100, 121, 121, 205, 43, 175, 254, 10, 52, 126, 74, 235, 7, 254, 221, 0, 16, 9, 126, 11, 21, 161, 169, 44, 163, 9, 202, 82, 26, 154, 130, 160, 20, 17, 176, 128, 17, 120, 181, 5, 122, 32, 197, 241, 84, 29, 180, 175, 63, 168, 185, 52, 251, 2, 47, 227, 2, 160, 69, 228, 61, 198, 68, 150, 49, 59, 153, 72, 182, 47, 167, 11, 62, 227, 52, 117, 206, 242, 202, 154, 159, 66, 147, 21, 249, 64, 209, 16, 182, 180, 104, 1, 200, 190, 192, 75, 26, 153, 180, 243, 45, 212, 18, 50, 242, 225, 122, 200, 204, 94, 131, 220, 162, 189, 210, 128, 137, 101, 205, 154, 103, 95, 85, 5, 184, 200, 0, 123, 245, 21, 160, 92, 137, 109, 182, 230, 35, 35, 0, 154, 13, 194, 25, 249, 112, 61, 148, 65, 5, 74, 244, 188, 173, 116, 235, 151, 226, 128, 9, 2, 176, 15, 85, 160, 152, 248, 160, 221, 95, 168, 235, 3, 236, 92, 203, 206, 44, 32, 202, 186, 194, 106, 110, 233, 191, 76, 0, 136, 19, 219, 216, 20, 14, 140, 124, 208, 57, 219, 172, 155, 100, 130, 1, 82, 147, 145, 182, 202, 0, 112, 180, 253, 141, 162, 9, 56, 168, 224, 0, 182, 162, 192, 106, 125, 0, 159, 118, 83, 206, 11, 188, 152, 16, 129, 34, 115, 19, 192, 76, 187, 73, 58, 243, 163, 211, 104, 121, 106, 165, 103, 194, 1, 43, 20, 35, 227, 41, 71, 152, 178, 0, 139, 155, 184, 23, 20, 225, 42, 166, 44, 173, 94, 201, 109, 129, 151, 27, 231, 235, 155, 117, 147, 12, 118, 113, 84, 82, 169, 220, 126, 44, 186, 179, 172, 81, 141, 140, 183, 75, 94, 128, 94, 42, 80, 84, 209, 140, 249, 199, 54, 6, 192, 6, 200, 154, 191, 143, 105, 55, 41, 203, 244, 16, 164, 114, 226, 10, 189, 243, 206, 71, 191, 46, 69, 135, 24, 57, 64, 53, 50, 30, 200, 228, 4, 48, 242, 193, 28, 221, 56, 14, 48, 235, 38, 185, 179, 106, 128, 229, 104, 2, 11, 171, 171, 233, 250, 42, 0, 64, 218, 200, 184, 2, 0, 114, 74, 185, 92, 143, 93, 94, 164, 6, 192, 196, 66, 224, 55, 48, 220, 53, 229, 38, 233, 101, 86, 185, 180, 217, 95, 68, 5, 164, 198, 203, 17, 0, 61, 14, 104, 199, 36, 64, 117, 81, 129, 85, 177, 58, 165, 205, 198, 224, 31, 134, 190, 51, 51, 119, 81, 23, 0, 157, 26, 127, 51, 196, 152, 112, 147, 244, 156, 192, 198, 52, 113, 174, 64, 27, 176, 87, 238, 244, 52, 29, 32, 110, 149, 25, 90, 127, 112, 169, 181, 160, 184, 32, 109, 191, 207, 34, 101, 155, 43, 76, 88, 66, 93, 0, 114, 89, 67, 94, 65, 140, 124, 48, 36, 221, 25, 194, 141, 233, 121, 39, 50, 33, 93, 158, 123, 131, 0, 188, 172, 60, 91, 180, 131, 223, 216, 167, 247, 11, 229, 218, 117, 73, 50, 146, 30, 0, 134, 107, 139, 101, 33, 70, 62, 24, 146, 91, 47, 17, 224, 73, 31, 245, 86, 207, 189, 177, 172, 81, 151, 8, 138, 237, 95, 106, 144, 15, 41, 55, 189, 229, 37, 253, 113, 157, 207, 198, 187, 72, 26, 35, 31, 140, 200, 148, 19, 44, 151, 76, 136, 100, 121, 229, 251, 42, 45, 136, 219, 196, 182, 238, 45, 204, 57, 31, 172, 79, 55, 52, 231, 151, 149, 76, 56, 193, 72, 213, 123, 149, 50, 96, 121, 117, 197, 10, 85, 137, 28, 246, 127, 97, 166, 177, 47, 253, 85, 82, 245, 73, 5, 128, 137, 88, 242, 186, 200, 36, 3, 136, 50, 32, 217, 1, 203, 190, 53, 170, 249, 98, 168, 4, 182, 22, 30, 12, 25, 103, 236, 193, 51, 30, 159, 39, 136, 126, 236, 63, 126, 239, 58, 87, 74, 52, 38, 214, 110, 142, 1, 68, 67, 40, 149, 206, 89, 218, 1, 0, 165, 12, 4, 140, 61, 65, 137, 204, 171, 66, 21, 0, 232, 199, 62, 84, 156, 97, 177, 208, 235, 35, 211, 12, 32, 178, 0, 89, 200, 220, 195, 91, 56, 148, 1, 133, 39, 112, 240, 96, 218, 144, 136, 14, 145, 69, 125, 204, 144, 18, 0, 226, 199, 206, 44, 42, 202, 176, 88, 232, 245, 144, 51, 135, 194, 229, 229, 180, 120, 176, 154, 45, 111, 250, 204, 194, 237, 83, 26, 194, 10, 235, 250, 128, 137, 116, 152, 21, 245, 64, 65, 81, 86, 109, 160, 4, 128, 248, 177, 223, 120, 104, 39, 136, 64, 69, 110, 11, 96, 152, 33, 77, 251, 51, 198, 207, 116, 85, 134, 130, 242, 109, 239, 109, 177, 112, 45, 16, 13, 72, 101, 146, 16, 244, 101, 151, 0, 174, 136, 78, 90, 102, 203, 179, 70, 52, 74, 0, 136, 31, 107, 37, 177, 36, 91, 36, 94, 151, 211, 22, 186, 153, 200, 37, 174, 17, 153, 162, 140, 241, 243, 78, 58, 92, 90, 248, 153, 31, 75, 101, 95, 145, 189, 225, 242, 165, 45, 162, 35, 176, 41, 151, 65, 154, 98, 107, 145, 129, 82, 144, 0, 40, 46, 42, 166, 126, 172, 85, 118, 164, 241, 144, 211, 198, 66, 153, 72, 103, 149, 184, 204, 241, 243, 115, 116, 164, 108, 235, 114, 4, 224, 85, 0, 224, 101, 242, 241, 193, 144, 148, 17, 8, 24, 160, 167, 191, 172, 57, 64, 160, 191, 102, 30, 5, 96, 203, 210, 45, 196, 129, 122, 200, 10, 29, 206, 40, 239, 150, 161, 77, 185, 145, 182, 236, 54, 75, 252, 188, 156, 102, 133, 62, 90, 4, 0, 52, 174, 89, 67, 237, 0, 202, 190, 152, 15, 8, 24, 160, 103, 51, 98, 89, 41, 52, 210, 236, 175, 192, 181, 224, 86, 228, 226, 51, 177, 55, 32, 231, 167, 37, 221, 169, 153, 217, 226, 231, 229, 116, 192, 248, 115, 172, 21, 126, 245, 239, 215, 60, 251, 63, 90, 200, 22, 185, 156, 52, 38, 96, 128, 158, 45, 155, 222, 211, 236, 175, 208, 150, 74, 47, 48, 242, 225, 198, 146, 51, 93, 254, 9, 101, 139, 159, 159, 167, 122, 0, 0, 8, 88, 191, 49, 247, 27, 115, 151, 238, 37, 131, 131, 141, 232, 8, 112, 134, 232, 21, 103, 83, 90, 105, 251, 43, 84, 84, 80, 64, 255, 132, 62, 177, 203, 72, 148, 152, 76, 241, 51, 240, 104, 53, 89, 176, 204, 2, 158, 239, 3, 208, 126, 82, 43, 27, 8, 136, 234, 127, 124, 131, 52, 24, 45, 171, 69, 175, 194, 202, 82, 14, 24, 239, 254, 81, 217, 201, 216, 254, 51, 242, 65, 135, 200, 16, 7, 174, 206, 2, 0, 188, 81, 248, 234, 220, 53, 88, 45, 173, 40, 137, 97, 198, 53, 72, 83, 108, 85, 139, 30, 113, 25, 3, 127, 66, 14, 216, 201, 64, 251, 141, 194, 11, 70, 62, 232, 16, 29, 226, 88, 190, 21, 1, 104, 95, 31, 152, 139, 117, 82, 164, 62, 194, 204, 197, 198, 4, 45, 86, 48, 143, 184, 33, 148, 167, 165, 237, 186, 170, 45, 51, 208, 166, 103, 22, 56, 13, 243, 244, 140, 124, 208, 146, 184, 216, 76, 203, 65, 27, 0, 176, 180, 61, 180, 232, 239, 73, 189, 184, 185, 139, 51, 81, 185, 53, 197, 60, 172, 149, 213, 233, 120, 15, 231, 53, 87, 26, 101, 38, 120, 124, 186, 242, 219, 142, 140, 123, 17, 24, 17, 29, 226, 216, 20, 106, 217, 104, 9, 60, 0, 206, 255, 190, 191, 127, 5, 100, 32, 144, 146, 84, 70, 62, 168, 169, 58, 91, 28, 84, 173, 186, 52, 29, 0, 111, 91, 75, 40, 144, 33, 210, 84, 61, 99, 246, 129, 72, 232, 199, 103, 236, 11, 50, 238, 69, 96, 64, 132, 71, 61, 237, 109, 155, 88, 139, 21, 107, 52, 2, 180, 86, 208, 4, 85, 88, 179, 157, 193, 200, 7, 21, 0, 94, 172, 8, 18, 139, 79, 76, 62, 99, 150, 157, 38, 104, 63, 62, 179, 192, 62, 142, 8, 155, 240, 40, 184, 123, 88, 33, 66, 180, 52, 86, 139, 154, 171, 9, 202, 110, 8, 171, 173, 224, 247, 210, 133, 237, 37, 0, 196, 18, 108, 60, 122, 2, 230, 0, 80, 46, 9, 5, 142, 38, 254, 42, 155, 254, 211, 164, 31, 157, 11, 198, 181, 23, 1, 131, 53, 90, 12, 169, 21, 70, 194, 58, 17, 208, 2, 227, 168, 139, 210, 33, 136, 117, 196, 32, 71, 6, 32, 164, 32, 115, 38, 81, 61, 16, 73, 83, 208, 5, 105, 142, 56, 233, 71, 183, 157, 25, 207, 67, 50, 40, 138, 140, 12, 128, 100, 7, 110, 12, 2, 50, 165, 3, 224, 229, 26, 219, 76, 90, 68, 221, 129, 200, 162, 244, 224, 147, 193, 209, 176, 39, 198, 243, 104, 213, 208, 254, 138, 185, 165, 165, 34, 0, 161, 229, 127, 251, 234, 206, 95, 253, 201, 0, 144, 38, 33, 228, 112, 169, 60, 16, 153, 49, 235, 192, 224, 34, 13, 204, 56, 158, 44, 16, 218, 86, 196, 22, 80, 79, 144, 208, 175, 230, 254, 247, 87, 247, 153, 124, 66, 157, 13, 47, 13, 72, 2, 160, 197, 235, 201, 21, 0, 89, 155, 102, 180, 59, 140, 124, 200, 141, 218, 193, 229, 45, 250, 6, 186, 194, 255, 23, 173, 60, 17, 44, 251, 36, 61, 236, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130]; diff --git a/tile_server/static/generated/sea.dart b/tile_server/static/generated/sea.dart new file mode 100644 index 00000000..f3707c9e --- /dev/null +++ b/tile_server/static/generated/sea.dart @@ -0,0 +1 @@ +final seaTileBytes = [137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 1, 0, 0, 0, 1, 0, 1, 3, 0, 0, 0, 102, 188, 58, 37, 0, 0, 0, 3, 80, 76, 84, 69, 170, 211, 223, 207, 236, 188, 245, 0, 0, 0, 31, 73, 68, 65, 84, 104, 129, 237, 193, 1, 13, 0, 0, 0, 194, 160, 247, 79, 109, 14, 55, 160, 0, 0, 0, 0, 0, 0, 0, 0, 190, 13, 33, 0, 0, 1, 154, 96, 225, 213, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130]; From eff50343acb7885e45e1ec61e6fc53e888156495 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 11 Apr 2024 12:00:36 +0100 Subject: [PATCH 167/168] Reduced unnecessary storage consumption --- tile_server/static/generated/favicon.dart | 3 ++- tile_server/static/generated/land.dart | 3 ++- tile_server/static/generated/sea.dart | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/tile_server/static/generated/favicon.dart b/tile_server/static/generated/favicon.dart index 259e34b4..7dd43c1b 100644 --- a/tile_server/static/generated/favicon.dart +++ b/tile_server/static/generated/favicon.dart @@ -1 +1,2 @@ -final faviconTileBytes = [0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 32, 0, 201, 45, 0, 0, 22, 0, 0, 0, 137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 1, 0, 0, 0, 1, 0, 8, 6, 0, 0, 0, 92, 114, 168, 102, 0, 0, 45, 144, 73, 68, 65, 84, 120, 218, 237, 157, 123, 124, 27, 229, 153, 239, 127, 26, 141, 70, 23, 91, 146, 109, 249, 126, 139, 147, 216, 9, 185, 185, 9, 16, 72, 40, 183, 64, 129, 246, 208, 238, 129, 237, 210, 82, 122, 202, 161, 91, 2, 11, 45, 44, 108, 161, 13, 112, 246, 0, 165, 52, 180, 205, 46, 109, 225, 244, 190, 103, 217, 148, 101, 89, 216, 93, 232, 133, 179, 101, 11, 9, 105, 32, 9, 73, 73, 76, 156, 224, 196, 151, 92, 124, 209, 197, 182, 44, 75, 178, 46, 51, 26, 73, 231, 15, 89, 178, 36, 75, 214, 109, 164, 153, 145, 222, 239, 231, 147, 79, 28, 105, 52, 126, 53, 153, 231, 55, 207, 251, 188, 207, 243, 188, 10, 254, 163, 23, 195, 40, 0, 191, 215, 9, 243, 88, 63, 204, 163, 199, 97, 29, 59, 1, 149, 190, 25, 205, 61, 215, 160, 121, 213, 39, 80, 219, 177, 9, 52, 83, 93, 200, 233, 9, 4, 66, 17, 81, 20, 42, 0, 201, 204, 76, 157, 195, 196, 249, 62, 88, 198, 250, 225, 152, 49, 163, 174, 227, 34, 52, 245, 92, 131, 166, 238, 109, 208, 55, 174, 18, 251, 251, 18, 8, 132, 56, 4, 23, 128, 120, 120, 158, 133, 101, 52, 226, 29, 152, 199, 250, 17, 166, 52, 104, 90, 181, 13, 77, 221, 219, 208, 216, 125, 37, 241, 14, 8, 4, 145, 201, 73, 0, 38, 236, 62, 184, 61, 172, 216, 99, 134, 182, 190, 153, 76, 47, 8, 4, 1, 160, 179, 61, 112, 194, 238, 131, 66, 93, 139, 21, 203, 87, 138, 61, 102, 76, 155, 71, 224, 24, 59, 134, 134, 149, 87, 136, 61, 20, 2, 65, 214, 80, 217, 30, 232, 246, 176, 168, 111, 21, 223, 248, 1, 160, 190, 117, 37, 124, 211, 86, 177, 135, 65, 32, 200, 158, 172, 5, 128, 64, 32, 148, 31, 178, 22, 0, 235, 224, 31, 192, 115, 115, 98, 15, 131, 64, 144, 45, 89, 199, 0, 164, 200, 200, 219, 79, 227, 200, 244, 121, 212, 117, 94, 130, 166, 158, 107, 208, 176, 242, 10, 24, 155, 214, 138, 61, 44, 2, 65, 54, 228, 45, 0, 111, 255, 231, 191, 225, 143, 111, 255, 22, 0, 160, 209, 106, 209, 220, 186, 12, 87, 95, 127, 19, 150, 175, 92, 19, 59, 230, 223, 94, 252, 49, 250, 251, 222, 143, 253, 123, 109, 239, 197, 248, 252, 237, 247, 1, 0, 206, 142, 12, 224, 159, 255, 225, 239, 97, 53, 143, 97, 237, 134, 139, 241, 63, 255, 234, 155, 168, 170, 210, 3, 0, 142, 31, 61, 128, 87, 95, 252, 9, 156, 179, 51, 184, 120, 203, 85, 184, 253, 174, 111, 164, 28, 195, 117, 55, 61, 6, 158, 103, 97, 29, 63, 9, 203, 232, 126, 28, 57, 240, 99, 4, 194, 10, 52, 245, 92, 131, 198, 149, 87, 162, 177, 231, 106, 48, 154, 26, 177, 175, 49, 129, 32, 89, 242, 22, 0, 135, 125, 18, 77, 45, 29, 248, 243, 47, 109, 199, 156, 195, 137, 211, 39, 251, 240, 204, 223, 222, 139, 47, 221, 245, 16, 46, 191, 250, 70, 0, 192, 208, 233, 227, 184, 230, 147, 159, 197, 138, 158, 200, 83, 89, 171, 173, 2, 0, 112, 156, 31, 127, 247, 212, 131, 248, 226, 95, 62, 136, 222, 139, 46, 195, 203, 255, 247, 7, 120, 101, 247, 243, 248, 242, 61, 143, 192, 238, 180, 227, 255, 236, 122, 12, 247, 239, 248, 30, 58, 186, 186, 241, 179, 103, 255, 55, 222, 248, 143, 221, 184, 241, 207, 111, 79, 253, 5, 104, 53, 218, 187, 46, 68, 123, 215, 133, 0, 0, 183, 203, 6, 243, 249, 15, 49, 241, 254, 79, 113, 236, 245, 7, 161, 111, 90, 131, 166, 149, 87, 161, 185, 231, 19, 168, 237, 188, 72, 236, 235, 77, 32, 72, 138, 130, 166, 0, 90, 93, 21, 154, 27, 151, 1, 141, 64, 247, 234, 94, 116, 116, 117, 227, 185, 239, 237, 192, 37, 151, 93, 11, 134, 209, 192, 53, 235, 192, 138, 158, 181, 232, 88, 214, 157, 240, 185, 161, 83, 253, 48, 24, 106, 176, 245, 202, 27, 0, 0, 55, 221, 126, 47, 118, 220, 125, 51, 190, 248, 149, 7, 209, 119, 96, 47, 214, 125, 108, 51, 214, 245, 110, 6, 0, 252, 217, 95, 124, 25, 191, 124, 254, 219, 105, 5, 32, 25, 189, 161, 9, 171, 55, 92, 143, 213, 27, 174, 7, 207, 179, 176, 219, 206, 96, 226, 124, 31, 250, 254, 253, 85, 120, 125, 30, 52, 173, 186, 38, 226, 33, 116, 95, 5, 117, 85, 189, 216, 215, 159, 64, 16, 21, 65, 99, 0, 189, 23, 94, 6, 138, 82, 98, 232, 84, 63, 214, 245, 110, 134, 219, 237, 68, 223, 159, 246, 227, 248, 7, 7, 208, 209, 213, 141, 222, 11, 47, 3, 0, 88, 39, 206, 163, 61, 78, 20, 76, 70, 19, 0, 192, 53, 235, 128, 213, 60, 138, 214, 182, 174, 216, 123, 237, 93, 61, 152, 180, 78, 228, 247, 229, 104, 53, 154, 218, 214, 160, 169, 109, 13, 112, 217, 23, 224, 247, 58, 49, 113, 190, 15, 230, 99, 187, 113, 252, 119, 223, 132, 198, 216, 129, 166, 85, 215, 162, 105, 229, 85, 36, 177, 136, 80, 145, 8, 30, 4, 172, 51, 53, 194, 237, 114, 0, 0, 62, 245, 103, 183, 197, 94, 255, 247, 151, 126, 134, 55, 127, 251, 47, 120, 248, 241, 231, 224, 247, 121, 161, 102, 212, 9, 159, 211, 234, 244, 112, 187, 103, 193, 113, 44, 106, 106, 23, 158, 204, 85, 85, 122, 4, 2, 28, 60, 30, 119, 44, 70, 144, 47, 26, 157, 17, 43, 215, 92, 133, 149, 107, 174, 2, 0, 76, 217, 134, 97, 29, 59, 129, 83, 255, 185, 3, 179, 179, 54, 152, 150, 109, 65, 211, 170, 107, 208, 188, 234, 90, 84, 213, 46, 43, 222, 85, 39, 16, 36, 130, 224, 2, 48, 61, 101, 129, 222, 80, 11, 0, 9, 110, 251, 117, 159, 254, 28, 190, 122, 251, 245, 24, 59, 63, 140, 106, 67, 13, 216, 179, 131, 9, 159, 243, 121, 221, 208, 84, 49, 208, 235, 141, 240, 121, 23, 150, 246, 60, 30, 55, 0, 20, 108, 252, 169, 104, 104, 234, 70, 67, 83, 55, 54, 92, 124, 19, 252, 94, 39, 108, 230, 83, 176, 12, 255, 30, 251, 247, 124, 15, 148, 218, 128, 166, 85, 159, 64, 211, 170, 109, 168, 239, 218, 74, 188, 3, 66, 89, 66, 191, 251, 135, 31, 163, 181, 179, 23, 173, 29, 27, 160, 209, 25, 11, 58, 217, 161, 119, 255, 11, 20, 165, 68, 207, 5, 27, 22, 189, 199, 48, 26, 104, 117, 122, 240, 124, 0, 109, 29, 93, 120, 243, 55, 47, 197, 222, 179, 59, 237, 224, 88, 22, 166, 186, 54, 52, 183, 47, 195, 209, 247, 247, 197, 222, 27, 63, 55, 132, 150, 214, 206, 162, 95, 8, 141, 206, 136, 101, 221, 151, 98, 89, 247, 165, 0, 0, 215, 172, 5, 227, 231, 142, 225, 204, 158, 157, 56, 50, 117, 22, 198, 214, 141, 104, 238, 185, 6, 77, 171, 175, 37, 75, 141, 132, 178, 129, 174, 93, 115, 51, 206, 13, 239, 197, 7, 7, 94, 65, 85, 149, 30, 45, 29, 189, 104, 237, 236, 141, 204, 155, 51, 224, 243, 122, 96, 157, 60, 15, 231, 148, 29, 253, 199, 14, 225, 205, 223, 189, 140, 237, 247, 63, 30, 9, 0, 186, 28, 24, 57, 221, 143, 85, 107, 55, 1, 0, 222, 121, 243, 53, 168, 213, 106, 180, 117, 44, 7, 195, 104, 16, 8, 112, 120, 247, 157, 55, 176, 105, 243, 149, 120, 125, 247, 143, 113, 233, 229, 215, 129, 97, 52, 216, 180, 249, 74, 188, 244, 15, 207, 226, 228, 241, 35, 232, 232, 234, 198, 111, 254, 237, 31, 99, 193, 194, 82, 98, 168, 105, 193, 218, 141, 45, 88, 187, 241, 191, 197, 150, 26, 173, 227, 135, 113, 228, 240, 47, 17, 8, 43, 208, 184, 242, 42, 52, 245, 108, 67, 195, 138, 203, 73, 48, 145, 32, 91, 20, 46, 135, 45, 86, 13, 232, 24, 253, 0, 214, 161, 183, 96, 27, 217, 7, 183, 109, 0, 77, 173, 23, 160, 181, 243, 99, 104, 237, 236, 197, 248, 172, 10, 43, 214, 127, 60, 246, 193, 228, 60, 128, 182, 142, 21, 216, 118, 195, 159, 199, 34, 254, 30, 143, 27, 255, 240, 252, 83, 56, 63, 114, 26, 148, 82, 137, 229, 221, 107, 241, 185, 47, 125, 21, 245, 141, 45, 0, 128, 177, 243, 195, 248, 167, 159, 125, 23, 147, 86, 51, 46, 88, 183, 41, 33, 15, 224, 228, 241, 35, 120, 249, 133, 31, 97, 110, 206, 137, 141, 23, 95, 142, 47, 220, 113, 63, 24, 70, 147, 48, 240, 51, 39, 222, 195, 5, 157, 53, 162, 92, 180, 57, 215, 84, 164, 196, 121, 244, 67, 216, 204, 167, 200, 82, 35, 65, 182, 36, 8, 64, 60, 172, 103, 26, 83, 103, 222, 133, 109, 104, 47, 172, 131, 111, 99, 195, 182, 199, 19, 4, 64, 108, 196, 20, 128, 100, 108, 19, 3, 176, 140, 245, 195, 60, 250, 33, 60, 30, 55, 76, 203, 183, 160, 169, 123, 27, 154, 87, 93, 11, 173, 177, 77, 236, 225, 17, 8, 105, 73, 43, 0, 201, 140, 190, 255, 42, 17, 128, 44, 32, 45, 210, 8, 114, 66, 214, 181, 0, 82, 68, 163, 51, 98, 197, 234, 203, 177, 98, 245, 229, 0, 22, 90, 164, 157, 126, 243, 49, 56, 102, 204, 48, 45, 219, 130, 198, 149, 87, 144, 22, 105, 4, 73, 64, 4, 160, 200, 212, 53, 116, 161, 174, 161, 11, 27, 46, 190, 105, 161, 69, 218, 249, 119, 112, 224, 221, 231, 72, 139, 52, 130, 232, 100, 61, 5, 152, 26, 217, 15, 85, 8, 146, 104, 10, 50, 109, 30, 65, 152, 117, 160, 205, 164, 21, 123, 40, 5, 17, 93, 106, 180, 140, 245, 99, 218, 54, 2, 99, 75, 47, 154, 87, 125, 2, 77, 221, 87, 195, 216, 186, 161, 240, 95, 64, 32, 100, 32, 107, 1, 224, 185, 57, 56, 198, 142, 73, 162, 19, 143, 190, 74, 141, 38, 35, 5, 154, 86, 23, 126, 50, 137, 192, 243, 44, 166, 204, 167, 99, 241, 3, 63, 199, 161, 169, 251, 42, 82, 183, 64, 40, 42, 89, 11, 128, 80, 36, 47, 53, 54, 182, 172, 70, 107, 231, 6, 180, 116, 108, 128, 161, 166, 69, 236, 235, 33, 25, 230, 92, 83, 176, 77, 124, 20, 105, 177, 62, 126, 18, 213, 166, 110, 52, 175, 254, 4, 26, 86, 92, 137, 250, 229, 91, 197, 30, 30, 161, 76, 40, 185, 0, 196, 195, 249, 103, 49, 125, 230, 0, 172, 131, 111, 193, 54, 180, 7, 84, 152, 71, 107, 199, 6, 180, 117, 109, 68, 115, 251, 186, 178, 122, 194, 23, 202, 148, 109, 24, 230, 115, 125, 48, 143, 126, 136, 57, 247, 52, 76, 43, 34, 129, 196, 166, 158, 109, 208, 213, 116, 136, 61, 60, 130, 76, 17, 85, 0, 146, 113, 218, 62, 194, 212, 200, 126, 216, 134, 246, 96, 102, 244, 48, 234, 234, 151, 161, 165, 115, 3, 90, 59, 122, 81, 215, 208, 37, 246, 240, 36, 67, 116, 169, 209, 58, 118, 2, 214, 241, 147, 80, 86, 53, 160, 113, 197, 21, 164, 110, 129, 144, 51, 146, 18, 128, 120, 120, 110, 14, 211, 231, 14, 98, 106, 228, 93, 88, 135, 246, 32, 224, 182, 162, 181, 179, 23, 205, 29, 235, 5, 169, 91, 40, 39, 102, 166, 206, 193, 60, 118, 28, 150, 209, 126, 204, 144, 22, 105, 132, 28, 144, 172, 0, 36, 227, 157, 29, 131, 109, 104, 47, 108, 195, 123, 97, 63, 179, 31, 213, 250, 250, 72, 154, 114, 215, 70, 52, 52, 117, 23, 254, 11, 202, 132, 133, 22, 105, 253, 176, 140, 245, 147, 22, 105, 132, 37, 145, 141, 0, 36, 51, 125, 246, 32, 166, 206, 252, 17, 214, 211, 111, 97, 206, 62, 140, 150, 246, 117, 104, 110, 95, 143, 214, 206, 94, 84, 27, 26, 196, 30, 158, 100, 136, 182, 72, 51, 143, 246, 99, 210, 114, 154, 212, 45, 16, 18, 144, 173, 0, 196, 195, 122, 166, 49, 57, 188, 15, 182, 161, 61, 176, 13, 239, 131, 134, 97, 98, 37, 206, 13, 173, 171, 73, 48, 113, 158, 248, 22, 105, 150, 209, 227, 164, 69, 26, 161, 60, 4, 32, 25, 167, 185, 31, 182, 225, 119, 96, 29, 124, 11, 78, 203, 113, 212, 55, 173, 68, 75, 199, 6, 180, 119, 109, 34, 75, 141, 113, 196, 90, 164, 141, 246, 195, 58, 113, 146, 180, 72, 171, 64, 202, 82, 0, 226, 225, 185, 57, 76, 14, 255, 17, 182, 225, 189, 176, 13, 238, 133, 34, 228, 71, 107, 199, 6, 180, 118, 246, 162, 165, 115, 3, 241, 14, 226, 136, 182, 72, 51, 159, 239, 35, 45, 210, 42, 132, 178, 23, 128, 100, 220, 147, 131, 17, 49, 152, 95, 106, 172, 53, 117, 160, 165, 99, 3, 218, 150, 109, 36, 75, 141, 113, 68, 91, 164, 153, 199, 142, 195, 114, 254, 120, 66, 139, 52, 125, 195, 42, 208, 76, 21, 153, 50, 148, 1, 21, 39, 0, 241, 68, 211, 155, 173, 131, 111, 193, 58, 180, 7, 156, 219, 130, 150, 121, 239, 128, 44, 53, 38, 50, 51, 117, 14, 214, 137, 143, 96, 62, 255, 33, 188, 115, 118, 176, 172, 7, 1, 206, 15, 5, 165, 4, 173, 214, 67, 165, 174, 134, 74, 173, 135, 82, 93, 13, 70, 87, 3, 90, 165, 131, 74, 99, 4, 163, 53, 66, 169, 210, 65, 165, 53, 130, 102, 170, 160, 210, 26, 64, 171, 170, 161, 82, 87, 131, 214, 26, 136, 144, 136, 76, 69, 11, 64, 50, 62, 231, 4, 172, 131, 111, 71, 150, 26, 207, 30, 202, 185, 69, 90, 37, 194, 243, 44, 120, 206, 143, 0, 239, 71, 128, 245, 33, 192, 249, 16, 8, 248, 192, 177, 94, 4, 56, 47, 2, 129, 200, 235, 28, 235, 137, 252, 204, 249, 16, 224, 188, 224, 3, 126, 176, 108, 228, 239, 69, 66, 162, 49, 66, 165, 49, 128, 214, 84, 167, 20, 18, 149, 198, 0, 90, 93, 5, 149, 218, 0, 90, 163, 135, 74, 173, 7, 173, 209, 147, 37, 206, 60, 32, 2, 176, 4, 75, 181, 72, 35, 75, 141, 194, 146, 74, 72, 22, 68, 195, 11, 142, 245, 130, 15, 176, 139, 132, 36, 192, 122, 193, 5, 252, 224, 3, 126, 112, 172, 7, 180, 74, 3, 74, 85, 181, 72, 72, 84, 234, 136, 183, 65, 132, 36, 17, 34, 0, 89, 18, 223, 34, 109, 114, 100, 31, 84, 138, 48, 90, 58, 54, 160, 165, 115, 3, 169, 91, 144, 16, 81, 33, 225, 184, 136, 96, 68, 133, 132, 99, 61, 224, 121, 118, 222, 51, 137, 122, 42, 139, 133, 132, 99, 61, 224, 3, 126, 208, 42, 13, 104, 141, 17, 180, 90, 63, 239, 133, 44, 8, 9, 163, 173, 1, 205, 232, 202, 66, 72, 136, 0, 228, 137, 211, 246, 17, 108, 167, 223, 134, 117, 104, 15, 156, 230, 62, 152, 26, 150, 163, 117, 217, 199, 208, 220, 182, 150, 4, 19, 203, 128, 120, 33, 97, 89, 15, 120, 214, 11, 158, 231, 82, 10, 9, 199, 121, 192, 7, 184, 140, 66, 162, 154, 23, 7, 37, 163, 75, 16, 18, 149, 182, 6, 74, 149, 54, 38, 36, 106, 77, 29, 40, 181, 22, 140, 198, 56, 31, 59, 41, 222, 114, 44, 17, 0, 1, 136, 214, 45, 216, 6, 247, 194, 54, 248, 22, 66, 172, 11, 45, 203, 122, 209, 218, 209, 139, 166, 214, 11, 72, 48, 177, 130, 89, 74, 72, 88, 214, 131, 32, 207, 45, 8, 9, 235, 141, 136, 9, 231, 3, 199, 249, 22, 4, 39, 224, 7, 205, 84, 129, 86, 87, 47, 18, 18, 70, 91, 27, 241, 56, 210, 8, 9, 173, 209, 71, 188, 147, 52, 66, 66, 4, 160, 8, 120, 28, 231, 35, 193, 196, 193, 61, 176, 159, 63, 132, 154, 154, 38, 180, 46, 219, 136, 230, 142, 245, 164, 110, 129, 144, 23, 169, 132, 132, 227, 124, 243, 193, 212, 136, 144, 112, 172, 7, 1, 214, 139, 0, 239, 143, 196, 76, 230, 133, 36, 242, 26, 155, 82, 72, 136, 0, 20, 153, 232, 82, 163, 109, 100, 31, 108, 131, 111, 195, 239, 28, 67, 115, 219, 58, 180, 118, 70, 114, 15, 136, 119, 64, 40, 37, 60, 207, 194, 239, 117, 33, 20, 226, 193, 178, 30, 34, 0, 165, 198, 231, 156, 192, 244, 185, 67, 145, 186, 133, 193, 61, 208, 105, 171, 208, 210, 217, 139, 150, 246, 117, 164, 110, 129, 80, 114, 136, 0, 136, 12, 105, 145, 70, 16, 19, 34, 0, 18, 130, 243, 207, 98, 114, 232, 29, 76, 142, 252, 17, 182, 161, 61, 100, 169, 145, 80, 116, 136, 0, 72, 24, 210, 34, 141, 80, 108, 136, 0, 200, 132, 132, 165, 198, 161, 61, 8, 249, 103, 209, 220, 190, 142, 180, 72, 35, 20, 4, 17, 0, 153, 226, 113, 156, 143, 52, 65, 33, 45, 210, 8, 5, 64, 4, 160, 76, 152, 62, 123, 16, 214, 161, 183, 49, 53, 188, 47, 214, 34, 173, 109, 217, 70, 52, 181, 173, 37, 117, 11, 132, 180, 16, 1, 40, 67, 72, 139, 52, 66, 182, 16, 1, 168, 0, 72, 139, 52, 66, 58, 136, 0, 84, 24, 164, 69, 26, 33, 30, 34, 0, 21, 78, 66, 139, 180, 177, 15, 80, 91, 215, 74, 90, 164, 85, 16, 68, 0, 8, 49, 146, 91, 164, 5, 220, 86, 52, 119, 172, 71, 93, 67, 23, 12, 53, 205, 208, 85, 213, 129, 86, 169, 193, 48, 58, 208, 140, 134, 120, 11, 101, 0, 17, 0, 66, 90, 162, 117, 11, 179, 230, 227, 112, 79, 143, 128, 157, 155, 66, 192, 239, 68, 128, 157, 67, 40, 176, 80, 239, 206, 168, 171, 192, 168, 52, 80, 169, 117, 80, 49, 58, 168, 24, 45, 84, 42, 77, 228, 111, 70, 11, 38, 250, 250, 252, 177, 42, 70, 75, 132, 68, 34, 16, 1, 32, 20, 4, 231, 159, 5, 239, 119, 35, 192, 186, 231, 255, 118, 129, 103, 61, 8, 248, 93, 8, 248, 156, 8, 6, 188, 224, 124, 78, 240, 156, 39, 242, 158, 127, 46, 242, 222, 188, 144, 240, 172, 27, 225, 80, 16, 42, 70, 19, 17, 147, 20, 66, 194, 168, 171, 34, 130, 145, 66, 72, 84, 106, 45, 84, 180, 134, 8, 73, 158, 16, 1, 32, 72, 2, 214, 51, 13, 158, 243, 68, 254, 204, 11, 73, 192, 231, 138, 8, 71, 156, 144, 4, 252, 78, 240, 1, 47, 56, 239, 44, 130, 156, 55, 173, 144, 168, 213, 58, 208, 42, 77, 76, 72, 212, 234, 170, 200, 191, 213, 81, 239, 68, 23, 17, 20, 149, 182, 162, 133, 132, 8, 0, 161, 172, 136, 9, 137, 207, 21, 17, 134, 192, 92, 130, 144, 112, 126, 103, 76, 56, 162, 158, 73, 144, 157, 139, 8, 9, 231, 77, 16, 18, 245, 188, 231, 17, 21, 17, 38, 234, 149, 48, 90, 208, 140, 166, 44, 132, 132, 8, 0, 129, 144, 4, 207, 205, 33, 24, 240, 39, 8, 73, 128, 117, 130, 103, 61, 17, 1, 137, 254, 157, 78, 72, 230, 167, 68, 0, 192, 196, 98, 32, 218, 121, 143, 100, 177, 144, 48, 76, 220, 20, 71, 21, 141, 155, 84, 129, 166, 153, 162, 215, 120, 16, 1, 32, 16, 138, 68, 182, 66, 18, 152, 23, 19, 206, 231, 4, 207, 186, 17, 240, 187, 16, 228, 60, 105, 133, 68, 197, 232, 98, 65, 212, 168, 88, 40, 85, 76, 94, 66, 66, 4, 128, 64, 144, 56, 201, 66, 194, 249, 102, 17, 96, 221, 8, 6, 124, 25, 133, 132, 103, 61, 145, 159, 211, 8, 9, 17, 0, 2, 161, 66, 136, 10, 9, 231, 153, 65, 136, 103, 193, 249, 102, 137, 0, 16, 8, 149, 12, 37, 246, 0, 8, 4, 130, 120, 16, 1, 32, 16, 42, 24, 34, 0, 4, 66, 5, 67, 4, 128, 64, 168, 96, 136, 0, 16, 8, 21, 12, 17, 0, 2, 161, 130, 161, 197, 30, 0, 129, 16, 101, 192, 113, 2, 78, 191, 3, 0, 64, 81, 74, 208, 97, 26, 148, 82, 1, 53, 173, 5, 173, 160, 161, 85, 234, 208, 174, 239, 20, 123, 152, 101, 5, 201, 3, 32, 136, 206, 128, 227, 4, 166, 125, 147, 240, 243, 190, 172, 142, 167, 20, 20, 40, 5, 5, 5, 148, 80, 82, 20, 40, 80, 80, 42, 148, 160, 40, 37, 40, 80, 160, 41, 37, 104, 74, 5, 37, 69, 67, 163, 212, 194, 200, 212, 160, 78, 99, 18, 251, 107, 74, 18, 226, 1, 16, 68, 35, 87, 195, 143, 18, 10, 135, 16, 10, 135, 0, 240, 8, 132, 178, 255, 28, 77, 69, 110, 119, 74, 161, 140, 136, 72, 156, 112, 68, 189, 13, 154, 82, 65, 173, 212, 84, 140, 183, 65, 60, 0, 66, 201, 201, 215, 240, 197, 32, 87, 111, 163, 90, 85, 141, 6, 109, 147, 216, 195, 206, 26, 226, 1, 16, 74, 134, 156, 12, 63, 74, 185, 123, 27, 196, 3, 32, 20, 29, 57, 26, 190, 24, 228, 228, 109, 64, 141, 106, 141, 161, 96, 111, 131, 120, 0, 132, 162, 81, 206, 134, 207, 7, 120, 76, 78, 78, 161, 177, 177, 1, 180, 138, 70, 40, 20, 134, 195, 49, 3, 214, 207, 65, 167, 211, 193, 104, 212, 67, 65, 229, 182, 202, 158, 179, 183, 17, 169, 240, 205, 217, 219, 48, 168, 141, 48, 48, 145, 254, 0, 196, 3, 32, 8, 78, 57, 27, 126, 148, 57, 183, 27, 44, 199, 65, 205, 48, 168, 214, 235, 49, 235, 112, 2, 0, 12, 70, 3, 92, 78, 23, 0, 160, 166, 86, 186, 59, 54, 71, 189, 13, 146, 8, 68, 16, 140, 1, 199, 9, 236, 55, 239, 193, 184, 251, 124, 89, 27, 63, 0, 120, 61, 126, 24, 13, 70, 120, 61, 254, 200, 191, 189, 94, 24, 140, 6, 80, 148, 2, 6, 163, 1, 94, 175, 87, 236, 33, 46, 73, 40, 28, 2, 31, 226, 201, 20, 128, 80, 56, 149, 240, 196, 143, 135, 15, 240, 80, 170, 40, 208, 42, 26, 74, 21, 5, 62, 192, 47, 58, 134, 202, 209, 253, 23, 11, 34, 0, 132, 188, 17, 218, 240, 139, 49, 175, 46, 6, 126, 191, 15, 106, 134, 1, 0, 168, 25, 6, 126, 191, 15, 58, 157, 14, 46, 167, 43, 54, 5, 208, 104, 52, 98, 15, 51, 43, 196, 191, 154, 4, 217, 81, 44, 87, 223, 239, 247, 65, 173, 137, 24, 20, 0, 184, 156, 46, 40, 41, 26, 205, 45, 205, 0, 0, 167, 211, 45, 246, 87, 7, 16, 113, 255, 53, 26, 45, 0, 64, 163, 209, 194, 235, 241, 195, 96, 52, 32, 24, 226, 97, 181, 88, 17, 12, 241, 48, 24, 13, 98, 15, 51, 43, 136, 7, 64, 200, 154, 98, 187, 250, 94, 143, 31, 117, 166, 90, 204, 216, 29, 168, 214, 235, 225, 245, 122, 209, 220, 210, 28, 155, 87, 91, 45, 86, 73, 4, 214, 248, 96, 196, 83, 137, 135, 162, 20, 48, 153, 76, 48, 79, 88, 96, 50, 201, 39, 237, 152, 8, 0, 33, 35, 165, 152, 227, 203, 105, 94, 221, 218, 214, 2, 0, 48, 79, 88, 98, 63, 203, 21, 34, 0, 132, 180, 148, 50, 184, 87, 78, 243, 106, 57, 65, 4, 128, 176, 8, 49, 162, 250, 81, 247, 31, 136, 204, 171, 103, 236, 14, 212, 55, 214, 195, 225, 152, 129, 213, 98, 133, 90, 195, 160, 182, 182, 78, 236, 75, 83, 118, 144, 68, 32, 66, 12, 49, 151, 243, 204, 19, 150, 69, 175, 149, 147, 171, 45, 4, 233, 86, 69, 146, 87, 79, 114, 129, 120, 0, 4, 73, 172, 227, 19, 99, 207, 204, 194, 170, 72, 29, 92, 78, 23, 156, 78, 55, 106, 106, 141, 9, 171, 39, 213, 42, 125, 78, 231, 148, 70, 84, 133, 32, 10, 149, 148, 185, 87, 14, 164, 203, 54, 76, 206, 74, 204, 5, 226, 1, 84, 32, 82, 120, 226, 19, 10, 135, 154, 119, 255, 147, 87, 79, 114, 153, 6, 144, 24, 64, 5, 65, 12, 95, 222, 164, 42, 56, 162, 233, 136, 19, 95, 173, 215, 99, 206, 237, 142, 253, 156, 45, 68, 0, 42, 0, 98, 248, 229, 65, 124, 16, 48, 186, 42, 50, 61, 57, 141, 58, 83, 45, 104, 21, 13, 62, 192, 99, 198, 238, 64, 99, 115, 67, 194, 231, 194, 161, 16, 154, 221, 141, 9, 175, 241, 170, 32, 0, 50, 5, 40, 107, 136, 225, 151, 23, 169, 178, 13, 83, 101, 37, 46, 194, 27, 68, 107, 221, 10, 212, 183, 45, 8, 3, 207, 205, 97, 206, 21, 34, 2, 80, 142, 16, 195, 175, 28, 178, 90, 61, 209, 41, 17, 240, 37, 222, 11, 52, 83, 141, 154, 122, 226, 1, 148, 21, 196, 240, 9, 169, 80, 80, 20, 248, 32, 159, 242, 61, 34, 0, 101, 0, 49, 124, 66, 190, 16, 1, 144, 57, 135, 44, 251, 225, 14, 184, 196, 30, 6, 161, 132, 164, 114, 245, 51, 37, 79, 133, 66, 169, 61, 0, 146, 8, 36, 115, 124, 65, 105, 183, 158, 34, 72, 27, 34, 0, 50, 102, 220, 61, 10, 62, 141, 178, 19, 8, 217, 64, 4, 64, 198, 204, 114, 51, 98, 15, 129, 32, 115, 136, 0, 200, 24, 63, 159, 123, 238, 55, 161, 50, 225, 130, 108, 202, 215, 137, 0, 200, 24, 54, 72, 4, 32, 19, 161, 80, 24, 118, 187, 29, 230, 9, 11, 102, 29, 78, 132, 67, 145, 29, 55, 248, 0, 15, 243, 132, 37, 101, 231, 161, 74, 130, 8, 128, 140, 153, 117, 146, 27, 58, 19, 233, 26, 139, 38, 55, 32, 173, 84, 136, 0, 200, 24, 46, 228, 35, 55, 116, 6, 138, 81, 66, 43, 71, 248, 112, 32, 229, 235, 68, 0, 100, 202, 89, 215, 8, 116, 85, 213, 21, 123, 67, 231, 75, 186, 18, 218, 114, 39, 0, 34, 0, 101, 133, 39, 16, 121, 242, 87, 234, 13, 157, 45, 209, 198, 162, 161, 80, 56, 214, 88, 52, 85, 3, 210, 74, 133, 8, 128, 76, 241, 242, 94, 114, 67, 103, 65, 170, 13, 59, 82, 109, 236, 81, 238, 240, 170, 32, 102, 167, 23, 103, 140, 18, 1, 144, 41, 211, 147, 230, 138, 190, 161, 179, 37, 90, 66, 11, 0, 38, 147, 9, 20, 165, 136, 149, 208, 154, 39, 44, 152, 156, 156, 74, 91, 40, 83, 78, 76, 135, 38, 83, 190, 78, 106, 1, 100, 138, 39, 196, 229, 86, 19, 94, 36, 138, 209, 169, 182, 216, 84, 100, 3, 82, 154, 2, 207, 46, 206, 5, 32, 30, 128, 76, 241, 251, 23, 158, 242, 173, 109, 45, 177, 27, 57, 254, 231, 82, 64, 150, 217, 100, 2, 163, 128, 223, 51, 183, 232, 101, 34, 0, 50, 100, 200, 121, 26, 161, 249, 245, 127, 177, 33, 203, 108, 242, 64, 65, 81, 8, 135, 23, 119, 255, 35, 2, 32, 67, 60, 156, 116, 203, 127, 165, 186, 42, 145, 79, 9, 109, 33, 72, 49, 3, 49, 85, 73, 48, 17, 0, 25, 194, 133, 2, 37, 191, 161, 211, 65, 150, 217, 82, 35, 151, 169, 17, 17, 0, 25, 34, 165, 26, 0, 178, 204, 150, 26, 185, 76, 141, 164, 21, 158, 37, 100, 5, 31, 10, 20, 126, 18, 129, 200, 187, 83, 109, 133, 33, 196, 38, 30, 197, 128, 8, 128, 204, 152, 241, 219, 37, 223, 4, 164, 34, 151, 217, 146, 72, 181, 181, 121, 170, 169, 81, 174, 123, 249, 21, 66, 170, 146, 96, 50, 5, 144, 25, 54, 159, 165, 240, 147, 16, 138, 142, 92, 166, 70, 196, 3, 144, 25, 44, 233, 252, 43, 11, 228, 50, 53, 34, 2, 32, 51, 164, 218, 5, 72, 42, 171, 18, 82, 70, 236, 169, 81, 170, 146, 96, 50, 5, 144, 25, 129, 176, 116, 2, 128, 4, 121, 145, 170, 36, 152, 8, 128, 204, 144, 210, 10, 0, 65, 254, 16, 1, 144, 17, 22, 239, 132, 228, 87, 0, 8, 137, 72, 105, 106, 196, 171, 130, 224, 185, 196, 122, 0, 34, 0, 50, 194, 230, 49, 139, 61, 4, 130, 140, 9, 168, 188, 152, 115, 37, 214, 144, 144, 32, 160, 76, 56, 106, 61, 12, 59, 39, 173, 8, 50, 65, 94, 56, 217, 185, 69, 37, 193, 68, 0, 100, 192, 97, 219, 1, 56, 57, 135, 216, 195, 32, 72, 156, 76, 189, 25, 26, 76, 245, 139, 183, 9, 23, 123, 208, 132, 165, 169, 228, 205, 63, 127, 254, 244, 175, 48, 126, 102, 233, 105, 79, 199, 202, 54, 108, 127, 244, 127, 136, 61, 84, 73, 176, 80, 128, 84, 7, 151, 211, 5, 167, 211, 141, 154, 90, 99, 172, 0, 137, 13, 248, 193, 211, 137, 49, 36, 34, 0, 18, 197, 197, 57, 241, 145, 253, 120, 197, 26, 127, 40, 20, 198, 96, 255, 8, 206, 14, 140, 46, 121, 92, 128, 35, 65, 209, 40, 94, 175, 23, 205, 45, 205, 177, 2, 36, 171, 197, 138, 154, 218, 72, 225, 81, 157, 169, 22, 51, 118, 7, 66, 122, 34, 0, 146, 103, 198, 111, 199, 192, 76, 63, 188, 188, 71, 236, 161, 136, 134, 203, 233, 130, 66, 161, 0, 0, 236, 220, 185, 19, 91, 182, 108, 73, 120, 255, 224, 193, 131, 120, 244, 209, 71, 197, 30, 166, 164, 73, 85, 128, 148, 12, 17, 0, 137, 97, 241, 78, 96, 120, 246, 52, 252, 21, 158, 242, 235, 245, 122, 161, 84, 42, 1, 0, 87, 95, 125, 245, 34, 1, 80, 171, 213, 98, 15, 81, 114, 100, 83, 128, 148, 92, 16, 68, 4, 64, 66, 140, 187, 71, 49, 226, 26, 76, 187, 145, 35, 129, 176, 20, 6, 163, 1, 14, 199, 12, 172, 22, 43, 212, 26, 6, 181, 181, 117, 152, 158, 156, 70, 157, 169, 22, 64, 164, 0, 105, 118, 102, 22, 227, 131, 103, 65, 205, 183, 7, 19, 77, 0, 44, 222, 9, 76, 184, 199, 176, 194, 216, 131, 58, 141, 73, 236, 107, 39, 58, 103, 93, 35, 56, 231, 26, 38, 137, 62, 243, 232, 116, 58, 4, 131, 65, 177, 135, 33, 43, 178, 41, 64, 82, 53, 48, 216, 220, 189, 60, 246, 111, 81, 4, 96, 198, 111, 143, 185, 185, 31, 78, 59, 209, 92, 213, 134, 53, 181, 235, 69, 190, 124, 226, 49, 228, 60, 141, 113, 247, 57, 98, 252, 113, 24, 140, 134, 148, 77, 44, 9, 185, 145, 92, 128, 100, 80, 27, 19, 222, 23, 69, 0, 6, 102, 250, 99, 115, 92, 62, 196, 99, 220, 125, 30, 78, 191, 3, 203, 140, 43, 208, 162, 107, 19, 249, 146, 149, 248, 90, 56, 78, 192, 234, 33, 41, 190, 201, 80, 148, 2, 52, 77, 102, 168, 197, 166, 228, 169, 192, 135, 44, 251, 83, 70, 183, 221, 1, 23, 78, 205, 156, 192, 9, 123, 159, 216, 215, 164, 100, 12, 56, 78, 192, 60, 55, 86, 144, 241, 75, 177, 251, 44, 65, 62, 148, 84, 98, 15, 219, 14, 44, 185, 174, 205, 135, 120, 88, 60, 19, 112, 176, 51, 104, 215, 118, 96, 121, 109, 143, 216, 215, 167, 104, 156, 176, 247, 193, 230, 181, 32, 20, 46, 172, 191, 127, 166, 228, 143, 82, 183, 157, 34, 136, 207, 82, 5, 72, 20, 40, 88, 92, 67, 177, 215, 75, 230, 1, 252, 201, 118, 8, 78, 54, 187, 116, 86, 235, 148, 21, 125, 230, 15, 208, 55, 117, 164, 84, 195, 43, 41, 125, 83, 71, 96, 241, 76, 20, 108, 252, 128, 124, 186, 207, 18, 164, 129, 46, 204, 193, 225, 181, 196, 254, 148, 68, 0, 250, 166, 142, 192, 193, 218, 179, 62, 222, 235, 245, 66, 87, 85, 141, 41, 223, 36, 222, 25, 127, 11, 3, 142, 19, 162, 93, 48, 161, 57, 106, 61, 140, 41, 223, 100, 225, 39, 74, 131, 84, 55, 230, 32, 72, 147, 162, 11, 192, 9, 123, 95, 65, 55, 60, 203, 251, 113, 116, 232, 79, 56, 100, 217, 143, 25, 127, 246, 34, 34, 69, 14, 219, 14, 8, 94, 209, 71, 54, 230, 32, 20, 66, 81, 5, 96, 192, 113, 2, 22, 207, 68, 206, 159, 75, 190, 169, 25, 53, 3, 203, 204, 4, 14, 141, 238, 151, 173, 55, 112, 200, 178, 63, 235, 41, 80, 46, 200, 165, 251, 44, 65, 154, 20, 45, 8, 24, 141, 112, 231, 67, 186, 140, 38, 141, 73, 139, 113, 247, 121, 204, 248, 166, 177, 162, 166, 71, 22, 75, 134, 197, 46, 234, 145, 75, 247, 89, 130, 52, 41, 138, 0, 12, 57, 79, 195, 60, 55, 150, 119, 144, 43, 155, 155, 122, 204, 54, 138, 245, 157, 189, 88, 111, 218, 88, 250, 171, 150, 37, 98, 21, 245, 136, 221, 125, 182, 156, 200, 84, 99, 223, 216, 216, 32, 250, 238, 62, 133, 32, 248, 20, 224, 172, 107, 4, 227, 238, 115, 130, 68, 184, 227, 137, 223, 247, 190, 181, 173, 5, 38, 147, 9, 22, 207, 4, 222, 51, 191, 131, 113, 247, 104, 129, 103, 23, 30, 139, 119, 2, 39, 103, 62, 172, 232, 138, 190, 114, 64, 46, 155, 124, 230, 139, 160, 2, 48, 238, 30, 45, 121, 62, 187, 151, 247, 224, 244, 236, 73, 73, 45, 25, 142, 187, 71, 49, 232, 24, 168, 248, 138, 190, 114, 160, 220, 151, 89, 5, 19, 128, 41, 159, 13, 35, 174, 65, 81, 82, 90, 67, 225, 16, 166, 124, 147, 216, 111, 222, 131, 179, 174, 145, 146, 255, 254, 120, 206, 186, 70, 48, 228, 28, 40, 121, 69, 159, 148, 186, 207, 150, 51, 229, 182, 204, 42, 136, 0, 204, 248, 237, 56, 229, 56, 41, 248, 77, 159, 235, 77, 237, 231, 125, 56, 227, 28, 196, 159, 108, 135, 4, 190, 76, 217, 49, 228, 60, 77, 42, 250, 202, 140, 114, 95, 102, 21, 68, 0, 226, 139, 123, 196, 38, 20, 14, 193, 193, 218, 177, 111, 226, 45, 12, 205, 158, 42, 217, 239, 29, 112, 156, 192, 168, 235, 12, 49, 254, 50, 163, 220, 151, 89, 11, 14, 95, 166, 43, 238, 17, 27, 46, 200, 226, 156, 107, 4, 124, 152, 47, 122, 169, 113, 223, 212, 17, 156, 60, 123, 18, 140, 154, 41, 187, 40, 113, 165, 83, 238, 203, 172, 5, 121, 0, 153, 138, 123, 42, 129, 163, 214, 195, 56, 59, 57, 130, 250, 198, 122, 0, 229, 23, 37, 38, 44, 38, 121, 69, 74, 206, 177, 150, 188, 5, 32, 151, 226, 30, 49, 169, 215, 52, 20, 237, 220, 125, 83, 71, 112, 242, 252, 9, 232, 170, 170, 203, 54, 74, 76, 40, 111, 242, 18, 128, 92, 139, 123, 196, 130, 166, 104, 52, 104, 155, 138, 114, 238, 104, 81, 79, 40, 148, 152, 239, 80, 110, 81, 98, 66, 121, 147, 151, 0, 84, 49, 6, 168, 40, 233, 119, 101, 165, 20, 202, 162, 156, 55, 190, 168, 167, 220, 163, 196, 132, 8, 197, 94, 102, 21, 171, 177, 75, 94, 2, 208, 99, 92, 141, 222, 250, 77, 208, 209, 85, 146, 238, 58, 67, 43, 132, 15, 190, 37, 23, 245, 148, 123, 148, 56, 27, 146, 111, 210, 116, 55, 51, 33, 61, 98, 101, 28, 230, 29, 3, 168, 211, 152, 240, 241, 214, 171, 177, 97, 121, 47, 66, 156, 8, 87, 44, 11, 84, 74, 70, 176, 115, 185, 56, 103, 202, 109, 186, 162, 81, 98, 0, 48, 153, 76, 160, 40, 69, 44, 74, 108, 158, 176, 96, 114, 114, 10, 124, 80, 154, 2, 41, 20, 201, 55, 105, 186, 155, 153, 144, 30, 177, 50, 14, 11, 206, 3, 184, 176, 249, 18, 172, 109, 91, 15, 29, 93, 85, 218, 43, 150, 5, 12, 165, 18, 228, 60, 51, 126, 59, 250, 167, 143, 101, 189, 226, 81, 78, 81, 226, 108, 72, 190, 73, 211, 221, 204, 132, 236, 41, 85, 44, 73, 144, 68, 160, 229, 134, 149, 248, 120, 235, 213, 104, 208, 54, 130, 82, 148, 188, 207, 104, 90, 170, 4, 232, 133, 71, 138, 122, 150, 38, 155, 155, 148, 162, 164, 115, 79, 72, 21, 177, 98, 73, 130, 254, 207, 108, 108, 216, 140, 78, 195, 10, 201, 4, 8, 77, 2, 44, 1, 218, 125, 83, 146, 201, 114, 148, 34, 169, 110, 210, 84, 55, 51, 97, 105, 196, 138, 37, 9, 46, 205, 209, 0, 161, 94, 101, 40, 254, 85, 91, 2, 154, 162, 5, 217, 113, 200, 207, 103, 119, 209, 43, 181, 24, 39, 213, 77, 154, 234, 102, 38, 44, 141, 88, 177, 164, 162, 228, 168, 214, 105, 76, 216, 210, 114, 197, 124, 63, 64, 91, 73, 243, 227, 163, 13, 28, 148, 20, 13, 180, 23, 126, 62, 62, 20, 40, 217, 216, 229, 72, 170, 180, 216, 84, 233, 179, 132, 220, 41, 69, 99, 151, 162, 38, 169, 175, 55, 109, 196, 184, 123, 20, 231, 221, 103, 74, 54, 135, 142, 70, 160, 91, 27, 90, 5, 57, 95, 32, 76, 4, 96, 41, 72, 247, 33, 121, 83, 244, 232, 76, 187, 190, 179, 164, 1, 194, 104, 4, 90, 168, 37, 192, 32, 89, 195, 38, 148, 49, 37, 11, 207, 150, 58, 64, 168, 163, 117, 130, 156, 39, 72, 60, 0, 66, 9, 41, 117, 44, 169, 164, 235, 51, 165, 8, 16, 70, 35, 208, 106, 101, 225, 145, 103, 139, 87, 152, 221, 123, 42, 1, 177, 130, 160, 28, 199, 145, 61, 17, 11, 160, 228, 11, 180, 209, 0, 97, 75, 85, 27, 104, 74, 248, 16, 68, 52, 2, 45, 196, 18, 224, 172, 12, 170, 29, 43, 29, 134, 137, 36, 123, 145, 50, 236, 252, 16, 45, 67, 99, 189, 105, 35, 122, 140, 107, 4, 207, 32, 164, 40, 5, 154, 26, 154, 4, 89, 2, 100, 201, 250, 191, 12, 32, 101, 216, 133, 32, 106, 138, 86, 177, 2, 132, 180, 64, 41, 192, 28, 89, 2, 148, 13, 164, 12, 59, 63, 36, 145, 163, 41, 116, 128, 80, 165, 16, 70, 0, 66, 161, 160, 152, 151, 37, 143, 241, 138, 83, 82, 42, 46, 164, 12, 187, 16, 36, 33, 0, 128, 176, 1, 66, 161, 60, 0, 185, 229, 0, 148, 251, 38, 22, 169, 224, 184, 64, 197, 150, 97, 11, 129, 100, 4, 0, 16, 46, 64, 168, 161, 133, 201, 61, 151, 91, 22, 160, 216, 155, 88, 240, 1, 30, 167, 250, 134, 75, 254, 189, 133, 74, 157, 61, 117, 108, 168, 76, 189, 164, 244, 72, 74, 0, 162, 20, 26, 32, 84, 211, 90, 65, 198, 33, 247, 22, 223, 165, 156, 23, 7, 184, 0, 190, 115, 223, 15, 241, 248, 157, 223, 195, 177, 247, 250, 69, 249, 190, 133, 148, 97, 31, 123, 175, 31, 143, 111, 255, 62, 190, 115, 223, 15, 43, 74, 4, 36, 41, 0, 64, 97, 1, 194, 38, 109, 115, 193, 191, 95, 236, 29, 134, 242, 65, 172, 146, 210, 0, 23, 192, 83, 247, 62, 139, 15, 15, 158, 4, 31, 224, 241, 204, 95, 63, 39, 154, 8, 228, 195, 177, 247, 250, 241, 204, 95, 63, 7, 62, 192, 227, 195, 131, 39, 241, 157, 251, 126, 136, 0, 39, 47, 239, 47, 95, 36, 43, 0, 81, 114, 13, 16, 210, 20, 13, 3, 99, 44, 248, 247, 122, 2, 242, 235, 98, 35, 70, 73, 41, 235, 231, 240, 212, 189, 207, 226, 228, 145, 83, 88, 182, 108, 25, 190, 254, 245, 175, 203, 74, 4, 226, 141, 223, 100, 50, 129, 97, 24, 124, 120, 240, 36, 158, 186, 247, 217, 138, 16, 1, 201, 11, 0, 144, 91, 128, 80, 168, 0, 160, 220, 230, 255, 64, 233, 75, 74, 89, 63, 135, 39, 239, 222, 133, 147, 71, 78, 161, 189, 189, 29, 239, 190, 251, 46, 118, 237, 218, 133, 103, 159, 125, 54, 38, 2, 253, 135, 7, 138, 250, 157, 25, 102, 113, 205, 71, 182, 174, 127, 188, 241, 223, 124, 243, 205, 176, 217, 108, 216, 183, 111, 31, 170, 170, 170, 112, 242, 200, 41, 60, 117, 239, 179, 96, 253, 18, 237, 119, 39, 16, 178, 16, 0, 32, 251, 0, 161, 80, 75, 128, 229, 146, 3, 80, 172, 246, 100, 62, 175, 31, 79, 222, 189, 11, 167, 251, 134, 209, 222, 222, 142, 253, 251, 247, 163, 189, 61, 82, 127, 253, 192, 3, 15, 196, 68, 224, 233, 175, 254, 160, 232, 34, 144, 15, 241, 198, 127, 235, 173, 183, 226, 213, 87, 95, 133, 82, 169, 196, 150, 45, 91, 176, 111, 223, 62, 24, 12, 6, 156, 60, 114, 10, 79, 222, 189, 171, 172, 69, 64, 54, 2, 16, 37, 83, 128, 80, 176, 37, 192, 96, 249, 254, 167, 23, 138, 207, 235, 199, 19, 219, 191, 159, 96, 252, 93, 93, 93, 9, 199, 68, 69, 32, 192, 5, 36, 39, 2, 253, 135, 7, 18, 140, 255, 165, 151, 94, 130, 82, 185, 208, 66, 254, 162, 139, 46, 194, 254, 253, 251, 81, 83, 83, 131, 211, 125, 195, 120, 242, 238, 93, 240, 121, 203, 115, 73, 81, 118, 2, 0, 44, 29, 32, 20, 108, 9, 48, 92, 57, 145, 224, 92, 57, 221, 55, 140, 225, 19, 103, 1, 0, 59, 118, 236, 88, 100, 252, 81, 164, 40, 2, 253, 135, 7, 240, 244, 87, 127, 144, 96, 252, 10, 133, 98, 209, 113, 189, 189, 189, 120, 244, 209, 71, 99, 223, 119, 232, 248, 25, 177, 135, 94, 20, 100, 41, 0, 81, 162, 1, 66, 70, 185, 16, 32, 20, 162, 17, 40, 0, 132, 194, 242, 202, 2, 140, 167, 216, 149, 121, 27, 47, 91, 143, 135, 118, 221, 11, 74, 73, 225, 129, 7, 30, 192, 27, 111, 188, 145, 246, 88, 41, 137, 64, 212, 248, 3, 92, 96, 73, 227, 7, 128, 215, 94, 123, 13, 143, 62, 250, 40, 40, 37, 133, 135, 118, 221, 139, 222, 45, 107, 83, 30, 39, 247, 61, 17, 100, 45, 0, 64, 36, 64, 184, 193, 20, 9, 16, 82, 10, 10, 203, 13, 43, 11, 62, 167, 139, 115, 22, 156, 3, 32, 247, 27, 35, 19, 91, 175, 187, 24, 127, 243, 221, 191, 66, 40, 28, 194, 77, 55, 221, 36, 121, 17, 200, 213, 248, 111, 185, 229, 22, 132, 194, 33, 252, 205, 119, 255, 10, 91, 175, 187, 56, 237, 121, 229, 190, 39, 130, 236, 5, 0, 88, 8, 16, 118, 234, 151, 11, 114, 62, 155, 207, 154, 242, 117, 69, 128, 135, 225, 131, 97, 180, 253, 234, 109, 172, 248, 238, 43, 184, 224, 161, 95, 96, 245, 55, 127, 137, 21, 207, 188, 130, 182, 95, 189, 13, 195, 7, 195, 80, 204, 27, 188, 220, 111, 140, 108, 136, 138, 64, 48, 24, 196, 77, 55, 221, 132, 215, 94, 123, 45, 237, 177, 15, 60, 240, 0, 118, 238, 220, 41, 138, 8, 196, 27, 255, 237, 183, 223, 158, 149, 241, 135, 17, 206, 104, 252, 128, 252, 247, 68, 40, 11, 1, 136, 210, 83, 115, 129, 32, 231, 73, 85, 6, 92, 253, 209, 40, 86, 62, 253, 47, 104, 255, 167, 63, 192, 120, 100, 16, 154, 9, 59, 40, 142, 135, 210, 23, 128, 198, 108, 135, 241, 200, 32, 218, 255, 233, 15, 88, 249, 244, 191, 160, 250, 163, 81, 217, 223, 24, 217, 178, 245, 186, 139, 113, 255, 211, 119, 34, 24, 12, 226, 150, 91, 110, 89, 82, 4, 118, 236, 216, 145, 32, 2, 3, 199, 6, 139, 62, 190, 100, 227, 127, 225, 133, 23, 4, 51, 254, 114, 216, 19, 65, 218, 163, 19, 137, 228, 86, 224, 245, 111, 126, 128, 206, 159, 190, 1, 102, 102, 46, 227, 103, 153, 153, 57, 116, 254, 244, 13, 44, 63, 120, 74, 214, 55, 70, 46, 92, 121, 227, 86, 220, 255, 244, 157, 8, 133, 66, 57, 137, 192, 83, 247, 60, 91, 84, 17, 200, 197, 248, 95, 126, 249, 229, 156, 140, 31, 40, 143, 61, 17, 202, 231, 46, 20, 144, 248, 36, 160, 250, 55, 63, 64, 227, 27, 135, 115, 62, 199, 178, 189, 39, 80, 255, 230, 7, 178, 189, 49, 114, 37, 31, 17, 96, 125, 108, 209, 68, 96, 224, 216, 96, 78, 198, 127, 219, 109, 183, 229, 100, 252, 64, 121, 236, 137, 64, 191, 49, 240, 107, 180, 212, 182, 160, 86, 103, 18, 36, 128, 38, 22, 238, 57, 15, 94, 248, 231, 215, 240, 238, 193, 163, 176, 216, 166, 10, 63, 33, 128, 13, 138, 0, 30, 81, 185, 129, 52, 55, 78, 38, 26, 223, 56, 140, 185, 118, 19, 70, 77, 85, 168, 111, 172, 135, 195, 49, 3, 171, 197, 10, 181, 134, 65, 109, 109, 157, 216, 151, 76, 112, 174, 188, 113, 43, 0, 224, 71, 143, 253, 18, 183, 220, 114, 11, 94, 124, 241, 69, 220, 122, 235, 173, 41, 143, 221, 177, 99, 7, 0, 224, 145, 71, 30, 193, 83, 247, 60, 139, 191, 253, 201, 131, 88, 179, 105, 149, 32, 227, 24, 56, 54, 136, 167, 238, 137, 4, 29, 183, 111, 223, 142, 159, 253, 236, 103, 25, 141, 95, 65, 41, 114, 50, 126, 64, 62, 123, 34, 68, 247, 202, 96, 253, 28, 140, 29, 106, 84, 211, 11, 217, 147, 148, 219, 239, 196, 121, 251, 25, 12, 207, 158, 194, 222, 241, 55, 241, 158, 249, 29, 28, 181, 30, 198, 144, 243, 180, 216, 227, 206, 137, 127, 124, 241, 63, 240, 234, 235, 111, 46, 105, 252, 52, 157, 125, 137, 49, 131, 48, 238, 86, 121, 210, 222, 56, 217, 210, 250, 210, 59, 8, 249, 217, 148, 105, 186, 229, 72, 188, 39, 112, 219, 109, 183, 225, 229, 151, 95, 78, 123, 108, 49, 60, 129, 168, 241, 179, 62, 22, 219, 183, 111, 199, 207, 127, 254, 243, 140, 198, 15, 32, 103, 227, 7, 228, 179, 9, 108, 114, 0, 58, 30, 218, 104, 48, 98, 198, 238, 64, 181, 94, 15, 62, 196, 131, 15, 241, 240, 194, 3, 59, 55, 133, 113, 247, 57, 208, 148, 10, 90, 165, 14, 85, 76, 53, 214, 212, 174, 23, 251, 187, 164, 229, 205, 183, 222, 77, 255, 166, 174, 26, 45, 219, 191, 129, 150, 143, 93, 136, 41, 127, 16, 142, 131, 123, 225, 126, 233, 121, 40, 150, 200, 139, 191, 68, 193, 193, 164, 8, 167, 125, 127, 142, 15, 194, 50, 95, 44, 210, 194, 168, 80, 77, 43, 83, 30, 167, 113, 251, 176, 122, 210, 11, 103, 151, 216, 87, 168, 116, 196, 123, 2, 81, 3, 91, 202, 19, 96, 89, 22, 79, 60, 241, 68, 193, 158, 64, 42, 227, 79, 199, 238, 221, 187, 113, 199, 29, 119, 0, 0, 238, 127, 250, 206, 156, 141, 95, 78, 120, 189, 94, 52, 183, 52, 131, 162, 20, 9, 79, 127, 0, 160, 227, 3, 85, 180, 42, 241, 9, 25, 21, 4, 63, 239, 131, 131, 181, 195, 60, 55, 6, 70, 169, 134, 90, 169, 129, 158, 49, 160, 73, 219, 34, 72, 243, 77, 33, 112, 123, 210, 68, 213, 181, 85, 104, 186, 243, 27, 168, 89, 191, 9, 238, 64, 16, 246, 183, 126, 13, 239, 235, 47, 64, 145, 97, 29, 126, 147, 50, 125, 45, 192, 144, 215, 143, 253, 206, 196, 157, 142, 174, 48, 86, 161, 71, 151, 122, 94, 95, 125, 242, 28, 156, 151, 8, 227, 222, 202, 133, 92, 68, 224, 241, 199, 31, 135, 223, 239, 199, 51, 207, 60, 147, 183, 8, 20, 98, 252, 209, 177, 86, 34, 52, 176, 16, 193, 172, 206, 144, 69, 23, 10, 135, 224, 231, 125, 240, 243, 62, 56, 89, 7, 198, 221, 231, 35, 130, 64, 169, 81, 205, 232, 97, 210, 54, 160, 69, 215, 38, 246, 119, 138, 161, 168, 50, 96, 217, 87, 31, 5, 213, 189, 1, 254, 96, 16, 83, 7, 246, 194, 243, 250, 238, 140, 198, 15, 0, 203, 21, 169, 51, 1, 231, 248, 224, 34, 227, 7, 128, 253, 78, 79, 90, 79, 64, 59, 110, 23, 251, 82, 136, 66, 178, 8, 112, 28, 135, 219, 111, 191, 61, 229, 177, 59, 119, 238, 4, 128, 152, 8, 60, 241, 139, 135, 179, 254, 61, 241, 198, 255, 181, 175, 125, 13, 207, 61, 247, 92, 218, 99, 139, 97, 252, 82, 223, 24, 54, 26, 128, 54, 24, 13, 152, 227, 185, 196, 24, 0, 80, 88, 157, 56, 23, 100, 225, 14, 184, 96, 241, 76, 224, 196, 116, 31, 222, 25, 127, 11, 135, 44, 251, 209, 55, 117, 4, 227, 238, 81, 209, 190, 180, 66, 163, 67, 199, 87, 30, 132, 113, 213, 90, 240, 60, 15, 219, 127, 189, 14, 247, 139, 207, 67, 145, 101, 134, 95, 157, 34, 181, 72, 156, 91, 162, 50, 204, 146, 166, 126, 156, 158, 93, 72, 250, 145, 210, 141, 33, 20, 75, 101, 61, 246, 94, 182, 22, 247, 125, 251, 43, 0, 128, 59, 238, 184, 3, 187, 119, 239, 78, 123, 158, 157, 59, 119, 70, 166, 4, 62, 22, 223, 186, 107, 23, 134, 250, 51, 231, 223, 15, 30, 31, 17, 213, 248, 229, 64, 252, 202, 68, 50, 148, 208, 117, 226, 129, 80, 68, 16, 166, 124, 147, 24, 112, 244, 199, 2, 139, 125, 83, 71, 74, 215, 101, 167, 74, 143, 186, 175, 125, 11, 117, 31, 219, 12, 132, 21, 152, 57, 184, 23, 222, 223, 254, 10, 10, 74, 1, 208, 42, 132, 149, 153, 43, 6, 133, 44, 5, 10, 169, 138, 186, 7, 171, 232, 100, 202, 122, 252, 216, 199, 215, 225, 158, 199, 239, 0, 144, 189, 8, 228, 82, 125, 151, 141, 241, 255, 226, 23, 191, 168, 72, 227, 7, 18, 251, 68, 44, 138, 1, 0, 197, 125, 42, 197, 2, 139, 188, 7, 83, 190, 73, 156, 115, 13, 131, 161, 34, 113, 4, 163, 186, 70, 176, 236, 189, 120, 212, 23, 94, 14, 69, 215, 42, 216, 217, 0, 102, 222, 250, 29, 60, 191, 255, 87, 104, 175, 255, 28, 148, 109, 93, 64, 56, 4, 238, 232, 1, 176, 71, 247, 67, 177, 196, 182, 95, 147, 97, 10, 203, 83, 120, 1, 93, 26, 6, 135, 221, 139, 227, 13, 12, 20, 104, 97, 82, 11, 11, 215, 80, 120, 135, 34, 41, 227, 245, 248, 81, 103, 170, 141, 5, 147, 227, 131, 78, 6, 163, 1, 86, 139, 21, 215, 222, 124, 5, 0, 224, 39, 79, 190, 16, 51, 196, 108, 166, 3, 217, 144, 141, 241, 223, 117, 215, 93, 80, 40, 20, 21, 103, 252, 153, 40, 249, 163, 41, 94, 16, 28, 172, 29, 163, 238, 179, 9, 129, 197, 182, 170, 142, 130, 91, 122, 113, 71, 246, 33, 112, 225, 229, 240, 217, 38, 48, 247, 155, 221, 80, 223, 248, 37, 168, 55, 109, 5, 20, 0, 123, 244, 61, 176, 199, 150, 54, 126, 0, 24, 11, 41, 177, 156, 90, 124, 76, 53, 173, 196, 21, 198, 170, 69, 113, 128, 75, 141, 186, 180, 43, 1, 190, 174, 194, 123, 20, 74, 149, 92, 210, 97, 139, 33, 2, 196, 248, 11, 67, 116, 223, 52, 57, 176, 104, 158, 27, 139, 44, 61, 210, 58, 232, 104, 29, 154, 116, 45, 104, 208, 54, 229, 116, 206, 176, 223, 11, 231, 79, 191, 5, 168, 212, 96, 62, 243, 151, 160, 47, 222, 6, 62, 200, 130, 63, 250, 14, 216, 255, 122, 37, 171, 32, 224, 31, 130, 106, 92, 73, 167, 158, 211, 247, 232, 52, 104, 97, 84, 89, 45, 3, 2, 128, 227, 202, 117, 98, 95, 230, 162, 177, 84, 58, 172, 193, 104, 88, 148, 245, 152, 44, 2, 44, 27, 137, 218, 167, 34, 42, 2, 233, 120, 232, 161, 135, 240, 253, 239, 127, 63, 237, 251, 196, 248, 51, 67, 75, 45, 40, 21, 10, 135, 192, 5, 89, 112, 65, 22, 78, 214, 1, 139, 103, 34, 97, 165, 161, 134, 169, 67, 187, 190, 51, 243, 137, 88, 22, 225, 48, 64, 85, 233, 17, 6, 48, 113, 98, 20, 179, 239, 157, 70, 135, 207, 151, 85, 254, 243, 48, 84, 24, 8, 41, 177, 134, 74, 189, 26, 80, 77, 43, 209, 179, 132, 209, 71, 241, 244, 180, 130, 107, 168, 17, 251, 178, 22, 141, 168, 251, 15, 68, 130, 201, 51, 118, 71, 198, 172, 199, 120, 17, 184, 235, 174, 187, 0, 96, 73, 17, 240, 249, 22, 23, 103, 109, 218, 180, 9, 91, 183, 166, 55, 232, 231, 159, 127, 30, 247, 221, 119, 31, 49, 254, 56, 82, 217, 186, 232, 30, 64, 54, 68, 5, 33, 186, 218, 48, 236, 28, 132, 70, 169, 134, 134, 214, 192, 168, 174, 75, 155, 194, 172, 224, 88, 176, 175, 60, 15, 254, 234, 219, 48, 49, 228, 65, 64, 213, 12, 218, 176, 1, 45, 174, 126, 80, 8, 103, 252, 189, 154, 155, 84, 192, 27, 65, 32, 207, 222, 32, 97, 37, 5, 243, 231, 175, 18, 251, 242, 21, 149, 124, 211, 97, 115, 17, 1, 173, 118, 241, 62, 15, 75, 213, 82, 16, 227, 207, 30, 89, 8, 64, 50, 129, 16, 155, 176, 218, 112, 206, 181, 196, 110, 52, 156, 31, 138, 63, 236, 134, 174, 230, 34, 184, 116, 29, 176, 87, 119, 131, 87, 170, 209, 225, 248, 0, 20, 210, 79, 5, 158, 252, 10, 135, 79, 93, 14, 216, 41, 26, 83, 175, 231, 183, 38, 48, 245, 169, 139, 17, 104, 172, 17, 251, 114, 21, 149, 232, 83, 197, 60, 97, 201, 57, 152, 156, 139, 8, 100, 75, 188, 241, 255, 229, 35, 183, 226, 178, 235, 55, 139, 125, 137, 36, 141, 44, 5, 32, 153, 76, 221, 123, 148, 97, 30, 93, 142, 247, 1, 199, 251, 89, 157, 239, 201, 175, 112, 248, 179, 203, 35, 143, 125, 211, 13, 74, 120, 6, 130, 240, 158, 206, 236, 49, 196, 227, 233, 105, 197, 244, 117, 23, 138, 125, 105, 36, 79, 178, 8, 176, 108, 100, 73, 47, 31, 226, 141, 255, 158, 199, 239, 192, 165, 159, 216, 152, 85, 130, 91, 37, 67, 202, 129, 147, 136, 55, 126, 0, 128, 66, 129, 182, 237, 12, 168, 26, 38, 235, 115, 132, 141, 70, 140, 127, 249, 134, 188, 171, 8, 43, 141, 107, 111, 190, 34, 150, 39, 112, 223, 125, 247, 225, 249, 231, 159, 207, 249, 28, 187, 118, 237, 138, 25, 255, 221, 255, 235, 118, 92, 123, 243, 21, 100, 131, 208, 44, 32, 2, 16, 199, 34, 227, 159, 199, 161, 217, 12, 207, 23, 238, 201, 250, 60, 190, 47, 126, 25, 193, 234, 242, 170, 247, 207, 68, 161, 233, 176, 215, 222, 124, 5, 238, 122, 236, 75, 0, 114, 23, 129, 93, 187, 118, 225, 225, 135, 31, 134, 66, 161, 192, 23, 254, 250, 102, 172, 219, 186, 170, 40, 27, 161, 148, 35, 68, 0, 230, 73, 103, 252, 211, 252, 197, 56, 199, 222, 138, 96, 119, 15, 184, 109, 215, 102, 60, 15, 119, 245, 53, 8, 173, 236, 46, 201, 152, 211, 53, 26, 77, 78, 205, 149, 11, 55, 124, 110, 91, 206, 34, 16, 111, 252, 247, 60, 126, 7, 62, 251, 229, 79, 203, 162, 68, 87, 42, 16, 1, 64, 102, 227, 143, 194, 221, 240, 105, 132, 140, 53, 105, 207, 19, 170, 51, 129, 251, 228, 103, 74, 54, 238, 116, 141, 70, 147, 83, 115, 229, 68, 178, 8, 236, 218, 181, 43, 237, 177, 201, 198, 31, 141, 39, 16, 178, 167, 226, 5, 32, 157, 241, 115, 218, 107, 224, 53, 62, 144, 248, 162, 74, 5, 246, 150, 91, 211, 158, 139, 253, 252, 109, 128, 74, 152, 157, 137, 178, 33, 93, 163, 209, 228, 134, 164, 114, 35, 94, 4, 30, 126, 248, 225, 148, 34, 240, 228, 147, 79, 38, 184, 253, 196, 248, 243, 163, 162, 5, 32, 147, 241, 27, 244, 6, 52, 54, 38, 166, 241, 6, 215, 172, 71, 112, 213, 234, 69, 159, 9, 174, 90, 141, 96, 247, 106, 136, 9, 69, 81, 89, 165, 230, 202, 129, 100, 17, 248, 193, 15, 126, 16, 123, 239, 153, 103, 158, 193, 19, 79, 60, 145, 224, 246, 39, 67, 92, 255, 236, 40, 139, 101, 192, 124, 200, 246, 201, 111, 208, 71, 154, 58, 78, 78, 46, 148, 82, 114, 87, 108, 131, 118, 48, 177, 101, 26, 119, 229, 182, 146, 127, 135, 84, 41, 183, 169, 82, 115, 229, 186, 12, 118, 195, 231, 34, 215, 244, 23, 223, 121, 17, 15, 62, 248, 32, 212, 106, 53, 88, 150, 197, 35, 143, 60, 146, 16, 237, 39, 100, 134, 166, 104, 52, 209, 139, 19, 170, 42, 82, 0, 210, 25, 255, 107, 254, 110, 120, 212, 159, 69, 242, 243, 36, 89, 4, 130, 107, 214, 33, 100, 170, 7, 101, 159, 6, 16, 153, 251, 7, 47, 40, 125, 190, 191, 193, 104, 88, 148, 114, 59, 61, 57, 189, 40, 53, 183, 90, 47, 79, 1, 0, 34, 34, 160, 173, 210, 224, 71, 143, 253, 18, 247, 222, 123, 47, 128, 72, 166, 225, 125, 223, 38, 25, 126, 217, 98, 164, 212, 139, 202, 128, 163, 84, 156, 0, 44, 101, 252, 223, 116, 95, 5, 184, 143, 2, 0, 62, 93, 219, 145, 240, 126, 130, 8, 40, 20, 8, 108, 190, 20, 234, 223, 71, 182, 195, 10, 92, 178, 69, 148, 53, 255, 84, 41, 183, 169, 82, 115, 229, 206, 149, 55, 110, 5, 173, 162, 241, 119, 15, 255, 4, 0, 240, 240, 223, 127, 13, 151, 108, 219, 36, 246, 176, 100, 65, 27, 179, 180, 248, 87, 148, 0, 100, 52, 254, 121, 190, 53, 158, 89, 4, 66, 221, 61, 177, 215, 67, 43, 165, 211, 239, 175, 144, 212, 92, 41, 115, 217, 245, 155, 161, 98, 104, 168, 181, 106, 244, 94, 186, 182, 240, 19, 150, 57, 26, 138, 134, 41, 133, 203, 159, 76, 197, 8, 64, 182, 198, 31, 37, 163, 8, 4, 131, 192, 124, 155, 241, 224, 178, 46, 177, 191, 94, 69, 176, 249, 106, 242, 212, 207, 134, 165, 92, 254, 100, 42, 66, 0, 114, 53, 254, 40, 153, 68, 192, 181, 124, 190, 10, 81, 153, 185, 44, 152, 64, 200, 23, 62, 16, 153, 214, 53, 54, 54, 128, 86, 209, 9, 27, 125, 232, 116, 58, 24, 141, 122, 40, 230, 155, 174, 100, 114, 249, 147, 41, 251, 101, 192, 124, 141, 63, 202, 183, 198, 143, 226, 119, 142, 177, 69, 175, 27, 244, 6, 232, 46, 189, 12, 225, 246, 44, 122, 19, 20, 25, 169, 119, 165, 37, 20, 70, 54, 59, 77, 27, 41, 117, 206, 198, 15, 228, 233, 1, 164, 83, 160, 100, 165, 18, 155, 66, 141, 63, 74, 58, 79, 160, 238, 47, 190, 0, 218, 237, 74, 88, 34, 44, 71, 254, 245, 39, 191, 22, 123, 8, 146, 228, 243, 247, 252, 247, 146, 252, 158, 76, 61, 23, 89, 199, 92, 214, 46, 127, 50, 121, 89, 233, 130, 2, 213, 193, 229, 116, 193, 233, 116, 163, 166, 214, 152, 160, 84, 98, 175, 61, 11, 101, 252, 81, 178, 9, 12, 150, 43, 175, 252, 148, 8, 64, 42, 62, 123, 231, 141, 69, 127, 224, 101, 74, 236, 234, 208, 24, 128, 150, 252, 55, 32, 205, 107, 212, 169, 186, 190, 214, 212, 26, 23, 41, 149, 88, 8, 109, 252, 81, 42, 89, 4, 128, 200, 14, 62, 132, 72, 26, 50, 128, 146, 60, 240, 210, 245, 92, 12, 186, 253, 232, 104, 104, 44, 248, 252, 130, 200, 86, 186, 20, 84, 49, 166, 1, 197, 50, 254, 40, 149, 44, 2, 79, 60, 241, 132, 216, 67, 144, 4, 81, 1, 40, 197, 3, 47, 85, 207, 197, 158, 214, 246, 188, 93, 254, 100, 242, 178, 80, 169, 166, 160, 22, 219, 248, 163, 84, 178, 8, 16, 22, 40, 197, 3, 47, 62, 177, 139, 86, 40, 209, 94, 215, 32, 152, 241, 3, 121, 174, 2, 196, 111, 53, 20, 12, 241, 145, 74, 52, 143, 31, 26, 77, 36, 241, 64, 140, 78, 44, 165, 50, 254, 40, 75, 173, 14, 24, 141, 181, 37, 253, 238, 4, 113, 72, 126, 224, 21, 131, 104, 79, 3, 131, 90, 139, 77, 93, 43, 209, 96, 200, 127, 190, 159, 138, 188, 36, 75, 138, 41, 168, 215, 94, 180, 184, 193, 103, 177, 140, 63, 74, 58, 79, 32, 28, 38, 173, 192, 42, 129, 248, 7, 94, 49, 167, 1, 109, 140, 30, 104, 45, 206, 185, 5, 243, 89, 196, 78, 65, 221, 127, 174, 25, 159, 92, 99, 137, 253, 187, 216, 198, 31, 37, 149, 8, 124, 56, 212, 135, 112, 147, 56, 49, 16, 66, 233, 40, 246, 3, 47, 219, 116, 222, 66, 40, 155, 59, 212, 187, 242, 147, 216, 51, 240, 91, 172, 107, 158, 193, 1, 170, 19, 223, 228, 74, 215, 143, 255, 91, 227, 71, 209, 74, 169, 209, 30, 160, 240, 209, 217, 147, 240, 25, 205, 128, 31, 162, 47, 133, 10, 141, 130, 52, 57, 77, 160, 181, 173, 165, 104, 15, 188, 92, 210, 121, 11, 161, 108, 4, 0, 0, 102, 87, 126, 6, 239, 205, 255, 252, 140, 54, 243, 246, 95, 249, 18, 159, 8, 21, 45, 195, 29, 60, 253, 58, 166, 77, 181, 160, 155, 104, 104, 2, 242, 47, 195, 37, 136, 71, 62, 25, 125, 249, 82, 144, 0, 84, 106, 10, 170, 20, 99, 32, 197, 228, 185, 255, 247, 116, 44, 239, 35, 20, 10, 99, 210, 54, 137, 250, 122, 19, 156, 46, 39, 76, 38, 19, 236, 118, 59, 140, 6, 99, 217, 79, 121, 138, 61, 189, 213, 64, 13, 19, 83, 252, 167, 126, 60, 229, 253, 63, 86, 66, 196, 142, 129, 20, 19, 169, 46, 251, 150, 154, 98, 62, 240, 244, 42, 3, 12, 138, 220, 54, 159, 17, 130, 178, 47, 6, 34, 20, 142, 20, 151, 125, 203, 137, 54, 70, 47, 138, 241, 3, 50, 244, 0, 114, 41, 141, 36, 8, 67, 165, 77, 121, 74, 69, 169, 2, 125, 75, 33, 59, 75, 201, 166, 52, 178, 84, 84, 106, 12, 36, 250, 61, 201, 6, 28, 249, 19, 53, 254, 169, 201, 105, 81, 199, 33, 59, 1, 72, 238, 121, 159, 174, 55, 62, 129, 32, 69, 52, 20, 13, 35, 165, 198, 220, 212, 12, 92, 46, 23, 212, 26, 226, 1, 100, 77, 54, 61, 239, 41, 226, 254, 151, 13, 229, 182, 245, 153, 145, 82, 195, 68, 107, 225, 155, 113, 1, 0, 88, 63, 7, 131, 192, 169, 189, 185, 34, 43, 107, 73, 87, 26, 233, 114, 186, 16, 10, 133, 99, 17, 106, 66, 113, 40, 245, 148, 167, 156, 182, 62, 107, 99, 244, 240, 205, 184, 34, 129, 212, 249, 13, 75, 131, 193, 72, 96, 213, 106, 17, 174, 120, 44, 215, 115, 201, 42, 8, 152, 170, 52, 178, 190, 177, 126, 81, 111, 124, 66, 121, 32, 245, 190, 19, 217, 16, 159, 206, 219, 208, 88, 47, 246, 112, 22, 33, 43, 15, 32, 26, 121, 142, 223, 250, 57, 26, 161, 6, 0, 147, 201, 4, 138, 34, 233, 170, 229, 138, 220, 182, 62, 139, 186, 252, 165, 32, 57, 152, 152, 109, 112, 81, 86, 30, 64, 57, 39, 219, 16, 22, 35, 231, 4, 164, 82, 166, 243, 2, 128, 90, 195, 192, 229, 138, 196, 22, 114, 9, 46, 202, 202, 3, 32, 84, 22, 114, 76, 64, 202, 183, 59, 111, 193, 215, 202, 96, 0, 235, 231, 0, 228, 22, 92, 148, 149, 7, 64, 168, 44, 228, 150, 128, 36, 86, 98, 79, 114, 224, 47, 26, 92, 4, 16, 11, 160, 166, 67, 150, 2, 80, 201, 9, 56, 149, 142, 20, 167, 129, 233, 118, 222, 45, 21, 153, 140, 124, 201, 177, 139, 54, 106, 2, 161, 12, 144, 66, 58, 111, 33, 16, 1, 32, 16, 242, 68, 140, 185, 190, 208, 16, 1, 32, 72, 30, 169, 77, 249, 74, 209, 170, 171, 84, 144, 85, 0, 153, 144, 156, 254, 154, 46, 77, 150, 80, 92, 74, 185, 182, 95, 10, 136, 0, 200, 4, 41, 85, 65, 86, 42, 26, 138, 150, 245, 124, 63, 21, 68, 0, 100, 2, 169, 130, 20, 31, 127, 136, 199, 4, 231, 134, 157, 151, 79, 13, 66, 38, 72, 12, 64, 6, 144, 42, 72, 105, 17, 21, 2, 64, 254, 241, 0, 34, 0, 50, 96, 169, 42, 200, 248, 52, 89, 66, 233, 137, 23, 3, 154, 162, 81, 5, 165, 172, 166, 9, 228, 177, 33, 3, 82, 165, 191, 166, 74, 147, 37, 136, 11, 31, 226, 225, 12, 177, 152, 224, 220, 152, 224, 220, 152, 227, 57, 184, 61, 156, 216, 195, 90, 18, 193, 61, 128, 116, 61, 250, 146, 123, 249, 17, 178, 39, 85, 250, 107, 170, 52, 89, 130, 180, 112, 134, 88, 152, 39, 45, 104, 110, 105, 70, 45, 173, 65, 53, 205, 96, 142, 231, 36, 229, 33, 8, 238, 1, 148, 83, 19, 7, 169, 64, 250, 239, 201, 31, 103, 136, 197, 152, 223, 133, 211, 19, 163, 176, 115, 28, 166, 230, 43, 247, 196, 70, 112, 1, 72, 23, 157, 78, 142, 98, 151, 3, 229, 214, 178, 138, 32, 60, 169, 58, 86, 77, 187, 167, 49, 19, 152, 195, 4, 231, 198, 148, 203, 37, 234, 52, 161, 232, 49, 0, 185, 53, 113, 200, 5, 226, 237, 16, 50, 145, 169, 164, 217, 171, 12, 98, 216, 62, 30, 139, 25, 148, 26, 193, 5, 32, 149, 226, 165, 138, 98, 151, 3, 165, 246, 118, 164, 150, 18, 75, 200, 76, 170, 142, 85, 169, 58, 91, 1, 88, 20, 64, 44, 201, 248, 132, 62, 161, 28, 155, 56, 8, 69, 57, 123, 59, 4, 225, 200, 38, 166, 147, 73, 12, 132, 106, 36, 42, 120, 56, 94, 110, 77, 28, 10, 65, 206, 45, 171, 42, 25, 185, 237, 46, 229, 12, 177, 112, 114, 44, 128, 133, 196, 35, 93, 149, 78, 144, 115, 151, 228, 91, 150, 107, 20, 187, 146, 189, 29, 57, 35, 231, 186, 138, 104, 226, 145, 91, 163, 128, 157, 247, 21, 60, 85, 144, 142, 204, 201, 144, 92, 230, 119, 149, 140, 212, 42, 25, 197, 168, 171, 40, 70, 252, 198, 31, 151, 120, 100, 203, 83, 12, 72, 70, 142, 192, 72, 177, 101, 149, 216, 196, 63, 113, 171, 85, 250, 184, 39, 110, 29, 92, 78, 23, 156, 78, 55, 106, 106, 141, 37, 25, 75, 185, 214, 85, 240, 33, 30, 78, 240, 177, 169, 66, 192, 233, 67, 87, 67, 99, 198, 207, 21, 237, 155, 146, 136, 53, 33, 138, 148, 42, 25, 43, 101, 119, 169, 169, 185, 89, 28, 57, 59, 136, 35, 103, 7, 49, 108, 49, 167, 77, 60, 146, 159, 212, 17, 100, 133, 212, 158, 184, 149, 82, 87, 17, 31, 107, 211, 154, 244, 224, 52, 10, 216, 82, 148, 49, 147, 41, 128, 0, 16, 111, 39, 61, 82, 171, 100, 172, 228, 186, 10, 62, 196, 199, 226, 4, 209, 122, 4, 226, 1, 16, 138, 138, 212, 158, 184, 229, 186, 34, 149, 45, 206, 16, 11, 235, 212, 194, 182, 97, 196, 3, 32, 20, 149, 74, 126, 226, 74, 129, 84, 2, 167, 53, 45, 228, 165, 16, 1, 32, 20, 21, 178, 42, 34, 77, 38, 56, 55, 218, 24, 61, 254, 63, 91, 213, 67, 145, 233, 13, 169, 197, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130]; +// Will be replaced automatically by GitHub Actions +final faviconTileBytes = []; diff --git a/tile_server/static/generated/land.dart b/tile_server/static/generated/land.dart index 48881eab..1029ac5f 100644 --- a/tile_server/static/generated/land.dart +++ b/tile_server/static/generated/land.dart @@ -1 +1,2 @@ -final landTileBytes = [137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 1, 0, 0, 0, 1, 0, 8, 3, 0, 0, 0, 107, 172, 88, 84, 0, 0, 2, 253, 80, 76, 84, 69, 51, 52, 49, 72, 52, 55, 60, 65, 58, 76, 85, 24, 157, 17, 17, 83, 81, 48, 162, 28, 28, 73, 74, 72, 84, 75, 67, 78, 83, 73, 92, 100, 43, 84, 85, 71, 85, 87, 84, 169, 46, 46, 110, 80, 76, 125, 90, 55, 104, 112, 57, 89, 110, 87, 129, 96, 61, 102, 109, 86, 113, 94, 97, 130, 88, 87, 134, 105, 70, 104, 105, 102, 94, 131, 93, 104, 115, 103, 121, 128, 77, 139, 112, 78, 140, 101, 88, 141, 89, 99, 113, 117, 107, 182, 78, 78, 142, 116, 84, 117, 117, 116, 149, 107, 95, 146, 121, 90, 151, 150, 59, 145, 110, 113, 117, 141, 112, 128, 134, 111, 138, 144, 96, 148, 150, 83, 172, 121, 91, 130, 132, 125, 154, 135, 104, 176, 113, 111, 129, 154, 119, 177, 153, 75, 136, 137, 133, 147, 153, 107, 150, 171, 86, 141, 169, 102, 157, 140, 115, 212, 117, 89, 153, 166, 102, 157, 149, 115, 165, 165, 91, 134, 164, 124, 200, 110, 114, 125, 185, 115, 151, 138, 138, 141, 150, 137, 134, 167, 129, 218, 89, 126, 224, 83, 126, 151, 136, 147, 206, 145, 85, 149, 150, 138, 142, 189, 107, 166, 171, 101, 172, 150, 116, 153, 171, 115, 156, 181, 103, 132, 189, 120, 221, 92, 129, 182, 122, 144, 224, 93, 131, 210, 110, 130, 162, 166, 123, 168, 179, 105, 180, 172, 102, 140, 184, 131, 148, 170, 137, 207, 140, 108, 206, 164, 86, 152, 187, 120, 155, 155, 151, 142, 195, 125, 147, 182, 133, 142, 194, 129, 149, 178, 138, 149, 184, 135, 167, 155, 147, 182, 183, 104, 157, 182, 131, 172, 165, 133, 183, 166, 121, 168, 184, 120, 151, 185, 137, 154, 179, 141, 157, 164, 153, 147, 197, 133, 157, 187, 134, 228, 110, 142, 148, 205, 128, 154, 189, 139, 150, 197, 137, 155, 196, 133, 172, 146, 168, 155, 185, 147, 180, 168, 139, 170, 188, 130, 184, 186, 118, 204, 173, 112, 165, 169, 156, 207, 142, 141, 228, 143, 119, 157, 192, 142, 153, 206, 133, 186, 195, 115, 155, 201, 141, 156, 209, 135, 158, 198, 145, 168, 168, 165, 161, 196, 145, 182, 172, 148, 169, 196, 138, 183, 186, 135, 168, 184, 157, 171, 198, 146, 163, 204, 149, 167, 193, 156, 182, 183, 151, 163, 213, 141, 171, 182, 166, 195, 202, 122, 188, 197, 135, 180, 171, 171, 231, 141, 150, 172, 203, 148, 203, 154, 166, 167, 205, 152, 171, 196, 157, 204, 179, 141, 184, 167, 178, 171, 205, 156, 195, 201, 136, 180, 199, 154, 228, 186, 120, 185, 183, 167, 172, 202, 163, 200, 174, 164, 173, 209, 158, 232, 146, 162, 175, 210, 161, 201, 210, 136, 181, 214, 155, 197, 186, 167, 212, 199, 139, 182, 202, 168, 219, 167, 166, 178, 212, 163, 198, 201, 155, 205, 169, 180, 187, 186, 183, 213, 185, 162, 182, 216, 164, 197, 197, 168, 182, 213, 169, 187, 212, 165, 196, 216, 153, 210, 215, 140, 186, 199, 181, 184, 225, 158, 214, 201, 153, 194, 189, 187, 186, 219, 166, 187, 214, 171, 209, 172, 191, 188, 219, 171, 248, 177, 155, 190, 225, 166, 189, 216, 178, 197, 201, 185, 215, 220, 148, 215, 201, 168, 199, 215, 171, 217, 186, 184, 191, 206, 193, 241, 203, 146, 198, 197, 196, 234, 180, 178, 216, 199, 183, 216, 212, 170, 226, 202, 170, 198, 228, 173, 199, 218, 183, 221, 226, 155, 224, 229, 159, 210, 204, 200, 247, 194, 173, 205, 234, 176, 213, 217, 185, 204, 216, 197, 233, 201, 184, 237, 188, 198, 204, 226, 196, 210, 230, 187, 229, 233, 166, 246, 198, 184, 232, 200, 198, 252, 214, 164, 214, 217, 200, 228, 218, 187, 215, 228, 201, 244, 199, 203, 232, 231, 184, 217, 217, 215, 232, 219, 199, 245, 220, 185, 217, 240, 195, 237, 241, 178, 243, 203, 210, 249, 230, 178, 226, 221, 219, 229, 233, 204, 228, 227, 212, 220, 234, 214, 252, 215, 204, 228, 226, 220, 229, 234, 211, 232, 227, 215, 245, 230, 200, 244, 216, 218, 227, 236, 219, 235, 238, 211, 237, 228, 219, 243, 231, 214, 247, 250, 191, 234, 234, 222, 238, 240, 213, 232, 231, 230, 247, 220, 226, 247, 250, 196, 241, 242, 214, 234, 243, 229, 243, 238, 233, 243, 243, 242, 246, 247, 235, 252, 238, 241, 244, 249, 241, 251, 243, 244, 251, 251, 245, 253, 246, 248, 254, 254, 254, 34, 181, 220, 167, 0, 0, 69, 129, 73, 68, 65, 84, 120, 156, 205, 125, 15, 124, 19, 215, 157, 167, 46, 221, 75, 66, 22, 54, 9, 189, 66, 73, 110, 113, 56, 183, 185, 101, 33, 9, 203, 166, 65, 33, 205, 101, 91, 214, 148, 166, 80, 177, 161, 136, 73, 90, 47, 56, 81, 2, 52, 9, 38, 50, 156, 147, 51, 24, 185, 118, 194, 31, 23, 145, 137, 11, 25, 11, 136, 173, 172, 107, 11, 66, 99, 251, 140, 229, 198, 161, 38, 169, 38, 85, 101, 23, 145, 176, 98, 178, 202, 110, 56, 217, 194, 161, 216, 92, 192, 28, 150, 13, 24, 125, 238, 247, 123, 111, 102, 52, 163, 153, 145, 70, 134, 238, 221, 239, 3, 99, 253, 153, 25, 205, 251, 190, 223, 255, 247, 123, 239, 89, 184, 255, 95, 201, 243, 238, 91, 23, 252, 60, 147, 246, 105, 107, 103, 69, 167, 68, 129, 80, 35, 199, 5, 66, 89, 168, 179, 147, 207, 244, 181, 229, 255, 69, 219, 76, 145, 119, 240, 165, 193, 11, 199, 153, 180, 79, 15, 0, 0, 53, 18, 2, 33, 175, 167, 77, 209, 22, 93, 44, 248, 206, 204, 16, 253, 255, 11, 0, 215, 251, 225, 135, 23, 248, 227, 124, 218, 167, 53, 21, 53, 53, 41, 30, 224, 36, 0, 218, 218, 2, 141, 92, 99, 22, 102, 8, 232, 48, 195, 159, 22, 128, 45, 91, 174, 227, 226, 227, 131, 47, 93, 184, 112, 161, 55, 237, 83, 0, 128, 54, 126, 253, 219, 192, 2, 82, 239, 6, 232, 151, 89, 154, 143, 76, 243, 239, 11, 192, 143, 126, 164, 247, 233, 78, 102, 39, 199, 174, 101, 54, 178, 89, 174, 62, 254, 214, 103, 128, 128, 154, 5, 188, 85, 21, 64, 50, 0, 18, 181, 227, 119, 45, 134, 204, 14, 122, 32, 16, 234, 252, 247, 7, 96, 203, 119, 191, 171, 199, 2, 155, 214, 110, 226, 54, 110, 100, 55, 110, 204, 118, 125, 231, 46, 0, 64, 80, 127, 68, 9, 94, 109, 219, 66, 164, 187, 157, 182, 58, 224, 81, 169, 131, 180, 230, 75, 164, 163, 14, 115, 7, 192, 108, 255, 113, 28, 251, 93, 32, 157, 211, 158, 222, 249, 52, 199, 176, 28, 203, 100, 189, 3, 202, 128, 79, 245, 9, 105, 199, 1, 242, 114, 41, 109, 129, 200, 247, 1, 3, 21, 40, 178, 190, 104, 54, 110, 4, 0, 166, 251, 143, 123, 9, 1, 88, 165, 249, 120, 231, 90, 110, 237, 78, 4, 224, 199, 89, 239, 240, 198, 201, 11, 131, 234, 79, 14, 64, 227, 189, 244, 101, 33, 109, 65, 139, 162, 53, 109, 222, 52, 54, 64, 166, 231, 101, 0, 16, 39, 95, 0, 193, 106, 191, 14, 0, 76, 247, 95, 205, 119, 9, 213, 164, 127, 190, 105, 19, 252, 51, 7, 97, 247, 73, 141, 18, 76, 209, 54, 218, 161, 156, 87, 108, 10, 188, 109, 75, 87, 3, 164, 209, 138, 246, 183, 113, 158, 0, 81, 153, 129, 113, 3, 96, 190, 255, 94, 162, 0, 188, 148, 254, 249, 211, 59, 57, 192, 112, 45, 179, 54, 187, 16, 85, 108, 17, 90, 13, 191, 20, 219, 32, 189, 8, 116, 106, 165, 128, 182, 154, 39, 127, 240, 75, 210, 114, 138, 192, 184, 1, 200, 161, 255, 12, 136, 65, 130, 63, 38, 78, 173, 176, 101, 254, 190, 69, 234, 205, 0, 85, 242, 105, 8, 212, 117, 18, 37, 16, 74, 233, 126, 196, 168, 5, 46, 51, 0, 192, 140, 126, 203, 161, 255, 12, 137, 145, 15, 217, 200, 154, 241, 219, 98, 250, 12, 141, 33, 165, 148, 43, 25, 32, 245, 82, 1, 13, 241, 25, 244, 69, 192, 140, 126, 203, 161, 255, 140, 239, 33, 31, 178, 145, 53, 227, 183, 197, 34, 131, 164, 236, 156, 214, 206, 203, 141, 86, 182, 95, 225, 50, 170, 0, 48, 167, 223, 204, 63, 254, 245, 223, 193, 154, 153, 207, 10, 172, 214, 130, 114, 21, 0, 250, 8, 96, 163, 201, 159, 198, 22, 122, 161, 62, 0, 38, 245, 155, 249, 199, 191, 126, 42, 200, 42, 104, 213, 240, 255, 237, 165, 153, 76, 125, 128, 52, 58, 16, 106, 135, 191, 237, 1, 160, 70, 165, 207, 104, 81, 252, 130, 73, 253, 198, 200, 135, 63, 61, 89, 205, 156, 4, 110, 241, 27, 70, 12, 208, 142, 102, 2, 41, 128, 92, 160, 3, 143, 165, 200, 90, 45, 221, 232, 70, 232, 183, 27, 76, 214, 236, 167, 180, 66, 179, 183, 109, 241, 28, 192, 238, 15, 144, 22, 43, 136, 229, 218, 219, 83, 167, 102, 244, 4, 109, 197, 55, 68, 191, 113, 173, 60, 15, 166, 219, 227, 247, 131, 199, 198, 243, 126, 207, 245, 220, 171, 216, 90, 148, 253, 36, 108, 249, 250, 26, 206, 67, 187, 95, 221, 202, 198, 167, 149, 103, 182, 235, 180, 63, 5, 0, 91, 204, 221, 8, 238, 14, 247, 172, 92, 217, 195, 251, 226, 85, 29, 126, 95, 124, 243, 188, 170, 248, 184, 17, 96, 109, 214, 34, 29, 86, 164, 102, 26, 13, 182, 68, 128, 192, 82, 15, 231, 37, 28, 64, 186, 59, 213, 208, 181, 140, 242, 82, 221, 104, 65, 237, 8, 49, 28, 203, 142, 31, 0, 120, 42, 175, 48, 121, 247, 238, 201, 3, 241, 217, 147, 55, 251, 195, 15, 46, 236, 200, 175, 226, 57, 79, 56, 46, 8, 225, 176, 16, 246, 101, 191, 133, 130, 138, 202, 117, 63, 166, 58, 10, 13, 182, 76, 32, 2, 30, 194, 8, 36, 74, 36, 238, 17, 168, 190, 54, 226, 31, 41, 47, 213, 107, 191, 6, 128, 10, 107, 49, 147, 211, 115, 42, 8, 158, 202, 215, 51, 37, 46, 76, 142, 11, 85, 11, 1, 128, 252, 166, 248, 202, 117, 60, 231, 111, 218, 60, 59, 191, 105, 221, 61, 243, 198, 193, 13, 197, 154, 79, 168, 153, 70, 131, 45, 81, 43, 9, 16, 83, 97, 34, 198, 6, 129, 181, 32, 134, 161, 144, 164, 1, 41, 233, 3, 160, 200, 55, 8, 131, 131, 189, 194, 23, 233, 57, 40, 243, 132, 79, 37, 172, 188, 231, 158, 205, 130, 63, 14, 0, 248, 249, 41, 179, 243, 227, 62, 206, 95, 117, 187, 208, 115, 123, 85, 60, 127, 191, 223, 236, 157, 100, 235, 103, 213, 124, 69, 204, 52, 49, 216, 244, 253, 129, 148, 11, 128, 97, 131, 183, 157, 178, 250, 38, 15, 253, 171, 184, 82, 63, 97, 102, 105, 232, 168, 19, 127, 77, 128, 224, 251, 194, 96, 239, 113, 146, 129, 168, 230, 114, 38, 124, 42, 46, 146, 191, 114, 246, 236, 184, 135, 71, 14, 168, 202, 95, 57, 133, 247, 3, 0, 179, 195, 241, 219, 227, 248, 145, 241, 197, 42, 209, 182, 201, 29, 95, 80, 145, 126, 34, 17, 1, 98, 176, 85, 237, 167, 41, 2, 42, 254, 68, 214, 3, 212, 214, 43, 174, 212, 109, 127, 200, 146, 28, 233, 15, 118, 53, 188, 230, 116, 213, 117, 247, 18, 4, 224, 159, 208, 219, 187, 30, 37, 80, 248, 162, 247, 184, 121, 0, 240, 169, 124, 155, 31, 20, 132, 252, 166, 86, 108, 109, 124, 114, 60, 220, 148, 31, 6, 0, 30, 164, 0, 172, 211, 3, 160, 133, 250, 41, 58, 162, 141, 84, 93, 160, 185, 0, 205, 52, 53, 216, 72, 168, 251, 58, 183, 73, 33, 119, 64, 12, 128, 67, 41, 231, 223, 203, 165, 190, 211, 82, 128, 179, 36, 69, 26, 25, 138, 118, 53, 55, 52, 247, 124, 65, 81, 248, 227, 111, 187, 59, 143, 227, 43, 243, 0, 224, 83, 185, 155, 238, 137, 68, 166, 244, 80, 0, 166, 116, 196, 215, 61, 152, 17, 128, 70, 154, 209, 106, 211, 136, 118, 133, 196, 190, 58, 190, 48, 35, 7, 36, 132, 14, 116, 30, 0, 125, 217, 70, 148, 94, 40, 149, 248, 224, 196, 68, 73, 91, 38, 0, 64, 94, 100, 0, 68, 26, 10, 54, 55, 116, 8, 255, 74, 232, 143, 36, 35, 199, 11, 38, 181, 55, 62, 212, 143, 133, 117, 247, 228, 239, 134, 139, 170, 154, 252, 124, 207, 236, 252, 121, 16, 207, 251, 59, 54, 135, 227, 15, 198, 241, 35, 205, 53, 50, 167, 170, 69, 59, 180, 109, 105, 59, 245, 224, 203, 181, 44, 192, 200, 7, 137, 138, 139, 41, 127, 75, 201, 47, 244, 135, 72, 144, 12, 250, 176, 81, 58, 201, 171, 69, 0, 141, 166, 165, 161, 161, 185, 43, 218, 63, 162, 4, 97, 36, 218, 213, 176, 255, 3, 64, 96, 16, 84, 2, 128, 96, 90, 12, 224, 169, 60, 60, 31, 230, 193, 42, 135, 195, 30, 206, 79, 77, 159, 39, 28, 246, 249, 195, 126, 242, 81, 58, 97, 122, 130, 36, 40, 100, 209, 102, 193, 105, 7, 117, 21, 40, 124, 131, 178, 111, 145, 198, 23, 210, 2, 80, 94, 196, 41, 219, 143, 44, 128, 40, 194, 109, 3, 33, 133, 33, 12, 133, 246, 150, 22, 2, 137, 9, 148, 125, 69, 168, 106, 44, 200, 241, 189, 145, 142, 134, 134, 134, 174, 96, 76, 129, 67, 127, 87, 67, 7, 81, 10, 231, 211, 242, 178, 153, 1, 200, 209, 143, 242, 134, 2, 91, 169, 160, 138, 162, 141, 253, 4, 207, 8, 159, 189, 177, 180, 240, 32, 158, 82, 144, 238, 15, 104, 127, 165, 186, 128, 40, 63, 146, 252, 59, 126, 60, 5, 0, 116, 113, 72, 233, 9, 7, 14, 238, 219, 199, 181, 96, 64, 116, 176, 192, 90, 96, 43, 174, 150, 0, 16, 233, 11, 192, 161, 57, 56, 148, 226, 132, 224, 110, 80, 137, 253, 253, 125, 127, 58, 0, 160, 135, 218, 69, 141, 213, 24, 104, 223, 4, 130, 141, 13, 225, 15, 22, 46, 45, 44, 124, 32, 244, 6, 120, 50, 1, 86, 235, 11, 104, 200, 138, 130, 196, 119, 66, 219, 143, 35, 12, 60, 0, 208, 142, 247, 109, 76, 157, 82, 110, 37, 127, 52, 142, 164, 18, 0, 17, 133, 230, 134, 174, 168, 200, 10, 35, 13, 145, 254, 88, 44, 150, 75, 147, 114, 34, 73, 24, 121, 49, 97, 31, 96, 20, 198, 106, 239, 86, 250, 215, 196, 125, 88, 188, 21, 54, 254, 184, 148, 7, 12, 144, 192, 40, 117, 70, 133, 168, 77, 53, 214, 70, 3, 0, 33, 1, 88, 33, 74, 32, 136, 54, 68, 99, 177, 158, 158, 158, 27, 221, 116, 74, 26, 171, 180, 79, 124, 65, 26, 241, 6, 125, 109, 42, 54, 109, 36, 151, 28, 15, 73, 0, 64, 231, 123, 149, 223, 23, 136, 142, 141, 198, 145, 212, 7, 128, 128, 176, 59, 74, 229, 160, 171, 33, 24, 139, 141, 223, 63, 204, 68, 129, 144, 222, 152, 46, 178, 51, 52, 68, 228, 138, 118, 171, 153, 59, 177, 235, 81, 252, 241, 226, 3, 7, 90, 85, 170, 143, 82, 145, 40, 72, 233, 142, 100, 6, 0, 64, 55, 54, 196, 168, 36, 4, 1, 129, 136, 113, 126, 122, 220, 68, 218, 126, 176, 240, 160, 170, 249, 141, 34, 99, 16, 177, 88, 31, 8, 88, 205, 57, 165, 197, 214, 183, 65, 6, 148, 92, 79, 168, 220, 74, 175, 151, 0, 72, 115, 36, 1, 128, 15, 79, 246, 26, 67, 208, 211, 64, 149, 65, 3, 40, 130, 232, 13, 106, 181, 130, 8, 0, 45, 7, 81, 93, 173, 47, 220, 10, 56, 128, 138, 110, 84, 121, 44, 7, 151, 154, 108, 63, 208, 122, 48, 27, 129, 182, 180, 15, 139, 217, 106, 107, 5, 8, 17, 43, 9, 146, 218, 145, 68, 0, 222, 221, 245, 150, 49, 2, 131, 29, 13, 93, 96, 22, 134, 250, 135, 64, 10, 188, 40, 85, 94, 31, 24, 116, 62, 12, 1, 174, 233, 208, 198, 144, 218, 69, 111, 132, 114, 194, 214, 165, 235, 169, 214, 150, 51, 118, 45, 109, 251, 178, 100, 69, 213, 132, 151, 87, 107, 162, 232, 10, 117, 64, 193, 168, 29, 73, 73, 4, 222, 59, 137, 199, 75, 163, 151, 210, 33, 232, 143, 53, 36, 147, 99, 93, 35, 253, 177, 248, 232, 112, 152, 227, 227, 64, 71, 226, 66, 207, 240, 145, 120, 110, 241, 189, 222, 3, 99, 43, 177, 207, 90, 218, 209, 37, 14, 28, 92, 42, 250, 125, 162, 159, 214, 216, 146, 83, 251, 9, 177, 54, 171, 108, 55, 53, 145, 20, 18, 35, 31, 8, 89, 46, 93, 186, 112, 9, 254, 247, 190, 123, 18, 254, 140, 38, 8, 2, 151, 144, 232, 95, 0, 0, 88, 32, 122, 203, 200, 80, 255, 104, 67, 89, 220, 19, 127, 109, 193, 240, 192, 140, 89, 19, 34, 61, 183, 196, 253, 62, 31, 231, 105, 109, 5, 190, 240, 121, 91, 91, 61, 222, 214, 92, 17, 9, 132, 210, 180, 149, 151, 180, 183, 200, 38, 13, 120, 6, 172, 57, 222, 17, 137, 45, 2, 8, 138, 172, 64, 186, 30, 4, 35, 31, 8, 89, 70, 175, 92, 25, 5, 26, 134, 255, 87, 198, 34, 95, 29, 29, 77, 92, 24, 165, 116, 137, 28, 207, 15, 141, 0, 0, 19, 16, 0, 251, 172, 97, 79, 220, 62, 45, 62, 16, 159, 115, 104, 24, 0, 144, 40, 44, 200, 47, 115, 211, 148, 45, 90, 109, 77, 168, 220, 186, 158, 74, 193, 62, 109, 36, 96, 138, 138, 170, 139, 13, 89, 135, 145, 15, 132, 44, 19, 163, 67, 51, 38, 206, 74, 140, 53, 79, 156, 56, 35, 49, 209, 50, 113, 243, 103, 189, 163, 101, 219, 167, 78, 180, 143, 94, 25, 158, 51, 113, 106, 100, 100, 164, 97, 234, 84, 199, 196, 4, 1, 32, 142, 0, 12, 15, 60, 58, 97, 98, 67, 240, 150, 184, 48, 173, 231, 202, 107, 19, 39, 44, 24, 24, 126, 244, 181, 59, 39, 190, 230, 152, 120, 231, 145, 120, 110, 15, 218, 238, 53, 250, 166, 96, 41, 232, 196, 64, 69, 238, 249, 233, 114, 155, 153, 76, 106, 138, 44, 177, 177, 137, 101, 163, 115, 102, 141, 89, 250, 71, 131, 99, 13, 83, 135, 71, 19, 131, 163, 115, 110, 9, 118, 223, 178, 123, 108, 250, 156, 225, 134, 155, 18, 253, 150, 232, 208, 212, 137, 35, 67, 177, 17, 0, 96, 19, 2, 96, 159, 208, 211, 19, 7, 14, 152, 232, 24, 237, 184, 233, 72, 207, 68, 251, 149, 169, 19, 123, 14, 89, 102, 9, 143, 78, 189, 126, 189, 32, 17, 107, 43, 88, 159, 115, 86, 6, 20, 128, 77, 63, 149, 104, 72, 150, 43, 67, 150, 237, 219, 23, 220, 50, 54, 117, 70, 100, 20, 0, 24, 29, 189, 112, 97, 116, 206, 156, 209, 81, 231, 140, 107, 150, 196, 232, 149, 137, 13, 101, 211, 199, 18, 13, 19, 65, 9, 2, 0, 163, 155, 4, 251, 180, 145, 137, 206, 120, 120, 160, 99, 194, 157, 143, 198, 135, 103, 205, 137, 199, 95, 155, 120, 101, 170, 61, 30, 183, 244, 196, 143, 128, 94, 200, 245, 153, 51, 80, 121, 78, 93, 137, 196, 230, 216, 122, 14, 117, 64, 212, 98, 183, 219, 203, 198, 174, 148, 77, 152, 142, 0, 92, 66, 0, 22, 140, 142, 150, 77, 29, 177, 36, 18, 35, 83, 183, 151, 205, 74, 140, 118, 77, 28, 139, 17, 0, 188, 225, 5, 211, 175, 76, 120, 77, 240, 9, 135, 44, 19, 167, 14, 12, 79, 95, 16, 15, 31, 154, 112, 101, 170, 19, 0, 16, 180, 0, 104, 171, 196, 180, 89, 109, 99, 170, 182, 226, 81, 27, 15, 235, 157, 90, 92, 84, 96, 34, 104, 210, 33, 203, 232, 168, 37, 54, 58, 58, 54, 6, 170, 240, 150, 104, 51, 112, 192, 40, 0, 0, 127, 102, 205, 25, 187, 169, 249, 179, 161, 155, 162, 93, 19, 0, 141, 137, 35, 111, 245, 143, 118, 221, 50, 58, 32, 76, 125, 244, 202, 52, 232, 118, 84, 130, 192, 251, 11, 166, 197, 227, 143, 78, 55, 4, 64, 91, 37, 102, 144, 250, 210, 39, 104, 123, 53, 74, 116, 150, 42, 1, 56, 169, 160, 168, 120, 28, 89, 76, 36, 203, 240, 104, 217, 77, 179, 102, 60, 58, 54, 113, 206, 172, 9, 163, 67, 55, 205, 234, 64, 0, 38, 76, 159, 113, 83, 255, 88, 195, 45, 179, 38, 206, 73, 142, 76, 157, 58, 107, 234, 196, 196, 201, 183, 134, 71, 103, 220, 50, 109, 194, 68, 97, 224, 200, 77, 211, 166, 149, 129, 18, 236, 185, 233, 200, 240, 196, 169, 211, 111, 233, 25, 53, 0, 64, 167, 74, 76, 155, 213, 206, 68, 5, 212, 145, 213, 166, 70, 111, 28, 89, 46, 13, 143, 158, 111, 14, 142, 142, 245, 55, 119, 129, 69, 60, 223, 124, 30, 1, 176, 199, 58, 18, 35, 67, 137, 33, 112, 1, 134, 18, 87, 130, 93, 137, 216, 200, 165, 119, 223, 29, 16, 58, 94, 59, 36, 8, 173, 241, 200, 107, 187, 7, 192, 17, 138, 247, 244, 140, 14, 28, 58, 20, 31, 24, 232, 1, 67, 216, 65, 60, 36, 165, 18, 212, 171, 18, 211, 4, 35, 25, 72, 174, 215, 40, 207, 198, 2, 215, 65, 22, 116, 255, 192, 248, 147, 3, 121, 121, 9, 1, 24, 1, 7, 32, 22, 75, 38, 250, 199, 198, 162, 49, 64, 34, 57, 244, 197, 133, 119, 7, 253, 126, 33, 188, 231, 199, 204, 218, 58, 28, 233, 9, 251, 91, 225, 63, 15, 47, 189, 254, 112, 216, 199, 135, 121, 47, 175, 118, 143, 245, 170, 196, 52, 193, 72, 6, 74, 213, 107, 88, 179, 156, 89, 161, 81, 126, 237, 161, 128, 151, 75, 15, 12, 244, 72, 47, 26, 28, 181, 151, 141, 68, 35, 209, 88, 114, 36, 26, 11, 98, 52, 4, 0, 196, 6, 49, 66, 102, 240, 18, 198, 196, 109, 145, 244, 171, 196, 210, 131, 145, 12, 148, 170, 215, 80, 122, 196, 45, 152, 234, 72, 107, 26, 107, 77, 19, 18, 18, 103, 55, 30, 64, 125, 187, 49, 32, 70, 24, 250, 169, 149, 52, 0, 6, 251, 34, 145, 232, 208, 8, 16, 201, 16, 67, 16, 24, 196, 236, 208, 72, 127, 132, 124, 203, 224, 37, 76, 246, 71, 39, 100, 80, 37, 198, 168, 131, 17, 99, 82, 212, 107, 20, 43, 100, 128, 134, 144, 105, 231, 166, 141, 31, 180, 137, 121, 133, 77, 27, 1, 175, 125, 212, 219, 50, 200, 45, 41, 0, 192, 182, 203, 233, 225, 145, 88, 180, 43, 24, 133, 246, 67, 44, 24, 13, 10, 95, 208, 83, 102, 178, 156, 33, 0, 102, 43, 72, 25, 249, 160, 71, 224, 202, 88, 173, 180, 185, 202, 122, 141, 20, 11, 144, 17, 63, 173, 7, 109, 85, 190, 17, 163, 201, 128, 215, 203, 120, 3, 161, 125, 36, 73, 160, 101, 27, 74, 41, 0, 190, 32, 217, 143, 145, 254, 104, 48, 8, 124, 223, 21, 140, 141, 37, 163, 93, 93, 35, 201, 161, 200, 160, 204, 31, 229, 25, 162, 51, 179, 21, 164, 140, 124, 208, 35, 28, 19, 246, 82, 169, 81, 214, 107, 40, 134, 138, 3, 217, 1, 144, 83, 75, 236, 143, 189, 52, 231, 26, 8, 24, 48, 0, 103, 25, 28, 28, 236, 237, 21, 142, 251, 143, 15, 6, 163, 208, 246, 40, 166, 62, 250, 41, 19, 16, 110, 136, 246, 41, 4, 132, 203, 128, 128, 217, 10, 82, 70, 62, 24, 145, 167, 49, 16, 240, 84, 168, 234, 53, 108, 210, 207, 146, 129, 164, 128, 118, 128, 65, 37, 2, 18, 7, 132, 90, 48, 172, 228, 54, 137, 3, 230, 186, 63, 38, 15, 143, 247, 146, 216, 159, 80, 116, 100, 44, 58, 20, 13, 70, 131, 152, 13, 27, 188, 160, 4, 128, 171, 176, 26, 120, 155, 146, 196, 250, 48, 52, 12, 103, 106, 159, 49, 129, 38, 19, 37, 160, 177, 209, 102, 211, 197, 170, 49, 100, 192, 202, 74, 106, 33, 249, 240, 198, 181, 107, 215, 178, 108, 11, 178, 81, 27, 230, 25, 245, 207, 149, 1, 56, 222, 43, 3, 16, 139, 38, 163, 132, 19, 128, 255, 21, 253, 127, 129, 140, 17, 25, 165, 233, 37, 137, 221, 211, 145, 191, 110, 158, 56, 148, 226, 51, 12, 246, 116, 169, 88, 236, 198, 98, 162, 210, 177, 102, 7, 255, 50, 234, 147, 196, 174, 12, 120, 165, 131, 30, 209, 1, 87, 83, 250, 86, 6, 96, 211, 166, 227, 66, 127, 159, 32, 28, 63, 30, 18, 130, 99, 9, 4, 32, 56, 150, 12, 166, 25, 9, 122, 46, 171, 227, 153, 201, 18, 27, 111, 90, 24, 167, 57, 100, 62, 34, 228, 130, 128, 56, 116, 33, 147, 88, 170, 193, 128, 167, 171, 248, 61, 194, 212, 59, 55, 98, 186, 156, 13, 168, 228, 81, 27, 7, 50, 242, 193, 152, 100, 0, 176, 1, 21, 84, 118, 132, 232, 208, 88, 144, 48, 66, 255, 23, 105, 46, 2, 29, 38, 100, 11, 180, 174, 153, 36, 177, 124, 71, 254, 148, 217, 116, 60, 53, 60, 187, 41, 135, 4, 73, 133, 53, 253, 19, 41, 188, 145, 218, 79, 236, 121, 35, 152, 56, 239, 166, 181, 192, 210, 7, 2, 94, 149, 194, 101, 53, 55, 96, 228, 131, 49, 201, 0, 144, 177, 93, 17, 0, 104, 250, 80, 127, 44, 58, 54, 18, 73, 247, 145, 6, 197, 241, 129, 34, 157, 100, 45, 253, 185, 248, 237, 29, 224, 21, 163, 38, 8, 3, 0, 112, 108, 5, 157, 48, 48, 0, 135, 204, 163, 204, 213, 198, 234, 85, 108, 88, 128, 72, 113, 0, 147, 254, 79, 147, 190, 10, 164, 143, 16, 234, 63, 81, 166, 95, 85, 0, 32, 158, 25, 32, 0, 4, 135, 198, 200, 120, 128, 214, 75, 148, 16, 168, 176, 106, 152, 128, 254, 92, 252, 246, 72, 216, 239, 139, 207, 190, 123, 74, 83, 124, 118, 126, 254, 228, 221, 241, 149, 43, 167, 172, 220, 63, 27, 249, 34, 211, 131, 84, 27, 182, 95, 236, 217, 0, 117, 127, 2, 33, 15, 81, 184, 88, 247, 47, 149, 180, 210, 75, 205, 39, 208, 149, 148, 14, 64, 69, 11, 2, 16, 67, 103, 32, 184, 107, 80, 11, 64, 106, 2, 131, 230, 231, 200, 245, 76, 120, 247, 237, 243, 34, 66, 213, 236, 184, 48, 121, 120, 246, 194, 184, 112, 251, 240, 194, 187, 227, 241, 42, 248, 63, 37, 146, 155, 78, 36, 100, 147, 37, 32, 68, 45, 57, 56, 249, 84, 225, 178, 7, 151, 206, 156, 105, 45, 175, 46, 22, 155, 46, 87, 18, 148, 231, 4, 132, 197, 37, 35, 143, 13, 104, 220, 4, 50, 38, 136, 198, 96, 151, 78, 251, 213, 37, 35, 58, 249, 39, 79, 56, 190, 238, 118, 97, 225, 148, 252, 252, 219, 135, 103, 239, 246, 11, 147, 251, 86, 174, 228, 253, 155, 31, 12, 135, 239, 233, 200, 40, 3, 250, 121, 143, 130, 10, 177, 96, 136, 20, 189, 5, 72, 213, 119, 128, 40, 220, 34, 235, 143, 193, 202, 21, 96, 227, 139, 241, 218, 114, 150, 45, 199, 199, 177, 229, 150, 20, 179, 216, 237, 46, 183, 252, 142, 186, 15, 8, 64, 52, 18, 17, 116, 1, 80, 149, 75, 84, 131, 217, 182, 169, 77, 2, 207, 251, 133, 121, 85, 43, 87, 162, 228, 207, 174, 226, 227, 183, 15, 172, 92, 7, 0, 204, 227, 179, 1, 160, 175, 3, 138, 173, 98, 199, 66, 219, 219, 209, 163, 67, 43, 80, 141, 10, 203, 170, 144, 110, 188, 182, 218, 106, 53, 40, 45, 204, 72, 22, 206, 229, 176, 75, 63, 237, 161, 74, 240, 120, 172, 63, 12, 13, 57, 174, 15, 64, 122, 213, 144, 58, 115, 231, 17, 22, 86, 85, 77, 238, 137, 76, 222, 188, 127, 243, 192, 236, 187, 155, 86, 230, 15, 44, 204, 14, 64, 113, 65, 145, 126, 18, 159, 227, 30, 16, 27, 5, 166, 29, 213, 30, 169, 4, 10, 181, 51, 56, 216, 199, 164, 158, 193, 90, 93, 174, 204, 154, 228, 144, 76, 70, 29, 0, 28, 224, 34, 111, 164, 41, 87, 125, 208, 205, 188, 112, 65, 31, 128, 11, 122, 3, 197, 69, 214, 2, 49, 19, 31, 223, 188, 112, 101, 79, 152, 239, 89, 185, 178, 41, 222, 20, 89, 183, 46, 30, 238, 232, 240, 251, 123, 154, 248, 240, 110, 93, 175, 128, 58, 215, 198, 121, 124, 182, 160, 72, 52, 0, 196, 171, 15, 144, 33, 240, 16, 131, 166, 33, 5, 0, 176, 162, 234, 34, 43, 253, 227, 53, 161, 116, 168, 18, 100, 237, 174, 20, 0, 255, 6, 186, 207, 15, 188, 124, 193, 96, 212, 84, 127, 164, 156, 45, 178, 146, 166, 120, 253, 60, 15, 61, 237, 3, 81, 240, 250, 225, 181, 151, 212, 77, 251, 253, 30, 47, 175, 117, 10, 224, 34, 155, 153, 222, 146, 74, 94, 100, 98, 80, 236, 25, 195, 243, 69, 53, 96, 224, 254, 171, 72, 180, 2, 110, 242, 15, 188, 140, 192, 241, 65, 177, 145, 126, 253, 230, 95, 248, 194, 196, 3, 155, 164, 98, 35, 190, 79, 39, 16, 254, 55, 10, 183, 97, 203, 247, 110, 45, 44, 44, 180, 22, 88, 51, 207, 164, 96, 169, 73, 108, 52, 138, 128, 20, 148, 170, 22, 167, 76, 128, 5, 179, 4, 0, 159, 158, 13, 76, 87, 130, 90, 42, 150, 187, 148, 205, 125, 84, 199, 144, 14, 22, 22, 110, 107, 59, 136, 134, 224, 13, 60, 22, 102, 59, 191, 45, 176, 148, 252, 5, 196, 178, 213, 39, 167, 252, 0, 151, 172, 11, 1, 130, 94, 174, 245, 130, 129, 8, 100, 169, 25, 243, 135, 79, 190, 7, 188, 206, 119, 111, 43, 216, 242, 225, 135, 130, 112, 67, 70, 74, 80, 245, 43, 74, 125, 151, 102, 195, 54, 20, 58, 72, 186, 190, 45, 7, 14, 160, 50, 32, 210, 241, 11, 199, 249, 11, 6, 74, 48, 51, 0, 173, 145, 252, 252, 252, 15, 252, 194, 254, 123, 226, 92, 252, 158, 41, 224, 254, 129, 33, 43, 174, 0, 109, 228, 81, 245, 69, 121, 65, 78, 142, 155, 56, 5, 64, 44, 166, 202, 90, 53, 32, 135, 255, 109, 57, 112, 0, 144, 211, 73, 255, 82, 61, 96, 0, 64, 230, 106, 161, 240, 131, 85, 241, 205, 15, 198, 215, 205, 190, 29, 0, 152, 12, 206, 0, 156, 206, 11, 66, 55, 196, 6, 66, 167, 205, 106, 35, 161, 46, 14, 225, 229, 232, 183, 42, 203, 70, 2, 129, 242, 204, 3, 160, 98, 121, 5, 121, 153, 141, 5, 84, 0, 176, 78, 84, 223, 199, 37, 214, 223, 165, 39, 3, 10, 55, 160, 145, 78, 136, 241, 146, 40, 141, 22, 153, 115, 252, 202, 121, 241, 121, 235, 132, 72, 252, 246, 184, 71, 152, 2, 177, 16, 244, 189, 112, 247, 131, 249, 147, 87, 230, 79, 89, 24, 102, 43, 138, 208, 94, 149, 143, 195, 97, 73, 33, 64, 154, 84, 148, 137, 9, 196, 124, 16, 125, 200, 44, 247, 213, 76, 157, 85, 100, 64, 122, 95, 210, 65, 32, 5, 64, 235, 224, 133, 65, 15, 138, 11, 9, 144, 224, 13, 66, 224, 139, 223, 125, 123, 126, 220, 231, 3, 0, 188, 194, 148, 123, 38, 175, 132, 15, 133, 41, 85, 194, 186, 124, 33, 114, 123, 142, 131, 231, 42, 162, 89, 173, 84, 253, 151, 54, 122, 78, 17, 130, 117, 176, 212, 76, 129, 161, 22, 0, 231, 235, 89, 16, 72, 1, 64, 202, 136, 125, 100, 154, 1, 207, 209, 19, 7, 123, 135, 87, 206, 139, 204, 94, 25, 230, 0, 0, 100, 253, 248, 228, 30, 47, 0, 208, 225, 175, 154, 231, 143, 95, 23, 0, 56, 30, 0, 189, 153, 170, 2, 51, 230, 34, 226, 208, 238, 125, 35, 27, 0, 52, 179, 166, 225, 128, 134, 215, 187, 84, 105, 192, 193, 116, 115, 40, 159, 201, 247, 246, 10, 131, 130, 7, 186, 254, 184, 71, 212, 26, 23, 122, 113, 146, 128, 48, 69, 64, 0, 60, 97, 8, 11, 238, 238, 1, 136, 166, 244, 212, 85, 205, 131, 176, 192, 72, 127, 230, 50, 100, 108, 130, 2, 98, 70, 52, 227, 73, 222, 0, 213, 143, 218, 217, 227, 205, 175, 55, 71, 101, 141, 15, 62, 221, 247, 150, 190, 117, 82, 9, 64, 42, 37, 201, 99, 171, 27, 249, 227, 66, 10, 35, 33, 252, 224, 131, 251, 103, 47, 36, 28, 224, 239, 88, 88, 53, 15, 140, 1, 2, 224, 203, 8, 64, 78, 67, 198, 217, 137, 170, 139, 204, 194, 223, 40, 105, 73, 157, 233, 243, 108, 243, 235, 255, 20, 19, 219, 195, 97, 174, 172, 188, 96, 102, 97, 39, 47, 32, 47, 12, 14, 42, 22, 52, 160, 115, 108, 252, 190, 148, 200, 0, 176, 241, 166, 117, 77, 113, 31, 23, 7, 207, 63, 190, 123, 221, 126, 172, 26, 226, 247, 11, 117, 61, 29, 62, 161, 202, 40, 89, 156, 219, 144, 177, 68, 134, 245, 67, 1, 227, 36, 184, 76, 109, 18, 68, 186, 235, 7, 116, 116, 93, 184, 16, 5, 165, 70, 4, 94, 78, 207, 87, 20, 96, 202, 90, 161, 124, 143, 83, 33, 241, 202, 137, 67, 68, 199, 227, 231, 209, 247, 241, 250, 189, 228, 37, 178, 217, 218, 86, 223, 206, 202, 214, 181, 107, 247, 24, 217, 228, 92, 134, 140, 83, 100, 53, 250, 194, 43, 27, 193, 12, 212, 38, 78, 169, 51, 88, 64, 65, 232, 123, 189, 11, 186, 142, 186, 189, 140, 124, 0, 213, 163, 240, 222, 121, 177, 223, 143, 167, 68, 64, 215, 79, 206, 58, 35, 53, 151, 33, 227, 20, 21, 233, 4, 18, 226, 248, 28, 27, 48, 17, 6, 81, 50, 92, 65, 162, 193, 233, 150, 50, 37, 140, 124, 160, 4, 17, 42, 141, 189, 5, 218, 106, 159, 191, 55, 29, 0, 159, 32, 8, 189, 189, 95, 244, 246, 242, 186, 119, 72, 167, 28, 134, 140, 101, 10, 4, 116, 66, 41, 243, 43, 188, 72, 100, 188, 132, 134, 203, 110, 23, 227, 35, 70, 62, 136, 196, 22, 219, 200, 248, 141, 31, 92, 128, 65, 210, 106, 17, 10, 201, 77, 228, 133, 54, 95, 43, 88, 193, 222, 11, 90, 30, 210, 37, 198, 236, 144, 113, 138, 192, 37, 88, 47, 15, 164, 74, 148, 113, 124, 14, 7, 94, 53, 238, 83, 166, 53, 68, 92, 246, 74, 206, 229, 202, 228, 118, 159, 20, 88, 239, 113, 210, 203, 148, 9, 36, 39, 65, 230, 8, 81, 101, 50, 242, 193, 128, 178, 158, 160, 67, 242, 112, 103, 234, 9, 51, 175, 128, 96, 43, 214, 153, 131, 149, 113, 17, 149, 74, 187, 211, 9, 49, 162, 34, 105, 40, 147, 203, 225, 112, 186, 185, 109, 75, 11, 196, 62, 224, 121, 190, 55, 21, 40, 137, 0, 12, 230, 50, 221, 202, 24, 0, 253, 129, 119, 175, 12, 64, 129, 28, 24, 100, 95, 1, 193, 150, 30, 68, 100, 4, 128, 117, 216, 237, 228, 159, 83, 253, 219, 24, 234, 187, 92, 240, 49, 97, 15, 116, 201, 192, 45, 190, 144, 90, 241, 166, 77, 52, 162, 230, 211, 224, 140, 124, 208, 33, 125, 193, 110, 73, 13, 120, 203, 243, 170, 50, 172, 128, 32, 105, 111, 101, 53, 29, 152, 170, 214, 204, 28, 224, 178, 59, 42, 57, 183, 203, 254, 29, 187, 83, 228, 2, 55, 70, 140, 118, 59, 125, 13, 106, 66, 252, 165, 162, 15, 85, 61, 222, 171, 208, 7, 148, 52, 133, 252, 57, 144, 66, 176, 149, 61, 145, 2, 128, 149, 68, 59, 131, 189, 145, 165, 223, 138, 135, 234, 226, 114, 120, 47, 4, 227, 81, 75, 134, 220, 18, 91, 233, 182, 83, 21, 0, 189, 141, 92, 224, 118, 83, 0, 228, 75, 88, 208, 146, 78, 23, 126, 96, 91, 223, 185, 145, 239, 245, 3, 248, 24, 240, 130, 98, 80, 79, 186, 109, 9, 181, 141, 99, 84, 132, 146, 82, 176, 93, 82, 222, 138, 35, 0, 72, 247, 76, 137, 54, 35, 31, 212, 148, 42, 183, 196, 135, 47, 40, 40, 2, 133, 88, 16, 127, 225, 133, 102, 75, 121, 134, 188, 28, 203, 166, 84, 160, 203, 238, 116, 124, 199, 46, 126, 14, 128, 200, 39, 33, 0, 128, 78, 249, 67, 51, 31, 2, 70, 181, 205, 252, 94, 129, 182, 132, 192, 67, 166, 242, 55, 142, 143, 11, 84, 130, 237, 118, 218, 157, 34, 6, 33, 69, 209, 135, 60, 221, 154, 145, 15, 42, 162, 69, 167, 34, 137, 77, 246, 133, 71, 95, 120, 33, 110, 161, 115, 244, 179, 231, 239, 92, 174, 74, 55, 138, 129, 195, 85, 233, 4, 3, 233, 80, 126, 199, 186, 128, 45, 22, 216, 37, 70, 101, 53, 217, 10, 82, 214, 210, 34, 251, 167, 222, 156, 22, 18, 72, 19, 108, 214, 101, 151, 111, 154, 242, 247, 165, 252, 18, 35, 31, 84, 164, 170, 34, 171, 102, 113, 62, 176, 63, 250, 122, 215, 240, 235, 175, 139, 58, 160, 200, 116, 141, 185, 219, 233, 112, 161, 13, 168, 84, 125, 234, 176, 131, 85, 120, 166, 242, 219, 208, 61, 162, 5, 178, 89, 139, 228, 164, 79, 139, 152, 161, 144, 122, 204, 212, 92, 64, 153, 100, 193, 198, 62, 128, 247, 149, 34, 248, 158, 128, 34, 222, 81, 117, 177, 134, 104, 165, 165, 28, 116, 122, 133, 96, 115, 56, 254, 66, 48, 46, 244, 189, 32, 43, 193, 34, 235, 67, 38, 98, 82, 41, 227, 155, 202, 160, 74, 244, 226, 139, 168, 40, 236, 246, 239, 124, 199, 69, 205, 38, 91, 76, 147, 215, 96, 50, 82, 217, 28, 236, 122, 47, 166, 53, 114, 226, 1, 134, 30, 92, 78, 7, 128, 207, 85, 234, 158, 147, 177, 154, 150, 106, 64, 57, 232, 12, 119, 125, 167, 89, 8, 190, 16, 108, 126, 61, 18, 73, 89, 129, 141, 203, 11, 204, 199, 164, 218, 156, 55, 48, 234, 139, 118, 199, 51, 79, 61, 227, 64, 7, 178, 18, 254, 195, 57, 196, 74, 90, 173, 197, 138, 165, 63, 73, 185, 78, 91, 246, 116, 181, 146, 24, 249, 224, 6, 223, 44, 247, 116, 187, 168, 33, 228, 160, 179, 53, 30, 253, 78, 100, 160, 225, 245, 230, 174, 23, 4, 2, 0, 239, 85, 126, 157, 243, 253, 233, 221, 25, 230, 9, 208, 133, 12, 209, 83, 196, 123, 112, 74, 58, 19, 7, 206, 170, 27, 113, 100, 83, 177, 222, 225, 56, 1, 112, 216, 53, 188, 39, 145, 241, 44, 25, 197, 172, 81, 49, 232, 228, 187, 94, 136, 135, 227, 241, 224, 235, 130, 101, 73, 29, 186, 113, 32, 81, 244, 235, 153, 21, 185, 197, 164, 202, 199, 100, 29, 110, 250, 152, 132, 63, 48, 150, 176, 139, 131, 142, 92, 69, 53, 142, 223, 21, 203, 11, 220, 100, 11, 86, 13, 8, 180, 45, 0, 107, 240, 101, 69, 182, 217, 34, 202, 160, 83, 120, 253, 245, 248, 192, 11, 175, 199, 125, 150, 218, 197, 111, 250, 133, 4, 31, 15, 251, 194, 62, 248, 122, 99, 129, 245, 31, 199, 247, 112, 233, 10, 184, 18, 4, 150, 117, 43, 236, 54, 87, 94, 4, 182, 119, 91, 168, 165, 197, 212, 28, 8, 61, 114, 179, 174, 74, 206, 161, 175, 4, 56, 156, 33, 168, 87, 86, 94, 46, 115, 134, 34, 232, 244, 198, 95, 104, 142, 14, 8, 62, 206, 82, 91, 187, 122, 117, 19, 159, 136, 183, 250, 54, 210, 175, 43, 30, 210, 137, 153, 76, 80, 107, 15, 186, 126, 12, 190, 186, 50, 6, 47, 235, 98, 61, 110, 3, 251, 202, 22, 93, 79, 253, 187, 203, 225, 52, 132, 64, 179, 216, 128, 186, 118, 138, 73, 5, 157, 225, 230, 23, 200, 228, 6, 0, 160, 182, 118, 241, 146, 58, 175, 242, 235, 114, 163, 106, 200, 76, 52, 92, 114, 85, 116, 204, 132, 146, 18, 136, 138, 134, 239, 59, 75, 134, 197, 116, 33, 208, 121, 208, 28, 8, 32, 72, 221, 85, 29, 174, 106, 199, 155, 148, 63, 197, 200, 7, 46, 28, 161, 115, 252, 8, 0, 0, 65, 9, 171, 252, 154, 213, 71, 32, 211, 120, 187, 39, 121, 115, 18, 180, 201, 240, 240, 216, 192, 112, 222, 137, 100, 220, 155, 60, 113, 109, 56, 121, 101, 152, 119, 234, 201, 44, 107, 118, 92, 216, 128, 92, 14, 41, 66, 5, 173, 160, 144, 50, 173, 63, 80, 161, 12, 128, 25, 249, 0, 1, 60, 81, 195, 46, 17, 128, 218, 29, 139, 151, 108, 86, 124, 205, 166, 164, 128, 31, 144, 114, 153, 252, 213, 171, 56, 208, 117, 69, 55, 189, 239, 63, 149, 55, 204, 241, 151, 243, 242, 142, 214, 39, 39, 213, 231, 149, 12, 95, 158, 159, 44, 169, 207, 203, 75, 180, 218, 117, 181, 214, 245, 1, 64, 114, 21, 34, 185, 29, 74, 4, 210, 74, 164, 88, 149, 56, 51, 242, 65, 188, 212, 105, 151, 0, 0, 90, 253, 88, 73, 141, 142, 27, 57, 54, 63, 41, 190, 138, 207, 175, 7, 44, 174, 228, 37, 244, 74, 93, 194, 245, 37, 113, 248, 174, 254, 218, 164, 250, 203, 127, 86, 127, 237, 230, 107, 239, 207, 31, 203, 203, 187, 156, 87, 31, 134, 144, 210, 32, 171, 98, 203, 232, 189, 100, 166, 202, 20, 2, 172, 177, 90, 228, 50, 10, 179, 27, 92, 22, 203, 142, 20, 2, 192, 6, 143, 151, 84, 201, 95, 211, 146, 35, 95, 98, 210, 21, 16, 235, 100, 50, 121, 229, 202, 164, 139, 201, 97, 95, 242, 98, 18, 139, 8, 211, 83, 220, 3, 243, 223, 15, 3, 23, 192, 73, 103, 223, 207, 187, 50, 118, 243, 181, 146, 250, 228, 159, 93, 76, 230, 189, 207, 115, 123, 48, 117, 224, 209, 161, 234, 2, 107, 133, 222, 231, 166, 168, 210, 33, 189, 218, 227, 176, 87, 42, 239, 202, 166, 94, 23, 20, 43, 190, 224, 210, 124, 15, 183, 29, 4, 201, 178, 100, 113, 173, 138, 86, 63, 190, 100, 29, 173, 215, 167, 234, 51, 124, 116, 254, 0, 39, 156, 154, 148, 7, 13, 154, 52, 127, 82, 201, 240, 169, 249, 99, 243, 75, 38, 77, 74, 164, 10, 94, 240, 222, 220, 149, 73, 151, 7, 134, 235, 231, 15, 95, 188, 57, 89, 82, 50, 144, 152, 148, 204, 59, 117, 118, 210, 149, 228, 205, 151, 137, 222, 128, 56, 138, 117, 234, 176, 1, 171, 19, 58, 154, 37, 201, 33, 192, 216, 84, 169, 7, 20, 53, 167, 122, 137, 99, 197, 143, 99, 146, 195, 194, 85, 45, 89, 173, 134, 160, 118, 7, 128, 80, 178, 153, 165, 213, 219, 200, 247, 222, 107, 147, 206, 38, 111, 62, 113, 234, 207, 78, 93, 195, 230, 37, 111, 94, 2, 61, 27, 230, 195, 97, 65, 136, 39, 40, 9, 87, 111, 62, 123, 54, 81, 159, 119, 53, 47, 239, 90, 222, 124, 224, 124, 208, 137, 8, 7, 178, 15, 37, 183, 93, 39, 179, 54, 174, 201, 158, 98, 179, 193, 217, 36, 81, 7, 188, 114, 56, 149, 122, 160, 194, 92, 225, 17, 71, 242, 43, 232, 10, 107, 33, 64, 16, 22, 63, 190, 228, 190, 191, 222, 92, 53, 156, 119, 214, 31, 134, 166, 36, 111, 190, 246, 243, 249, 3, 208, 172, 249, 239, 39, 254, 236, 90, 114, 210, 217, 163, 171, 255, 110, 245, 142, 29, 171, 69, 170, 189, 152, 151, 151, 247, 254, 181, 249, 121, 39, 234, 129, 249, 243, 74, 46, 95, 94, 114, 185, 254, 216, 192, 169, 146, 129, 212, 35, 143, 175, 165, 6, 132, 78, 177, 3, 250, 222, 69, 34, 117, 104, 139, 50, 64, 47, 199, 49, 156, 236, 51, 207, 49, 189, 66, 131, 161, 170, 116, 65, 144, 96, 88, 178, 26, 154, 92, 123, 172, 164, 228, 242, 177, 73, 215, 230, 215, 159, 61, 145, 151, 156, 116, 249, 253, 188, 139, 87, 225, 83, 53, 157, 186, 120, 246, 236, 169, 163, 103, 47, 94, 62, 123, 236, 226, 197, 139, 167, 106, 63, 61, 123, 236, 236, 167, 171, 143, 157, 104, 82, 254, 160, 75, 255, 65, 198, 227, 25, 130, 163, 9, 189, 190, 243, 41, 135, 152, 161, 98, 237, 89, 47, 73, 39, 22, 217, 70, 138, 6, 107, 74, 30, 215, 178, 1, 208, 209, 179, 147, 234, 235, 47, 214, 231, 157, 200, 155, 15, 42, 224, 196, 164, 19, 87, 39, 93, 91, 82, 114, 241, 68, 222, 69, 93, 196, 180, 180, 250, 241, 18, 121, 226, 156, 107, 193, 19, 250, 17, 183, 45, 119, 207, 112, 227, 90, 231, 51, 107, 49, 120, 149, 186, 222, 48, 72, 50, 110, 191, 19, 254, 43, 146, 162, 235, 150, 44, 222, 161, 121, 254, 159, 159, 45, 41, 41, 57, 123, 13, 254, 159, 72, 214, 31, 155, 127, 226, 98, 162, 254, 114, 61, 244, 245, 251, 159, 150, 148, 212, 214, 126, 211, 12, 4, 224, 98, 136, 166, 101, 227, 51, 236, 198, 181, 172, 94, 196, 157, 177, 224, 67, 151, 52, 193, 171, 211, 145, 91, 93, 26, 75, 178, 155, 78, 85, 86, 184, 166, 100, 201, 226, 52, 62, 184, 47, 250, 217, 216, 246, 99, 181, 167, 78, 157, 58, 118, 244, 83, 56, 212, 30, 251, 244, 232, 167, 120, 172, 253, 230, 15, 106, 107, 255, 66, 159, 105, 128, 78, 169, 62, 90, 188, 164, 70, 122, 232, 5, 118, 221, 136, 59, 211, 124, 52, 67, 0, 212, 3, 170, 46, 162, 21, 76, 223, 192, 73, 148, 50, 155, 158, 22, 103, 55, 151, 60, 174, 2, 97, 226, 185, 117, 37, 186, 29, 123, 223, 125, 6, 0, 92, 155, 15, 116, 236, 152, 250, 67, 234, 107, 103, 24, 5, 54, 158, 47, 160, 79, 250, 3, 170, 149, 134, 193, 114, 58, 185, 68, 163, 164, 55, 46, 80, 179, 185, 100, 9, 160, 176, 154, 8, 196, 15, 244, 155, 95, 91, 91, 2, 28, 112, 159, 226, 61, 90, 132, 197, 143, 63, 190, 100, 201, 229, 19, 39, 78, 76, 58, 11, 65, 166, 234, 244, 29, 143, 173, 147, 30, 218, 105, 191, 17, 149, 16, 250, 3, 170, 14, 209, 54, 102, 37, 105, 160, 195, 120, 96, 4, 96, 40, 89, 178, 228, 241, 199, 23, 3, 20, 96, 239, 106, 107, 63, 189, 136, 164, 70, 1, 49, 90, 189, 24, 44, 230, 146, 146, 146, 205, 85, 84, 219, 121, 255, 13, 28, 162, 209, 64, 205, 58, 64, 81, 169, 84, 86, 47, 113, 139, 15, 237, 92, 160, 219, 221, 24, 184, 7, 52, 107, 65, 25, 18, 163, 51, 160, 74, 108, 131, 60, 138, 147, 133, 200, 242, 14, 38, 54, 88, 168, 169, 218, 188, 110, 29, 168, 66, 240, 113, 242, 242, 78, 212, 62, 254, 248, 99, 139, 101, 33, 129, 191, 59, 74, 84, 21, 41, 237, 161, 208, 241, 171, 147, 46, 31, 39, 137, 223, 52, 219, 242, 216, 102, 241, 161, 23, 232, 50, 106, 185, 149, 20, 110, 28, 216, 100, 174, 94, 136, 145, 15, 50, 129, 83, 136, 90, 192, 149, 77, 14, 88, 7, 246, 0, 46, 239, 160, 209, 1, 198, 228, 25, 187, 122, 53, 49, 233, 42, 169, 86, 15, 253, 178, 68, 238, 92, 17, 0, 113, 17, 119, 4, 96, 120, 126, 201, 112, 168, 189, 133, 118, 165, 202, 197, 88, 125, 31, 125, 104, 183, 190, 168, 150, 91, 201, 20, 215, 118, 115, 185, 89, 70, 62, 40, 155, 134, 113, 23, 171, 239, 116, 166, 200, 141, 63, 79, 150, 119, 112, 231, 176, 197, 70, 75, 104, 116, 126, 125, 183, 148, 214, 11, 200, 16, 236, 88, 34, 87, 112, 6, 112, 105, 211, 127, 59, 59, 41, 25, 10, 144, 210, 94, 178, 50, 88, 167, 2, 130, 29, 75, 104, 222, 129, 77, 31, 111, 165, 84, 108, 13, 208, 132, 169, 153, 220, 44, 35, 31, 52, 228, 178, 103, 183, 6, 116, 121, 135, 92, 0, 224, 120, 104, 217, 65, 107, 106, 237, 183, 93, 127, 183, 67, 82, 241, 202, 101, 237, 199, 242, 142, 125, 17, 106, 75, 141, 5, 52, 42, 125, 237, 29, 143, 213, 144, 135, 118, 59, 244, 213, 128, 181, 58, 16, 106, 99, 217, 92, 235, 133, 210, 169, 210, 110, 224, 114, 33, 81, 135, 148, 46, 239, 224, 204, 5, 128, 43, 121, 39, 132, 80, 123, 185, 60, 53, 25, 36, 65, 20, 113, 240, 117, 222, 147, 218, 251, 111, 137, 146, 177, 80, 232, 233, 64, 91, 75, 75, 27, 206, 114, 36, 227, 97, 235, 30, 79, 41, 2, 234, 21, 177, 6, 193, 193, 198, 141, 129, 208, 206, 156, 235, 133, 52, 119, 121, 209, 254, 162, 145, 32, 209, 68, 173, 184, 188, 195, 79, 114, 0, 64, 128, 56, 31, 84, 129, 183, 113, 223, 65, 9, 0, 128, 224, 49, 145, 11, 86, 175, 254, 165, 136, 192, 23, 23, 142, 135, 26, 89, 17, 165, 182, 70, 58, 12, 86, 147, 98, 130, 199, 69, 191, 208, 165, 47, 169, 107, 25, 235, 250, 239, 85, 231, 86, 47, 164, 33, 6, 212, 161, 225, 32, 71, 138, 1, 190, 251, 221, 151, 44, 47, 167, 125, 107, 88, 181, 233, 187, 54, 233, 34, 102, 57, 219, 66, 1, 91, 1, 66, 208, 40, 66, 32, 121, 208, 171, 23, 75, 16, 132, 66, 21, 41, 137, 144, 234, 181, 214, 45, 214, 32, 96, 244, 240, 229, 15, 205, 156, 57, 147, 25, 71, 187, 21, 247, 224, 156, 79, 232, 186, 92, 106, 127, 217, 205, 89, 190, 49, 87, 29, 144, 27, 86, 109, 134, 47, 190, 79, 67, 91, 156, 184, 81, 100, 45, 88, 106, 45, 22, 87, 254, 147, 245, 225, 234, 197, 187, 196, 86, 115, 84, 39, 182, 181, 41, 38, 187, 87, 61, 150, 142, 0, 169, 47, 208, 62, 188, 124, 40, 182, 142, 119, 129, 32, 104, 197, 51, 246, 23, 245, 4, 201, 169, 84, 62, 160, 139, 45, 111, 189, 245, 163, 7, 10, 20, 191, 97, 60, 66, 198, 147, 44, 152, 199, 227, 1, 217, 198, 228, 18, 139, 249, 150, 162, 130, 245, 104, 186, 118, 137, 158, 193, 142, 199, 137, 46, 8, 52, 74, 166, 17, 57, 160, 93, 28, 7, 170, 145, 17, 120, 76, 12, 16, 117, 181, 117, 10, 0, 142, 44, 17, 53, 174, 25, 177, 107, 159, 128, 192, 64, 175, 240, 206, 173, 148, 59, 183, 203, 101, 217, 250, 214, 91, 191, 249, 205, 143, 30, 144, 115, 40, 90, 127, 189, 149, 238, 140, 224, 229, 121, 222, 227, 209, 14, 233, 85, 88, 109, 68, 31, 138, 66, 142, 114, 176, 148, 85, 24, 133, 246, 22, 121, 36, 172, 70, 82, 24, 181, 143, 137, 63, 151, 153, 3, 20, 191, 81, 80, 94, 93, 156, 75, 238, 136, 197, 225, 73, 109, 225, 157, 86, 235, 88, 214, 35, 2, 255, 252, 207, 191, 248, 209, 3, 52, 85, 175, 13, 50, 124, 137, 184, 23, 32, 104, 77, 24, 45, 162, 185, 141, 174, 127, 251, 30, 133, 96, 113, 103, 104, 169, 212, 120, 52, 134, 100, 133, 99, 113, 40, 159, 93, 34, 251, 14, 242, 213, 230, 226, 183, 234, 162, 2, 100, 5, 189, 149, 11, 244, 233, 169, 74, 183, 211, 197, 164, 125, 232, 86, 115, 28, 190, 181, 172, 23, 17, 248, 231, 127, 254, 13, 128, 240, 192, 3, 86, 208, 63, 214, 153, 214, 2, 235, 204, 130, 2, 91, 81, 81, 113, 121, 5, 23, 78, 36, 18, 126, 31, 175, 72, 131, 170, 200, 182, 84, 178, 249, 20, 130, 37, 235, 247, 42, 24, 64, 222, 1, 141, 122, 138, 50, 2, 37, 210, 67, 228, 16, 193, 234, 175, 92, 160, 79, 12, 58, 188, 105, 206, 6, 155, 38, 113, 36, 37, 246, 95, 231, 110, 123, 67, 68, 128, 208, 111, 148, 244, 139, 95, 252, 226, 165, 31, 33, 189, 244, 214, 111, 63, 51, 0, 0, 52, 65, 202, 233, 41, 129, 246, 45, 94, 26, 82, 2, 160, 120, 221, 162, 64, 96, 117, 149, 226, 65, 100, 206, 196, 250, 187, 204, 161, 76, 177, 201, 201, 86, 12, 30, 158, 80, 141, 24, 96, 10, 196, 67, 146, 212, 244, 200, 97, 17, 164, 101, 251, 244, 123, 191, 215, 249, 174, 2, 1, 125, 250, 13, 34, 241, 0, 114, 136, 173, 168, 92, 235, 193, 200, 221, 12, 14, 207, 142, 101, 33, 35, 194, 167, 144, 52, 97, 74, 8, 56, 57, 126, 115, 99, 89, 166, 211, 5, 214, 129, 36, 43, 88, 183, 78, 146, 167, 58, 151, 68, 122, 165, 50, 85, 10, 6, 128, 143, 71, 226, 97, 111, 24, 142, 126, 206, 23, 143, 52, 227, 138, 146, 101, 211, 190, 254, 176, 91, 232, 254, 215, 44, 8, 164, 56, 68, 134, 162, 192, 6, 50, 82, 84, 12, 82, 82, 205, 226, 212, 110, 148, 246, 191, 67, 13, 103, 8, 0, 198, 71, 146, 45, 216, 161, 104, 27, 169, 253, 130, 70, 75, 85, 105, 110, 0, 196, 109, 39, 164, 144, 16, 86, 100, 141, 92, 70, 213, 20, 169, 82, 112, 188, 252, 29, 83, 102, 79, 105, 138, 111, 190, 123, 246, 20, 129, 23, 242, 243, 123, 0, 128, 89, 51, 166, 79, 187, 243, 235, 47, 30, 234, 25, 29, 251, 223, 127, 52, 139, 130, 36, 44, 191, 32, 244, 18, 21, 19, 4, 229, 129, 249, 208, 180, 249, 7, 117, 183, 52, 18, 89, 192, 87, 133, 30, 209, 225, 195, 181, 37, 202, 7, 117, 187, 156, 105, 202, 128, 117, 3, 177, 232, 41, 0, 35, 112, 88, 186, 110, 48, 178, 144, 145, 228, 65, 84, 114, 53, 78, 234, 107, 154, 61, 60, 89, 136, 207, 171, 26, 200, 95, 71, 0, 24, 218, 78, 16, 120, 120, 237, 139, 135, 34, 209, 254, 243, 163, 99, 72, 9, 252, 255, 191, 79, 254, 246, 221, 93, 47, 1, 253, 34, 155, 124, 168, 233, 127, 125, 252, 209, 59, 123, 183, 110, 45, 45, 44, 213, 89, 54, 156, 243, 38, 18, 181, 59, 160, 253, 135, 15, 255, 165, 217, 36, 152, 211, 142, 255, 41, 60, 162, 186, 48, 157, 63, 115, 42, 24, 8, 231, 174, 220, 195, 231, 239, 142, 63, 248, 96, 211, 221, 17, 94, 232, 32, 0, 212, 5, 251, 23, 0, 2, 119, 222, 251, 15, 63, 101, 185, 61, 117, 117, 117, 29, 193, 161, 100, 236, 208, 33, 120, 37, 206, 240, 168, 46, 182, 89, 161, 111, 65, 19, 254, 226, 55, 166, 160, 248, 95, 95, 138, 180, 117, 155, 14, 11, 8, 137, 196, 98, 2, 192, 142, 7, 205, 182, 66, 17, 56, 137, 234, 194, 180, 30, 144, 148, 0, 245, 57, 125, 241, 123, 38, 223, 35, 240, 252, 237, 183, 47, 140, 111, 170, 108, 34, 0, 112, 92, 93, 52, 56, 235, 206, 59, 239, 159, 118, 255, 79, 201, 175, 120, 19, 177, 161, 228, 80, 157, 206, 99, 148, 23, 17, 36, 8, 20, 153, 184, 66, 6, 224, 203, 207, 239, 210, 2, 224, 227, 195, 77, 171, 17, 128, 195, 181, 198, 131, 186, 154, 223, 150, 35, 20, 55, 229, 5, 211, 19, 111, 69, 25, 160, 85, 10, 225, 217, 85, 241, 170, 252, 225, 41, 61, 241, 217, 155, 235, 188, 18, 0, 156, 231, 80, 172, 236, 206, 167, 124, 143, 78, 251, 111, 4, 1, 33, 113, 168, 95, 23, 129, 212, 227, 212, 108, 43, 42, 32, 88, 0, 125, 247, 71, 34, 201, 12, 242, 101, 138, 222, 181, 165, 139, 0, 161, 175, 149, 16, 4, 76, 179, 128, 42, 66, 193, 241, 48, 243, 139, 76, 186, 148, 75, 132, 8, 83, 34, 225, 248, 228, 129, 201, 241, 112, 213, 131, 126, 31, 238, 1, 36, 230, 4, 247, 28, 9, 222, 123, 239, 158, 67, 247, 222, 139, 125, 18, 78, 240, 61, 209, 100, 226, 144, 225, 61, 189, 32, 54, 66, 171, 15, 11, 94, 113, 204, 217, 159, 72, 128, 147, 196, 130, 215, 110, 67, 84, 126, 84, 88, 90, 186, 117, 235, 222, 119, 62, 166, 60, 96, 211, 105, 63, 199, 222, 74, 0, 56, 108, 218, 7, 82, 71, 40, 88, 44, 107, 53, 117, 29, 43, 150, 252, 187, 40, 179, 241, 85, 119, 207, 187, 123, 243, 192, 236, 252, 7, 167, 116, 248, 124, 205, 41, 0, 16, 130, 167, 190, 254, 112, 248, 181, 233, 143, 178, 60, 14, 246, 70, 1, 1, 35, 30, 240, 29, 234, 234, 106, 110, 238, 138, 14, 37, 112, 108, 216, 239, 5, 196, 6, 52, 181, 51, 108, 197, 242, 130, 15, 17, 129, 223, 45, 106, 9, 4, 210, 218, 207, 113, 15, 255, 53, 182, 255, 247, 111, 230, 2, 128, 34, 66, 1, 78, 157, 105, 210, 34, 84, 58, 112, 190, 131, 20, 117, 133, 193, 250, 133, 125, 225, 158, 142, 120, 221, 78, 79, 36, 200, 43, 179, 194, 123, 94, 188, 255, 235, 175, 133, 103, 204, 120, 109, 24, 218, 227, 241, 68, 13, 164, 192, 227, 111, 238, 34, 212, 140, 32, 32, 88, 62, 136, 18, 116, 127, 251, 142, 207, 17, 129, 143, 138, 7, 18, 3, 97, 127, 155, 106, 219, 7, 207, 159, 19, 14, 56, 173, 217, 149, 217, 128, 180, 17, 138, 211, 101, 110, 137, 134, 74, 187, 3, 43, 140, 193, 45, 38, 162, 224, 241, 145, 137, 124, 62, 15, 46, 156, 133, 252, 164, 200, 8, 177, 255, 240, 223, 190, 126, 239, 180, 105, 211, 23, 12, 97, 123, 234, 98, 201, 168, 78, 53, 167, 151, 23, 219, 47, 129, 16, 236, 231, 189, 58, 28, 64, 104, 46, 69, 224, 109, 82, 65, 192, 171, 238, 230, 36, 44, 240, 201, 251, 38, 154, 64, 72, 59, 12, 194, 162, 69, 200, 126, 161, 251, 59, 162, 115, 85, 73, 10, 171, 232, 82, 207, 12, 57, 144, 220, 160, 50, 37, 198, 174, 216, 120, 255, 180, 59, 167, 77, 159, 177, 189, 206, 195, 133, 99, 137, 164, 86, 13, 248, 142, 40, 219, 79, 168, 33, 193, 199, 19, 250, 245, 99, 62, 155, 10, 1, 250, 161, 136, 3, 213, 2, 151, 205, 178, 128, 222, 48, 72, 5, 182, 32, 115, 228, 224, 86, 70, 192, 136, 0, 93, 234, 153, 220, 133, 113, 160, 137, 84, 111, 188, 252, 253, 71, 94, 188, 119, 218, 244, 233, 51, 202, 162, 29, 231, 207, 31, 209, 10, 1, 31, 211, 180, 191, 171, 57, 8, 141, 211, 221, 74, 176, 39, 22, 179, 17, 77, 248, 241, 250, 243, 68, 86, 144, 3, 253, 3, 62, 63, 26, 180, 175, 19, 67, 112, 250, 132, 116, 178, 39, 203, 226, 131, 140, 124, 144, 201, 202, 97, 128, 227, 146, 93, 100, 13, 177, 78, 117, 209, 128, 203, 177, 138, 46, 245, 76, 111, 70, 68, 72, 157, 20, 253, 217, 35, 223, 127, 246, 225, 25, 211, 167, 79, 127, 180, 63, 26, 246, 196, 146, 65, 233, 78, 212, 12, 111, 74, 104, 154, 143, 8, 36, 18, 3, 58, 129, 162, 135, 172, 201, 104, 21, 93, 162, 245, 239, 157, 79, 224, 54, 164, 132, 23, 188, 96, 208, 158, 254, 26, 81, 131, 71, 59, 252, 180, 225, 158, 176, 224, 207, 180, 210, 148, 22, 0, 185, 172, 194, 165, 90, 13, 75, 209, 94, 205, 24, 153, 75, 187, 212, 115, 90, 86, 248, 251, 143, 60, 249, 236, 166, 89, 211, 203, 182, 207, 232, 226, 185, 67, 99, 146, 37, 16, 205, 48, 159, 72, 99, 128, 104, 20, 143, 231, 195, 94, 157, 76, 17, 135, 0, 196, 86, 21, 74, 62, 209, 222, 210, 45, 229, 8, 192, 64, 36, 220, 195, 111, 220, 249, 52, 85, 131, 181, 71, 19, 116, 153, 61, 47, 120, 136, 153, 22, 92, 209, 0, 160, 152, 2, 200, 146, 25, 108, 105, 141, 117, 59, 181, 249, 182, 85, 170, 165, 158, 49, 26, 86, 3, 112, 224, 192, 147, 143, 172, 120, 246, 103, 220, 130, 89, 253, 209, 89, 219, 235, 184, 126, 73, 11, 136, 102, 56, 158, 38, 1, 177, 161, 161, 32, 162, 16, 231, 117, 246, 81, 227, 124, 100, 65, 182, 254, 245, 31, 203, 110, 209, 71, 91, 75, 75, 215, 151, 174, 47, 180, 109, 59, 224, 89, 123, 47, 117, 134, 106, 207, 38, 112, 122, 157, 15, 218, 159, 219, 246, 20, 234, 130, 2, 172, 153, 82, 125, 173, 55, 68, 90, 161, 94, 234, 153, 84, 8, 168, 1, 168, 121, 18, 56, 96, 39, 119, 192, 53, 163, 121, 104, 86, 89, 115, 84, 146, 1, 209, 12, 243, 93, 105, 237, 143, 225, 159, 96, 208, 160, 235, 120, 4, 96, 40, 241, 198, 59, 95, 106, 232, 227, 119, 230, 254, 237, 95, 80, 0, 142, 129, 76, 12, 8, 3, 67, 67, 130, 222, 42, 141, 134, 164, 221, 124, 192, 141, 147, 183, 228, 119, 122, 202, 49, 109, 169, 103, 151, 6, 0, 174, 243, 201, 71, 158, 125, 22, 144, 237, 172, 155, 181, 125, 100, 142, 35, 153, 20, 55, 23, 18, 205, 176, 247, 144, 138, 255, 135, 98, 253, 244, 149, 96, 48, 251, 33, 74, 0, 72, 68, 108, 123, 63, 215, 98, 240, 249, 127, 36, 0, 36, 146, 167, 136, 94, 232, 143, 13, 213, 84, 20, 120, 175, 111, 135, 6, 201, 44, 152, 43, 17, 32, 222, 145, 26, 128, 157, 43, 86, 60, 251, 83, 174, 21, 247, 113, 157, 227, 24, 177, 63, 58, 38, 217, 1, 209, 12, 239, 81, 49, 64, 127, 87, 16, 68, 32, 216, 223, 229, 195, 237, 135, 116, 126, 1, 132, 96, 23, 105, 221, 153, 183, 215, 151, 254, 46, 29, 129, 149, 203, 136, 29, 72, 94, 62, 9, 103, 12, 197, 98, 31, 36, 60, 75, 195, 137, 204, 171, 244, 100, 39, 55, 106, 126, 199, 120, 1, 120, 249, 251, 207, 62, 91, 45, 110, 90, 248, 232, 130, 232, 140, 57, 35, 65, 159, 207, 135, 218, 153, 33, 102, 120, 163, 74, 4, 64, 254, 1, 129, 161, 46, 172, 160, 214, 127, 240, 104, 100, 127, 66, 162, 238, 109, 133, 91, 127, 253, 241, 231, 41, 94, 248, 156, 200, 192, 239, 147, 201, 83, 165, 219, 222, 139, 197, 130, 171, 122, 184, 165, 137, 156, 214, 161, 85, 82, 42, 94, 52, 157, 43, 112, 75, 0, 164, 24, 248, 167, 143, 60, 255, 51, 143, 180, 109, 227, 2, 176, 135, 179, 162, 177, 4, 209, 78, 12, 126, 205, 120, 52, 54, 48, 58, 20, 237, 138, 67, 60, 196, 235, 74, 1, 120, 2, 212, 7, 226, 19, 113, 76, 170, 151, 47, 47, 42, 42, 90, 52, 215, 86, 72, 20, 227, 87, 169, 47, 148, 76, 254, 235, 151, 123, 31, 88, 186, 75, 240, 122, 121, 147, 237, 215, 201, 140, 42, 226, 69, 179, 41, 51, 146, 36, 182, 212, 213, 237, 241, 138, 59, 54, 115, 220, 147, 143, 60, 95, 157, 218, 200, 215, 1, 14, 193, 140, 134, 126, 98, 158, 68, 0, 210, 253, 160, 32, 232, 193, 243, 9, 31, 154, 118, 61, 242, 199, 250, 177, 253, 97, 31, 215, 202, 135, 201, 41, 7, 106, 58, 59, 55, 117, 118, 238, 91, 100, 219, 250, 229, 58, 81, 6, 146, 103, 81, 45, 22, 70, 192, 159, 50, 183, 73, 77, 250, 10, 196, 228, 217, 82, 241, 162, 217, 100, 9, 229, 128, 177, 177, 68, 16, 100, 158, 118, 224, 247, 31, 217, 164, 0, 32, 24, 4, 30, 152, 177, 189, 63, 5, 128, 79, 211, 254, 40, 56, 130, 130, 215, 80, 114, 99, 49, 1, 1, 144, 223, 123, 17, 0, 32, 92, 91, 172, 200, 70, 100, 224, 147, 171, 127, 164, 34, 241, 206, 27, 102, 55, 233, 41, 208, 177, 22, 227, 88, 135, 132, 234, 128, 161, 161, 177, 100, 76, 106, 242, 147, 43, 42, 170, 229, 246, 119, 118, 36, 187, 102, 1, 19, 148, 197, 68, 43, 239, 225, 59, 52, 237, 71, 63, 144, 247, 27, 114, 110, 52, 22, 197, 240, 90, 86, 237, 98, 251, 1, 1, 79, 103, 162, 155, 4, 68, 135, 255, 248, 127, 68, 165, 240, 59, 179, 21, 179, 86, 157, 207, 198, 177, 14, 9, 73, 178, 90, 60, 158, 67, 67, 201, 126, 113, 251, 230, 21, 10, 0, 186, 35, 184, 194, 246, 172, 233, 51, 230, 148, 69, 105, 7, 183, 38, 162, 205, 154, 246, 67, 68, 44, 24, 75, 46, 223, 195, 251, 19, 66, 152, 79, 157, 80, 83, 33, 221, 255, 124, 231, 50, 154, 22, 73, 142, 137, 16, 236, 53, 201, 188, 186, 217, 32, 69, 188, 104, 114, 0, 141, 250, 1, 208, 185, 117, 253, 201, 40, 121, 166, 125, 43, 158, 175, 150, 1, 136, 196, 130, 201, 145, 40, 63, 203, 1, 14, 65, 148, 56, 105, 224, 199, 170, 21, 96, 148, 4, 2, 188, 42, 140, 81, 215, 23, 192, 221, 89, 175, 106, 243, 203, 206, 206, 214, 3, 7, 184, 198, 206, 78, 65, 144, 0, 248, 36, 153, 28, 123, 151, 32, 176, 200, 28, 0, 250, 196, 200, 241, 162, 118, 145, 109, 93, 114, 99, 157, 146, 69, 192, 25, 110, 35, 99, 29, 216, 230, 106, 0, 96, 159, 4, 64, 52, 58, 148, 196, 189, 54, 231, 148, 141, 60, 250, 104, 23, 185, 34, 158, 72, 15, 6, 154, 99, 233, 94, 160, 186, 190, 192, 163, 225, 215, 214, 3, 28, 189, 255, 0, 223, 249, 173, 195, 34, 11, 36, 63, 173, 93, 143, 0, 148, 94, 39, 0, 82, 180, 144, 121, 193, 77, 66, 210, 144, 145, 133, 52, 0, 34, 95, 68, 128, 251, 233, 203, 41, 29, 40, 45, 178, 207, 207, 177, 143, 216, 231, 28, 225, 60, 254, 214, 1, 112, 88, 212, 8, 52, 159, 79, 215, 255, 234, 236, 157, 22, 0, 128, 128, 222, 255, 139, 78, 137, 3, 14, 127, 90, 91, 91, 123, 116, 47, 6, 11, 215, 51, 155, 142, 145, 15, 102, 200, 206, 186, 201, 152, 129, 133, 136, 183, 39, 154, 28, 27, 138, 117, 236, 169, 60, 116, 36, 34, 68, 212, 0, 196, 248, 71, 31, 29, 41, 155, 211, 213, 74, 44, 122, 90, 56, 212, 220, 159, 72, 115, 0, 212, 218, 216, 195, 85, 232, 9, 108, 39, 192, 220, 219, 217, 185, 75, 4, 224, 40, 14, 149, 109, 53, 47, 3, 218, 5, 10, 228, 182, 51, 38, 219, 207, 58, 196, 177, 114, 11, 72, 176, 223, 207, 181, 70, 113, 191, 229, 61, 47, 214, 29, 249, 32, 26, 149, 0, 24, 162, 8, 244, 243, 79, 204, 73, 52, 204, 138, 6, 251, 81, 233, 165, 137, 64, 34, 29, 0, 181, 54, 246, 112, 213, 186, 10, 9, 0, 16, 224, 55, 68, 25, 216, 81, 15, 0, 252, 193, 52, 0, 6, 179, 26, 13, 79, 215, 171, 122, 114, 209, 226, 250, 74, 167, 197, 159, 8, 131, 167, 7, 108, 217, 209, 21, 243, 251, 195, 117, 235, 196, 246, 119, 131, 115, 154, 160, 59, 237, 68, 59, 157, 179, 202, 166, 207, 24, 26, 218, 189, 121, 221, 126, 185, 237, 13, 20, 128, 129, 244, 223, 83, 101, 239, 12, 231, 136, 119, 30, 64, 0, 34, 20, 129, 13, 103, 107, 79, 37, 175, 33, 0, 54, 51, 187, 213, 152, 216, 121, 75, 201, 34, 233, 85, 79, 36, 78, 192, 177, 5, 214, 141, 0, 196, 7, 60, 97, 31, 109, 115, 205, 63, 84, 134, 7, 146, 67, 93, 162, 17, 0, 4, 250, 147, 100, 167, 5, 190, 211, 133, 46, 81, 176, 106, 247, 238, 85, 155, 165, 190, 95, 69, 254, 36, 180, 75, 228, 49, 138, 236, 157, 241, 36, 121, 15, 2, 208, 93, 79, 1, 56, 119, 10, 248, 15, 109, 225, 86, 214, 196, 106, 88, 214, 172, 237, 87, 157, 146, 86, 245, 4, 238, 79, 101, 37, 153, 222, 15, 193, 179, 203, 105, 1, 255, 179, 213, 67, 182, 45, 71, 35, 80, 9, 78, 65, 50, 17, 4, 141, 72, 196, 63, 218, 65, 16, 0, 173, 80, 133, 8, 52, 40, 152, 127, 243, 238, 46, 146, 15, 212, 250, 128, 140, 124, 200, 4, 0, 215, 141, 63, 121, 142, 234, 193, 111, 157, 131, 136, 224, 18, 6, 72, 115, 179, 239, 86, 99, 198, 200, 165, 3, 160, 244, 17, 89, 92, 136, 194, 97, 119, 218, 237, 110, 223, 145, 30, 75, 92, 72, 180, 122, 5, 14, 226, 129, 206, 242, 21, 24, 9, 116, 196, 198, 146, 35, 65, 10, 64, 108, 85, 48, 57, 4, 175, 144, 61, 102, 76, 119, 136, 10, 96, 55, 182, 125, 85, 51, 213, 129, 90, 239, 53, 13, 0, 35, 126, 37, 160, 11, 162, 12, 28, 5, 22, 24, 35, 222, 224, 115, 25, 87, 195, 34, 100, 66, 3, 40, 1, 208, 245, 17, 113, 37, 24, 167, 47, 62, 47, 223, 194, 129, 4, 8, 224, 172, 183, 182, 118, 62, 191, 162, 156, 88, 193, 142, 232, 24, 8, 63, 34, 16, 217, 21, 141, 37, 163, 40, 3, 157, 157, 117, 79, 240, 61, 34, 2, 11, 197, 255, 232, 6, 104, 157, 192, 52, 0, 244, 28, 119, 36, 31, 178, 64, 183, 104, 9, 191, 117, 254, 34, 149, 129, 47, 109, 89, 87, 195, 50, 67, 42, 53, 161, 55, 181, 194, 141, 203, 99, 245, 204, 94, 25, 177, 248, 195, 100, 222, 163, 183, 85, 25, 9, 116, 244, 211, 205, 182, 86, 225, 86, 51, 67, 251, 99, 221, 146, 251, 74, 17, 216, 45, 169, 128, 230, 104, 150, 4, 142, 71, 220, 253, 64, 143, 252, 66, 183, 240, 197, 31, 14, 203, 44, 112, 145, 248, 66, 95, 206, 53, 92, 13, 43, 7, 170, 38, 33, 177, 180, 235, 8, 163, 25, 83, 112, 145, 185, 166, 194, 228, 224, 107, 22, 49, 93, 113, 30, 58, 89, 25, 9, 116, 196, 112, 159, 169, 88, 16, 141, 225, 174, 88, 68, 14, 16, 210, 236, 96, 71, 150, 7, 241, 232, 237, 125, 34, 145, 215, 15, 63, 188, 129, 34, 176, 44, 113, 106, 44, 65, 92, 129, 223, 253, 173, 209, 106, 88, 50, 153, 198, 70, 100, 4, 70, 62, 136, 4, 237, 71, 91, 208, 117, 207, 238, 168, 4, 64, 130, 135, 72, 224, 217, 106, 101, 44, 136, 149, 18, 201, 88, 52, 56, 118, 53, 152, 66, 0, 152, 64, 1, 65, 179, 241, 16, 50, 37, 84, 130, 198, 70, 11, 1, 16, 93, 129, 13, 39, 206, 94, 75, 246, 254, 154, 140, 33, 148, 115, 89, 0, 176, 102, 249, 85, 36, 50, 106, 32, 206, 29, 102, 228, 3, 33, 151, 180, 58, 92, 93, 48, 63, 95, 6, 64, 80, 71, 2, 148, 186, 18, 160, 1, 48, 38, 138, 69, 37, 41, 232, 228, 21, 92, 208, 156, 45, 128, 207, 188, 86, 12, 74, 223, 9, 145, 5, 54, 36, 64, 15, 190, 77, 18, 102, 54, 150, 243, 50, 153, 174, 179, 102, 249, 85, 36, 162, 121, 172, 116, 13, 107, 70, 62, 16, 74, 21, 232, 242, 241, 184, 12, 64, 162, 123, 185, 22, 128, 206, 32, 0, 64, 54, 28, 236, 82, 50, 129, 16, 36, 99, 195, 93, 205, 217, 36, 32, 51, 0, 30, 50, 243, 90, 84, 131, 203, 206, 163, 41, 164, 195, 40, 115, 189, 137, 112, 171, 241, 165, 166, 130, 61, 60, 167, 220, 102, 211, 170, 96, 87, 74, 132, 124, 66, 79, 85, 10, 128, 196, 198, 21, 202, 124, 152, 232, 15, 39, 163, 253, 65, 220, 119, 110, 36, 24, 12, 166, 62, 230, 187, 19, 231, 163, 193, 174, 96, 214, 12, 78, 102, 14, 104, 21, 20, 44, 112, 52, 113, 234, 218, 177, 29, 165, 100, 212, 96, 17, 124, 49, 192, 235, 100, 25, 196, 45, 72, 77, 76, 169, 178, 234, 126, 10, 206, 159, 61, 85, 107, 230, 239, 152, 189, 82, 1, 128, 239, 249, 229, 229, 105, 237, 239, 24, 75, 4, 163, 137, 100, 48, 26, 77, 142, 140, 13, 241, 10, 4, 128, 123, 195, 188, 63, 107, 246, 5, 1, 200, 96, 182, 125, 137, 148, 22, 248, 86, 34, 1, 65, 81, 9, 25, 69, 249, 117, 49, 10, 165, 206, 5, 104, 31, 159, 179, 62, 103, 98, 74, 149, 190, 241, 133, 0, 80, 57, 102, 192, 135, 249, 20, 0, 231, 189, 222, 231, 170, 211, 0, 8, 146, 141, 7, 49, 36, 130, 87, 81, 41, 157, 213, 217, 205, 175, 245, 122, 127, 108, 102, 17, 84, 4, 192, 200, 17, 64, 82, 176, 192, 134, 207, 18, 103, 33, 38, 90, 72, 18, 198, 91, 107, 48, 149, 232, 215, 48, 16, 113, 17, 171, 205, 76, 169, 178, 89, 213, 203, 135, 16, 210, 169, 40, 72, 1, 16, 247, 250, 170, 211, 1, 136, 129, 31, 24, 13, 10, 208, 245, 53, 29, 29, 171, 36, 0, 60, 94, 31, 121, 0, 38, 235, 83, 80, 14, 200, 80, 207, 68, 98, 108, 17, 128, 101, 126, 239, 155, 181, 71, 47, 83, 111, 96, 107, 185, 162, 164, 32, 69, 0, 64, 181, 201, 212, 103, 185, 118, 51, 48, 183, 93, 59, 85, 43, 5, 64, 79, 101, 101, 221, 145, 104, 164, 67, 41, 1, 201, 161, 126, 63, 231, 241, 122, 224, 117, 77, 133, 216, 254, 3, 228, 65, 228, 67, 102, 34, 93, 104, 77, 251, 80, 25, 159, 122, 253, 16, 142, 19, 33, 248, 214, 39, 171, 56, 238, 40, 120, 196, 84, 17, 190, 67, 170, 42, 210, 252, 44, 246, 161, 153, 214, 239, 129, 91, 203, 102, 77, 125, 234, 149, 207, 176, 14, 157, 249, 25, 20, 0, 232, 231, 232, 17, 127, 56, 62, 76, 246, 154, 149, 49, 8, 38, 131, 173, 100, 240, 186, 243, 128, 167, 174, 2, 8, 184, 159, 114, 37, 35, 31, 244, 136, 143, 156, 233, 139, 68, 120, 9, 128, 244, 103, 209, 204, 202, 217, 2, 8, 108, 56, 124, 250, 36, 199, 53, 93, 77, 38, 71, 75, 105, 142, 120, 61, 65, 64, 148, 130, 138, 7, 170, 177, 83, 203, 215, 50, 255, 104, 157, 105, 181, 150, 103, 147, 1, 189, 213, 67, 116, 198, 203, 1, 0, 127, 103, 247, 121, 178, 201, 104, 120, 207, 158, 61, 135, 58, 130, 49, 176, 121, 67, 168, 225, 35, 193, 14, 0, 64, 220, 26, 196, 203, 121, 168, 244, 155, 24, 187, 20, 206, 156, 19, 41, 204, 241, 242, 249, 138, 209, 106, 237, 172, 156, 85, 27, 118, 156, 6, 250, 229, 22, 238, 3, 120, 146, 51, 20, 129, 207, 11, 187, 81, 50, 121, 98, 106, 138, 37, 119, 138, 65, 183, 246, 123, 15, 49, 89, 30, 66, 171, 120, 244, 234, 5, 16, 0, 14, 56, 125, 4, 68, 61, 248, 227, 127, 144, 66, 161, 68, 82, 218, 123, 55, 25, 83, 168, 33, 31, 15, 148, 117, 12, 63, 114, 238, 156, 4, 64, 31, 128, 113, 46, 218, 71, 181, 185, 98, 126, 148, 206, 24, 198, 178, 79, 17, 128, 211, 27, 182, 112, 152, 25, 56, 81, 64, 211, 228, 191, 222, 114, 254, 252, 182, 210, 82, 149, 44, 147, 150, 51, 89, 199, 209, 53, 28, 192, 218, 245, 75, 170, 0, 128, 142, 177, 36, 154, 120, 69, 36, 208, 209, 21, 3, 153, 128, 64, 48, 57, 100, 114, 172, 70, 34, 63, 182, 63, 122, 46, 122, 230, 92, 4, 219, 207, 133, 17, 140, 51, 61, 234, 147, 116, 226, 211, 234, 183, 9, 0, 167, 151, 85, 215, 36, 78, 30, 171, 173, 181, 210, 49, 212, 207, 75, 145, 25, 62, 87, 38, 202, 24, 249, 144, 145, 216, 244, 8, 68, 191, 255, 9, 0, 67, 164, 253, 7, 86, 60, 169, 52, 2, 88, 177, 146, 0, 11, 192, 251, 114, 89, 249, 48, 34, 246, 253, 25, 145, 1, 56, 207, 25, 124, 25, 225, 84, 14, 152, 110, 124, 250, 30, 69, 160, 138, 91, 133, 11, 22, 212, 47, 250, 181, 98, 32, 253, 215, 229, 169, 139, 25, 249, 144, 153, 210, 0, 48, 154, 77, 204, 90, 246, 116, 209, 113, 33, 140, 4, 106, 82, 0, 28, 240, 144, 48, 185, 231, 208, 17, 243, 60, 224, 239, 59, 23, 147, 229, 255, 220, 57, 84, 130, 194, 153, 230, 186, 14, 4, 0, 87, 108, 177, 187, 20, 173, 72, 175, 121, 91, 181, 131, 34, 176, 165, 224, 77, 204, 17, 191, 95, 92, 168, 24, 72, 95, 158, 94, 17, 146, 107, 250, 156, 53, 154, 155, 85, 105, 129, 136, 143, 104, 253, 138, 21, 207, 111, 98, 21, 38, 176, 21, 66, 53, 33, 220, 115, 228, 136, 233, 202, 157, 72, 20, 27, 221, 23, 197, 46, 223, 222, 16, 62, 132, 53, 59, 117, 118, 187, 195, 33, 62, 2, 86, 173, 74, 43, 64, 203, 135, 20, 173, 58, 76, 0, 216, 245, 215, 92, 61, 120, 3, 201, 15, 216, 20, 19, 160, 12, 168, 3, 128, 204, 75, 68, 105, 200, 229, 48, 250, 198, 105, 25, 195, 33, 17, 80, 126, 47, 63, 242, 228, 179, 63, 83, 134, 2, 94, 193, 239, 241, 241, 71, 122, 76, 214, 172, 180, 246, 33, 227, 115, 100, 153, 245, 86, 182, 210, 225, 194, 150, 179, 78, 123, 144, 85, 172, 111, 35, 173, 1, 199, 200, 7, 5, 45, 251, 4, 218, 255, 201, 239, 55, 172, 242, 28, 189, 150, 76, 94, 107, 226, 158, 183, 237, 21, 235, 41, 62, 90, 196, 170, 57, 192, 56, 201, 160, 71, 172, 211, 112, 174, 9, 107, 233, 32, 174, 77, 231, 129, 239, 211, 242, 40, 232, 122, 9, 128, 156, 244, 159, 40, 253, 125, 135, 28, 78, 178, 234, 52, 46, 255, 90, 233, 178, 151, 53, 171, 24, 200, 69, 22, 69, 118, 56, 159, 192, 55, 76, 250, 61, 14, 64, 251, 193, 35, 186, 239, 222, 154, 95, 190, 151, 76, 94, 109, 130, 118, 46, 95, 62, 151, 50, 65, 193, 76, 117, 81, 152, 53, 203, 243, 40, 57, 196, 173, 55, 97, 82, 34, 75, 39, 125, 66, 246, 145, 71, 158, 125, 86, 124, 14, 17, 129, 44, 63, 161, 164, 214, 62, 0, 160, 15, 0, 104, 112, 116, 69, 35, 132, 103, 80, 230, 88, 71, 115, 36, 130, 85, 70, 78, 186, 16, 115, 37, 93, 146, 27, 254, 232, 79, 149, 216, 117, 154, 56, 197, 255, 249, 222, 42, 136, 140, 147, 151, 201, 34, 84, 207, 125, 68, 17, 248, 184, 112, 145, 18, 2, 171, 209, 163, 208, 97, 133, 135, 190, 151, 58, 217, 237, 200, 52, 215, 72, 170, 17, 194, 26, 209, 159, 138, 175, 69, 14, 200, 220, 102, 37, 17, 219, 127, 38, 118, 46, 184, 221, 190, 253, 92, 95, 68, 112, 224, 66, 95, 206, 237, 107, 153, 200, 153, 136, 189, 172, 161, 199, 229, 112, 145, 249, 95, 164, 217, 248, 100, 78, 29, 153, 132, 223, 171, 22, 115, 3, 95, 187, 31, 60, 194, 177, 99, 181, 187, 241, 243, 69, 82, 149, 221, 231, 133, 138, 110, 45, 54, 146, 1, 58, 172, 96, 83, 228, 84, 141, 12, 32, 37, 9, 128, 151, 1, 128, 151, 197, 215, 7, 48, 25, 97, 126, 243, 163, 86, 236, 250, 115, 193, 134, 237, 142, 50, 135, 195, 1, 29, 30, 105, 112, 108, 47, 43, 179, 55, 56, 252, 168, 23, 206, 52, 160, 19, 232, 112, 176, 149, 84, 21, 145, 137, 112, 96, 15, 100, 197, 212, 218, 215, 231, 231, 248, 62, 172, 77, 119, 46, 164, 8, 252, 197, 195, 77, 215, 142, 45, 91, 182, 140, 204, 230, 45, 159, 43, 21, 152, 109, 53, 161, 252, 53, 195, 10, 238, 204, 53, 229, 18, 0, 207, 2, 0, 255, 67, 249, 133, 233, 205, 143, 122, 160, 141, 205, 208, 228, 221, 117, 145, 51, 29, 101, 14, 103, 228, 76, 20, 154, 220, 16, 117, 148, 117, 81, 104, 136, 67, 224, 74, 49, 189, 219, 233, 116, 57, 237, 169, 79, 124, 224, 43, 156, 57, 215, 135, 30, 163, 243, 254, 175, 137, 8, 60, 177, 159, 76, 46, 36, 197, 237, 220, 242, 82, 209, 36, 170, 227, 59, 189, 128, 71, 179, 201, 70, 6, 5, 168, 2, 96, 5, 26, 1, 229, 23, 102, 23, 154, 70, 237, 215, 188, 189, 57, 122, 238, 76, 95, 95, 15, 91, 233, 239, 139, 110, 183, 59, 42, 215, 70, 250, 182, 67, 203, 225, 203, 24, 89, 182, 153, 85, 61, 44, 46, 178, 73, 38, 133, 186, 113, 50, 187, 0, 119, 232, 106, 112, 190, 214, 5, 74, 211, 125, 255, 127, 166, 8, 220, 250, 148, 19, 218, 127, 153, 234, 1, 142, 93, 100, 35, 170, 96, 175, 202, 5, 174, 208, 25, 36, 5, 239, 242, 57, 171, 114, 88, 193, 145, 101, 82, 129, 4, 0, 53, 2, 105, 0, 152, 136, 188, 209, 248, 7, 237, 209, 115, 61, 2, 196, 61, 108, 165, 243, 80, 7, 206, 80, 217, 184, 137, 139, 136, 118, 33, 170, 119, 21, 107, 39, 171, 111, 131, 165, 116, 56, 92, 125, 219, 203, 236, 101, 219, 241, 29, 230, 106, 239, 253, 47, 34, 2, 143, 218, 107, 65, 21, 94, 251, 64, 188, 228, 185, 173, 162, 71, 164, 188, 141, 118, 109, 90, 112, 48, 173, 140, 114, 88, 193, 46, 110, 4, 98, 8, 0, 21, 114, 133, 17, 16, 201, 76, 209, 145, 31, 2, 191, 232, 185, 134, 178, 32, 225, 114, 14, 55, 31, 112, 56, 220, 149, 78, 215, 83, 224, 231, 118, 40, 28, 194, 116, 194, 137, 60, 116, 233, 117, 28, 163, 114, 108, 7, 253, 129, 11, 244, 147, 185, 163, 211, 36, 4, 54, 98, 108, 156, 76, 74, 8, 216, 244, 134, 207, 53, 245, 130, 16, 41, 226, 226, 27, 140, 252, 129, 203, 105, 16, 5, 137, 74, 206, 178, 118, 83, 15, 248, 58, 74, 35, 32, 82, 150, 205, 143, 90, 59, 34, 125, 49, 108, 96, 179, 35, 122, 46, 162, 106, 165, 219, 69, 252, 92, 136, 4, 163, 125, 18, 52, 106, 114, 59, 20, 51, 58, 201, 94, 37, 14, 167, 179, 18, 193, 112, 185, 28, 83, 239, 19, 17, 216, 212, 132, 214, 48, 121, 148, 86, 119, 47, 210, 42, 1, 221, 109, 5, 136, 195, 192, 40, 63, 209, 103, 1, 81, 201, 89, 58, 234, 48, 120, 105, 85, 24, 1, 137, 24, 157, 181, 186, 100, 226, 203, 28, 246, 237, 196, 242, 219, 29, 13, 231, 116, 199, 200, 57, 254, 181, 142, 142, 115, 61, 154, 175, 56, 140, 141, 85, 43, 89, 224, 104, 181, 211, 141, 79, 233, 118, 58, 42, 43, 239, 76, 33, 112, 49, 153, 60, 85, 91, 75, 16, 208, 5, 64, 39, 247, 57, 83, 250, 249, 44, 36, 42, 57, 203, 185, 142, 62, 8, 94, 127, 150, 110, 4, 184, 204, 113, 151, 191, 204, 222, 28, 41, 3, 159, 191, 172, 204, 105, 119, 234, 12, 15, 224, 117, 236, 19, 206, 237, 93, 0, 64, 214, 169, 77, 44, 56, 10, 226, 73, 61, 188, 175, 149, 187, 83, 244, 7, 110, 221, 89, 119, 150, 172, 222, 182, 31, 190, 121, 254, 99, 115, 85, 84, 218, 199, 118, 233, 46, 110, 45, 42, 57, 203, 185, 158, 158, 51, 2, 175, 49, 2, 186, 119, 146, 201, 191, 187, 161, 185, 140, 181, 59, 65, 153, 243, 29, 187, 245, 236, 12, 94, 199, 58, 236, 101, 193, 8, 167, 251, 243, 106, 114, 87, 98, 247, 123, 234, 234, 26, 26, 182, 219, 65, 156, 238, 164, 147, 41, 118, 220, 202, 122, 176, 122, 166, 246, 242, 9, 15, 87, 141, 14, 209, 214, 172, 183, 210, 187, 187, 174, 33, 20, 149, 156, 5, 133, 220, 163, 53, 2, 92, 70, 0, 58, 202, 206, 0, 111, 87, 210, 53, 169, 140, 167, 239, 1, 103, 55, 235, 195, 175, 71, 158, 237, 101, 13, 205, 13, 46, 59, 250, 178, 19, 40, 2, 37, 203, 88, 14, 162, 99, 16, 132, 179, 77, 28, 154, 129, 95, 107, 7, 26, 171, 179, 77, 162, 101, 13, 6, 83, 169, 146, 179, 128, 144, 243, 125, 94, 141, 17, 200, 72, 188, 35, 72, 179, 28, 198, 63, 202, 162, 203, 135, 118, 206, 120, 41, 252, 116, 242, 159, 59, 35, 244, 113, 236, 51, 118, 240, 101, 215, 254, 121, 137, 56, 92, 82, 205, 237, 63, 134, 170, 240, 234, 7, 152, 28, 250, 152, 218, 65, 207, 219, 89, 72, 113, 91, 227, 133, 213, 24, 84, 114, 22, 6, 247, 70, 255, 25, 0, 240, 83, 131, 211, 180, 196, 55, 52, 159, 203, 12, 128, 3, 181, 26, 110, 198, 101, 24, 135, 235, 81, 43, 137, 63, 159, 118, 61, 221, 211, 225, 103, 111, 221, 65, 71, 11, 190, 85, 193, 213, 157, 34, 25, 202, 165, 41, 59, 248, 118, 247, 133, 75, 25, 232, 66, 119, 10, 1, 214, 56, 20, 98, 240, 96, 97, 34, 62, 117, 36, 144, 157, 58, 202, 208, 252, 101, 203, 19, 184, 156, 196, 213, 113, 186, 42, 115, 42, 117, 0, 223, 171, 174, 239, 76, 31, 235, 16, 231, 87, 31, 254, 214, 74, 142, 251, 0, 60, 130, 83, 4, 128, 185, 139, 158, 135, 219, 189, 157, 177, 253, 128, 64, 10, 128, 12, 43, 235, 49, 120, 176, 144, 163, 38, 18, 200, 68, 126, 112, 124, 98, 193, 51, 244, 141, 164, 0, 244, 27, 201, 186, 221, 24, 2, 131, 28, 144, 181, 143, 43, 77, 204, 100, 1, 199, 203, 221, 115, 46, 118, 38, 82, 247, 147, 122, 113, 220, 120, 21, 199, 53, 157, 189, 92, 187, 80, 140, 139, 183, 46, 98, 223, 206, 220, 254, 75, 151, 222, 174, 144, 30, 200, 97, 188, 222, 48, 105, 58, 117, 133, 159, 212, 49, 2, 74, 234, 233, 233, 59, 87, 86, 86, 22, 36, 189, 222, 188, 29, 195, 95, 158, 104, 23, 220, 80, 198, 137, 17, 103, 70, 102, 119, 211, 93, 16, 92, 70, 203, 236, 43, 9, 125, 47, 247, 153, 190, 136, 155, 173, 92, 32, 149, 14, 128, 42, 4, 93, 88, 34, 207, 58, 90, 148, 29, 128, 98, 209, 73, 206, 186, 180, 34, 5, 64, 207, 8, 40, 72, 32, 78, 125, 164, 185, 172, 25, 172, 186, 223, 14, 126, 3, 58, 120, 96, 4, 64, 195, 147, 121, 171, 208, 176, 44, 186, 14, 60, 28, 167, 221, 196, 58, 143, 212, 247, 226, 207, 96, 233, 137, 235, 190, 13, 146, 24, 128, 242, 175, 169, 95, 42, 1, 240, 142, 8, 64, 119, 119, 247, 201, 75, 151, 122, 155, 122, 241, 205, 96, 231, 165, 94, 248, 160, 187, 151, 114, 64, 193, 34, 91, 233, 214, 194, 69, 11, 244, 23, 110, 76, 3, 64, 27, 9, 168, 136, 151, 242, 188, 125, 142, 230, 62, 161, 161, 225, 12, 25, 244, 65, 35, 151, 203, 178, 46, 232, 238, 153, 178, 8, 140, 124, 112, 45, 147, 120, 96, 3, 78, 247, 124, 179, 80, 202, 14, 81, 0, 6, 191, 114, 215, 93, 63, 188, 212, 125, 219, 95, 221, 214, 13, 239, 254, 230, 43, 151, 106, 238, 186, 235, 174, 175, 84, 17, 0, 108, 123, 201, 153, 239, 175, 203, 182, 155, 9, 1, 32, 179, 17, 192, 176, 14, 58, 61, 22, 59, 215, 215, 12, 109, 46, 235, 19, 67, 252, 172, 145, 102, 58, 85, 58, 76, 77, 103, 99, 228, 3, 142, 153, 81, 0, 14, 147, 128, 192, 179, 255, 237, 111, 144, 28, 41, 5, 224, 228, 29, 120, 252, 171, 170, 75, 111, 223, 117, 233, 82, 211, 93, 183, 225, 187, 222, 219, 6, 9, 0, 20, 169, 238, 55, 247, 127, 249, 78, 230, 250, 99, 2, 64, 102, 35, 128, 13, 38, 97, 15, 230, 61, 236, 219, 207, 192, 123, 129, 196, 82, 110, 123, 101, 150, 132, 81, 250, 160, 138, 203, 97, 98, 201, 95, 70, 62, 112, 116, 220, 20, 0, 120, 243, 244, 233, 38, 100, 130, 58, 58, 116, 76, 1, 232, 188, 163, 24, 204, 1, 116, 255, 224, 109, 151, 122, 239, 232, 253, 10, 126, 246, 195, 121, 151, 82, 0, 156, 124, 243, 205, 127, 193, 234, 211, 140, 225, 48, 30, 50, 27, 1, 225, 92, 80, 12, 237, 207, 52, 59, 28, 93, 208, 253, 81, 116, 36, 129, 165, 157, 217, 18, 70, 218, 81, 37, 205, 62, 117, 25, 136, 60, 119, 245, 183, 54, 108, 216, 80, 242, 254, 233, 211, 167, 137, 57, 176, 41, 0, 232, 253, 225, 202, 187, 238, 184, 116, 27, 8, 253, 87, 46, 221, 213, 116, 9, 1, 184, 112, 91, 111, 10, 128, 223, 109, 126, 243, 36, 57, 221, 154, 225, 71, 8, 0, 89, 140, 0, 116, 249, 153, 62, 204, 238, 156, 11, 118, 149, 69, 34, 231, 130, 61, 24, 74, 64, 247, 103, 77, 24, 105, 1, 96, 215, 226, 142, 100, 36, 117, 235, 206, 166, 56, 69, 90, 184, 108, 179, 125, 59, 25, 52, 57, 122, 128, 91, 244, 121, 10, 0, 164, 175, 244, 222, 209, 13, 124, 255, 246, 109, 63, 252, 225, 87, 86, 94, 186, 180, 242, 175, 68, 43, 240, 229, 231, 165, 139, 158, 47, 127, 168, 84, 30, 88, 201, 8, 64, 22, 35, 64, 9, 60, 85, 224, 132, 51, 103, 98, 68, 1, 60, 5, 65, 118, 246, 132, 145, 22, 0, 12, 65, 158, 177, 59, 158, 217, 84, 233, 116, 178, 166, 116, 72, 37, 186, 149, 39, 232, 184, 217, 233, 63, 44, 35, 203, 114, 136, 74, 16, 152, 224, 43, 23, 254, 230, 135, 151, 170, 238, 234, 94, 183, 110, 29, 170, 63, 162, 14, 137, 18, 92, 206, 226, 234, 201, 236, 34, 98, 59, 11, 53, 119, 197, 181, 153, 128, 123, 31, 126, 130, 0, 240, 8, 157, 50, 157, 133, 34, 56, 208, 201, 247, 69, 207, 181, 226, 146, 36, 14, 51, 179, 212, 180, 0, 144, 40, 156, 125, 2, 130, 4, 187, 9, 167, 128, 60, 42, 154, 207, 147, 167, 37, 58, 252, 128, 173, 180, 144, 2, 80, 117, 219, 29, 183, 85, 129, 244, 223, 65, 155, 13, 34, 80, 117, 151, 228, 7, 112, 210, 250, 249, 132, 101, 52, 81, 244, 253, 95, 173, 169, 217, 127, 203, 137, 19, 223, 254, 54, 2, 96, 58, 18, 8, 99, 0, 176, 187, 3, 58, 197, 101, 110, 183, 108, 125, 0, 128, 115, 88, 135, 41, 167, 128, 80, 37, 180, 100, 11, 25, 55, 163, 244, 89, 231, 50, 81, 4, 46, 244, 74, 156, 160, 113, 132, 240, 66, 162, 111, 73, 30, 165, 148, 251, 153, 136, 118, 77, 83, 211, 7, 39, 78, 156, 45, 249, 11, 136, 49, 111, 73, 38, 187, 102, 89, 184, 64, 224, 229, 71, 254, 251, 190, 125, 166, 118, 43, 7, 39, 61, 226, 119, 80, 61, 198, 100, 74, 24, 25, 2, 32, 167, 26, 95, 52, 161, 13, 165, 245, 95, 192, 203, 100, 87, 41, 32, 56, 157, 221, 19, 196, 139, 201, 118, 143, 239, 124, 249, 229, 191, 252, 185, 197, 114, 235, 254, 15, 78, 156, 58, 90, 82, 242, 181, 178, 228, 208, 212, 169, 142, 163, 255, 161, 182, 246, 44, 0, 16, 155, 106, 9, 133, 66, 251, 214, 188, 186, 239, 87, 242, 186, 159, 153, 8, 93, 194, 136, 35, 195, 0, 103, 86, 0, 20, 156, 3, 194, 237, 204, 28, 44, 139, 38, 83, 76, 59, 108, 233, 148, 33, 200, 30, 12, 157, 61, 246, 194, 15, 234, 143, 157, 61, 75, 204, 230, 222, 251, 190, 153, 220, 30, 76, 206, 250, 249, 215, 254, 195, 15, 110, 25, 154, 213, 21, 180, 124, 10, 0, 156, 154, 48, 150, 28, 154, 136, 0, 188, 10, 0, 224, 10, 128, 217, 119, 65, 140, 40, 194, 224, 113, 2, 160, 228, 28, 136, 148, 204, 56, 135, 114, 2, 117, 213, 6, 17, 130, 15, 179, 133, 195, 221, 201, 19, 63, 248, 121, 237, 169, 100, 55, 169, 58, 92, 248, 131, 255, 148, 116, 148, 37, 23, 252, 228, 47, 255, 178, 246, 155, 13, 211, 167, 151, 53, 3, 0, 245, 239, 207, 154, 184, 96, 194, 189, 8, 192, 43, 0, 128, 185, 141, 13, 90, 207, 164, 234, 93, 174, 3, 0, 197, 117, 149, 68, 21, 24, 36, 149, 196, 134, 167, 226, 57, 118, 225, 178, 63, 80, 4, 50, 231, 67, 186, 199, 146, 255, 19, 0, 248, 52, 73, 43, 111, 151, 254, 245, 173, 99, 13, 115, 146, 101, 223, 190, 239, 107, 181, 223, 156, 245, 147, 91, 127, 50, 189, 230, 38, 188, 231, 253, 95, 125, 152, 67, 0, 214, 172, 217, 183, 207, 228, 78, 184, 125, 173, 17, 221, 52, 175, 1, 101, 7, 128, 174, 42, 91, 169, 171, 15, 196, 65, 173, 180, 145, 212, 3, 59, 78, 43, 233, 147, 195, 245, 157, 187, 150, 45, 92, 181, 5, 44, 91, 77, 211, 254, 55, 235, 235, 143, 93, 188, 124, 249, 234, 213, 159, 255, 160, 182, 246, 211, 107, 165, 226, 184, 234, 127, 220, 59, 243, 166, 205, 183, 126, 245, 153, 9, 220, 19, 247, 114, 15, 127, 245, 254, 84, 248, 14, 0, 4, 214, 128, 14, 84, 46, 244, 197, 53, 182, 27, 73, 67, 142, 43, 124, 152, 0, 0, 139, 103, 92, 14, 93, 73, 192, 205, 50, 200, 110, 64, 106, 170, 88, 182, 225, 247, 167, 211, 233, 147, 63, 156, 252, 229, 134, 101, 203, 182, 120, 86, 173, 90, 181, 165, 198, 83, 83, 245, 147, 205, 53, 64, 54, 49, 126, 252, 234, 151, 95, 222, 241, 231, 204, 83, 236, 211, 154, 233, 120, 0, 192, 175, 214, 188, 178, 239, 87, 82, 139, 27, 165, 197, 243, 233, 170, 225, 1, 67, 40, 76, 145, 169, 2, 43, 178, 224, 157, 174, 42, 192, 96, 222, 169, 99, 45, 217, 45, 7, 126, 171, 129, 64, 134, 226, 232, 225, 223, 118, 239, 90, 118, 223, 178, 42, 0, 163, 32, 181, 130, 205, 59, 140, 238, 116, 60, 139, 100, 4, 136, 10, 104, 108, 9, 4, 90, 26, 197, 157, 177, 189, 141, 109, 109, 129, 212, 178, 160, 184, 127, 132, 118, 93, 188, 27, 0, 0, 137, 148, 221, 110, 157, 40, 9, 139, 108, 12, 18, 58, 21, 203, 150, 29, 54, 196, 224, 244, 233, 79, 127, 254, 79, 20, 140, 151, 10, 183, 138, 121, 148, 143, 172, 11, 171, 117, 166, 227, 89, 36, 35, 208, 40, 238, 144, 205, 109, 82, 237, 12, 238, 85, 47, 141, 44, 66, 97, 118, 39, 24, 211, 37, 118, 46, 169, 150, 74, 69, 118, 123, 101, 134, 253, 82, 182, 108, 233, 252, 195, 39, 6, 0, 188, 255, 243, 247, 229, 215, 191, 221, 69, 108, 97, 233, 39, 191, 255, 195, 7, 245, 16, 89, 117, 119, 195, 97, 195, 135, 226, 209, 34, 26, 129, 128, 184, 65, 56, 18, 89, 16, 223, 219, 6, 127, 60, 45, 235, 3, 102, 123, 91, 143, 114, 169, 49, 212, 225, 117, 220, 51, 38, 163, 153, 172, 94, 181, 108, 89, 73, 247, 201, 223, 107, 112, 248, 167, 159, 159, 82, 188, 35, 170, 112, 239, 250, 127, 213, 131, 202, 66, 141, 64, 64, 177, 60, 182, 204, 236, 129, 150, 64, 104, 95, 233, 191, 23, 0, 227, 216, 46, 78, 186, 114, 213, 194, 101, 203, 90, 171, 54, 236, 56, 44, 41, 199, 19, 10, 6, 0, 18, 215, 51, 219, 186, 94, 23, 0, 98, 4, 168, 238, 11, 52, 182, 164, 51, 123, 142, 143, 146, 86, 87, 147, 17, 128, 198, 64, 91, 118, 223, 211, 60, 49, 108, 245, 150, 31, 238, 252, 199, 101, 64, 157, 251, 127, 80, 127, 88, 97, 41, 118, 73, 203, 153, 149, 106, 33, 176, 16, 35, 176, 147, 112, 62, 110, 239, 162, 106, 191, 66, 212, 205, 173, 202, 145, 94, 87, 147, 17, 0, 175, 98, 247, 137, 27, 64, 170, 130, 134, 234, 45, 91, 86, 113, 53, 4, 140, 247, 54, 28, 62, 188, 116, 175, 84, 104, 85, 186, 75, 3, 192, 190, 53, 175, 112, 191, 2, 77, 23, 240, 166, 107, 187, 84, 251, 43, 76, 110, 19, 156, 94, 87, 147, 89, 4, 2, 57, 237, 192, 158, 149, 82, 81, 134, 122, 129, 122, 182, 98, 203, 150, 10, 171, 92, 107, 86, 186, 158, 168, 191, 110, 241, 104, 1, 35, 240, 202, 190, 246, 0, 23, 106, 107, 76, 147, 129, 113, 60, 67, 122, 93, 77, 54, 0, 188, 28, 215, 22, 16, 235, 241, 204, 151, 165, 25, 254, 186, 20, 101, 232, 213, 69, 149, 203, 149, 183, 159, 151, 46, 34, 37, 75, 44, 57, 90, 66, 175, 172, 0, 43, 40, 234, 192, 64, 203, 38, 89, 252, 205, 239, 122, 150, 162, 244, 186, 154, 236, 34, 80, 46, 47, 54, 107, 186, 44, 205, 144, 24, 249, 160, 191, 137, 147, 77, 134, 160, 48, 149, 35, 179, 16, 35, 192, 114, 162, 18, 132, 235, 169, 62, 108, 131, 174, 25, 199, 51, 164, 165, 73, 50, 2, 128, 91, 212, 216, 222, 144, 89, 205, 108, 89, 154, 49, 49, 226, 193, 229, 54, 200, 53, 61, 111, 251, 72, 134, 64, 42, 183, 178, 4, 0, 128, 87, 65, 4, 218, 229, 39, 9, 72, 182, 112, 156, 15, 161, 72, 147, 100, 145, 241, 64, 160, 218, 35, 27, 155, 113, 172, 133, 165, 253, 109, 114, 200, 48, 28, 152, 130, 224, 119, 115, 233, 239, 88, 246, 97, 36, 112, 156, 184, 123, 180, 197, 237, 215, 161, 3, 210, 67, 157, 44, 0, 224, 98, 188, 30, 178, 43, 89, 227, 184, 214, 194, 210, 167, 204, 165, 161, 207, 217, 164, 33, 198, 66, 50, 102, 100, 121, 121, 205, 43, 175, 254, 10, 52, 126, 74, 235, 7, 254, 221, 0, 16, 9, 126, 11, 21, 161, 169, 44, 163, 9, 202, 82, 26, 154, 130, 160, 20, 17, 176, 128, 17, 120, 181, 5, 122, 32, 197, 241, 84, 29, 180, 175, 63, 168, 185, 52, 251, 2, 47, 227, 2, 160, 69, 228, 61, 198, 68, 150, 49, 59, 153, 72, 182, 47, 167, 11, 62, 227, 52, 117, 206, 242, 202, 154, 159, 66, 147, 21, 249, 64, 209, 16, 182, 180, 104, 1, 200, 190, 192, 75, 26, 153, 180, 243, 45, 212, 18, 50, 242, 225, 122, 200, 204, 94, 131, 220, 162, 189, 210, 128, 137, 101, 205, 154, 103, 95, 85, 5, 184, 200, 0, 123, 245, 21, 160, 92, 137, 109, 182, 230, 35, 35, 0, 154, 13, 194, 25, 249, 112, 61, 148, 65, 5, 74, 244, 188, 173, 116, 235, 151, 226, 128, 9, 2, 176, 15, 85, 160, 152, 248, 160, 221, 95, 168, 235, 3, 236, 92, 203, 206, 44, 32, 202, 186, 194, 106, 110, 233, 191, 76, 0, 136, 19, 219, 216, 20, 14, 140, 124, 208, 57, 219, 172, 155, 100, 130, 1, 82, 147, 145, 182, 202, 0, 112, 180, 253, 141, 162, 9, 56, 168, 224, 0, 182, 162, 192, 106, 125, 0, 159, 118, 83, 206, 11, 188, 152, 16, 129, 34, 115, 19, 192, 76, 187, 73, 58, 243, 163, 211, 104, 121, 106, 165, 103, 194, 1, 43, 20, 35, 227, 41, 71, 152, 178, 0, 139, 155, 184, 23, 20, 225, 42, 166, 44, 173, 94, 201, 109, 129, 151, 27, 231, 235, 155, 117, 147, 12, 118, 113, 84, 82, 169, 220, 126, 44, 186, 179, 172, 81, 141, 140, 183, 75, 94, 128, 94, 42, 80, 84, 209, 140, 249, 199, 54, 6, 192, 6, 200, 154, 191, 143, 105, 55, 41, 203, 244, 16, 164, 114, 226, 10, 189, 243, 206, 71, 191, 46, 69, 135, 24, 57, 64, 53, 50, 30, 200, 228, 4, 48, 242, 193, 28, 221, 56, 14, 48, 235, 38, 185, 179, 106, 128, 229, 104, 2, 11, 171, 171, 233, 250, 42, 0, 64, 218, 200, 184, 2, 0, 114, 74, 185, 92, 143, 93, 94, 164, 6, 192, 196, 66, 224, 55, 48, 220, 53, 229, 38, 233, 101, 86, 185, 180, 217, 95, 68, 5, 164, 198, 203, 17, 0, 61, 14, 104, 199, 36, 64, 117, 81, 129, 85, 177, 58, 165, 205, 198, 224, 31, 134, 190, 51, 51, 119, 81, 23, 0, 157, 26, 127, 51, 196, 152, 112, 147, 244, 156, 192, 198, 52, 113, 174, 64, 27, 176, 87, 238, 244, 52, 29, 32, 110, 149, 25, 90, 127, 112, 169, 181, 160, 184, 32, 109, 191, 207, 34, 101, 155, 43, 76, 88, 66, 93, 0, 114, 89, 67, 94, 65, 140, 124, 48, 36, 221, 25, 194, 141, 233, 121, 39, 50, 33, 93, 158, 123, 131, 0, 188, 172, 60, 91, 180, 131, 223, 216, 167, 247, 11, 229, 218, 117, 73, 50, 146, 30, 0, 134, 107, 139, 101, 33, 70, 62, 24, 146, 91, 47, 17, 224, 73, 31, 245, 86, 207, 189, 177, 172, 81, 151, 8, 138, 237, 95, 106, 144, 15, 41, 55, 189, 229, 37, 253, 113, 157, 207, 198, 187, 72, 26, 35, 31, 140, 200, 148, 19, 44, 151, 76, 136, 100, 121, 229, 251, 42, 45, 136, 219, 196, 182, 238, 45, 204, 57, 31, 172, 79, 55, 52, 231, 151, 149, 76, 56, 193, 72, 213, 123, 149, 50, 96, 121, 117, 197, 10, 85, 137, 28, 246, 127, 97, 166, 177, 47, 253, 85, 82, 245, 73, 5, 128, 137, 88, 242, 186, 200, 36, 3, 136, 50, 32, 217, 1, 203, 190, 53, 170, 249, 98, 168, 4, 182, 22, 30, 12, 25, 103, 236, 193, 51, 30, 159, 39, 136, 126, 236, 63, 126, 239, 58, 87, 74, 52, 38, 214, 110, 142, 1, 68, 67, 40, 149, 206, 89, 218, 1, 0, 165, 12, 4, 140, 61, 65, 137, 204, 171, 66, 21, 0, 232, 199, 62, 84, 156, 97, 177, 208, 235, 35, 211, 12, 32, 178, 0, 89, 200, 220, 195, 91, 56, 148, 1, 133, 39, 112, 240, 96, 218, 144, 136, 14, 145, 69, 125, 204, 144, 18, 0, 226, 199, 206, 44, 42, 202, 176, 88, 232, 245, 144, 51, 135, 194, 229, 229, 180, 120, 176, 154, 45, 111, 250, 204, 194, 237, 83, 26, 194, 10, 235, 250, 128, 137, 116, 152, 21, 245, 64, 65, 81, 86, 109, 160, 4, 128, 248, 177, 223, 120, 104, 39, 136, 64, 69, 110, 11, 96, 152, 33, 77, 251, 51, 198, 207, 116, 85, 134, 130, 242, 109, 239, 109, 177, 112, 45, 16, 13, 72, 101, 146, 16, 244, 101, 151, 0, 174, 136, 78, 90, 102, 203, 179, 70, 52, 74, 0, 136, 31, 107, 37, 177, 36, 91, 36, 94, 151, 211, 22, 186, 153, 200, 37, 174, 17, 153, 162, 140, 241, 243, 78, 58, 92, 90, 248, 153, 31, 75, 101, 95, 145, 189, 225, 242, 165, 45, 162, 35, 176, 41, 151, 65, 154, 98, 107, 145, 129, 82, 144, 0, 40, 46, 42, 166, 126, 172, 85, 118, 164, 241, 144, 211, 198, 66, 153, 72, 103, 149, 184, 204, 241, 243, 115, 116, 164, 108, 235, 114, 4, 224, 85, 0, 224, 101, 242, 241, 193, 144, 148, 17, 8, 24, 160, 167, 191, 172, 57, 64, 160, 191, 102, 30, 5, 96, 203, 210, 45, 196, 129, 122, 200, 10, 29, 206, 40, 239, 150, 161, 77, 185, 145, 182, 236, 54, 75, 252, 188, 156, 102, 133, 62, 90, 4, 0, 52, 174, 89, 67, 237, 0, 202, 190, 152, 15, 8, 24, 160, 103, 51, 98, 89, 41, 52, 210, 236, 175, 192, 181, 224, 86, 228, 226, 51, 177, 55, 32, 231, 167, 37, 221, 169, 153, 217, 226, 231, 229, 116, 192, 248, 115, 172, 21, 126, 245, 239, 215, 60, 251, 63, 90, 200, 22, 185, 156, 52, 38, 96, 128, 158, 45, 155, 222, 211, 236, 175, 208, 150, 74, 47, 48, 242, 225, 198, 146, 51, 93, 254, 9, 101, 139, 159, 159, 167, 122, 0, 0, 8, 88, 191, 49, 247, 27, 115, 151, 238, 37, 131, 131, 141, 232, 8, 112, 134, 232, 21, 103, 83, 90, 105, 251, 43, 84, 84, 80, 64, 255, 132, 62, 177, 203, 72, 148, 152, 76, 241, 51, 240, 104, 53, 89, 176, 204, 2, 158, 239, 3, 208, 126, 82, 43, 27, 8, 136, 234, 127, 124, 131, 52, 24, 45, 171, 69, 175, 194, 202, 82, 14, 24, 239, 254, 81, 217, 201, 216, 254, 51, 242, 65, 135, 200, 16, 7, 174, 206, 2, 0, 188, 81, 248, 234, 220, 53, 88, 45, 173, 40, 137, 97, 198, 53, 72, 83, 108, 85, 139, 30, 113, 25, 3, 127, 66, 14, 216, 201, 64, 251, 141, 194, 11, 70, 62, 232, 16, 29, 226, 88, 190, 21, 1, 104, 95, 31, 152, 139, 117, 82, 164, 62, 194, 204, 197, 198, 4, 45, 86, 48, 143, 184, 33, 148, 167, 165, 237, 186, 170, 45, 51, 208, 166, 103, 22, 56, 13, 243, 244, 140, 124, 208, 146, 184, 216, 76, 203, 65, 27, 0, 176, 180, 61, 180, 232, 239, 73, 189, 184, 185, 139, 51, 81, 185, 53, 197, 60, 172, 149, 213, 233, 120, 15, 231, 53, 87, 26, 101, 38, 120, 124, 186, 242, 219, 142, 140, 123, 17, 24, 17, 29, 226, 216, 20, 106, 217, 104, 9, 60, 0, 206, 255, 190, 191, 127, 5, 100, 32, 144, 146, 84, 70, 62, 168, 169, 58, 91, 28, 84, 173, 186, 52, 29, 0, 111, 91, 75, 40, 144, 33, 210, 84, 61, 99, 246, 129, 72, 232, 199, 103, 236, 11, 50, 238, 69, 96, 64, 132, 71, 61, 237, 109, 155, 88, 139, 21, 107, 52, 2, 180, 86, 208, 4, 85, 88, 179, 157, 193, 200, 7, 21, 0, 94, 172, 8, 18, 139, 79, 76, 62, 99, 150, 157, 38, 104, 63, 62, 179, 192, 62, 142, 8, 155, 240, 40, 184, 123, 88, 33, 66, 180, 52, 86, 139, 154, 171, 9, 202, 110, 8, 171, 173, 224, 247, 210, 133, 237, 37, 0, 196, 18, 108, 60, 122, 2, 230, 0, 80, 46, 9, 5, 142, 38, 254, 42, 155, 254, 211, 164, 31, 157, 11, 198, 181, 23, 1, 131, 53, 90, 12, 169, 21, 70, 194, 58, 17, 208, 2, 227, 168, 139, 210, 33, 136, 117, 196, 32, 71, 6, 32, 164, 32, 115, 38, 81, 61, 16, 73, 83, 208, 5, 105, 142, 56, 233, 71, 183, 157, 25, 207, 67, 50, 40, 138, 140, 12, 128, 100, 7, 110, 12, 2, 50, 165, 3, 224, 229, 26, 219, 76, 90, 68, 221, 129, 200, 162, 244, 224, 147, 193, 209, 176, 39, 198, 243, 104, 213, 208, 254, 138, 185, 165, 165, 34, 0, 161, 229, 127, 251, 234, 206, 95, 253, 201, 0, 144, 38, 33, 228, 112, 169, 60, 16, 153, 49, 235, 192, 224, 34, 13, 204, 56, 158, 44, 16, 218, 86, 196, 22, 80, 79, 144, 208, 175, 230, 254, 247, 87, 247, 153, 124, 66, 157, 13, 47, 13, 72, 2, 160, 197, 235, 201, 21, 0, 89, 155, 102, 180, 59, 140, 124, 200, 141, 218, 193, 229, 45, 250, 6, 186, 194, 255, 23, 173, 60, 17, 44, 251, 36, 61, 236, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130]; +// Will be replaced automatically by GitHub Actions +final landTileBytes = []; diff --git a/tile_server/static/generated/sea.dart b/tile_server/static/generated/sea.dart index f3707c9e..196bd5af 100644 --- a/tile_server/static/generated/sea.dart +++ b/tile_server/static/generated/sea.dart @@ -1 +1,2 @@ -final seaTileBytes = [137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 1, 0, 0, 0, 1, 0, 1, 3, 0, 0, 0, 102, 188, 58, 37, 0, 0, 0, 3, 80, 76, 84, 69, 170, 211, 223, 207, 236, 188, 245, 0, 0, 0, 31, 73, 68, 65, 84, 104, 129, 237, 193, 1, 13, 0, 0, 0, 194, 160, 247, 79, 109, 14, 55, 160, 0, 0, 0, 0, 0, 0, 0, 0, 190, 13, 33, 0, 0, 1, 154, 96, 225, 213, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130]; +// Will be replaced automatically by GitHub Actions +final seaTileBytes = []; From cd50945b2725bfdc519ff14e6723119e96089615 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 11 Apr 2024 12:18:17 +0100 Subject: [PATCH 168/168] Prepare for v9 release Improved example application --- CHANGELOG.md | 2 ++ example/lib/screens/configure_download/configure_download.dart | 2 +- pubspec.yaml | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24600ca5..0d5e0233 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,12 +68,14 @@ And without further ado, let's get the biggest changes out of the way first: With those out of the way, we can take a look at the smaller changes: +* Improved recovery system to monitor which tiles can be skipped on re-downloading (via `DownloadableRegion.start`) * Improved error handling (especially in backends) * Added `StoreManagement.pruneTilesOlderThan` method * Added shortcut for getting multiple stats: `StoreStats.all` * Added secondary check to `FMTCImageProvider` to ensure responses are valid images * Replaced public facing `RegionType`/`type` with Dart 3 exhaustive switch statements through `BaseRegion/DownloadableRegion.when` & `RecoverableRegion.toRegion` * Removed HTTP/2 support +* Fixed a whole bunch of bugs In addition, there's been more action in the surrounding enviroment: diff --git a/example/lib/screens/configure_download/configure_download.dart b/example/lib/screens/configure_download/configure_download.dart index 35953448..7ed25b95 100644 --- a/example/lib/screens/configure_download/configure_download.dart +++ b/example/lib/screens/configure_download/configure_download.dart @@ -75,7 +75,7 @@ class ConfigureDownloadPopup extends StatelessWidget { suffixText: 'max. tps', value: (provider) => provider.rateLimit, min: 1, - max: 50000, + max: 300, maxEligibleTilesPreview: 20, onChanged: (provider, value) => provider.rateLimit = value, diff --git a/pubspec.yaml b/pubspec.yaml index af3b4465..f3084559 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_map_tile_caching description: Plugin for 'flutter_map' providing advanced caching functionality, with ability to download map regions for offline use. -version: 9.0.0-dev.8 +version: 9.0.0 repository: https://github.com/JaffaKetchup/flutter_map_tile_caching issue_tracker: https://github.com/JaffaKetchup/flutter_map_tile_caching/issues