timezone_finder 1.0.0 copy "timezone_finder: ^1.0.0" to clipboard
timezone_finder: ^1.0.0 copied to clipboard

Makes package:timezone easier to use — turn a place on Earth into the Location it sits in.

example/example.dart

// Conference call across four university hospitals.
//
// Shows the whole pipeline this package is built for:
//
//   place name → geocoder → coordinates → IANA zone → civil time

import 'dart:convert';
import 'dart:io';
import 'dart:math' as math;

import 'package:timezone/data/latest_all.dart' as tz;
import 'package:timezone/timezone.dart';
import 'package:timezone_finder/timezone_finder.dart';

typedef Place = ({String name, double longitude, double latitude});

typedef TimeSlot = ({
  String city,
  TZDateTime localStartTime,
  TZDateTime localEndTime,
});

String _civil(TZDateTime t) {
  final y = t.year.toString().padLeft(4, '0');
  final mo = t.month.toString().padLeft(2, '0');
  final d = t.day.toString().padLeft(2, '0');
  final h = t.hour.toString().padLeft(2, '0');
  final mi = t.minute.toString().padLeft(2, '0');
  return '$y-$mo-$d $h:$mi';
}

/// Nominatim free-text search.
Future<Place> searchWithNominatim(String query) => _geocode(
  Uri.parse(
    'https://nominatim.openstreetmap.org/search'
    '?q=${Uri.encodeQueryComponent(query)}&limit=1&format=geojson',
  ),
);

/// Nominatim structured search.
Future<Place> advancedSearchWithNominatim(
  Map<String, dynamic> queryComponents,
) => _geocode(
  Uri.parse(
    'https://nominatim.openstreetmap.org/search'
    '?${queryComponents.entries.map((e) => '${Uri.encodeQueryComponent(e.key)}=${Uri.encodeQueryComponent(e.value)}').join('&')}&limit=1&format=geojson',
  ),
);

/// Fetches [uri] and returns the first hit's name and Point coordinates.
///
/// Nominatim search returns a FeatureCollection even with `limit=1`. This
/// example takes `features.first`. Hospital name comes from `properties.name`.
Future<Place> _geocode(Uri uri) async {
  final client = HttpClient();
  try {
    final request = await client.getUrl(uri);
    // Nominatim rejects stock library User-Agents.
    request.headers.set(
      HttpHeaders.userAgentHeader,
      'timezone_finder/example/1.0.0 (https://github.com/pm-gwatch/timezone_finder)',
    );
    final response = await request.close();
    final text = await response.transform(utf8.decoder).join();
    final bodyPreview = text.replaceAll(RegExp(r'\s+'), ' ').trim();
    if (response.statusCode != HttpStatus.ok) {
      throw HttpException(
        '${uri.host} returned ${response.statusCode} '
        '${response.reasonPhrase}\n'
        '  request: $uri\n'
        '  body:    ${bodyPreview.substring(0, math.min(bodyPreview.length, 160))}',
      );
    }
    final body = jsonDecode(text) as Map<String, dynamic>;
    final features = body['features'] as List<dynamic>;
    if (features.isEmpty) {
      throw HttpException('No match from ${uri.host}', uri: uri);
    }

    final feature = features.first as Map<String, dynamic>;
    final properties = feature['properties'] as Map<String, dynamic>;
    final geometry = feature['geometry'] as Map<String, dynamic>;
    final coordinates = geometry['coordinates'] as List<dynamic>;
    // Display-only: Nominatim structured search sometimes omits the name.
    final name = properties['name'];
    return (
      name: name is String && name.isNotEmpty ? name : uri.host,
      longitude: (coordinates[0] as num).toDouble(),
      latitude: (coordinates[1] as num).toDouble(),
    );
  } finally {
    client.close();
  }
}

/// Nominatim allows at most one request per second.
Future<void> _nominatimPause() =>
    Future<void>.delayed(const Duration(seconds: 1));

TimeSlot _schedule(
  String city,
  Place place,
  TZDateTime start,
  Duration length,
) {
  final location = findLocation(place.longitude, place.latitude);
  if (location == null) {
    throw StateError('No land time zone for $city (${place.name})');
  }
  final localStartTime = TZDateTime.from(start, location);
  final localEndTime = localStartTime.add(length);
  return (
    city: city,
    localStartTime: localStartTime,
    localEndTime: localEndTime,
  );
}

void _printConfCallTable(List<TimeSlot> rows) {
  final header = <String>['City', 'Start (local)', 'End (local)', 'Zone'];
  final body = <List<String>>[
    for (final row in rows)
      <String>[
        row.city,
        _civil(row.localStartTime),
        _civil(row.localEndTime),
        // tzdb abbreviation; some zones only have a numeric one, e.g. +01.
        row.localStartTime.timeZoneName,
      ],
  ];
  final table = <List<String>>[header, ...body];
  final widths = List<int>.generate(
    header.length,
    (c) => table.map((r) => r[c].length).reduce(math.max),
  );

  String line(List<String> cells) => [
    for (var c = 0; c < cells.length; c++) cells[c].padRight(widths[c]),
  ].join('  ');

  final rule = [for (final w in widths) '-' * w].join('  ');

  print('Conference Call');
  print(rule);
  print(line(header));
  print(rule);
  for (final row in body) {
    print(line(row));
  }
  print(rule);
}

Future<void> main() async {
  tz.initializeTimeZones();

  late final Place geneva;
  late final Place casablanca;
  late final Place montreal;
  late final Place tokyo;
  try {
    // Geneva University Hospitals
    geneva = await searchWithNominatim('hug geneva');

    await _nominatimPause();

    // Hospital Ibn Rochd, Casablanca
    casablanca = await searchWithNominatim('hospital ibn rochd casablanca');

    await _nominatimPause();

    // CHUM, Montreal
    montreal = await searchWithNominatim(
      'centre hospitalier universite montreal',
    );

    await _nominatimPause();

    // Tokyo Medical University Hospital
    tokyo = await advancedSearchWithNominatim({
      'amenity': 'hospital',
      'street': 'nishi shinjuku 6',
      'city': 'tokyo',
      'country': 'japan',
    });
  } on IOException catch (error) {
    stderr
      ..writeln('Geocoding failed.')
      ..writeln(error);
    exitCode = 1;
    return;
  }

  final start = TZDateTime.utc(2026, 9, 16, 12, 0);
  const callLength = Duration(minutes: 45);
  final confCall = <TimeSlot>[
    _schedule('Geneva', geneva, start, callLength),
    _schedule('Casablanca', casablanca, start, callLength),
    _schedule('Montreal', montreal, start, callLength),
    _schedule('Tokyo', tokyo, start, callLength),
  ];

  _printConfCallTable(confCall);
  print('TZBB boundary release: $boundaryDataVersion');
}
0
likes
160
points
1.17k
downloads

Documentation

API reference

Publisher

verified publisherg.watch

Weekly Downloads

Makes package:timezone easier to use — turn a place on Earth into the Location it sits in.

Repository (GitHub)
View/report issues

Topics

#timezone #geospatial #iana

License

MIT (license)

Dependencies

timezone, web

More

Packages that depend on timezone_finder