Developer resources

Troubleshooting

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:

  • PictureRecorder
  • Canvas
  • drawImageRect
  • toImage
  • ImageByteFormat.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_flutter and 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 a TileOverlay 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.