import 'dart:async'; import 'dart:convert'; import 'dart:isolate'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; // cf https://github.com/topojson/topojson-specification class TopoJson { Future parse(String jsonData) async { try { return Isolate.run(() { final data = jsonDecode(jsonData) as Map; return Topology.parse(data); }); } catch (error, stack) { debugPrint('failed to parse TopoJSON with error=$error\n$stack'); return null; } } } enum TopoJsonObjectType { topology, point, multipoint, linestring, multilinestring, polygon, multipolygon, geometrycollection } TopoJsonObjectType? _parseTopoJsonObjectType(String? data) { switch (data) { case 'Topology': return TopoJsonObjectType.topology; case 'Point': return TopoJsonObjectType.point; case 'MultiPoint': return TopoJsonObjectType.multipoint; case 'LineString': return TopoJsonObjectType.linestring; case 'MultiLineString': return TopoJsonObjectType.multilinestring; case 'Polygon': return TopoJsonObjectType.polygon; case 'MultiPolygon': return TopoJsonObjectType.multipolygon; case 'GeometryCollection': return TopoJsonObjectType.geometrycollection; } return null; } class TopologyJsonObject { final List? bbox; TopologyJsonObject.parse(Map data) : bbox = data.containsKey('bbox') ? (data['bbox'] as List).cast().toList() : null; } class Topology extends TopologyJsonObject { final Map objects; final List>> arcs; final Transform? transform; Topology.parse(super.data) : objects = Map.fromEntries((data['objects'] as Map).cast().entries.map((kv) { final name = kv.key; final geometry = Geometry.build(kv.value); return geometry != null ? MapEntry(name, geometry) : null; }).whereNotNull()), arcs = (data['arcs'] as List).cast().map((arc) => arc.cast().map((position) => position.cast()).toList()).toList(), transform = data.containsKey('transform') ? Transform.parse((data['transform'] as Map).cast()) : null, super.parse(); List> _arcAt(int index) { var arc = arcs[index < 0 ? ~index : index]; if (transform != null) { var x = 0, y = 0; arc = arc.map((quantized) { final absolute = List.of(quantized); absolute[0] = (x += quantized[0] as int) * transform!.scale[0] + transform!.translate[0]; absolute[1] = (y += quantized[1] as int) * transform!.scale[1] + transform!.translate[1]; return absolute; }).toList(); } return index < 0 ? arc.reversed.toList() : arc; } List> _toLine(List>> arcs) { return arcs.fold(>[], (prev, arc) => [...prev, ...prev.isEmpty ? arc : arc.skip(1)]); } List> _decodeRingArcs(List ringArcs) { return _toLine(ringArcs.map(_arcAt).toList()); } List>> _decodePolygonArcs(List> polyArcs) { return polyArcs.map(_decodeRingArcs).toList(); } List>>> _decodeMultiPolygonArcs(List>> multiPolyArcs) { return multiPolyArcs.map(_decodePolygonArcs).toList(); } // cf https://en.wikipedia.org/wiki/Even%E2%80%93odd_rule bool _pointInRing(List point, List> poly) { final x = point[0]; final y = point[1]; final length = poly.length; var j = length - 1; var c = false; for (var i = 0; i < length; i++) { if (((poly[i][1] > y) != (poly[j][1] > y)) && (x < poly[i][0] + (poly[j][0] - poly[i][0]) * (y - poly[i][1]) / (poly[j][1] - poly[i][1]))) { c = !c; } j = i; } return c; } bool _pointInRings(List point, List>> rings) { return rings.any((ring) => _pointInRing(point, ring)); } } class Transform { final List scale; final List translate; Transform.parse(Map data) : scale = (data['scale'] as List).cast(), translate = (data['translate'] as List).cast(); } abstract class Geometry extends TopologyJsonObject { final dynamic id; final Map? properties; Geometry.parse(super.data) : id = data.containsKey('id') ? data['id'] : null, properties = data.containsKey('properties') ? data['properties'] as Map? : null, super.parse(); static Geometry? build(Map data) { final type = _parseTopoJsonObjectType(data['type'] as String?); switch (type) { case TopoJsonObjectType.topology: case null: return null; case TopoJsonObjectType.point: return Point.parse(data); case TopoJsonObjectType.multipoint: return MultiPoint.parse(data); case TopoJsonObjectType.linestring: return LineString.parse(data); case TopoJsonObjectType.multilinestring: return MultiLineString.parse(data); case TopoJsonObjectType.polygon: return Polygon.parse(data); case TopoJsonObjectType.multipolygon: return MultiPolygon.parse(data); case TopoJsonObjectType.geometrycollection: return GeometryCollection.parse(data); } } bool containsPoint(Topology topology, List point) => false; } class Point extends Geometry { final List coordinates; Point.parse(super.data) : coordinates = (data['coordinates'] as List).cast(), super.parse(); } class MultiPoint extends Geometry { final List> coordinates; MultiPoint.parse(super.data) : coordinates = (data['coordinates'] as List).cast().map((position) => position.cast()).toList(), super.parse(); } class LineString extends Geometry { final List arcs; LineString.parse(super.data) : arcs = (data['arcs'] as List).cast(), super.parse(); } class MultiLineString extends Geometry { final List> arcs; MultiLineString.parse(super.data) : arcs = (data['arcs'] as List).cast().map((arc) => arc.cast()).toList(), super.parse(); } class Polygon extends Geometry { final List> arcs; Polygon.parse(super.data) : arcs = (data['arcs'] as List).cast().map((arc) => arc.cast()).toList(), super.parse(); List>>? _rings; List>> rings(Topology topology) { _rings ??= topology._decodePolygonArcs(arcs); return _rings!; } @override bool containsPoint(Topology topology, List point) { return topology._pointInRings(point, rings(topology)); } } class MultiPolygon extends Geometry { final List>> arcs; MultiPolygon.parse(super.data) : arcs = (data['arcs'] as List).cast().map((polygon) => polygon.cast().map((arc) => arc.cast()).toList()).toList(), super.parse(); List>>>? _polygons; List>>> polygons(Topology topology) { _polygons ??= topology._decodeMultiPolygonArcs(arcs); return _polygons!; } @override bool containsPoint(Topology topology, List point) { return polygons(topology).any((polygon) => topology._pointInRings(point, polygon)); } } class GeometryCollection extends Geometry { final List geometries; GeometryCollection.parse(super.data) : geometries = (data['geometries'] as List).cast>().map(Geometry.build).whereNotNull().toList(), super.parse(); @override bool containsPoint(Topology topology, List point) { return geometries.any((geometry) => geometry.containsPoint(topology, point)); } }