diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fc3b27..31947cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 7.2.0 + +* added `PmTilesVectorTileProvider` for pmtiles format archives + ## 7.1.0 * support vector theme raster layers diff --git a/example/pubspec.lock b/example/pubspec.lock index 1ec58a6..28a4710 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + archive: + dependency: transitive + description: + name: archive + sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" + url: "https://pub.dev" + source: hosted + version: "3.4.10" async: dependency: transitive description: @@ -41,6 +49,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.18.0" + convert: + dependency: transitive + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" executor_lib: dependency: transitive description: @@ -103,10 +127,10 @@ packages: dependency: transitive description: name: http - sha256: d4872660c46d929f6b8a9ef4e7a7eff7e49bbf0c4ec3f385ee32df5119175139 + sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.2.0" http_parser: dependency: transitive description: @@ -123,6 +147,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.19.0" + js: + dependency: transitive + description: + name: js + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf + url: "https://pub.dev" + source: hosted + version: "0.7.1" latlong2: dependency: "direct main" description: @@ -259,6 +291,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + pmtiles: + dependency: transitive + description: + name: pmtiles + sha256: "7e3135c64ec3647e1dd8929fd3c371c8e1c6809b24f1d910d3198a7f41e2068c" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29" + url: "https://pub.dev" + source: hosted + version: "3.7.4" polylabel: dependency: transitive description: @@ -267,6 +315,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" proj4dart: dependency: transitive description: @@ -358,7 +414,7 @@ packages: path: ".." relative: true source: path - version: "7.0.1" + version: "7.1.0" vector_math: dependency: transitive description: diff --git a/lib/src/cache/atlas_image_cache.dart b/lib/src/cache/atlas_image_cache.dart index 710784e..33ba474 100644 --- a/lib/src/cache/atlas_image_cache.dart +++ b/lib/src/cache/atlas_image_cache.dart @@ -5,8 +5,8 @@ import 'dart:ui'; import 'package:executor_lib/executor_lib.dart'; import 'package:vector_tile_renderer/vector_tile_renderer.dart'; -import 'storage_cache.dart'; import 'extensions.dart'; +import 'storage_cache.dart'; class AtlasImageCache { final Theme _theme; diff --git a/lib/src/grid/tile_layer_composer.dart b/lib/src/grid/tile_layer_composer.dart index 5877891..6795230 100644 --- a/lib/src/grid/tile_layer_composer.dart +++ b/lib/src/grid/tile_layer_composer.dart @@ -1,8 +1,8 @@ import 'package:vector_tile_renderer/vector_tile_renderer.dart'; +import '../style/style.dart'; import 'tile_layer_model.dart'; import 'tile_model.dart'; -import '../style/style.dart'; class TileLayerComposer { List compose( diff --git a/lib/src/options.dart b/lib/src/options.dart index 737dc49..d978ffb 100644 --- a/lib/src/options.dart +++ b/lib/src/options.dart @@ -2,11 +2,11 @@ import 'dart:io'; import 'package:vector_tile_renderer/vector_tile_renderer.dart'; +import 'style/style.dart'; import 'tile_offset.dart'; import 'tile_providers.dart'; import 'vector_tile_layer.dart' as vmt; import 'vector_tile_layer_mode.dart'; -import 'style/style.dart'; class VectorTileLayerOptions { final TileProviders tileProviders; diff --git a/lib/src/provider/memory_vector_tile_provider.dart b/lib/src/provider/memory_vector_tile_provider.dart new file mode 100644 index 0000000..f5dfe3b --- /dev/null +++ b/lib/src/provider/memory_vector_tile_provider.dart @@ -0,0 +1,36 @@ +import 'dart:typed_data'; + +import '../cache/memory_cache.dart'; +import '../tile_identity.dart'; +import '../vector_tile_provider.dart'; + +class MemoryCacheVectorTileProvider extends VectorTileProvider { + final VectorTileProvider delegate; + late final MemoryCache _cache; + + @override + int get maximumZoom => delegate.maximumZoom; + + @override + int get minimumZoom => delegate.minimumZoom; + + MemoryCacheVectorTileProvider( + {required this.delegate, required int maxSizeBytes}) { + _cache = MemoryCache(maxSizeBytes: maxSizeBytes); + } + + @override + Future provide(TileIdentity tile) async { + final key = tile.toCacheKey(); + var value = _cache.get(key); + if (value == null) { + value = await delegate.provide(tile); + _cache.put(key, value); + } + return value; + } +} + +extension _TileCacheKey on TileIdentity { + String toCacheKey() => '$z.$x.$y'; +} diff --git a/lib/src/provider/network_vector_tile_provider.dart b/lib/src/provider/network_vector_tile_provider.dart new file mode 100644 index 0000000..632572b --- /dev/null +++ b/lib/src/provider/network_vector_tile_provider.dart @@ -0,0 +1,94 @@ +import 'dart:typed_data'; + +import 'package:http/http.dart'; +import 'package:http/retry.dart'; + +import '../provider_exception.dart'; +import '../tile_identity.dart'; +import '../vector_tile_provider.dart'; + +class NetworkVectorTileProvider extends VectorTileProvider { + @override + final TileProviderType type; + final _UrlProvider _urlProvider; + final Map? httpHeaders; + + @override + final int maximumZoom; + + @override + final int minimumZoom; + + /// [urlTemplate] the URL template, e.g. `'https://tiles.stadiamaps.com/data/openmaptiles/{z}/{x}/{y}.pbf?api_key=$apiKey'` + /// [httpHeaders] HTTP headers to include in requests, suitable for passing + /// `Authentication` header instead of an `api_key` in the URL template + /// [maximumZoom] the maximum zoom supported by the tile provider, not to be + /// confused with the maximum zoom of the map widget. The map widget will + /// automatically use vector tiles from lower zoom levels once the maximum + /// supported by this provider is reached. + NetworkVectorTileProvider( + {required String urlTemplate, + this.type = TileProviderType.vector, + this.httpHeaders, + this.maximumZoom = 16, + this.minimumZoom = 1}) + : _urlProvider = _UrlProvider(urlTemplate); + + @override + Future provide(TileIdentity tile) async { + _checkTile(tile); + final uri = Uri.parse(_urlProvider.url(tile)); + final client = RetryClient(Client()); + try { + final response = await client.get(uri, headers: httpHeaders); + if (response.statusCode == 200) { + return response.bodyBytes; + } + final logSafeUri = uri.toString().split(RegExp(r'\?')).first; + throw ProviderException( + message: + 'Cannot retrieve tile: HTTP ${response.statusCode}: $logSafeUri ${response.body}', + statusCode: response.statusCode, + retryable: _isRetryable(response.statusCode) + ? Retryable.retry + : Retryable.none); + } on ClientException catch (e) { + throw ProviderException(message: e.message, retryable: Retryable.retry); + } finally { + client.close(); + } + } + + void _checkTile(TileIdentity tile) { + if (tile.z > maximumZoom || tile.z < minimumZoom || !tile.isValid()) { + throw ProviderException( + message: 'Invalid tile coordinates $tile', + retryable: Retryable.none, + statusCode: 400); + } + } + + _isRetryable(int statusCode) => statusCode == 503 || statusCode == 408; +} + +class _UrlProvider { + final String urlTemplate; + + _UrlProvider(this.urlTemplate); + + String url(TileIdentity identity) { + return urlTemplate.replaceAllMapped(RegExp(r'\{(x|y|z)\}'), (match) { + switch (match.group(1)) { + case 'x': + return identity.x.toInt().toString(); + case 'y': + return identity.y.toInt().toString(); + case 'z': + return identity.z.toInt().toString(); + default: + throw Exception( + 'unexpected url template: $urlTemplate - token ${match.group(1)} is not supported'); + } + }); + } +} diff --git a/lib/src/provider/pmtiles_vector_tile_provider.dart b/lib/src/provider/pmtiles_vector_tile_provider.dart new file mode 100644 index 0000000..8a17366 --- /dev/null +++ b/lib/src/provider/pmtiles_vector_tile_provider.dart @@ -0,0 +1,58 @@ +import 'dart:typed_data'; + +import 'package:pmtiles/pmtiles.dart'; + +import '../../vector_map_tiles.dart'; +import '../provider_exception.dart'; + +/// A network tile provider that uses HTTP range requests with +/// a [pmtiles archive](https://docs.protomaps.com/pmtiles/). +/// A [PmTilesProvider] is stateful since it must load the pmtiles +/// index before loading any tiles. +/// +/// Instances of [PmTilesArchive] should +/// be long-lived to reduce network calls, and [PmTilesArchive.close] must be called +/// to release resources when it is no longer needed. +class PmTilesVectorTileProvider extends VectorTileProvider { + PmTilesArchive archive; + @override + final TileProviderType type; + + @override + final int maximumZoom; + @override + final int minimumZoom; + + PmTilesVectorTileProvider( + {required this.archive, + required this.type, + required this.minimumZoom, + required this.maximumZoom}); + + @override + Future provide(TileIdentity tile) async { + _checkZoom(archive, tile); + final tileId = ZXY(tile.z, tile.x, tile.y).toTileId(); + try { + final t = await archive.tile(tileId); + return Uint8List.fromList(t.bytes()); + } catch (e) { + if (e is TileNotFoundException) { + throw ProviderException( + message: 'not found: $tile', + retryable: Retryable.none, + statusCode: 404); + } + rethrow; + } + } + + void _checkZoom(PmTilesArchive archive, TileIdentity tile) { + if (tile.z < archive.header.minZoom || tile.z > archive.header.maxZoom) { + throw ProviderException( + message: + '${tile.z} must be in [${archive.header.minZoom}..${archive.header.maxZoom}]', + retryable: Retryable.none); + } + } +} diff --git a/lib/src/style/style.dart b/lib/src/style/style.dart index 49337de..34e1a2c 100644 --- a/lib/src/style/style.dart +++ b/lib/src/style/style.dart @@ -5,6 +5,7 @@ import 'package:http/http.dart'; import 'package:latlong2/latlong.dart'; import 'package:vector_tile_renderer/vector_tile_renderer.dart'; +import '../provider/network_vector_tile_provider.dart'; import '../tile_providers.dart'; import '../vector_tile_provider.dart'; import 'uri_mapper.dart'; diff --git a/lib/src/vector_tile_layer.dart b/lib/src/vector_tile_layer.dart index 0d3d9a5..44a0cc3 100644 --- a/lib/src/vector_tile_layer.dart +++ b/lib/src/vector_tile_layer.dart @@ -2,15 +2,15 @@ import 'dart:io'; import 'package:flutter/material.dart' hide Theme; import 'package:flutter_map/flutter_map.dart'; -import 'style/style.dart'; -import 'vector_tile_layer_mode.dart'; import 'package:vector_tile_renderer/vector_tile_renderer.dart'; import 'extensions.dart'; import 'grid/grid_layer.dart'; import 'options.dart'; +import 'style/style.dart'; import 'tile_offset.dart'; import 'tile_providers.dart'; +import 'vector_tile_layer_mode.dart'; import 'vector_tile_provider.dart'; /// A widget for a vector tile layer, to be used as a child diff --git a/lib/src/vector_tile_provider.dart b/lib/src/vector_tile_provider.dart index 35c8e21..ab45e91 100644 --- a/lib/src/vector_tile_provider.dart +++ b/lib/src/vector_tile_provider.dart @@ -1,16 +1,12 @@ import 'dart:typed_data'; -import 'package:http/http.dart'; -import 'package:http/retry.dart'; - -import 'cache/memory_cache.dart'; -import 'provider_exception.dart'; import 'tile_identity.dart'; enum TileProviderType { vector, raster } abstract class VectorTileProvider { - /// provides a tile as a `pbf` or `mvt` format + /// provides a tile as a `pbf` or `mvt` format for [type] of [TileProviderType.vector] + /// or `png` for [TileProviderType.raster] Future provide(TileIdentity tile); int get maximumZoom; @@ -19,120 +15,3 @@ abstract class VectorTileProvider { TileProviderType get type => TileProviderType.vector; } - -class NetworkVectorTileProvider extends VectorTileProvider { - @override - final TileProviderType type; - final _UrlProvider _urlProvider; - final Map? httpHeaders; - - @override - final int maximumZoom; - - @override - final int minimumZoom; - - /// [urlTemplate] the URL template, e.g. `'https://tiles.stadiamaps.com/data/openmaptiles/{z}/{x}/{y}.pbf?api_key=$apiKey'` - /// [httpHeaders] HTTP headers to include in requests, suitable for passing - /// `Authentication` header instead of an `api_key` in the URL template - /// [maximumZoom] the maximum zoom supported by the tile provider, not to be - /// confused with the maximum zoom of the map widget. The map widget will - /// automatically use vector tiles from lower zoom levels once the maximum - /// supported by this provider is reached. - NetworkVectorTileProvider( - {required String urlTemplate, - this.type = TileProviderType.vector, - this.httpHeaders, - this.maximumZoom = 16, - this.minimumZoom = 1}) - : _urlProvider = _UrlProvider(urlTemplate); - - @override - Future provide(TileIdentity tile) async { - _checkTile(tile); - final uri = Uri.parse(_urlProvider.url(tile)); - final client = RetryClient(Client()); - try { - final response = await client.get(uri, headers: httpHeaders); - if (response.statusCode == 200) { - return response.bodyBytes; - } - final logSafeUri = uri.toString().split(RegExp(r'\?')).first; - throw ProviderException( - message: - 'Cannot retrieve tile: HTTP ${response.statusCode}: $logSafeUri ${response.body}', - statusCode: response.statusCode, - retryable: _isRetryable(response.statusCode) - ? Retryable.retry - : Retryable.none); - } on ClientException catch (e) { - throw ProviderException(message: e.message, retryable: Retryable.retry); - } finally { - client.close(); - } - } - - void _checkTile(TileIdentity tile) { - if (tile.z > maximumZoom || tile.z < minimumZoom || !tile.isValid()) { - throw ProviderException( - message: 'Invalid tile coordinates $tile', - retryable: Retryable.none, - statusCode: 400); - } - } - - _isRetryable(int statusCode) => statusCode == 503 || statusCode == 408; -} - -class MemoryCacheVectorTileProvider extends VectorTileProvider { - final VectorTileProvider delegate; - late final MemoryCache _cache; - - @override - int get maximumZoom => delegate.maximumZoom; - - @override - int get minimumZoom => delegate.minimumZoom; - - MemoryCacheVectorTileProvider( - {required this.delegate, required int maxSizeBytes}) { - _cache = MemoryCache(maxSizeBytes: maxSizeBytes); - } - - @override - Future provide(TileIdentity tile) async { - final key = tile.toCacheKey(); - var value = _cache.get(key); - if (value == null) { - value = await delegate.provide(tile); - _cache.put(key, value); - } - return value; - } -} - -class _UrlProvider { - final String urlTemplate; - - _UrlProvider(this.urlTemplate); - - String url(TileIdentity identity) { - return urlTemplate.replaceAllMapped(RegExp(r'\{(x|y|z)\}'), (match) { - switch (match.group(1)) { - case 'x': - return identity.x.toInt().toString(); - case 'y': - return identity.y.toInt().toString(); - case 'z': - return identity.z.toInt().toString(); - default: - throw Exception( - 'unexpected url template: $urlTemplate - token ${match.group(1)} is not supported'); - } - }); - } -} - -extension _TileCacheKey on TileIdentity { - String toCacheKey() => '$z.$x.$y'; -} diff --git a/lib/vector_map_tiles.dart b/lib/vector_map_tiles.dart index 7e8a0a9..188ed4a 100644 --- a/lib/vector_map_tiles.dart +++ b/lib/vector_map_tiles.dart @@ -1,5 +1,8 @@ library vector_map_tiles; +export 'src/provider/memory_vector_tile_provider.dart'; +export 'src/provider/network_vector_tile_provider.dart'; +export 'src/provider/pmtiles_vector_tile_provider.dart'; export 'src/style/style.dart'; export 'src/theme_extensions.dart'; export 'src/tile_identity.dart'; diff --git a/pubspec.lock b/pubspec.lock index fd2006c..5db0b10 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -17,6 +17,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.2.0" + archive: + dependency: transitive + description: + name: archive + sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" + url: "https://pub.dev" + source: hosted + version: "3.4.10" args: dependency: transitive description: @@ -162,10 +170,10 @@ packages: dependency: "direct main" description: name: http - sha256: d4872660c46d929f6b8a9ef4e7a7eff7e49bbf0c4ec3f385ee32df5119175139 + sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.2.0" http_multi_server: dependency: transitive description: @@ -374,6 +382,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + pmtiles: + dependency: "direct main" + description: + name: pmtiles + sha256: "7e3135c64ec3647e1dd8929fd3c371c8e1c6809b24f1d910d3198a7f41e2068c" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29" + url: "https://pub.dev" + source: hosted + version: "3.7.4" polylabel: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 32b1465..78e58ae 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: vector_map_tiles description: A plugin for `flutter_map` that enables the use of vector tiles. -version: 7.1.0 +version: 7.2.0 homepage: https://github.com/greensopinion/flutter-vector-map-tiles environment: @@ -18,6 +18,7 @@ dependencies: #path: ../vector_tile_renderer async: ^2.8.2 executor_lib: ^1.1.1 + pmtiles: ^1.3.0 #path: ../executor_lib dev_dependencies: