class NSWTopo::GeoJSON::Collection

def self.load(json, projection: nil, name: nil)

def self.load(json, projection: nil, name: nil)
  collection = JSON.parse(json)
  crs_name = collection.dig "crs", "properties", "name"
  projection ||= crs_name ? Projection.new(crs_name) : DEFAULT_PROJECTION
  name ||= collection["name"]
  collection["features"].select do |feature|
    feature["geometry"]
  end.map do |feature|
    geometry, properties = feature.values_at "geometry", "properties"
    type, coordinates = geometry.values_at "type", "coordinates"
    raise Error, "unsupported geometry type: #{type}" unless TYPES === type
    GeoJSON.const_get(type)[coordinates, properties]
  end.then do |features|
    new projection: projection, features: features, name: name
  end
rescue JSON::ParserError
  raise Error, "invalid GeoJSON data"
end

def <<(feature)

def <<(feature)
  tap { @features << feature }
end

def bbox

def bbox
  GeoJSON.polygon [bounds.inject(&:product).values_at(0,2,3,1,0)], projection: @projection
end

def bbox_centre

def bbox_centre
  midpoint = bounds.map { |min, max| (max + min) / 2 }
  GeoJSON.point midpoint, projection: @projection
end

def bbox_extents

def bbox_extents
  bounds.map { |min, max| max - min }
end

def bounds

TODO: what about empty collections?
def bounds
  map(&:bounds).transpose.map(&:flatten).map(&:minmax)
end

def buffer(*margins, **options)

def buffer(*margins, **options)
  map do |feature|
    feature.buffer(*margins, **options)
  end.then do |features|
    with_features features
  end
end

def clip(polygon)

def clip(polygon)
  OS.ogr2ogr "-f", "GeoJSON", "-lco", "RFC7946=NO", "-clipsrc", polygon.wkt, "/vsistdout/", "GeoJSON:/vsistdin/" do |stdin|
    stdin.puts to_json
  end.then do |json|
    Collection.load json, projection: @projection
  end
end

def dissolve_points

def dissolve_points
  with_features map(&:dissolve_points)
end

def each(&block)

def each(&block)
  block_given? ? tap { @features.each(&block) } : @features.each
end

def explode

def explode
  with_features flat_map(&:explode)
end

def initialize(projection: DEFAULT_PROJECTION, features: [], name: nil)

def initialize(projection: DEFAULT_PROJECTION, features: [], name: nil)
  @projection, @features, @name = projection, features, name
end

def map!(&block)

def map!(&block)
  tap { @features.map!(&block) }
end

def merge(other)

def merge(other)
  raise Error, "can't merge different projections" unless @projection == other.projection
  with_features @features + other.features
end

def merge!(other)

def merge!(other)
  raise Error, "can't merge different projections" unless @projection == other.projection
  tap { @features.concat other.features }
end

def minimum_bbox_angle(*margins)

def minimum_bbox_angle(*margins)
  dissolve_points.union.first.minimum_bbox_angle(*margins)
end

def multi

def multi
  with_features map(&:multi)
end

def reject!(&block)

def reject!(&block)
  tap { @features.reject!(&block) }
end

def reproject_to(projection)

def reproject_to(projection)
  return self if self.projection == projection
  json = OS.ogr2ogr "-t_srs", projection, "-f", "GeoJSON", "-lco", "RFC7946=NO", "/vsistdout/", "GeoJSON:/vsistdin/" do |stdin|
    stdin.puts to_json
  end
  Collection.load json, projection: projection
end

def reproject_to_wgs84

def reproject_to_wgs84
  reproject_to Projection.wgs84
end

def rotate_by_degrees!(angle)

def rotate_by_degrees!(angle)
  map! { |feature| feature.rotate_by_degrees(angle) }
end

def to_h

def to_h
  {
    "type" => "FeatureCollection",
    "name" => @name,
    "crs" => { "type" => "name", "properties" => { "name" => @projection } },
    "features" => map(&:to_h)
  }.compact
end

def to_json(**extras)

def to_json(**extras)
  to_h.merge(extras).to_json
end

def union

def union
  return self if none?
  with_features [inject(&:+)]
end

def with_features(features)

def with_features(features)
  Collection.new projection: @projection, name: @name, features: features
end

def with_name(name)

def with_name(name)
  Collection.new projection: @projection, name: name, features: @features
end

def with_sql(sql, name: @name)

def with_sql(sql, name: @name)
  json = OS.ogr2ogr *%w[-f GeoJSON -lco RFC7946=NO /vsistdout/ GeoJSON:/vsistdin/ -dialect SQLite -sql], sql do |stdin|
    stdin.puts to_json
  end
  Collection.load(json, projection: @projection).with_name(name)
rescue OS::Error
  raise "GDAL with SQLite support required"
end