Developer resources

Flutter

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 the 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, the HTTP package and the image package to your
project.

flutter pub add google_maps_flutter
flutter pub add http
flutter pub add image

The image package is used for client-side overzooming above the
native bathymetry zoom range.

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

Native bathymetry tiles are available from Z7 through Z13. At higher map
zoom levels, the provider requests the appropriate Z13 parent tile, crops
the required portion and scales it to a 256 × 256 tile.

The example below performs overzoom image processing with the pure Dart
image package. This avoids runtime PNG generation through
Flutter dart:ui, which may cause rendering problems with
Google Maps on iOS at zoom-level transitions.

import 'dart:typed_data';

import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:http/http.dart' as http;
import 'package:image/image.dart' as image;

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 overzoomScale = 1 << (zoom - sourceZoom);

    final sourceX = normalizedX ~/ overzoomScale;
    final sourceY = y ~/ overzoomScale;

    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 the original PNG bytes unchanged.
    if (zoom <= maxNativeZoom) {
      return Tile(
        tileSize,
        tileSize,
        response.bodyBytes,
      );
    }

    // Higher zoom: crop the appropriate part of the Z13 parent tile.
    final childX = normalizedX % overzoomScale;
    final childY = y % overzoomScale;

    return _cropAndScale(
      response.bodyBytes,
      childX,
      childY,
      overzoomScale,
    );
  }

  Tile _cropAndScale(
    Uint8List sourceBytes,
    int childX,
    int childY,
    int overzoomScale,
  ) {
    try {
      final source = image.decodePng(sourceBytes);

      if (source == null) {
        return TileProvider.noTile;
      }

      final cropLeft =
          (childX * source.width / overzoomScale).floor();

      final cropTop =
          (childY * source.height / overzoomScale).floor();

      final cropRight =
          ((childX + 1) * source.width / overzoomScale).ceil();

      final cropBottom =
          ((childY + 1) * source.height / overzoomScale).ceil();

      final cropped = image.copyCrop(
        source,
        x: cropLeft,
        y: cropTop,
        width: (cropRight - cropLeft).clamp(
          1,
          source.width - cropLeft,
        ),
        height: (cropBottom - cropTop).clamp(
          1,
          source.height - cropTop,
        ),
      );

      final resized = image.copyResize(
        cropped,
        width: tileSize,
        height: tileSize,
        interpolation: image.Interpolation.linear,
      );

      final pngBytes = Uint8List.fromList(
        image.encodePng(resized),
      );

      return Tile(
        tileSize,
        tileSize,
        pngBytes,
      );
    } catch (_) {
      return TileProvider.noTile;
    }
  }
}

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,

      // See the iOS note below.
      tileSize: defaultTargetPlatform == TargetPlatform.iOS
          ? 512
          : 256,

      transparency: 0.0,
      zIndex: 1,
      fadeIn: false,
      visible: true,
    ),
  },
)

This example requires the following Flutter import when using
defaultTargetPlatform:

import 'package:flutter/foundation.dart';

iOS Tile Size

On high-density iOS displays, 256 × 256 bathymetry tiles may cause contour
depth labels to appear smaller than expected when displayed with Google Maps.

If this occurs, keep the actual Bathymetry API tile data at 256 × 256 and
continue returning Tile(256, 256, bytes) from the tile provider,
but set the Google Maps TileOverlay tile size to
512.

This changes how Google Maps interprets the overlay on iOS. It does not
change the dimensions of the PNG files returned by the Bathymetry API.

Other platforms and map SDKs may behave differently, so this adjustment
should not be assumed to be required outside Google Maps on iOS.

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.
It only enlarges the existing Z13 data.

Client-Side Overzooming on iOS

When using Flutter with google_maps_flutter on iOS, avoid
generating overzoomed PNG tiles through Flutter dart:ui
rendering APIs such as PictureRecorder, Canvas,
drawImageRect, toImage and
ImageByteFormat.png.

Runtime PNG files generated through this path have been observed to disappear
at some Google Maps zoom-level transitions on iOS.

The example on this page instead uses package:image to decode,
crop, resize and encode the parent tile while preserving PNG transparency.

See the
Troubleshooting guide
for additional details.

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 caching, 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.
  • Native tiles should be returned without unnecessary decoding or
    re-encoding.
  • Client-side overzooming in this example uses
    package:image rather than Flutter dart:ui.
  • On Google Maps for iOS, a logical overlay tile size of
    512 may improve the apparent size of depth labels.
  • A Bathymetry API key embedded in a distributed Flutter application should
    not be treated as a secret.