importPlaces static method

Future<ImportResult> importPlaces()

Imports places from a JSON file selected by the user.

Returns an ImportResult containing validated places and any errors. Validation rules:

  • lat and lng are REQUIRED - items without these are skipped
  • id: auto-generated if missing (UUID)
  • timestamp: uses current time if missing
  • note: defaults to empty string if missing
  • address: defaults to "Unknown Location" if missing.

Implementation

static Future<ImportResult> importPlaces() async {
  final result = ImportResult();

  try {
    final pickResult = await FilePicker.platform.pickFiles(
      type: FileType.custom,
      allowedExtensions: ['json'],
      withData: true,
    );

    if (pickResult == null || pickResult.files.isEmpty) {
      result.cancelled = true;
      return result;
    }

    final file = pickResult.files.first;
    if (file.bytes == null) {
      result.errors.add('Failed to read file data');
      return result;
    }

    final jsonString = utf8.decode(file.bytes!);
    final dynamic decoded;

    try {
      decoded = jsonDecode(jsonString);
    } catch (e) {
      result.errors.add('Invalid JSON format: $e');
      return result;
    }

    if (decoded is! List) {
      result.errors.add('Expected a JSON array of place objects');
      return result;
    }

    final uuid = const Uuid();

    for (int i = 0; i < decoded.length; i++) {
      final item = decoded[i];

      if (item is! Map<String, dynamic>) {
        result.errors.add('Item ${i + 1}: Not a valid object, skipped');
        result.skippedCount++;
        continue;
      }

      // Validate required fields: lat and lng.
      final lat = item['lat'];
      final lng = item['lng'];

      if (lat == null || lng == null) {
        result.errors.add(
          'Item ${i + 1}: Missing required lat/lng fields, skipped',
        );
        result.skippedCount++;
        continue;
      }

      if (lat is! num || lng is! num) {
        result.errors.add('Item ${i + 1}: lat/lng must be numbers, skipped');
        result.skippedCount++;
        continue;
      }

      // Validate lat/lng ranges.

      if (lat < -90 || lat > 90) {
        result.errors.add(
          'Item ${i + 1}: lat must be between -90 and 90, skipped',
        );
        result.skippedCount++;
        continue;
      }

      if (lng < -180 || lng > 180) {
        result.errors.add(
          'Item ${i + 1}: lng must be between -180 and 180, skipped',
        );
        result.skippedCount++;
        continue;
      }

      // Auto-complete missing optional fields.
      // Note: address field is IGNORED from JSON - will be auto-generated via geocoding.
      final place = Place(
        id: (item['id'] as String?) ?? uuid.v4(),
        lat: lat.toDouble(),
        lng: lng.toDouble(),
        note: (item['note'] as String?) ?? '',
        timestamp:
            (item['timestamp'] as String?) ??
            DateTime.now().toUtc().toIso8601String(),
        address: null, // Address will be fetched via reverse geocoding.
        isLocal: false,
      );

      result.places.add(place);
    }

    return result;
  } catch (e) {
    result.errors.add('Unexpected error: $e');
    return result;
  }
}