Add 1224Tools Bathymetry to a Flutter application using
google_maps_flutter and a custom transparent tile overlay.
Overview
1224Tools Bathymetry uses standard XYZ raster tiles. In Flutter applications
using google_maps_flutter, the bathymetry layer can be displayed
above th
Troubleshooting notes for common integration issues observed when displaying 1224Tools Bathymetry with mobile map SDKs.
iOS: Depth Labels Appear Too Small
When 256 × 256 bathymetry tiles are displayed with Google Maps on a high-density iOS display, contour depth labels may appear smaller than expected.
If this occurs, keep the Bathymetry API tiles themselves at their original 256 × 256 size, but configure the Google Maps tile overlay to use a logical tile size of 512 where supported by the SDK.
For example, with google_maps_flutter:
TileOverlay(
tileOverlayId: const TileOverlayId(
'1224tools-bathymetry',
),
tileProvider: bathymetryTileProvider,
tileSize: 512,
fadeIn: false,
zIndex: 1,
)
The tile provider can continue returning the original 256 × 256 PNG data:
return Tile(
256,
256,
pngBytes,
);
This does not change the dimensions of the source tiles provided by the Bathymetry API. It only changes how the overlay is interpreted and displayed by Google Maps.
This behavior has been confirmed with Google Maps on iOS. Other map SDKs and platforms may handle high-density tile rendering differently.
iOS: Bathymetry Disappears While Zooming
In Flutter applications using google_maps_flutter, bathymetry
tiles may disappear at certain zoom transitions when higher zoom levels are
generated dynamically from parent tiles.
This issue has been observed when overzoomed PNG tiles are generated at
runtime using Flutter dart:ui image rendering APIs such as:
PictureRecorderCanvasdrawImageRecttoImageImageByteFormat.png
If bathymetry disappears while crossing zoom levels on iOS, avoid using this rendering path for dynamically generated tile PNGs.
Recommended Approach
Keep native Bathymetry API tiles unchanged and return their PNG bytes directly at native zoom levels.
When client-side overzooming is required above the native tile zoom range,
decode, crop, resize and encode the parent PNG using a pure Dart image
library such as package:image.
A typical overzoom flow is:
final source = image.decodePng(parentTileBytes);
if (source == null) {
return TileProvider.noTile;
}
final cropped = image.copyCrop(
source,
x: cropX,
y: cropY,
width: cropWidth,
height: cropHeight,
);
final resized = image.copyResize(
cropped,
width: 256,
height: 256,
);
final pngBytes = Uint8List.fromList(
image.encodePng(resized),
);
return Tile(
256,
256,
pngBytes,
);
Preserve transparency when processing the PNG. Bathymetry tiles use transparent backgrounds so the underlying basemap remains visible.
Native Zoom Levels
Native 1224Tools Bathymetry tiles are available from Z7 through Z13. Applications that continue displaying bathymetry above Z13 may implement client-side overzooming by cropping the appropriate area from the Z13 parent tile.
Overzooming does not add additional bathymetric detail. It only enlarges the existing Z13 data for display at higher map zoom levels.
Platform Notes
- The small-label issue described above has been confirmed with Google Maps on iOS.
-
The zoom-disappearance issue described above has been confirmed with
Flutter,
google_maps_flutterand Google Maps on iOS. - Other SDKs, including MapLibre and Mapbox, may behave differently.
- Do not assume these workarounds are required on platforms where the issue does not occur.
Still Having Problems?
When reporting a rendering issue, include the map SDK, platform, tile zoom level, device type and whether the issue affects contour lines, depth labels, or both.
e existing Google Maps basemap with aTileOverlay and
custom TileProvider.
The tiles have transparent backgrounds, allowing contour lines and depth labels to appear above the existing map without replacing the basemap.
Packages
Add Google Maps for Flutter and the HTTP package to your project.
flutter pub add google_maps_flutter
flutter pub add http
If your application already uses google_maps_flutter, only the
HTTP dependency may need to be added.
Tile URL
https://tiles.1224tools.com/v1/{z}/{x}/{y}.png?key=YOUR_API_KEY
Replace YOUR_API_KEY with the API key provided for your
1224Tools Bathymetry account.
Google Maps and Bathymetry API Keys
Your Google Maps Platform API key and your 1224Tools Bathymetry API key
are separate credentials. Continue using your existing Google Maps API key
for google_maps_flutter, and use your Bathymetry API key only
for bathymetry tile requests.
Create the Tile Provider
The following provider requests native bathymetry tiles from Z7 through Z13. At higher map zoom levels, it requests the appropriate Z13 parent tile, crops the required portion and scales it to the requested 256 × 256 tile.
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:http/http.dart' as http;
class BathymetryTileProvider extends TileProvider {
BathymetryTileProvider({
required this.apiKey,
required this.client,
});
final String apiKey;
final http.Client client;
static const int tileSize = 256;
static const int minNativeZoom = 7;
static const int maxNativeZoom = 13;
@override
Future<Tile> getTile(int x, int y, int? zoom) async {
if (zoom == null || zoom < minNativeZoom) {
return TileProvider.noTile;
}
final tileCount = 1 << zoom;
// Y does not wrap vertically.
if (y < 0 || y >= tileCount) {
return TileProvider.noTile;
}
// Normalize X because the world can wrap horizontally.
final normalizedX = ((x % tileCount) + tileCount) % tileCount;
final sourceZoom =
zoom > maxNativeZoom ? maxNativeZoom : zoom;
final overzoomFactor = 1 << (zoom - sourceZoom);
final sourceX = normalizedX ~/ overzoomFactor;
final sourceY = y ~/ overzoomFactor;
final uri = Uri.https(
'tiles.1224tools.com',
'/v1/$sourceZoom/$sourceX/$sourceY.png',
{
'key': apiKey,
},
);
final response = await client.get(uri);
if (response.statusCode != 200) {
return TileProvider.noTile;
}
// Native tile: return it directly.
if (zoom <= maxNativeZoom) {
return Tile(
tileSize,
tileSize,
response.bodyBytes,
);
}
// Higher zoom: crop the appropriate part of the Z13 parent tile.
final childX = normalizedX % overzoomFactor;
final childY = y % overzoomFactor;
return _cropAndScale(
response.bodyBytes,
childX,
childY,
overzoomFactor,
);
}
Future<Tile> _cropAndScale(
Uint8List sourceBytes,
int childX,
int childY,
int overzoomFactor,
) async {
final codec = await ui.instantiateImageCodec(sourceBytes);
try {
final frame = await codec.getNextFrame();
final sourceImage = frame.image;
try {
final sourceWidth =
sourceImage.width / overzoomFactor;
final sourceHeight =
sourceImage.height / overzoomFactor;
final sourceRect = ui.Rect.fromLTWH(
childX * sourceWidth,
childY * sourceHeight,
sourceWidth,
sourceHeight,
);
final destinationRect = ui.Rect.fromLTWH(
0,
0,
tileSize.toDouble(),
tileSize.toDouble(),
);
final recorder = ui.PictureRecorder();
final canvas = ui.Canvas(recorder);
final paint = ui.Paint()
..filterQuality = ui.FilterQuality.high;
canvas.drawImageRect(
sourceImage,
sourceRect,
destinationRect,
paint,
);
final picture = recorder.endRecording();
try {
final outputImage = await picture.toImage(
tileSize,
tileSize,
);
try {
final byteData = await outputImage.toByteData(
format: ui.ImageByteFormat.png,
);
if (byteData == null) {
return TileProvider.noTile;
}
return Tile(
tileSize,
tileSize,
byteData.buffer.asUint8List(),
);
} finally {
outputImage.dispose();
}
} finally {
picture.dispose();
}
} finally {
sourceImage.dispose();
}
} finally {
codec.dispose();
}
}
}
Add the Tile Overlay
Create the provider once and pass it to a TileOverlay.
late final http.Client _httpClient;
late final BathymetryTileProvider _bathymetryTileProvider;
@override
void initState() {
super.initState();
_httpClient = http.Client();
_bathymetryTileProvider = BathymetryTileProvider(
apiKey: const String.fromEnvironment(
'BATHYMETRY_API_KEY',
),
client: _httpClient,
);
}
@override
void dispose() {
_httpClient.close();
super.dispose();
}
Then add the overlay to your GoogleMap widget.
GoogleMap(
initialCameraPosition: const CameraPosition(
target: LatLng(34.5, 135.2),
zoom: 10,
),
tileOverlays: {
TileOverlay(
tileOverlayId: const TileOverlayId(
'1224tools-bathymetry',
),
tileProvider: _bathymetryTileProvider,
tileSize: 256,
transparency: 0.0,
zIndex: 1,
fadeIn: false,
visible: true,
),
},
)
Providing the API Key
The example reads the Bathymetry API key from a Dart environment value instead of placing the key directly in source code.
flutter run \
--dart-define=BATHYMETRY_API_KEY=YOUR_API_KEY
Environment values help keep credentials out of source control, but an API key distributed inside a client application should not be treated as a secret. Protect production keys with the usage controls and restrictions available for your account.
Zoom Levels
Native bathymetric contour tiles are available from Z7 through Z13.
The example above continues displaying bathymetry at higher map zoom levels by cropping and enlarging the appropriate part of the Z13 parent tile.
Overzooming above Z13 does not introduce additional bathymetric detail.
Adjusting Opacity
TileOverlay.transparency controls how transparent the overlay
appears. A value of 0.0 displays the tile at full opacity.
TileOverlay(
tileOverlayId: const TileOverlayId(
'1224tools-bathymetry',
),
tileProvider: _bathymetryTileProvider,
// 40% transparent = 60% visible.
transparency: 0.4,
)
Show or Hide the Layer
Use the visible property when your application provides a
bathymetry layer toggle.
TileOverlay(
tileOverlayId: const TileOverlayId(
'1224tools-bathymetry',
),
tileProvider: _bathymetryTileProvider,
visible: showBathymetry,
)
Empty Tiles and Errors
Some locations do not contain contours within the supported depth range. The tile provider should treat unavailable tile responses as no tile and allow the underlying basemap to remain visible.
Production applications may also add retry behavior, logging or application specific error handling as appropriate.
Important Notes
- This guide uses
google_maps_flutter. - Native tile zoom levels are Z7 through Z13.
- Tiles use the XYZ scheme and Web Mercator projection.
- Tile images are transparent 256 × 256 PNG files.
- Contour depth labels are included in the tile images.
- Overzooming above Z13 does not add additional bathymetric detail.
- A Bathymetry API key embedded in a distributed Flutter application should not be treated as a secret.